From 06b7bcd2f1acbc0410fc84aa146ccc7040c48b78 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 19 Jul 2022 14:06:05 +0000 Subject: [PATCH 01/15] Add basic models --- custom_components/mealie/models.py | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 custom_components/mealie/models.py diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py new file mode 100644 index 0000000..2f9015a --- /dev/null +++ b/custom_components/mealie/models.py @@ -0,0 +1,50 @@ +from typing import Optional, List +from pydantic import BaseModel +from homeassistant.backports.enum import StrEnum + + +"""Mealie API objects""" + + +class EntryTypes(StrEnum): + """Enum to represent the entry types.""" + + BREAKFAST = "breakfast" + LUNCH = "lunch" + DINNER = "dinner" + SIDE = "side" + + +class Recipe(BaseModel): + """Recipe model.""" + + name: str + slug: str + + +class MealPlan(BaseModel): + """Meal plan model.""" + + date: str + entryType: str + title: Optional[str] + text: Optional[str] + recipeId: Optional[str] + recipe: Optional[Recipe] + + +class About(BaseModel): + """About model.""" + + version: str + # TODO: add configuration URL here? + + +class MealieData: + """Mealie API data.""" + + def __init__(self): + self.mealPlans = [] + + mealPlans: list[MealPlan] + about: About From 14876d3a8591a7ae172457617c5bb30b23a01836 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 19 Jul 2022 14:06:54 +0000 Subject: [PATCH 02/15] Move Coordinator to it's own file --- custom_components/mealie/__init__.py | 37 ++----------------------- custom_components/mealie/const.py | 10 +++++++ custom_components/mealie/coordinator.py | 36 ++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 35 deletions(-) create mode 100644 custom_components/mealie/coordinator.py diff --git a/custom_components/mealie/__init__.py b/custom_components/mealie/__init__.py index 04dac1b..6aa363a 100644 --- a/custom_components/mealie/__init__.py +++ b/custom_components/mealie/__init__.py @@ -7,6 +7,7 @@ import asyncio import logging from datetime import timedelta +from .coordinator import MealieDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -19,8 +20,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from homeassistant.helpers.update_coordinator import UpdateFailed from .api import MealieApi from .const import DOMAIN @@ -38,7 +37,7 @@ def clean_obj(obj): obj = { k: v for (k, v) in obj.items() - if v not in [None, [], {}] and 'id' not in k.lower() + if v not in [None, [], {}] and "id" not in k.lower() } elif isinstance(obj, list): for idx, i in enumerate(obj): @@ -84,38 +83,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -class MealieDataUpdateCoordinator(DataUpdateCoordinator): - """Class to manage fetching data from the API.""" - - def __init__( - self, - hass: HomeAssistant, - client: MealieApi, - ) -> None: - """Initialize.""" - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) - self.api = client - self.platforms = [] - - async def _async_update_data(self): - """Update data via library.""" - - try: - data = {} - data['app/about'] = await self.api.async_get_api_app_about() - data[ - 'groups/mealplans/today' - ] = await self.api.async_get_api_groups_mealplans_today() - for idx, recipe in enumerate(data['groups/mealplans/today']): - data['groups/mealplans/today'][idx]['recipe'].update( - await self.api.async_get_api_recipes(recipe['recipe']['slug']) - ) - return data - except Exception as exception: - _LOGGER.exception(exception) - raise UpdateFailed() from exception - - async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" coordinator = hass.data[DOMAIN][entry.entry_id] diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index 0e91000..4813a5b 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -1,10 +1,20 @@ """Constants for Mealie.""" # Base component constants +from datetime import timedelta +import logging + + NAME = "Mealie" DOMAIN = "mealie" DOMAIN_DATA = f"{DOMAIN}_data" VERSION = "0.1.0" +LOGGER = logging.getLogger(__package__) + + +UPDATE_INTERVAL = timedelta(seconds=30) + + ISSUE_URL = "https://github.com/mealie-recipes/mealie-hacs/issues" SOURCE_REPO = "hay-kot/mealie" diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py new file mode 100644 index 0000000..b6fe201 --- /dev/null +++ b/custom_components/mealie/coordinator.py @@ -0,0 +1,36 @@ +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, UPDATE_INTERVAL +from .api import MealieApi +from .models import About, MealPlan, MealieData + + +class MealieDataUpdateCoordinator(DataUpdateCoordinator[MealieData]): + """Class to manage fetching data from the API.""" + + def __init__( + self, + hass: HomeAssistant, + client: MealieApi, + ) -> None: + """Initialize.""" + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + self.api = client + self.platforms = [] + + async def _async_update_data(self) -> MealieData: + """Update data via library.""" + + try: + data = MealieData() + data.about = About.parse_obj(await self.api.async_get_api_app_about()) + mealplans = await self.api.async_get_api_groups_mealplans_today() + + for mealplan in mealplans: + data.mealPlans.append(MealPlan.parse_obj(mealplan)) + + return data + except Exception as exception: + LOGGER.exception(exception) + raise UpdateFailed() from exception From 0a1590d85c4ee30e55555f878a90a5a34b5d59cd Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 19 Jul 2022 14:22:34 +0000 Subject: [PATCH 03/15] Update MealieEntity --- custom_components/mealie/entity.py | 45 +++++++++++++----------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index 5b94ff6..7020c9e 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -4,40 +4,33 @@ import time from homeassistant.const import CONF_HOST, CONF_USERNAME +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import DOMAIN -from .const import ICONS -from .const import NAME +from .const import DOMAIN, ICONS, NAME +from .coordinator import MealieDataUpdateCoordinator -class MealieEntity(CoordinatorEntity): + +class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): """mealie Entity class.""" - def __init__(self, coordinator, config_entry): + def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: + """Initialize the Mealie entity""" super().__init__(coordinator) - self.api = self.coordinator.api - self.coordinator = coordinator - self.config_entry = config_entry - self.endpoint = "app/about" - @property - def unique_id(self): - """Return a unique ID to use for this entity.""" - return self.config_entry.entry_id + self._attr_unique_id = self.coordinator.config_entry.entry_id - @property - def device_info(self): - about_data = self.coordinator.data.get("app/about") - config_data = self.config_entry.data - return { - "identifiers": {(DOMAIN, self.config_entry.entry_id)}, - "name": str(config_data.get(CONF_USERNAME)), - "model": str(about_data.get("version")), - "manufacturer": NAME, - "configuration_url": str(config_data.get(CONF_HOST)), - "suggested_area": "Kitchen", - } + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, + name=self.coordinator.config_entry.data.get(CONF_USERNAME), + sw_version=self.coordinator.data.about.version, + manufacturer=NAME, + configuration_url=self.coordinator.config_entry.data.get(CONF_HOST), + suggested_area="Kitchen", + entry_type=DeviceEntryType.SERVICE, + ) class MealPlanEntity(MealieEntity): @@ -62,7 +55,7 @@ def icon(self): def _get_recipes(self): mealplans = self.coordinator.data.get(self.endpoint, {}) - self.recipes = [i['recipe'] for i in mealplans if i['entryType'] == self.meal] + self.recipes = [i["recipe"] for i in mealplans if i["entryType"] == self.meal] self.idx = self._get_time_based_index() def _get_time_based_index(self, interval=60): From 978f11f6e7b261c488657a17aebee0f1cc9444dc Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 19 Jul 2022 16:13:33 +0000 Subject: [PATCH 04/15] Use callback from coordinator to update --- custom_components/mealie/entity.py | 49 ++++++++++++++++-------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index 7020c9e..0376547 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -1,9 +1,10 @@ """MealieEntity class""" from __future__ import annotations -import time +from .models import Recipe from homeassistant.const import CONF_HOST, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -20,8 +21,6 @@ def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: """Initialize the Mealie entity""" super().__init__(coordinator) - self._attr_unique_id = self.coordinator.config_entry.entry_id - self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self.coordinator.config_entry.entry_id)}, name=self.coordinator.config_entry.data.get(CONF_USERNAME), @@ -36,35 +35,39 @@ def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: class MealPlanEntity(MealieEntity): """mealie Meal Plan Entity class.""" - def __init__(self, meal, coordinator, config_entry): - super().__init__(coordinator, config_entry) - self.config_entry = config_entry - self.endpoint = "groups/mealplans/today" + def __init__(self, meal, coordinator): + super().__init__( + coordinator, + ) + + self._attr_unique_id = meal + self.meal = meal - self.idx = None - self.recipes = [] + self.recipe: Recipe | None = None @property def name(self): - return f"Meal Plan {self.meal.title()}" + return None if not self.recipe else self.recipe.name @property def icon(self): """Return the icon of the camera.""" return ICONS.get(self.meal) - def _get_recipes(self): - mealplans = self.coordinator.data.get(self.endpoint, {}) - self.recipes = [i["recipe"] for i in mealplans if i["entryType"] == self.meal] - self.idx = self._get_time_based_index() - - def _get_time_based_index(self, interval=60): - return round( - ((int(time.time()) % interval) / interval) * (len(self.recipes) - 1) + @property + def native_value(self): + return None if not self.recipe else self.recipe.name + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.recipe = next( + ( + mealPlan.recipe + for mealPlan in self.coordinator.data.mealPlans + if mealPlan.entryType == self.meal + ), + None, ) - async def async_update(self): - self._get_recipes() - - async def async_added_to_hass(self) -> None: - self._get_recipes() + self.async_write_ha_state() From f0b40e0a382218493d532a5e6ec7ff802fa3e3c6 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 19 Jul 2022 17:34:37 +0000 Subject: [PATCH 05/15] Fix Entity name --- custom_components/mealie/entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index 0376547..5354708 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -47,7 +47,7 @@ def __init__(self, meal, coordinator): @property def name(self): - return None if not self.recipe else self.recipe.name + return f"Meal plan {self.meal}" @property def icon(self): From 65b0fc8db78d1146a9230576f3a0d0a979eee50c Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Wed, 20 Jul 2022 07:31:47 +0000 Subject: [PATCH 06/15] Simplified init --- custom_components/mealie/__init__.py | 35 +++++++++------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/custom_components/mealie/__init__.py b/custom_components/mealie/__init__.py index 6aa363a..b667703 100644 --- a/custom_components/mealie/__init__.py +++ b/custom_components/mealie/__init__.py @@ -65,40 +65,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): client = MealieApi(username, password, host, session) coordinator = MealieDataUpdateCoordinator(hass, client=client) - await coordinator.async_refresh() + await coordinator.async_config_entry_first_refresh() if not coordinator.last_update_success: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator - for platform in PLATFORMS: - if entry.options.get(platform, True): - coordinator.platforms.append(platform) - hass.async_add_job( - hass.config_entries.async_forward_entry_setup(entry, platform) - ) - - entry.add_update_listener(async_reload_entry) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Handle removal of an entry.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - unloaded = all( - await asyncio.gather( - *[ - hass.config_entries.async_forward_entry_unload(entry, platform) - for platform in PLATFORMS - if platform in coordinator.platforms - ] - ) - ) - if unloaded: - hass.data[DOMAIN].pop(entry.entry_id) - - return unloaded + + # Unload platforms. + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + # Clean up. + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: From 4161bca763c69aa1018ab1cea50c236c2578388b Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Wed, 20 Jul 2022 11:45:05 +0200 Subject: [PATCH 07/15] Update API, remove separate coordinator --- custom_components/mealie/__init__.py | 25 ++- custom_components/mealie/api.py | 228 +++++++++--------------- custom_components/mealie/coordinator.py | 36 ---- 3 files changed, 102 insertions(+), 187 deletions(-) delete mode 100644 custom_components/mealie/coordinator.py diff --git a/custom_components/mealie/__init__.py b/custom_components/mealie/__init__.py index b667703..08011a9 100644 --- a/custom_components/mealie/__init__.py +++ b/custom_components/mealie/__init__.py @@ -4,25 +4,25 @@ For more details about this integration, please refer to https://github.com/mealie-recipes/mealie-hacs """ -import asyncio import logging from datetime import timedelta -from .coordinator import MealieDataUpdateCoordinator +from config.custom_components.mealie.models import MealieData + +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_USERNAME, CONF_PASSWORD, CONF_HOST, - CONF_ACCESS_TOKEN, ) from homeassistant.core import Config from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .api import MealieApi -from .const import DOMAIN +from .api import MealieApi, MealieError +from .const import DOMAIN, LOGGER from .const import PLATFORMS from .const import STARTUP_MESSAGE @@ -64,7 +64,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): session = async_get_clientsession(hass) client = MealieApi(username, password, host, session) - coordinator = MealieDataUpdateCoordinator(hass, client=client) + async def async_update_mealie() -> MealieData: + try: + return await client.async_get_updated_mealie_data() + except MealieError as err: + raise UpdateFailed("Mealie API communication error") from err + + coordinator: DataUpdateCoordinator[MealieData] = DataUpdateCoordinator( + hass, + LOGGER, + name=f"{DOMAIN}", + update_interval=SCAN_INTERVAL, + update_method=async_update_mealie, + ) + await coordinator.async_config_entry_first_refresh() if not coordinator.last_update_success: diff --git a/custom_components/mealie/api.py b/custom_components/mealie/api.py index 8489716..0a6b52a 100644 --- a/custom_components/mealie/api.py +++ b/custom_components/mealie/api.py @@ -1,60 +1,24 @@ from __future__ import annotations import asyncio -from functools import wraps +import json import logging import socket -from urllib.parse import urlencode +from typing import Any import aiohttp import async_timeout +from config.custom_components.mealie.models import About, MealPlan, MealieData + TIMEOUT = 10 _LOGGER: logging.Logger = logging.getLogger(__package__) -def apirequest(func) -> dict: - """Decorator for request methods.""" - - @wraps(func) - async def wrapper(self, *args, **kwargs) -> dict | bytes | None: - """Get information from the API.""" - - if not kwargs.get('headers'): - kwargs['headers'] = self._headers - - if not kwargs.get('skip_auth') and "Authorization" not in kwargs.get('headers'): - kwargs['headers'].update(await self.async_get_api_auth_token()) - - try: - async with async_timeout.timeout(TIMEOUT): - return await func(self, *args, **kwargs) - - except asyncio.TimeoutError as exception: - _LOGGER.error( - "Timeout error fetching information from %s - %s", - args[0], - exception, - ) - - except (KeyError, TypeError) as exception: - _LOGGER.error( - "Error parsing information from %s - %s", - args[0], - exception, - ) - except (aiohttp.ClientError, socket.gaierror) as exception: - _LOGGER.error( - "Error fetching information from %s - %s", - args[0], - exception, - ) - except Exception as exception: # pylint: disable=broad-except - _LOGGER.error("Something really wrong happened! - %s", exception) - - return wrapper +class MealieError(Exception): + """Mealie error.""" class MealieApi: @@ -63,131 +27,105 @@ class MealieApi: def __init__( self, username: str, password: str, host: str, session: aiohttp.ClientSession ) -> None: + self._session = session + + self._host = host self._username = username self._password = password - self._host = host - self._session = session + self._headers = { "Content-type": "application/json; charset=UTF-8", "Accept": "application/json", } - @apirequest - async def _get( - self, - url: str, - params: dict = None, - data: dict = None, - headers: dict = None, - as_bytes: bool = False, - skip_auth: bool = False, - ): - response = await self._session.get( - f"{self._host}/api/" + url, - params=params, - json=data, - headers=headers, - ) - return await (response.read() if as_bytes else response.json()) - - @apirequest - async def _put( - self, - url: str, - params: dict = None, - data: dict = None, - headers: dict = None, - ): - return await self._session.put( - f"{self._host}/api/" + url, - params=params, - json=data, - headers=headers, - ) + async def request( + self, uri: str, method: str = "GET", headers={}, skip_auth=False, data={} + ) -> dict[str, Any]: + """Handle a request to the Mealie instance""" + url = f"{self._host}/api/{uri}" - @apirequest - async def _patch( - self, - url: str, - params: dict = None, - data: dict = None, - headers: dict = None, - ): - return await self._session.patch( - f"{self._host}/api/" + url, - params=params, - json=data, - headers=headers, - ) + if self._session is None: + self._session = aiohttp.ClientSession() + # self._close_session = True - @apirequest - async def _post( - self, - url: str, - params: dict = None, - data: dict = None, - headers: dict = None, - skip_auth: bool = True, - ): - return await self._session.post( - f"{self._host}/api/" + url, - params=params, - data=data, - headers=headers, - ) + if not skip_auth and self._headers.get("Authorization") is None: + await self.async_get_api_auth_token() - async def async_get_api_app_about(self) -> dict: - """Get data from the API.""" - return await self._get("app/about", skip_auth=True) + headers = self._headers | headers - async def async_get_api_groups_mealplans_today(self) -> dict: - """Get today's mealplan from the API.""" - return await self._get("groups/mealplans/today") - - async def async_get_api_recipes(self, recipe_slug) -> dict: - """Get recipe details from the API.""" - return await self._get(f"recipes/{recipe_slug}") - - async def async_get_api_recipes_exports(self, recipe_slug, template="recipes.md"): - """Get formatted recipe data from the API.""" - return await self._get( - f"recipes/{recipe_slug}/exports", - params={"template_name": template}, - as_bytes=True, - ) + try: + async with async_timeout.timeout(TIMEOUT): + response = await self._session.request( + method=method, url=url, data=data, headers=headers + ) - async def async_get_api_media_recipes_images(self, recipe_id) -> bytes: - """Get the image for a recipe from the API.""" - filename = "min-original.webp" - url = f"media/recipes/{recipe_id}/images/{filename}" - return await self._get( - url, headers={"Content-type": "image/webp"}, as_bytes=True - ) + except asyncio.TimeoutError as exception: + raise MealieError( + "Timeout occurred while connecting to the Mealie API." + ) from exception + except (aiohttp.ClientError, socket.gaierror) as exception: + raise MealieError( + "Error occurred while communicating with Mealie." + ) from exception + + content_type = response.headers.get("Content-Type", "") + if response.status // 100 in [4, 5]: + contents = await response.read() + response.close() + + if content_type == "application/json": + raise MealieError(response.status, json.loads(contents.decode("utf8"))) + raise MealieError(response.status, {"message": contents.decode("utf8")}) - async def async_set_title(self, value: str) -> None: + if "application/json" in content_type: + return await response.json() + + text = await response.text() + return {"message": text} + + async def async_get_updated_mealie_data(self) -> MealieData: + """Update data from Mealie.""" + data = MealieData() + + data.about = await self.async_get_api_app_about() + data.mealPlans = await self.async_get_api_groups_mealplans_today() + + return data + + async def async_get_api_app_about(self) -> About: """Get data from the API.""" - url = "https://jsonplaceholder.typicode.com/posts/1" - await self._patch(url, data={"title": value}, headers=self._headers) + response = await self.request("admin/about") + return About.parse_obj(response) + + async def async_get_api_groups_mealplans_today(self) -> list[MealPlan]: + """Get today's mealplan from the API.""" + response = await self.request("groups/mealplans/today") + return [MealPlan.parse_obj(mealplan) for mealplan in response] + + # async def async_get_api_media_recipes_images(self, recipe_id) -> bytes: + # """Get the image for a recipe from the API.""" + # filename = "min-original.webp" + # url = f"media/recipes/{recipe_id}/images/{filename}" + # return await self.request( + # url, headers={"Content-type": "image/webp"}, as_bytes=True + # ) async def async_get_api_auth_token(self) -> str: """Gets an access token from the API.""" - url = "auth/token" - payload = urlencode( - { - "username": self._username, - "password": self._password, - "grant_type": "password", - } - ) + payload = { + "username": self._username, + "password": self._password, + "grant_type": "password", + } - response = await self._post( - url, + response = await self.request( + "auth/token", + method="POST", data=payload, headers={"Content-type": "application/x-www-form-urlencoded"}, skip_auth=True, ) - data = await response.json() - access_token = data.get("access_token") + access_token = response.get("access_token") self._headers["Authorization"] = f"Bearer {access_token}" return {"Authorization": f"Bearer {access_token}"} diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py deleted file mode 100644 index b6fe201..0000000 --- a/custom_components/mealie/coordinator.py +++ /dev/null @@ -1,36 +0,0 @@ -from homeassistant.core import HomeAssistant -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed - -from .const import DOMAIN, LOGGER, UPDATE_INTERVAL -from .api import MealieApi -from .models import About, MealPlan, MealieData - - -class MealieDataUpdateCoordinator(DataUpdateCoordinator[MealieData]): - """Class to manage fetching data from the API.""" - - def __init__( - self, - hass: HomeAssistant, - client: MealieApi, - ) -> None: - """Initialize.""" - super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) - self.api = client - self.platforms = [] - - async def _async_update_data(self) -> MealieData: - """Update data via library.""" - - try: - data = MealieData() - data.about = About.parse_obj(await self.api.async_get_api_app_about()) - mealplans = await self.api.async_get_api_groups_mealplans_today() - - for mealplan in mealplans: - data.mealPlans.append(MealPlan.parse_obj(mealplan)) - - return data - except Exception as exception: - LOGGER.exception(exception) - raise UpdateFailed() from exception From b3e2927012c1998fcb4d9fd41c792f8440d8fc79 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Wed, 20 Jul 2022 12:37:47 +0200 Subject: [PATCH 08/15] Add back Update entity --- custom_components/mealie/entity.py | 45 ++++++++++--------- custom_components/mealie/models.py | 1 + custom_components/mealie/update.py | 71 +++++++----------------------- 3 files changed, 40 insertions(+), 77 deletions(-) diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index 5354708..6d90215 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -1,23 +1,25 @@ """MealieEntity class""" from __future__ import annotations +from abc import abstractmethod -from .models import Recipe from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) +from .models import MealieData, Recipe from .const import DOMAIN, ICONS, NAME -from .coordinator import MealieDataUpdateCoordinator - -class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): +class MealieEntity(CoordinatorEntity[DataUpdateCoordinator[MealieData]]): """mealie Entity class.""" - def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: + def __init__(self, coordinator: DataUpdateCoordinator[MealieData]) -> None: """Initialize the Mealie entity""" super().__init__(coordinator) @@ -31,6 +33,16 @@ def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: entry_type=DeviceEntryType.SERVICE, ) + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._process_update() + super()._handle_coordinator_update() + + @callback + @abstractmethod + def _process_update(self) -> None: + """Process an update from the coordinator""" + class MealPlanEntity(MealieEntity): """mealie Meal Plan Entity class.""" @@ -40,26 +52,17 @@ def __init__(self, meal, coordinator): coordinator, ) - self._attr_unique_id = meal - self.meal = meal self.recipe: Recipe | None = None - @property - def name(self): - return f"Meal plan {self.meal}" - - @property - def icon(self): - """Return the icon of the camera.""" - return ICONS.get(self.meal) + self._attr_unique_id = self.meal + self._attr_name = f"Meal plan {self.meal}" + self._attr_icon = ICONS.get(self.meal) - @property - def native_value(self): - return None if not self.recipe else self.recipe.name + self._process_update() @callback - def _handle_coordinator_update(self) -> None: + def _process_update(self) -> None: """Handle updated data from the coordinator.""" self.recipe = next( ( @@ -69,5 +72,3 @@ def _handle_coordinator_update(self) -> None: ), None, ) - - self.async_write_ha_state() diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py index 2f9015a..2168f5e 100644 --- a/custom_components/mealie/models.py +++ b/custom_components/mealie/models.py @@ -37,6 +37,7 @@ class About(BaseModel): """About model.""" version: str + versionLatest: str # TODO: add configuration URL here? diff --git a/custom_components/mealie/update.py b/custom_components/mealie/update.py index d21804f..c884bf3 100644 --- a/custom_components/mealie/update.py +++ b/custom_components/mealie/update.py @@ -1,12 +1,11 @@ """Sensor platform for Mealie.""" from __future__ import annotations - -import aiohttp +from .models import MealieData from homeassistant.components.update import UpdateEntity, UpdateEntityFeature +from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import NAME -from .const import SOURCE_REPO from .const import DOMAIN from .const import UPDATE from .entity import MealieEntity @@ -14,67 +13,29 @@ async def async_setup_entry(hass, entry, async_add_devices): """Setup sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] - async_add_devices([MealieUpdate(coordinator, entry)]) + coordinator: DataUpdateCoordinator[MealieData] = hass.data[DOMAIN][entry.entry_id] + async_add_devices([MealieUpdate(coordinator)]) class MealieUpdate(MealieEntity, UpdateEntity): """mealie Update class.""" - def __init__(self, coordinator, config_entry): - super().__init__(coordinator, config_entry) + def __init__(self, coordinator: DataUpdateCoordinator[MealieData]): + super().__init__(coordinator) UpdateEntity.__init__(self) + self._latest_version = None self._release_url = None self._release_notes = None - async def _get_update_data(self): - url = f"https://api.github.com/repos/{SOURCE_REPO}/releases" - - async with aiohttp.ClientSession() as session: - async with session.get(url) as response: - if response: - json = await response.json() - self._latest_version = json[0]['tag_name'] - self._release_url = json[0]['html_url'] - self._release_notes = json[0]['body'] - - async def async_update(self): - await self._get_update_data() - - async def async_added_to_hass(self) -> None: - await self._get_update_data() - - @property - def unique_id(self): - """Return a unique ID to use for this entity.""" - return f"{self.config_entry.entry_id}_{self.endpoint}_{UPDATE}" - - @property - def installed_version(self): - about_data = self.coordinator.data.get(self.endpoint) - return about_data['version'] - - @property - def latest_version(self): - return self._latest_version - - @property - def release_url(self): - return self._release_url - - @property - def name(self): - """Return the name of the update.""" - return f"{NAME} {UPDATE.title()}" + self._attr_unique_id = f"{DOMAIN}_{UPDATE}" + self._attr_supported_features = UpdateEntityFeature.RELEASE_NOTES - @property - def title(self): - return NAME + self._process_update() - async def async_release_notes(self) -> str | None: - return self._release_notes + @callback + def _process_update(self) -> None: + """Handle updated data from the coordinator.""" - @property - def supported_features(self): - return UpdateEntityFeature.RELEASE_NOTES + self._attr_installed_version = self.coordinator.data.about.version + self._attr_latest_version = self.coordinator.data.about.versionLatest From 965415f2831bb71e0b93aaab3a74ef966f2d2486 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Mon, 25 Jul 2022 14:37:21 +0200 Subject: [PATCH 09/15] Config flow changes --- custom_components/mealie/__init__.py | 60 ++------- custom_components/mealie/config_flow.py | 116 ++++++++++++------ custom_components/mealie/const.py | 20 ++- custom_components/mealie/coordinator.py | 41 +++++++ custom_components/mealie/strings.json | 35 ++++++ custom_components/mealie/translations/en.json | 24 +++- 6 files changed, 207 insertions(+), 89 deletions(-) create mode 100644 custom_components/mealie/coordinator.py create mode 100644 custom_components/mealie/strings.json diff --git a/custom_components/mealie/__init__.py b/custom_components/mealie/__init__.py index 08011a9..f536fae 100644 --- a/custom_components/mealie/__init__.py +++ b/custom_components/mealie/__init__.py @@ -4,11 +4,8 @@ For more details about this integration, please refer to https://github.com/mealie-recipes/mealie-hacs """ -import logging from datetime import timedelta -from config.custom_components.mealie.models import MealieData - -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .coordinator import MealieDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -21,31 +18,11 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .api import MealieApi, MealieError -from .const import DOMAIN, LOGGER -from .const import PLATFORMS -from .const import STARTUP_MESSAGE +from .api import MealieApi +from .const import DOMAIN, PLATFORMS SCAN_INTERVAL = timedelta(seconds=30) -_LOGGER: logging.Logger = logging.getLogger(__package__) - - -def clean_obj(obj): - """Returns a copy of the object with any empty values removed.""" - if isinstance(obj, dict): - obj = { - k: v - for (k, v) in obj.items() - if v not in [None, [], {}] and "id" not in k.lower() - } - elif isinstance(obj, list): - for idx, i in enumerate(obj): - obj[idx] = clean_obj(i) - - return obj - - async def async_setup(hass: HomeAssistant, config: Config): """Set up this integration using YAML is not supported.""" return True @@ -55,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up this integration using UI.""" if hass.data.get(DOMAIN) is None: hass.data.setdefault(DOMAIN, {}) - _LOGGER.info(STARTUP_MESSAGE) username = entry.data.get(CONF_USERNAME) password = entry.data.get(CONF_PASSWORD) @@ -64,19 +40,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): session = async_get_clientsession(hass) client = MealieApi(username, password, host, session) - async def async_update_mealie() -> MealieData: - try: - return await client.async_get_updated_mealie_data() - except MealieError as err: - raise UpdateFailed("Mealie API communication error") from err - - coordinator: DataUpdateCoordinator[MealieData] = DataUpdateCoordinator( - hass, - LOGGER, - name=f"{DOMAIN}", - update_interval=SCAN_INTERVAL, - update_method=async_update_mealie, - ) + coordinator = MealieDataUpdateCoordinator(hass, client=client) await coordinator.async_config_entry_first_refresh() @@ -86,19 +50,21 @@ async def async_update_mealie() -> MealieData: hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) + return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Handle removal of an entry.""" +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) - # Unload platforms. - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - # Clean up. +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle removal of an entry.""" hass.data[DOMAIN].pop(entry.entry_id) - - return unload_ok + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: diff --git a/custom_components/mealie/config_flow.py b/custom_components/mealie/config_flow.py index 42f567b..81be6e3 100644 --- a/custom_components/mealie/config_flow.py +++ b/custom_components/mealie/config_flow.py @@ -1,71 +1,117 @@ """Adds config flow for Mealie.""" from __future__ import annotations +from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession - +import homeassistant.helpers.config_validation as cv from .api import MealieApi -from .const import DOMAIN +from .const import CONF_ENTRIES, CONST_ENTRIES, DOMAIN, NAME class MealieFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Config flow for mealie.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL - def __init__(self): - """Initialize.""" - self._errors = {} + _connection_data: dict[str, Any] = {} + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> OptionsFlowHandler: + """Get the options flow for this handler.""" + + return OptionsFlowHandler(config_entry) - async def async_step_user(self, user_input=None): + async def async_step_user( + self: config_entries.ConfigFlow, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initialized by the user.""" - self._errors = {} - # Uncomment the next 2 lines if only a single instance of the integration is allowed: if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") + errors: dict[str, Any] = {} + if user_input is not None: - valid = await self._test_credentials( - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - user_input[CONF_HOST], - ) + valid = await self._test_credentials(user_input) if valid: - return self.async_create_entry( - title=user_input[CONF_USERNAME], data=user_input - ) - else: - self._errors["base"] = "auth" + self._connection_data = user_input + return await self.async_step_options() - return await self._show_config_form(user_input) + errors["base"] = "invalid_auth" - return await self._show_config_form(user_input) + data_schema = { + vol.Required(CONF_HOST): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } - async def _show_config_form(self, user_input): # pylint: disable=unused-argument - """Show the configuration form to edit location data.""" return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_HOST): str, - } + step_id="user", data_schema=vol.Schema(data_schema), errors=errors + ) + + async def async_step_options( + self: config_entries.ConfigFlow, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + + if user_input is not None: + return self.async_create_entry( + title=NAME, data=self._connection_data, options=user_input + ) + + data_schema = { + vol.Optional(CONF_ENTRIES, default=CONST_ENTRIES): cv.multi_select( + CONST_ENTRIES ), - errors=self._errors, + } + + return self.async_show_form( + step_id="options", data_schema=vol.Schema(data_schema) ) - async def _test_credentials(self, username, password, host): + async def _test_credentials(self, options: dict[str, str]): """Return true if credentials is valid.""" try: session = async_create_clientsession(self.hass) - client = MealieApi(username, password, host, session) + client = MealieApi( + options[CONF_USERNAME], + options[CONF_PASSWORD], + options[CONF_HOST], + session, + ) await client.async_get_api_app_about() return True except Exception: # pylint: disable=broad-except - pass - return False + return False + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self: config_entries.OptionsFlow, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options step.""" + + if user_input is not None: + return self.async_create_entry(title=NAME, data=user_input) + + data_schema = { + vol.Optional( + CONF_ENTRIES, + default=self.config_entry.options.get(CONF_ENTRIES, CONST_ENTRIES), + ): cv.multi_select(CONST_ENTRIES), + } + return self.async_show_form(step_id="init", data_schema=vol.Schema(data_schema)) diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index 4813a5b..d896ee2 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -2,6 +2,7 @@ # Base component constants from datetime import timedelta import logging +from typing import Final NAME = "Mealie" @@ -11,9 +12,22 @@ LOGGER = logging.getLogger(__package__) - UPDATE_INTERVAL = timedelta(seconds=30) +CONF_ENTRIES = "entries" + +CONST_ENTRY_BREAKFAST = "breakfast" +CONST_ENTRY_LUNCH = "lunch" +CONST_ENTRY_DINNER = "dinner" +CONST_ENTRY_SIDE = "side" + +CONST_ENTRIES: Final = [ + CONST_ENTRY_BREAKFAST, + CONST_ENTRY_LUNCH, + CONST_ENTRY_DINNER, + CONST_ENTRY_SIDE, +] + ISSUE_URL = "https://github.com/mealie-recipes/mealie-hacs/issues" SOURCE_REPO = "hay-kot/mealie" @@ -32,11 +46,13 @@ SWITCH = "switch" CAMERA = "camera" UPDATE = "update" -PLATFORMS = [CAMERA, UPDATE, SENSOR] +PLATFORMS = [SENSOR] # Defaults DEFAULT_NAME = DOMAIN +CONF_EXTENDED = "extended" + STARTUP_MESSAGE = f""" ------------------------------------------------------------------- diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py new file mode 100644 index 0000000..81fae16 --- /dev/null +++ b/custom_components/mealie/coordinator.py @@ -0,0 +1,41 @@ +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER, UPDATE_INTERVAL +from .api import MealieApi +from .models import About, MealPlan, MealieData + + +class MealieDataUpdateCoordinator(DataUpdateCoordinator[MealieData]): + """Class to manage fetching data from the API.""" + + def __init__( + self, + hass: HomeAssistant, + client: MealieApi, + ) -> None: + """Initialize.""" + super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) + self.api = client + self.platforms = [] + + async def _async_update_data(self) -> MealieData: + """Update data via library.""" + + try: + data = MealieData() + # data.about = About.parse_obj(await self.api.async_get_api_app_about()) + mealplans = await self.api.async_get_api_groups_mealplans_today() + + for mealplan in mealplans: + data.mealPlans.append(MealPlan.parse_obj(mealplan)) + + return data + except Exception as exception: + LOGGER.exception(exception) + raise UpdateFailed() from exception + + async def async_load_recipe_img(self, recipe_id: str) -> bytes: + """Load an image for a recipe.""" + + return await self.api.async_get_api_media_recipes_images(recipe_id) diff --git a/custom_components/mealie/strings.json b/custom_components/mealie/strings.json new file mode 100644 index 0000000..45e06c3 --- /dev/null +++ b/custom_components/mealie/strings.json @@ -0,0 +1,35 @@ +{ + "config": { + "step": { + "user": { + "title": "Mealie", + "description": "If you need help with the configuration have a look here: https://github.com/mealie-recipes/mealie-hacs", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "options": { + "data": { + "entries": "Select the entry types to track" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth]" + }, + "abort": { + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + }, + "options": { + "step": { + "init": { + "data": { + "entries": "Select the entry types to track" + } + } + } + } +} diff --git a/custom_components/mealie/translations/en.json b/custom_components/mealie/translations/en.json index ec8ab33..526ec39 100644 --- a/custom_components/mealie/translations/en.json +++ b/custom_components/mealie/translations/en.json @@ -5,17 +5,31 @@ "title": "Mealie", "description": "If you need help with the configuration have a look here: https://github.com/mealie-recipes/mealie-hacs", "data": { + "host": "Mealie Host (http://[ip]:[port])", "username": "Username/Email", - "password": "Password", - "host": "Mealie Host (http://[ip]:[port])" + "password": "Password" + } + }, + "options": { + "data": { + "entries": "Select the entry types to track" } } }, "error": { - "auth": "Username/Password is wrong." + "invalid_auth": "Invalid authentication" }, "abort": { - "single_instance_allowed": "Only a single instance is allowed." + "single_instance_allowed": "Already configured. Only a single configuration possible." + } + }, + "options": { + "step": { + "init": { + "data": { + "entries": "Select the entry types to track" + } + } } } -} \ No newline at end of file +} From 0522220fb269902c6f90ba9cbcd3c20d293fb891 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Mon, 25 Jul 2022 22:45:58 +0200 Subject: [PATCH 10/15] config flow for included extra items --- custom_components/mealie/config_flow.py | 19 +- custom_components/mealie/const.py | 27 +- custom_components/mealie/entity.py | 19 +- custom_components/mealie/models.py | 40 ++- custom_components/mealie/sensor.py | 332 +++++++++++++++--------- 5 files changed, 280 insertions(+), 157 deletions(-) diff --git a/custom_components/mealie/config_flow.py b/custom_components/mealie/config_flow.py index 81be6e3..323da4b 100644 --- a/custom_components/mealie/config_flow.py +++ b/custom_components/mealie/config_flow.py @@ -4,13 +4,19 @@ import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.const import CONF_INCLUDE, CONF_PASSWORD, CONF_USERNAME, CONF_HOST from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv from .api import MealieApi -from .const import CONF_ENTRIES, CONST_ENTRIES, DOMAIN, NAME +from .const import ( + CONF_ENTRIES, + CONST_ENTRIES, + CONST_INCLUDES, + DOMAIN, + NAME, +) class MealieFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -71,6 +77,10 @@ async def async_step_options( vol.Optional(CONF_ENTRIES, default=CONST_ENTRIES): cv.multi_select( CONST_ENTRIES ), + vol.Optional( + CONF_INCLUDE, + default=CONST_INCLUDES, + ): cv.multi_select(CONST_INCLUDES), } return self.async_show_form( @@ -113,5 +123,10 @@ async def async_step_init( CONF_ENTRIES, default=self.config_entry.options.get(CONF_ENTRIES, CONST_ENTRIES), ): cv.multi_select(CONST_ENTRIES), + vol.Optional( + CONF_INCLUDE, + default=self.config_entry.options.get(CONF_INCLUDE, CONST_INCLUDES), + ): cv.multi_select(CONST_INCLUDES), } + return self.async_show_form(step_id="init", data_schema=vol.Schema(data_schema)) diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index d896ee2..d78abd9 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -51,15 +51,20 @@ # Defaults DEFAULT_NAME = DOMAIN -CONF_EXTENDED = "extended" +CONST_INCLUDE_INSTRUCTIONS = "instructions" +CONST_INCLUDE_INGREDIENTS = "ingredients" +CONST_INCLUDE_TAGS = "tags" +CONST_INCLUDE_TOOLS = "tools" +CONST_INCLUDE_NUTRITION = "nutrition" +CONST_INCLUDE_COMMENTS = "comments" +CONST_INCLUDE_CATEGORIES = "categories" - -STARTUP_MESSAGE = f""" -------------------------------------------------------------------- -{NAME} -Version: {VERSION} -This is a custom integration! -If you have any issues with this you need to open an issue here: -{ISSUE_URL} -------------------------------------------------------------------- -""" +CONST_INCLUDES: Final = [ + CONST_INCLUDE_INSTRUCTIONS, + CONST_INCLUDE_INGREDIENTS, + CONST_INCLUDE_TAGS, + CONST_INCLUDE_TOOLS, + CONST_INCLUDE_NUTRITION, + CONST_INCLUDE_COMMENTS, + CONST_INCLUDE_CATEGORIES, +] diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index 6d90215..0c2b5b4 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -1,6 +1,7 @@ """MealieEntity class""" from __future__ import annotations from abc import abstractmethod +from .coordinator import MealieDataUpdateCoordinator from homeassistant.const import CONF_HOST, CONF_USERNAME @@ -9,17 +10,15 @@ from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, - DataUpdateCoordinator, ) -from .models import MealieData, Recipe -from .const import DOMAIN, ICONS, NAME +from .models import MealPlan, Recipe -class MealieEntity(CoordinatorEntity[DataUpdateCoordinator[MealieData]]): +class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): """mealie Entity class.""" - def __init__(self, coordinator: DataUpdateCoordinator[MealieData]) -> None: + def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: """Initialize the Mealie entity""" super().__init__(coordinator) @@ -54,6 +53,7 @@ def __init__(self, meal, coordinator): self.meal = meal self.recipe: Recipe | None = None + self.meal_plan: MealPlan | None = None self._attr_unique_id = self.meal self._attr_name = f"Meal plan {self.meal}" @@ -64,11 +64,16 @@ def __init__(self, meal, coordinator): @callback def _process_update(self) -> None: """Handle updated data from the coordinator.""" - self.recipe = next( + + self.meal_plan = next( ( - mealPlan.recipe + mealPlan for mealPlan in self.coordinator.data.mealPlans if mealPlan.entryType == self.meal ), None, ) + + self.recipe = ( + self.meal_plan.recipe if self.meal_plan.recipeId is not None else None + ) diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py index 2168f5e..99163cb 100644 --- a/custom_components/mealie/models.py +++ b/custom_components/mealie/models.py @@ -1,25 +1,41 @@ -from typing import Optional, List +from dataclasses import dataclass + from pydantic import BaseModel +from typing import Any, Optional from homeassistant.backports.enum import StrEnum """Mealie API objects""" -class EntryTypes(StrEnum): - """Enum to represent the entry types.""" - - BREAKFAST = "breakfast" - LUNCH = "lunch" - DINNER = "dinner" - SIDE = "side" - - class Recipe(BaseModel): """Recipe model.""" name: str slug: str + id: str + + recipeIngredient: list[Any] # TODO: we can type this + tools: list[Any] # TODO: we can type this + tags: list[Any] # TODO: we can type this + recipeCategory: list[Any] # TODO: We can type this + + recipeYield: Optional[str] + totalTime: Optional[str] + prepTime: Optional[str] + cookTime: Optional[str] + performTime: Optional[str] + rating: Optional[str] + description: Optional[str] + orgURL: Optional[str] + + # TODO: This isn't included in the API request + # recipeInstructions: Any + # nutrition: Any + # comments: Any + # assets: list[Any] # TODO: we can type this + # notes: list[Any] # TODO: we can type this + # extras: list[Any] # TODO: we can type this class MealPlan(BaseModel): @@ -37,7 +53,7 @@ class About(BaseModel): """About model.""" version: str - versionLatest: str + # versionLatest: str # TODO: add configuration URL here? @@ -48,4 +64,4 @@ def __init__(self): self.mealPlans = [] mealPlans: list[MealPlan] - about: About + about: Optional[About] diff --git a/custom_components/mealie/sensor.py b/custom_components/mealie/sensor.py index c1c051f..ab37020 100644 --- a/custom_components/mealie/sensor.py +++ b/custom_components/mealie/sensor.py @@ -1,146 +1,228 @@ """Sensor platform for Mealie.""" from __future__ import annotations +from typing import Any + +from homeassistant.config_entries import ConfigEntry + +from homeassistant.const import CONF_HOST, CONF_INCLUDE +from homeassistant.core import callback from homeassistant.components.sensor import SensorEntity -from . import clean_obj -from .const import DOMAIN -from .const import SENSOR +from .coordinator import MealieDataUpdateCoordinator +from .const import ( + CONF_ENTRIES, + CONST_INCLUDE_CATEGORIES, + CONST_INCLUDE_INGREDIENTS, + CONST_INCLUDE_TAGS, + CONST_INCLUDE_TOOLS, + DOMAIN, +) from .entity import MealPlanEntity -ICONS = { - "breakfast": "mdi:egg-fried", - "lunch": "mdi:bread-slice", - "dinner": "mdi:pot-steam", - "side": "mdi:bowl-mix-outline", -} - -async def async_setup_entry(hass, entry, async_add_devices): +async def async_setup_entry(hass, entry: ConfigEntry, async_add_devices): """Setup sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: MealieDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_devices( [ MealPlanSensor(meal, coordinator, entry) - for meal in ["breakfast", "lunch", "dinner", "side"] + for meal in entry.options[CONF_ENTRIES] ] ) class MealPlanSensor(MealPlanEntity, SensorEntity): - """mealie Sensor class.""" + """Mealie Sensor class.""" + + def __init__(self, meal, coordinator, entry: ConfigEntry): + + self._attr_extra_state_attributes = {} - def __init__(self, meal, coordinator, config_entry): - super().__init__(meal, coordinator, config_entry) + self._options = entry.options + + super().__init__(meal, coordinator) SensorEntity.__init__(self) - @staticmethod - def _format_instructions(instructions): - text = "" - for idx, i in enumerate(instructions): - if title := i.get('title'): - text += f"\n## {title}\n" - text += f"### Step {idx+1}\n\n{i.get('text')}\n" - return None if text == "" else text - - @staticmethod - def _format_ingredients(ingredients): - text = "" - for i in ingredients: - if title := i.get('title'): - text += f"\n## {title}\n" - if any(k in i for k in ['unit', 'food']): - text += f"- [ ]{' ' + str(i.get('quantity', '')) if i.get('quantity') else ''}" - for key in ['unit', 'food']: - text += f" {i.get(key, {}).get('name', '')}" if i.get(key) else "" - text += f"{', ' + i.get('note', '') if i.get('note', '') else ''}\n" - else: - text += f"- [ ] {i.get('note')}\n" - return None if text == "" else text - - @staticmethod - def _format_tags(tags): - text = ', '.join([t['name'] for t in tags]) - return None if text == "" else text - - @staticmethod - def _format_categories(categories): - text = ', '.join([c['name'] for c in categories]) - return None if text == "" else text - - @staticmethod - def _format_nutrition(nutrition): - text = "" - if nutrition: - text = "| Type | Amount |\n|:-----|-------:|\n" - for n in {k.replace("Content", ""): v for k, v in nutrition.items()}: - text += f"| {n.title()} | {nutrition[n]} |\n" - return None if text == "" else text - - @staticmethod - def _format_tools(tools): - text = "" - for t in tools: - text += f"- [ ] {t.get('name')}\n" - return None if text == "" else text - - @staticmethod - def _format_comments(comments): - text = "" - for c in sorted(comments, key=lambda x: x['createdAt']): - text += f"* {c.get('text')} by {c.get('user', {}).get('username', 'Anonymous')} @ {c.get('createdAt')}\n" - return None if text == "" else text - - @property - def unique_id(self): - """Return a unique ID to use for this entity.""" - return f"{self.config_entry.entry_id}_{self.endpoint}_{self.meal}_{SENSOR}" - - @property - def native_value(self): - return None if not self.recipes else self.recipes[self.idx]['name'] - - @property - def extra_state_attributes(self): - attrs = {} - if self.recipes: - recipe = self.recipes[self.idx] - attrs = { - "instructions": clean_obj(recipe.get("recipeInstructions")), - "instructions_md": self._format_instructions( - clean_obj(recipe.get("recipeInstructions", [])) - ), - "ingredients": clean_obj(recipe.get("recipeIngredient")), - "ingredients_md": self._format_ingredients( - clean_obj(recipe.get("recipeIngredient", [])) - ), - "tools": clean_obj(recipe.get("tools")), - "tools_md": self._format_tools(clean_obj(recipe.get("tools", {}))), - "nutrition": clean_obj(recipe.get("nutrition")), - "nutrition_md": self._format_nutrition( - clean_obj(recipe.get("nutrition", {})) - ), - "comments": clean_obj(recipe.get("comments")), - "comments_md": self._format_comments( - clean_obj(recipe.get("comments", [])) - ), - "tags": clean_obj(recipe.get("tags")), - "tags_md": self._format_tags(recipe.get("tags", [])), - "categories": clean_obj(recipe.get("recipeCategory")), - "categories_md": self._format_categories( - recipe.get("recipeCategory", []) - ), - "yield": recipe.get("recipeYield"), - "total_time": recipe.get("totalTime"), - "prep_time": recipe.get("prepTime"), - "cook_time": recipe.get("performTime"), - "rating": recipe.get("rating"), - "description": recipe.get("description"), - "name": recipe.get("name"), - "original_url": recipe.get("orgURL"), - "assets": clean_obj(recipe.get("assets", [])), - "notes": clean_obj(recipe.get("notes", [])), - "extras": clean_obj(recipe.get("extras", {})), - } + def get_attribute_from_recipe(self, prop) -> Any | None: + """Extract an attribute from the recipe.""" + return getattr(self.recipe, prop) if self.recipe is not None else None + + def get_attribute_list_from_recipe(self, prop) -> list: + """Extract an attribute from the recipe.""" + data = self.get_attribute_from_recipe(prop) + return data if data is not None else [] - return clean_obj(attrs) + @callback + def _process_update(self) -> None: + super()._process_update() + + self._attr_native_value = ( + self.recipe.name if self.recipe is not None else self.meal_plan.title + ) + + self._attr_extra_state_attributes.update( + { + "mealie_url": f"{self.coordinator.config_entry.data.get(CONF_HOST)}/recipe/{self.recipe.slug}" + if self.recipe is not None + else None + } + ) + + self._attr_extra_state_attributes.update( + [ + ("type", "note" if self.recipe is None else "recipe"), + ("description", self.get_attribute_from_recipe("description")), + ("yield", self.get_attribute_from_recipe("recipeYield")), + ("total_time", self.get_attribute_from_recipe("totalTime")), + ("prep_time", self.get_attribute_from_recipe("prepTime")), + ("cook_time", self.get_attribute_from_recipe("cookTime")), + ("perform_time", self.get_attribute_from_recipe("performTime")), + ("total_time", self.get_attribute_from_recipe("totalTime")), + ("rating", self.get_attribute_from_recipe("rating")), + ("original_url", self.get_attribute_from_recipe("orgURL")), + ] + ) + + include_options = self._options.get(CONF_INCLUDE, []) + + if CONST_INCLUDE_INGREDIENTS in include_options: + ingredients = _clean_obj( + self.get_attribute_list_from_recipe("recipeIngredient") + ) + self._attr_extra_state_attributes.update( + [ + ("ingredients", ingredients), + ("ingredients_md", _format_ingredients(ingredients)), + ] + ) + + if CONST_INCLUDE_TOOLS in include_options: + tools = _clean_obj(self.get_attribute_list_from_recipe("tools")) + self._attr_extra_state_attributes.update( + [ + ("tools", tools), + ("tools_md", _format_tools(tools)), + ] + ) + + if CONST_INCLUDE_TAGS in include_options: + tags = _clean_obj(self.get_attribute_list_from_recipe("tags")) + self._attr_extra_state_attributes.update( + [ + ("tags", tags), + ("tags_md", _format_tags(tags)), + ] + ) + + if CONST_INCLUDE_CATEGORIES in include_options: + categories = _clean_obj( + self.get_attribute_list_from_recipe("recipeCategory") + ) + self._attr_extra_state_attributes.update( + [ + ("categories", categories), + ("categories_md", _format_categories(categories)), + ] + ) + + # if self._options[CONF_INCLUDE_INSTRUCTIONS]: + # instructions = _clean_obj( + # self.get_attribute_list_from_recipe("recipeInstructions") + # ) + # self._attr_extra_state_attributes.update( + # [ + # ("instructions", instructions), + # ("instructions_md", _format_instructions(instructions)), + # ] + # ) + + # "nutrition": self.clean_obj(recipe.get("nutrition")), + # "nutrition_md": self._format_nutrition( + # self.clean_obj(recipe.get("nutrition", {})) + # ), + # "comments": self.clean_obj(recipe.get("comments")), + # "comments_md": self._format_comments( + # self.clean_obj(recipe.get("comments", [])) + # ), + # + # "assets": self.clean_obj(recipe.get("assets", [])), + # "notes": self.clean_obj(recipe.get("notes", [])), + # "extras": self.clean_obj(recipe.get("extras", {})), + # ] + # ) + + +def _format_instructions(instructions): + text = "" + for idx, i in enumerate(instructions): + if title := i.get("title"): + text += f"\n## {title}\n" + text += f"### Step {idx+1}\n\n{i.get('text')}\n" + return None if text == "" else text + + +def _format_ingredients(ingredients): + text = "" + for i in ingredients: + if title := i.get("title"): + text += f"\n## {title}\n" + if any(k in i for k in ["unit", "food"]): + text += ( + f"- [ ]{' ' + str(i.get('quantity', '')) if i.get('quantity') else ''}" + ) + for key in ["unit", "food"]: + text += f" {i.get(key, {}).get('name', '')}" if i.get(key) else "" + text += f"{', ' + i.get('note', '') if i.get('note', '') else ''}\n" + else: + text += f"- [ ] {i.get('note')}\n" + return None if text == "" else text + + +def _format_tags(tags): + text = ", ".join([t["name"] for t in tags]) + return None if text == "" else text + + +def _format_categories(categories): + text = ", ".join([c["name"] for c in categories]) + return None if text == "" else text + + +def _format_nutrition(nutrition): + text = "" + if nutrition: + text = "| Type | Amount |\n|:-----|-------:|\n" + for n in {k.replace("Content", ""): v for k, v in nutrition.items()}: + text += f"| {n.title()} | {nutrition[n]} |\n" + return None if text == "" else text + + +def _format_tools(tools): + text = "" + for t in tools: + text += f"- [ ] {t.get('name')}\n" + return None if text == "" else text + + +def _format_comments(comments): + text = "" + for c in sorted(comments, key=lambda x: x["createdAt"]): + text += f"* {c.get('text')} by {c.get('user', {}).get('username', 'Anonymous')} @ {c.get('createdAt')}\n" + return None if text == "" else text + + +def _clean_obj(obj): + """Returns a copy of the object with any empty values removed.""" + if isinstance(obj, dict): + obj = { + k: v + for (k, v) in obj.items() + if v not in [None, [], {}, ""] and "id" not in k.lower() + } + elif isinstance(obj, list): + for idx, i in enumerate(obj): + obj[idx] = _clean_obj(i) + + return obj From 9b0fdb73234777e5cbc44aff81bca6acdbba5ed0 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 26 Jul 2022 09:21:22 +0200 Subject: [PATCH 11/15] bring back update, some other fixes --- custom_components/mealie/api.py | 9 --------- custom_components/mealie/const.py | 2 +- custom_components/mealie/coordinator.py | 4 +++- custom_components/mealie/entity.py | 5 ++--- custom_components/mealie/models.py | 1 + custom_components/mealie/sensor.py | 24 +++++++++++++++++++++--- custom_components/mealie/update.py | 17 +++++++++-------- 7 files changed, 37 insertions(+), 25 deletions(-) diff --git a/custom_components/mealie/api.py b/custom_components/mealie/api.py index 0a6b52a..f4c7a79 100644 --- a/custom_components/mealie/api.py +++ b/custom_components/mealie/api.py @@ -83,15 +83,6 @@ async def request( text = await response.text() return {"message": text} - async def async_get_updated_mealie_data(self) -> MealieData: - """Update data from Mealie.""" - data = MealieData() - - data.about = await self.async_get_api_app_about() - data.mealPlans = await self.async_get_api_groups_mealplans_today() - - return data - async def async_get_api_app_about(self) -> About: """Get data from the API.""" response = await self.request("admin/about") diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index d78abd9..63c7565 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -46,7 +46,7 @@ SWITCH = "switch" CAMERA = "camera" UPDATE = "update" -PLATFORMS = [SENSOR] +PLATFORMS = [SENSOR, UPDATE] # Defaults DEFAULT_NAME = DOMAIN diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py index 81fae16..91e962b 100644 --- a/custom_components/mealie/coordinator.py +++ b/custom_components/mealie/coordinator.py @@ -24,7 +24,9 @@ async def _async_update_data(self) -> MealieData: try: data = MealieData() - # data.about = About.parse_obj(await self.api.async_get_api_app_about()) + + data.about = About.parse_obj(await self.api.async_get_api_app_about()) + mealplans = await self.api.async_get_api_groups_mealplans_today() for mealplan in mealplans: diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index 0c2b5b4..fabd565 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -13,6 +13,7 @@ ) from .models import MealPlan, Recipe +from .const import DOMAIN, ICONS, NAME class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): @@ -74,6 +75,4 @@ def _process_update(self) -> None: None, ) - self.recipe = ( - self.meal_plan.recipe if self.meal_plan.recipeId is not None else None - ) + self.recipe = self.meal_plan.recipe if self.meal_plan is not None else None diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py index 99163cb..6223c93 100644 --- a/custom_components/mealie/models.py +++ b/custom_components/mealie/models.py @@ -62,6 +62,7 @@ class MealieData: def __init__(self): self.mealPlans = [] + self.about = None mealPlans: list[MealPlan] about: Optional[About] diff --git a/custom_components/mealie/sensor.py b/custom_components/mealie/sensor.py index ab37020..337ded1 100644 --- a/custom_components/mealie/sensor.py +++ b/custom_components/mealie/sensor.py @@ -59,7 +59,11 @@ def _process_update(self) -> None: super()._process_update() self._attr_native_value = ( - self.recipe.name if self.recipe is not None else self.meal_plan.title + self.recipe.name + if self.recipe is not None + else self.meal_plan.title + if self.meal_plan is not None + else None ) self._attr_extra_state_attributes.update( @@ -72,8 +76,22 @@ def _process_update(self) -> None: self._attr_extra_state_attributes.update( [ - ("type", "note" if self.recipe is None else "recipe"), - ("description", self.get_attribute_from_recipe("description")), + ( + "type", + "recipe" + if self.recipe is not None + else "note" + if self.meal_plan is not None + else None, + ), + ( + "description", + self.get_attribute_from_recipe("description") + if self.recipe is not None + else self.meal_plan.text + if self.meal_plan is not None + else None, + ), ("yield", self.get_attribute_from_recipe("recipeYield")), ("total_time", self.get_attribute_from_recipe("totalTime")), ("prep_time", self.get_attribute_from_recipe("prepTime")), diff --git a/custom_components/mealie/update.py b/custom_components/mealie/update.py index c884bf3..44e84a8 100644 --- a/custom_components/mealie/update.py +++ b/custom_components/mealie/update.py @@ -1,10 +1,10 @@ """Sensor platform for Mealie.""" from __future__ import annotations -from .models import MealieData + +from .coordinator import MealieDataUpdateCoordinator from homeassistant.components.update import UpdateEntity, UpdateEntityFeature from homeassistant.core import callback -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN from .const import UPDATE @@ -13,14 +13,14 @@ async def async_setup_entry(hass, entry, async_add_devices): """Setup sensor platform.""" - coordinator: DataUpdateCoordinator[MealieData] = hass.data[DOMAIN][entry.entry_id] + coordinator: MealieDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_devices([MealieUpdate(coordinator)]) class MealieUpdate(MealieEntity, UpdateEntity): """mealie Update class.""" - def __init__(self, coordinator: DataUpdateCoordinator[MealieData]): + def __init__(self, coordinator: MealieDataUpdateCoordinator) -> None: super().__init__(coordinator) UpdateEntity.__init__(self) @@ -28,8 +28,8 @@ def __init__(self, coordinator: DataUpdateCoordinator[MealieData]): self._release_url = None self._release_notes = None - self._attr_unique_id = f"{DOMAIN}_{UPDATE}" - self._attr_supported_features = UpdateEntityFeature.RELEASE_NOTES + self._attr_unique_id = UPDATE + # self._attr_supported_features = UpdateEntityFeature. self._process_update() @@ -37,5 +37,6 @@ def __init__(self, coordinator: DataUpdateCoordinator[MealieData]): def _process_update(self) -> None: """Handle updated data from the coordinator.""" - self._attr_installed_version = self.coordinator.data.about.version - self._attr_latest_version = self.coordinator.data.about.versionLatest + if self.coordinator.data.about is not None: + self._attr_installed_version = self.coordinator.data.about.version + # self._attr_latest_version = self.coordinator.data.about.versionLatest From 1ee90cd1505a137a8e70688de8ab5c8d4184fc07 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 26 Jul 2022 15:04:09 +0200 Subject: [PATCH 12/15] Add additional data from API if needed --- custom_components/mealie/__init__.py | 3 +- custom_components/mealie/api.py | 33 +++++++---- custom_components/mealie/const.py | 6 ++ custom_components/mealie/coordinator.py | 37 ++++++++++-- custom_components/mealie/models.py | 20 +++---- custom_components/mealie/sensor.py | 79 ++++++++++++++++--------- 6 files changed, 120 insertions(+), 58 deletions(-) diff --git a/custom_components/mealie/__init__.py b/custom_components/mealie/__init__.py index f536fae..b3c143d 100644 --- a/custom_components/mealie/__init__.py +++ b/custom_components/mealie/__init__.py @@ -23,6 +23,7 @@ SCAN_INTERVAL = timedelta(seconds=30) + async def async_setup(hass: HomeAssistant, config: Config): """Set up this integration using YAML is not supported.""" return True @@ -40,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): session = async_get_clientsession(hass) client = MealieApi(username, password, host, session) - coordinator = MealieDataUpdateCoordinator(hass, client=client) + coordinator = MealieDataUpdateCoordinator(hass, client=client, entry=entry) await coordinator.async_config_entry_first_refresh() diff --git a/custom_components/mealie/api.py b/custom_components/mealie/api.py index f4c7a79..ddfd628 100644 --- a/custom_components/mealie/api.py +++ b/custom_components/mealie/api.py @@ -9,12 +9,11 @@ import aiohttp import async_timeout -from config.custom_components.mealie.models import About, MealPlan, MealieData - -TIMEOUT = 10 +from .const import LOGGER +from .models import About, MealPlan, MealieData, Recipe -_LOGGER: logging.Logger = logging.getLogger(__package__) +TIMEOUT = 10 class MealieError(Exception): @@ -80,12 +79,15 @@ async def request( if "application/json" in content_type: return await response.json() + if "image/webp" in content_type: + return await response.read() + text = await response.text() return {"message": text} async def async_get_api_app_about(self) -> About: """Get data from the API.""" - response = await self.request("admin/about") + response = await self.request("app/about") return About.parse_obj(response) async def async_get_api_groups_mealplans_today(self) -> list[MealPlan]: @@ -93,13 +95,20 @@ async def async_get_api_groups_mealplans_today(self) -> list[MealPlan]: response = await self.request("groups/mealplans/today") return [MealPlan.parse_obj(mealplan) for mealplan in response] - # async def async_get_api_media_recipes_images(self, recipe_id) -> bytes: - # """Get the image for a recipe from the API.""" - # filename = "min-original.webp" - # url = f"media/recipes/{recipe_id}/images/{filename}" - # return await self.request( - # url, headers={"Content-type": "image/webp"}, as_bytes=True - # ) + async def async_get_api_recipe(self, recipe_slug) -> Recipe: + """Get recipe details from the API.""" + response = await self.request(f"recipes/{recipe_slug}") + return Recipe.parse_obj(response) + + async def async_get_api_media_recipes_images(self, recipe_id) -> bytes | None: + """Get the image for a recipe from the API.""" + filename = "min-original.webp" + response = await self.request( + f"media/recipes/{recipe_id}/images/{filename}", + headers={"Content-type": "image/webp"}, + ) + # LOGGER.warn(response) + return response async def async_get_api_auth_token(self) -> str: """Gets an access token from the API.""" diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index 63c7565..d1c2bee 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -58,6 +58,9 @@ CONST_INCLUDE_NUTRITION = "nutrition" CONST_INCLUDE_COMMENTS = "comments" CONST_INCLUDE_CATEGORIES = "categories" +CONST_INCLUDE_ASSETS = "assets" +CONST_INCLUDE_NOTES = "notes" +CONST_INCLUDE_EXTRAS = "extras" CONST_INCLUDES: Final = [ CONST_INCLUDE_INSTRUCTIONS, @@ -67,4 +70,7 @@ CONST_INCLUDE_NUTRITION, CONST_INCLUDE_COMMENTS, CONST_INCLUDE_CATEGORIES, + CONST_INCLUDE_ASSETS, + CONST_INCLUDE_NOTES, + CONST_INCLUDE_EXTRAS, ] diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py index 91e962b..871d260 100644 --- a/custom_components/mealie/coordinator.py +++ b/custom_components/mealie/coordinator.py @@ -1,7 +1,16 @@ +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_INCLUDE from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, LOGGER, UPDATE_INTERVAL +from .const import ( + CONST_INCLUDE_COMMENTS, + CONST_INCLUDE_INSTRUCTIONS, + CONST_INCLUDE_NUTRITION, + DOMAIN, + LOGGER, + UPDATE_INTERVAL, +) from .api import MealieApi from .models import About, MealPlan, MealieData @@ -10,15 +19,25 @@ class MealieDataUpdateCoordinator(DataUpdateCoordinator[MealieData]): """Class to manage fetching data from the API.""" def __init__( - self, - hass: HomeAssistant, - client: MealieApi, + self, hass: HomeAssistant, client: MealieApi, entry: ConfigEntry ) -> None: """Initialize.""" super().__init__(hass, LOGGER, name=DOMAIN, update_interval=UPDATE_INTERVAL) self.api = client self.platforms = [] + extra_request_options = [ + CONST_INCLUDE_INSTRUCTIONS, + CONST_INCLUDE_NUTRITION, + CONST_INCLUDE_COMMENTS, + ] + + entry_options = entry.options.get(CONF_INCLUDE, []) + + self.needs_additional_requests = any( + item in entry_options for item in extra_request_options + ) + async def _async_update_data(self) -> MealieData: """Update data via library.""" @@ -30,7 +49,15 @@ async def _async_update_data(self) -> MealieData: mealplans = await self.api.async_get_api_groups_mealplans_today() for mealplan in mealplans: - data.mealPlans.append(MealPlan.parse_obj(mealplan)) + meal_plan = MealPlan.parse_obj(mealplan) + + # Request the dedicated recipe endpoint if we need more info + if self.needs_additional_requests and meal_plan.recipe is not None: + meal_plan.recipe = await self.api.async_get_api_recipe( + meal_plan.recipe.slug + ) + + data.mealPlans.append(meal_plan) return data except Exception as exception: diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py index 6223c93..d421f82 100644 --- a/custom_components/mealie/models.py +++ b/custom_components/mealie/models.py @@ -30,12 +30,12 @@ class Recipe(BaseModel): orgURL: Optional[str] # TODO: This isn't included in the API request - # recipeInstructions: Any - # nutrition: Any - # comments: Any - # assets: list[Any] # TODO: we can type this - # notes: list[Any] # TODO: we can type this - # extras: list[Any] # TODO: we can type this + recipeInstructions: Optional[Any] + nutrition: Optional[Any] = None + comments: Optional[list[Any]] + assets: Optional[list[Any]] # TODO: we can type this + notes: Optional[list[Any]] # TODO: we can type this + extras: Optional[dict[str, Any]] # TODO: we can type this class MealPlan(BaseModel): @@ -60,9 +60,5 @@ class About(BaseModel): class MealieData: """Mealie API data.""" - def __init__(self): - self.mealPlans = [] - self.about = None - - mealPlans: list[MealPlan] - about: Optional[About] + mealPlans: list[MealPlan] = [] + about: Optional[About] = None diff --git a/custom_components/mealie/sensor.py b/custom_components/mealie/sensor.py index 337ded1..2528855 100644 --- a/custom_components/mealie/sensor.py +++ b/custom_components/mealie/sensor.py @@ -12,11 +12,18 @@ from .coordinator import MealieDataUpdateCoordinator from .const import ( CONF_ENTRIES, + CONST_INCLUDE_ASSETS, CONST_INCLUDE_CATEGORIES, + CONST_INCLUDE_COMMENTS, + CONST_INCLUDE_EXTRAS, CONST_INCLUDE_INGREDIENTS, + CONST_INCLUDE_INSTRUCTIONS, + CONST_INCLUDE_NOTES, + CONST_INCLUDE_NUTRITION, CONST_INCLUDE_TAGS, CONST_INCLUDE_TOOLS, DOMAIN, + LOGGER, ) from .entity import MealPlanEntity @@ -50,7 +57,7 @@ def get_attribute_from_recipe(self, prop) -> Any | None: return getattr(self.recipe, prop) if self.recipe is not None else None def get_attribute_list_from_recipe(self, prop) -> list: - """Extract an attribute from the recipe.""" + """Extract an attribute from the recipe as a list.""" data = self.get_attribute_from_recipe(prop) return data if data is not None else [] @@ -145,31 +152,47 @@ def _process_update(self) -> None: ] ) - # if self._options[CONF_INCLUDE_INSTRUCTIONS]: - # instructions = _clean_obj( - # self.get_attribute_list_from_recipe("recipeInstructions") - # ) - # self._attr_extra_state_attributes.update( - # [ - # ("instructions", instructions), - # ("instructions_md", _format_instructions(instructions)), - # ] - # ) - - # "nutrition": self.clean_obj(recipe.get("nutrition")), - # "nutrition_md": self._format_nutrition( - # self.clean_obj(recipe.get("nutrition", {})) - # ), - # "comments": self.clean_obj(recipe.get("comments")), - # "comments_md": self._format_comments( - # self.clean_obj(recipe.get("comments", [])) - # ), - # - # "assets": self.clean_obj(recipe.get("assets", [])), - # "notes": self.clean_obj(recipe.get("notes", [])), - # "extras": self.clean_obj(recipe.get("extras", {})), - # ] - # ) + if CONST_INCLUDE_INSTRUCTIONS in include_options: + instructions = _clean_obj( + self.get_attribute_list_from_recipe("recipeInstructions") + ) + self._attr_extra_state_attributes.update( + [ + ("instructions", instructions), + ("instructions_md", _format_instructions(instructions)), + ] + ) + + if CONST_INCLUDE_NUTRITION in include_options: + nutrition = _clean_obj(self.get_attribute_from_recipe("nutrition")) + self._attr_extra_state_attributes.update( + [ + ("nutrition", nutrition), + ("nutrition_md", _format_nutrition(nutrition)), + ] + ) + + if CONST_INCLUDE_COMMENTS in include_options: + comments = _clean_obj(self.get_attribute_list_from_recipe("comments")) + LOGGER.info(comments) + self._attr_extra_state_attributes.update( + [ + ("comments", comments), + ("comments_md", _format_comments(comments)), + ] + ) + + if CONST_INCLUDE_ASSETS in include_options: + assets = _clean_obj(self.get_attribute_list_from_recipe("assets")) + self._attr_extra_state_attributes.update([("assets", assets)]) + + if CONST_INCLUDE_NOTES in include_options: + notes = _clean_obj(self.get_attribute_list_from_recipe("notes")) + self._attr_extra_state_attributes.update([("notes", notes)]) + + if CONST_INCLUDE_EXTRAS in include_options: + extras = _clean_obj(self.get_attribute_from_recipe("extras")) + self._attr_extra_state_attributes.update([("extras", extras)]) def _format_instructions(instructions): @@ -212,8 +235,8 @@ def _format_nutrition(nutrition): text = "" if nutrition: text = "| Type | Amount |\n|:-----|-------:|\n" - for n in {k.replace("Content", ""): v for k, v in nutrition.items()}: - text += f"| {n.title()} | {nutrition[n]} |\n" + for n in {k.replace("Content", ""): v for k, v in nutrition.items()}: + text += f"| {n.title()} | {nutrition[n]} |\n" return None if text == "" else text From 7cb08eb45bc3371daf086c89f476c17fc0217f56 Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 26 Jul 2022 17:26:20 +0200 Subject: [PATCH 13/15] Add camera --- custom_components/mealie/api.py | 15 ++---- custom_components/mealie/camera.py | 63 ++++++++++++++++++------- custom_components/mealie/const.py | 2 +- custom_components/mealie/coordinator.py | 11 +++-- custom_components/mealie/entity.py | 2 +- custom_components/mealie/models.py | 5 ++ 6 files changed, 61 insertions(+), 37 deletions(-) diff --git a/custom_components/mealie/api.py b/custom_components/mealie/api.py index ddfd628..97ea0d6 100644 --- a/custom_components/mealie/api.py +++ b/custom_components/mealie/api.py @@ -2,7 +2,6 @@ import asyncio import json -import logging import socket from typing import Any @@ -11,7 +10,7 @@ from .const import LOGGER -from .models import About, MealPlan, MealieData, Recipe +from .models import About, MealPlan, Recipe TIMEOUT = 10 @@ -72,6 +71,8 @@ async def request( contents = await response.read() response.close() + LOGGER.error(contents) + if content_type == "application/json": raise MealieError(response.status, json.loads(contents.decode("utf8"))) raise MealieError(response.status, {"message": contents.decode("utf8")}) @@ -100,16 +101,6 @@ async def async_get_api_recipe(self, recipe_slug) -> Recipe: response = await self.request(f"recipes/{recipe_slug}") return Recipe.parse_obj(response) - async def async_get_api_media_recipes_images(self, recipe_id) -> bytes | None: - """Get the image for a recipe from the API.""" - filename = "min-original.webp" - response = await self.request( - f"media/recipes/{recipe_id}/images/{filename}", - headers={"Content-type": "image/webp"}, - ) - # LOGGER.warn(response) - return response - async def async_get_api_auth_token(self) -> str: """Gets an access token from the API.""" payload = { diff --git a/custom_components/mealie/camera.py b/custom_components/mealie/camera.py index 84dd393..2498654 100644 --- a/custom_components/mealie/camera.py +++ b/custom_components/mealie/camera.py @@ -1,44 +1,71 @@ """Sensor platform for Mealie.""" from __future__ import annotations +from homeassistant.const import CONF_HOST + +from homeassistant.helpers.httpx_client import get_async_client +from .coordinator import MealieDataUpdateCoordinator + from homeassistant.components.camera import Camera +from homeassistant.core import callback -from .const import CAMERA +from .const import CONF_ENTRIES from .const import DOMAIN from .entity import MealPlanEntity +GET_IMAGE_TIMEOUT = 10 + + async def async_setup_entry(hass, entry, async_add_devices): """Setup sensor platform.""" - coordinator = hass.data[DOMAIN][entry.entry_id] + coordinator: MealieDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] async_add_devices( - [ - MealPlanCamera(meal, coordinator, entry) - for meal in ["breakfast", "lunch", "dinner", "side"] - ] + [MealPlanCamera(meal, coordinator) for meal in entry.options[CONF_ENTRIES]] ) class MealPlanCamera(MealPlanEntity, Camera): """mealie Camera class.""" - def __init__(self, meal, coordinator, config_entry): - super().__init__(meal, coordinator, config_entry) + def __init__(self, meal, coordinator: MealieDataUpdateCoordinator): + self._old_recipe_id: str | None = None + + self._needs_refresh = False + self._recipe_img: bytes | None + self._media_url: str | None = None + + super().__init__(meal, coordinator) Camera.__init__(self) - @property - def unique_id(self): - """Return a unique ID to use for this entity.""" - return f"{self.config_entry.entry_id}_{self.endpoint}_{self.meal}_{CAMERA}" + @callback + def _process_update(self) -> None: + super()._process_update() + + if ( + self.recipe is not None + and self.recipe.image + and (self.recipe.id is not self._old_recipe_id) + ): + self._old_recipe_id = self.recipe.id + + self._media_url = f"{self.coordinator.config_entry.data.get(CONF_HOST)}/api/media/recipes/{self.recipe.id}/images/min-original.webp" + + self._needs_refresh = True async def async_camera_image( self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return bytes of camera image.""" - return ( - None - if not self.recipes - else await self.coordinator.api.async_get_api_media_recipes_images( - self.recipes[self.idx]['id'] + if self.recipe is None or self.recipe.image is None: + return None + + if self._needs_refresh and self._media_url is not None: + async_client = get_async_client(self.hass) + response = await async_client.get( + self._media_url, timeout=GET_IMAGE_TIMEOUT ) - ) + response.raise_for_status() + self._recipe_img = response.content + + return self._recipe_img diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index d1c2bee..d08560d 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -46,7 +46,7 @@ SWITCH = "switch" CAMERA = "camera" UPDATE = "update" -PLATFORMS = [SENSOR, UPDATE] +PLATFORMS = [SENSOR, UPDATE, CAMERA] # Defaults DEFAULT_NAME = DOMAIN diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py index 871d260..e73f9bc 100644 --- a/custom_components/mealie/coordinator.py +++ b/custom_components/mealie/coordinator.py @@ -4,8 +4,11 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + CONST_INCLUDE_ASSETS, CONST_INCLUDE_COMMENTS, + CONST_INCLUDE_EXTRAS, CONST_INCLUDE_INSTRUCTIONS, + CONST_INCLUDE_NOTES, CONST_INCLUDE_NUTRITION, DOMAIN, LOGGER, @@ -30,6 +33,9 @@ def __init__( CONST_INCLUDE_INSTRUCTIONS, CONST_INCLUDE_NUTRITION, CONST_INCLUDE_COMMENTS, + CONST_INCLUDE_EXTRAS, + CONST_INCLUDE_NOTES, + CONST_INCLUDE_ASSETS, ] entry_options = entry.options.get(CONF_INCLUDE, []) @@ -63,8 +69,3 @@ async def _async_update_data(self) -> MealieData: except Exception as exception: LOGGER.exception(exception) raise UpdateFailed() from exception - - async def async_load_recipe_img(self, recipe_id: str) -> bytes: - """Load an image for a recipe.""" - - return await self.api.async_get_api_media_recipes_images(recipe_id) diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index fabd565..c0d27f9 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -13,7 +13,7 @@ ) from .models import MealPlan, Recipe -from .const import DOMAIN, ICONS, NAME +from .const import DOMAIN, ICONS, LOGGER, NAME class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py index d421f82..2c381b0 100644 --- a/custom_components/mealie/models.py +++ b/custom_components/mealie/models.py @@ -15,6 +15,8 @@ class Recipe(BaseModel): slug: str id: str + image: Optional[str] + recipeIngredient: list[Any] # TODO: we can type this tools: list[Any] # TODO: we can type this tags: list[Any] # TODO: we can type this @@ -60,5 +62,8 @@ class About(BaseModel): class MealieData: """Mealie API data.""" + def __init__(self): + self.mealPlans = [] + mealPlans: list[MealPlan] = [] about: Optional[About] = None From 3f794bbe09b8807a9c572bd8f372dd63c3e15c8f Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 26 Jul 2022 20:45:16 +0200 Subject: [PATCH 14/15] Update en.json --- custom_components/mealie/translations/en.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/custom_components/mealie/translations/en.json b/custom_components/mealie/translations/en.json index 526ec39..7b038a2 100644 --- a/custom_components/mealie/translations/en.json +++ b/custom_components/mealie/translations/en.json @@ -12,7 +12,8 @@ }, "options": { "data": { - "entries": "Select the entry types to track" + "entries": "Select the entry types to track", + "include": "Select additional information to include" } } }, @@ -27,7 +28,8 @@ "step": { "init": { "data": { - "entries": "Select the entry types to track" + "entries": "Select the entry types to track", + "include": "Select additional information to include" } } } From d743a3be34c177bc75ff23733887c7177b67db5d Mon Sep 17 00:00:00 2001 From: Tom Matheussen Date: Tue, 26 Jul 2022 20:50:51 +0200 Subject: [PATCH 15/15] import cleanup --- custom_components/mealie/__init__.py | 11 +++-------- custom_components/mealie/api.py | 4 ---- custom_components/mealie/camera.py | 12 ++++-------- custom_components/mealie/config_flow.py | 13 +++++-------- custom_components/mealie/const.py | 1 - custom_components/mealie/coordinator.py | 4 ++-- custom_components/mealie/entity.py | 10 ++++------ custom_components/mealie/models.py | 4 ++-- custom_components/mealie/sensor.py | 8 +++----- custom_components/mealie/update.py | 6 ++---- 10 files changed, 25 insertions(+), 48 deletions(-) diff --git a/custom_components/mealie/__init__.py b/custom_components/mealie/__init__.py index b3c143d..b4c2675 100644 --- a/custom_components/mealie/__init__.py +++ b/custom_components/mealie/__init__.py @@ -5,21 +5,16 @@ https://github.com/mealie-recipes/mealie-hacs """ from datetime import timedelta -from .coordinator import MealieDataUpdateCoordinator from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_USERNAME, - CONF_PASSWORD, - CONF_HOST, -) -from homeassistant.core import Config -from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .api import MealieApi from .const import DOMAIN, PLATFORMS +from .coordinator import MealieDataUpdateCoordinator SCAN_INTERVAL = timedelta(seconds=30) diff --git a/custom_components/mealie/api.py b/custom_components/mealie/api.py index 97ea0d6..4dc8012 100644 --- a/custom_components/mealie/api.py +++ b/custom_components/mealie/api.py @@ -8,8 +8,6 @@ import aiohttp import async_timeout -from .const import LOGGER - from .models import About, MealPlan, Recipe TIMEOUT = 10 @@ -71,8 +69,6 @@ async def request( contents = await response.read() response.close() - LOGGER.error(contents) - if content_type == "application/json": raise MealieError(response.status, json.loads(contents.decode("utf8"))) raise MealieError(response.status, {"message": contents.decode("utf8")}) diff --git a/custom_components/mealie/camera.py b/custom_components/mealie/camera.py index 2498654..019b72b 100644 --- a/custom_components/mealie/camera.py +++ b/custom_components/mealie/camera.py @@ -1,19 +1,15 @@ """Sensor platform for Mealie.""" from __future__ import annotations -from homeassistant.const import CONF_HOST - -from homeassistant.helpers.httpx_client import get_async_client -from .coordinator import MealieDataUpdateCoordinator - from homeassistant.components.camera import Camera +from homeassistant.const import CONF_HOST from homeassistant.core import callback +from homeassistant.helpers.httpx_client import get_async_client -from .const import CONF_ENTRIES -from .const import DOMAIN +from .const import CONF_ENTRIES, DOMAIN +from .coordinator import MealieDataUpdateCoordinator from .entity import MealPlanEntity - GET_IMAGE_TIMEOUT = 10 diff --git a/custom_components/mealie/config_flow.py b/custom_components/mealie/config_flow.py index 323da4b..e0a0cc6 100644 --- a/custom_components/mealie/config_flow.py +++ b/custom_components/mealie/config_flow.py @@ -1,22 +1,19 @@ """Adds config flow for Mealie.""" from __future__ import annotations + from typing import Any import voluptuous as vol + from homeassistant import config_entries -from homeassistant.const import CONF_INCLUDE, CONF_PASSWORD, CONF_USERNAME, CONF_HOST +from homeassistant.const import CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv + from .api import MealieApi -from .const import ( - CONF_ENTRIES, - CONST_ENTRIES, - CONST_INCLUDES, - DOMAIN, - NAME, -) +from .const import CONF_ENTRIES, CONST_ENTRIES, CONST_INCLUDES, DOMAIN, NAME class MealieFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): diff --git a/custom_components/mealie/const.py b/custom_components/mealie/const.py index d08560d..e27dfe8 100644 --- a/custom_components/mealie/const.py +++ b/custom_components/mealie/const.py @@ -4,7 +4,6 @@ import logging from typing import Final - NAME = "Mealie" DOMAIN = "mealie" DOMAIN_DATA = f"{DOMAIN}_data" diff --git a/custom_components/mealie/coordinator.py b/custom_components/mealie/coordinator.py index e73f9bc..5764083 100644 --- a/custom_components/mealie/coordinator.py +++ b/custom_components/mealie/coordinator.py @@ -3,6 +3,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from .api import MealieApi from .const import ( CONST_INCLUDE_ASSETS, CONST_INCLUDE_COMMENTS, @@ -14,8 +15,7 @@ LOGGER, UPDATE_INTERVAL, ) -from .api import MealieApi -from .models import About, MealPlan, MealieData +from .models import About, MealieData, MealPlan class MealieDataUpdateCoordinator(DataUpdateCoordinator[MealieData]): diff --git a/custom_components/mealie/entity.py b/custom_components/mealie/entity.py index c0d27f9..fc56a18 100644 --- a/custom_components/mealie/entity.py +++ b/custom_components/mealie/entity.py @@ -1,19 +1,17 @@ """MealieEntity class""" from __future__ import annotations -from abc import abstractmethod -from .coordinator import MealieDataUpdateCoordinator +from abc import abstractmethod from homeassistant.const import CONF_HOST, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from .const import DOMAIN, ICONS, NAME +from .coordinator import MealieDataUpdateCoordinator from .models import MealPlan, Recipe -from .const import DOMAIN, ICONS, LOGGER, NAME class MealieEntity(CoordinatorEntity[MealieDataUpdateCoordinator]): diff --git a/custom_components/mealie/models.py b/custom_components/mealie/models.py index 2c381b0..197c787 100644 --- a/custom_components/mealie/models.py +++ b/custom_components/mealie/models.py @@ -1,9 +1,9 @@ from dataclasses import dataclass +from typing import Any, Optional from pydantic import BaseModel -from typing import Any, Optional -from homeassistant.backports.enum import StrEnum +from homeassistant.backports.enum import StrEnum """Mealie API objects""" diff --git a/custom_components/mealie/sensor.py b/custom_components/mealie/sensor.py index 2528855..9f23f18 100644 --- a/custom_components/mealie/sensor.py +++ b/custom_components/mealie/sensor.py @@ -1,15 +1,13 @@ """Sensor platform for Mealie.""" from __future__ import annotations + from typing import Any +from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry - from homeassistant.const import CONF_HOST, CONF_INCLUDE from homeassistant.core import callback -from homeassistant.components.sensor import SensorEntity - -from .coordinator import MealieDataUpdateCoordinator from .const import ( CONF_ENTRIES, CONST_INCLUDE_ASSETS, @@ -25,6 +23,7 @@ DOMAIN, LOGGER, ) +from .coordinator import MealieDataUpdateCoordinator from .entity import MealPlanEntity @@ -174,7 +173,6 @@ def _process_update(self) -> None: if CONST_INCLUDE_COMMENTS in include_options: comments = _clean_obj(self.get_attribute_list_from_recipe("comments")) - LOGGER.info(comments) self._attr_extra_state_attributes.update( [ ("comments", comments), diff --git a/custom_components/mealie/update.py b/custom_components/mealie/update.py index 44e84a8..1f1a052 100644 --- a/custom_components/mealie/update.py +++ b/custom_components/mealie/update.py @@ -1,13 +1,11 @@ """Sensor platform for Mealie.""" from __future__ import annotations -from .coordinator import MealieDataUpdateCoordinator - from homeassistant.components.update import UpdateEntity, UpdateEntityFeature from homeassistant.core import callback -from .const import DOMAIN -from .const import UPDATE +from .const import DOMAIN, UPDATE +from .coordinator import MealieDataUpdateCoordinator from .entity import MealieEntity