Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 41 additions & 24 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,43 @@
{
"image": "ghcr.io/ludeeus/devcontainer/integration:stable",
"context": "..",
"appPort": [
"9123:8123"
],
"postCreateCommand": "for req in $(jq -c -r '.requirements | .[]' custom_components/grocy/manifest.json); do python -m pip install $req; done && container install",
"extensions": [
"ms-python.python",
"github.vscode-pull-request-github",
"tabnine.tabnine-vscode",
"ms-python.vscode-pylance"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"python.pythonPath": "/usr/bin/python3",
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
"files.trimTrailingWhitespace": true
"name": "ludeeus/integration_blueprint",
"image": "mcr.microsoft.com/devcontainers/python:3.13",
"postCreateCommand": "scripts/setup",
"forwardPorts": [8123],
"portsAttributes": {
"8123": {
"label": "Home Assistant",
"onAutoForward": "notify"
}
},
"customizations": {
"vscode": {
"extensions": [
"charliermarsh.ruff",
"github.vscode-pull-request-github",
"ms-python.python",
"ms-python.vscode-pylance",
"ryanluker.vscode-coverage-gutters"
],
"settings": {
"files.eol": "\n",
"editor.tabSize": 4,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.formatOnType": false,
"files.trimTrailingWhitespace": true,
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true,
"python.defaultInterpreterPath": "/usr/local/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
}
}
}
},
"remoteUser": "vscode",
"features": {
"ghcr.io/devcontainers-extra/features/apt-packages:1": {
"packages": ["ffmpeg", "libturbojpeg0", "libpcap-dev"]
}
}
}
}
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,14 @@ dmypy.json
.pyre/

# End of https://www.gitignore.io/api/python

# artifacts
*/build/*
*/dist/*

# misc
.ruff_cache

# Home Assistant configuration
config/*
!config/configuration.yaml
25 changes: 25 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml

target-version = "py313"

[lint]
select = [
"ALL",
]

ignore = [
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
]

[lint.flake8-pytest-style]
fixture-parentheses = false

[lint.pyupgrade]
keep-runtime-typing = true

[lint.mccabe]
max-complexity = 25
17 changes: 17 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "Run Home Assistant on port 8123",
"type": "shell",
"command": "scripts/develop",
"problemMatcher": []
},
{
"label": "Rerun setup",
"type": "shell",
"command": "scripts/setup",
"problemMatcher": []
}
]
}
File renamed without changes.
7 changes: 7 additions & 0 deletions configuration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
default_config:

logger:
default: info
logs:
custom_components.grocy: debug
pygrocy.grocy_api_client: debug
6 changes: 2 additions & 4 deletions custom_components/grocy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
"""
Custom integration to integrate Grocy with Home Assistant.
"""Custom integration to integrate Grocy with Home Assistant.

For more details about this integration, please refer to
https://github.com/custom-components/grocy
"""
from __future__ import annotations

import logging
from typing import List

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -67,7 +65,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
return unloaded


async def _async_get_available_entities(grocy_data: GrocyData) -> List[str]:
async def _async_get_available_entities(grocy_data: GrocyData) -> list[str]:
"""Return a list of available entities based on enabled Grocy features."""
available_entities = []
grocy_config = await grocy_data.async_get_config()
Expand Down
16 changes: 8 additions & 8 deletions custom_components/grocy/binary_sensor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
"""Binary sensor platform for Grocy."""
from __future__ import annotations

import logging
from collections.abc import Callable, Mapping
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, List
import logging
from typing import Any

from homeassistant.components.binary_sensor import (
BinarySensorEntity,
Expand All @@ -24,7 +24,7 @@
ATTR_OVERDUE_TASKS,
DOMAIN,
)
from .coordinator import GrocyDataUpdateCoordinator
from .coordinator import GrocyCoordinatorData, GrocyDataUpdateCoordinator
from .entity import GrocyEntity

_LOGGER = logging.getLogger(__name__)
Expand All @@ -35,7 +35,7 @@ async def async_setup_entry(
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
):
"""Setup binary sensor platform."""
"""Initialize binary sensor platform."""
coordinator: GrocyDataUpdateCoordinator = hass.data[DOMAIN]
entities = []
for description in BINARY_SENSORS:
Expand All @@ -56,8 +56,8 @@ async def async_setup_entry(
class GrocyBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Grocy binary sensor entity description."""

attributes_fn: Callable[[List[Any]], Mapping[str, Any] | None] = lambda _: None
exists_fn: Callable[[List[str]], bool] = lambda _: True
attributes_fn: Callable[[list[Any]], GrocyCoordinatorData | None] = lambda _: None
exists_fn: Callable[[list[str]], bool] = lambda _: True
entity_registry_enabled_default: bool = False


Expand Down Expand Up @@ -141,6 +141,6 @@ class GrocyBinarySensorEntity(GrocyEntity, BinarySensorEntity):
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
entity_data = self.coordinator.data.get(self.entity_description.key, None)
entity_data = self.coordinator.data[self.entity_description.key]

return len(entity_data) > 0 if entity_data else False
3 changes: 2 additions & 1 deletion custom_components/grocy/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Adds config flow for Grocy."""
import logging

from collections import OrderedDict
import logging

import voluptuous as vol
from homeassistant import config_entries
Expand Down
2 changes: 1 addition & 1 deletion custom_components/grocy/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

ISSUE_URL: Final = "https://github.com/custom-components/grocy/issues"

PLATFORMS: Final = ["binary_sensor", "sensor"]
PLATFORMS: Final = ["binary_sensor", "sensor", "todo"]

SCAN_INTERVAL = timedelta(seconds=30)

Expand Down
56 changes: 42 additions & 14 deletions custom_components/grocy/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
"""Data update coordinator for Grocy."""

from __future__ import annotations

from dataclasses import dataclass
import logging
from typing import Any, Dict, List

from pygrocy2.data_models.battery import Battery
from pygrocy2.data_models.chore import Chore
from pygrocy2.data_models.product import Product, ShoppingListProduct
from pygrocy2.data_models.task import Task
from pygrocy2.grocy import Grocy

from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from pygrocy2.grocy import Grocy

from .const import (
CONF_API_KEY,
Expand All @@ -18,12 +24,35 @@
SCAN_INTERVAL,
)
from .grocy_data import GrocyData
from .helpers import extract_base_url_and_path
from .helpers import MealPlanItemWrapper, extract_base_url_and_path

_LOGGER = logging.getLogger(__name__)


class GrocyDataUpdateCoordinator(DataUpdateCoordinator[Dict[str, Any]]):
@dataclass
class GrocyCoordinatorData:
batteries: list[Battery] | None = None
chores: list[Chore] | None = None
expired_products: list[Product] | None = None
expiring_products: list[Product] | None = None
meal_plan: list[MealPlanItemWrapper] | None = None
missing_products: list[Product] | None = None
overdue_batteries: list[Battery] | None = None
overdue_chores: list[Chore] | None = None
overdue_products: list[Product] | None = None
overdue_tasks: list[Task] | None = None
shopping_list: list[ShoppingListProduct] | None = None
stock: list[Product] | None = None
tasks: list[Task] | None = None

def __setitem__(self, key, value):
setattr(self, key, value)

def __getitem__(self, key: str):
return getattr(self, key)


class GrocyDataUpdateCoordinator(DataUpdateCoordinator[GrocyCoordinatorData]):
"""Grocy data update coordinator."""

def __init__(
Expand All @@ -50,23 +79,22 @@ def __init__(
)
self.grocy_data = GrocyData(hass, self.grocy_api)

self.available_entities: List[str] = []
self.entities: List[Entity] = []
self.available_entities: list[str] = []
self.entities: list[Entity] = []

async def _async_update_data(self) -> dict[str, Any]:
async def _async_update_data(self) -> GrocyCoordinatorData:
"""Fetch data."""
data: dict[str, Any] = {}

data = GrocyCoordinatorData()
for entity in self.entities:
if not entity.enabled:
_LOGGER.debug("Entity %s is disabled.", entity.entity_id)
_LOGGER.debug("Entity %s is disabled", entity.entity_id)
continue

try:
data[
entity.entity_description.key
] = await self.grocy_data.async_update_data(
entity.entity_description.key
data[entity.entity_description.key] = (
await self.grocy_data.async_update_data(
entity.entity_description.key
)
)
except Exception as error: # pylint: disable=broad-except
raise UpdateFailed(f"Update failed: {error}") from error
Expand Down
8 changes: 3 additions & 5 deletions custom_components/grocy/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
from __future__ import annotations

import json
from collections.abc import Mapping
from typing import Any

from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, NAME, VERSION
from .coordinator import GrocyDataUpdateCoordinator
from .coordinator import GrocyCoordinatorData, GrocyDataUpdateCoordinator
from .json_encoder import CustomJSONEncoder


Expand Down Expand Up @@ -42,9 +40,9 @@ def device_info(self) -> DeviceInfo:
)

@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
def extra_state_attributes(self) -> GrocyCoordinatorData | None:
"""Return the extra state attributes."""
data = self.coordinator.data.get(self.entity_description.key)
data = self.coordinator.data[self.entity_description.key]
if data and hasattr(self.entity_description, "attributes_fn"):
return json.loads(
json.dumps(
Expand Down
Loading