Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Todo Entities #2

Merged
merged 13 commits into from
Oct 19, 2024
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
5 changes: 3 additions & 2 deletions custom_components/grocy/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Adds config flow for Grocy."""
import logging
from collections import OrderedDict
import logging

from pygrocy import Grocy
import voluptuous as vol

from homeassistant import config_entries
from pygrocy import Grocy

from .const import (
CONF_API_KEY,
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
48 changes: 38 additions & 10 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 pygrocy import Grocy
from pygrocy.data_models.battery import Battery
from pygrocy.data_models.chore import Chore
from pygrocy.data_models.meal_items import MealPlanItem
from pygrocy.data_models.product import Product, ShoppingListProduct
from pygrocy.data_models.task import Task

from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from pygrocy 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,16 +79,15 @@ 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:
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
27 changes: 16 additions & 11 deletions custom_components/grocy/grocy_data.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"""Communication with Grocy API."""
from __future__ import annotations

import logging
from datetime import datetime, timedelta
from typing import List
import logging

from aiohttp import hdrs, web
from pygrocy import Grocy
from pygrocy.data_models.battery import Battery
from pygrocy.data_models.chore import Chore

from homeassistant.components.http import HomeAssistantView
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from pygrocy.data_models.battery import Battery

from .const import (
ATTR_BATTERIES,
Expand Down Expand Up @@ -38,7 +40,7 @@
class GrocyData:
"""Handles communication and gets the data."""

def __init__(self, hass, api):
def __init__(self, hass: HomeAssistant, api: Grocy) -> None: # noqa: D107
"""Initialize Grocy data."""
self.hass = hass
self.api = api
Expand Down Expand Up @@ -70,7 +72,7 @@ async def async_update_stock(self):
async def async_update_chores(self):
"""Update chores data."""

def wrapper():
def wrapper() -> list[Chore]:
return self.api.chores(True)

return await self.hass.async_add_executor_job(wrapper)
Expand Down Expand Up @@ -103,7 +105,9 @@ async def async_update_overdue_tasks(self):

and_query_filter = [
f"due_date<{datetime.now().date()}",
# It's not possible to pass an empty value to Grocy, so use a regex that matches non-empty values to exclude empty str due_date.
# It's not possible to pass an empty value to Grocy
# so use a regex that matches non-empty values
# to exclude empty str due_date.
r"due_date§.*\S.*",
]

Expand Down Expand Up @@ -155,7 +159,8 @@ def wrapper():
async def async_update_meal_plan(self):
"""Update meal plan data."""

# The >= condition is broken before Grocy 3.3.1. So use > to maintain backward compatibility.
# The >= condition is broken before Grocy 3.3.1.
# So use > to maintain backward compatibility.
yesterday = datetime.now() - timedelta(1)
query_filter = [f"day>{yesterday.date()}"]

Expand All @@ -166,15 +171,15 @@ def wrapper():

return await self.hass.async_add_executor_job(wrapper)

async def async_update_batteries(self) -> List[Battery]:
async def async_update_batteries(self) -> list[Battery]:
"""Update batteries."""

def wrapper():
return self.api.batteries(get_details=True)

return await self.hass.async_add_executor_job(wrapper)

async def async_update_overdue_batteries(self) -> List[Battery]:
async def async_update_overdue_batteries(self) -> list[Battery]:
"""Update overdue batteries."""

def wrapper():
Expand All @@ -187,7 +192,7 @@ def wrapper():
async def async_setup_endpoint_for_image_proxy(
hass: HomeAssistant, config_entry: ConfigEntry
):
"""Setup and register the image api for grocy images with HA."""
"""Do setup and register the image api for grocy images with HA."""
session = async_get_clientsession(hass)

url = config_entry.get(CONF_URL)
Expand All @@ -210,7 +215,7 @@ class GrocyPictureView(HomeAssistantView):
url = "/api/grocy/{picture_type}/{filename}"
name = "api:grocy:picture"

def __init__(self, session, base_url, api_key):
def __init__(self, session, base_url, api_key) -> None: # noqa: D107
self._session = session
self._base_url = base_url
self._api_key = api_key
Expand Down
8 changes: 4 additions & 4 deletions custom_components/grocy/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
from __future__ import annotations

import base64
from typing import Any, Dict, Tuple
from typing import Any
from urllib.parse import urlparse

from pygrocy.data_models.meal_items import MealPlanItem


def extract_base_url_and_path(url: str) -> Tuple[str, str]:
def extract_base_url_and_path(url: str) -> tuple[str, str]:
"""Extract the base url and path from a given URL."""
parsed_url = urlparse(url)

Expand All @@ -18,7 +18,7 @@ def extract_base_url_and_path(url: str) -> Tuple[str, str]:
class MealPlanItemWrapper:
"""Wrapper around the pygrocy MealPlanItem."""

def __init__(self, meal_plan: MealPlanItem):
def __init__(self, meal_plan: MealPlanItem) -> None: # noqa: D107
self._meal_plan = meal_plan

@property
Expand All @@ -35,7 +35,7 @@ def picture_url(self) -> str | None:
return f"/api/grocy/recipepictures/{str(b64name, 'utf-8')}"
return None

def as_dict(self) -> Dict[str, Any]:
def as_dict(self) -> dict[str, Any]:
"""Return attributes for the pygrocy MealPlanItem object including picture URL."""
props = self.meal_plan.as_dict()
props["picture_url"] = self.picture_url
Expand Down
17 changes: 8 additions & 9 deletions custom_components/grocy/sensor.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""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 homeassistant.components.sensor import (
SensorEntity,
Expand All @@ -30,7 +29,7 @@
PRODUCTS,
TASKS,
)
from .coordinator import GrocyDataUpdateCoordinator
from .coordinator import GrocyCoordinatorData, GrocyDataUpdateCoordinator
from .entity import GrocyEntity

_LOGGER = logging.getLogger(__name__)
Expand All @@ -41,7 +40,7 @@ async def async_setup_entry(
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
):
"""Setup sensor platform."""
"""Do setup sensor platform."""
coordinator: GrocyDataUpdateCoordinator = hass.data[DOMAIN]
entities = []
for description in SENSORS:
Expand All @@ -51,7 +50,7 @@ async def async_setup_entry(
entities.append(entity)
else:
_LOGGER.debug(
"Entity description '%s' is not available.",
"Entity description '%s' is not available",
description.key,
)

Expand All @@ -62,8 +61,8 @@ async def async_setup_entry(
class GrocySensorEntityDescription(SensorEntityDescription):
"""Grocy 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[GrocyCoordinatorData | None] = lambda _: None
exists_fn: Callable[[list[str]], bool] = lambda _: True
entity_registry_enabled_default: bool = False


Expand Down Expand Up @@ -149,6 +148,6 @@ class GrocySensorEntity(GrocyEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
entity_data = self.coordinator.data.get(self.entity_description.key, None)
entity_data = self.coordinator.data[self.entity_description.key]

return len(entity_data) if entity_data else 0
Loading