From 441c55936d895a31cd1d28697e0973fd21183090 Mon Sep 17 00:00:00 2001 From: Mikhail Epifanov Date: Tue, 26 Mar 2024 22:46:04 +0100 Subject: [PATCH 1/2] use normal async client for api calls --- cookbook/connectors/homeassistant.py | 62 ++++++++++++++++------------ requirements.txt | 2 +- 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index e653c3f3d..bdd52c8b1 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -1,26 +1,33 @@ import logging from logging import Logger +from typing import Dict +from urllib.parse import urljoin -from homeassistant_api import Client, HomeassistantAPIError, Domain +from aiohttp import ClientError, request from cookbook.connectors.connector import Connector from cookbook.models import ShoppingListEntry, ConnectorConfig, Space class HomeAssistant(Connector): - _domains_cache: dict[str, Domain] _config: ConnectorConfig _logger: Logger - _client: Client def __init__(self, config: ConnectorConfig): if not config.token or not config.url or not config.todo_entity: raise ValueError("config for HomeAssistantConnector in incomplete") - self._domains_cache = dict() self._config = config self._logger = logging.getLogger("connector.HomeAssistant") - self._client = Client(self._config.url, self._config.token, async_cache_session=False, use_async=True) + + async def send_api_call(self, method: str, path: str, data: Dict) -> str: + headers = { + "Authorization": f"Bearer {self._config.token}", + "Content-Type": "application/json" + } + async with request(method, urljoin(self._config.url, path), headers=headers, json=data) as response: + response.raise_for_status() + return await response.json() async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None: if not self._config.on_shopping_list_entry_created_enabled: @@ -28,15 +35,17 @@ class HomeAssistant(Connector): item, description = _format_shopping_list_entry(shopping_list_entry) - todo_domain = self._domains_cache.get('todo') - try: - if todo_domain is None: - todo_domain = await self._client.async_get_domain('todo') - self._domains_cache['todo'] = todo_domain + logging.debug(f"adding {item=} to {self._config.name}") - logging.debug(f"pushing {item} to {self._config.name}") - await todo_domain.add_item(entity_id=self._config.todo_entity, item=item) - except HomeassistantAPIError as err: + data = { + "entity_id": self._config.todo_entity, + "item": item, + "description": description, + } + + try: + await self.send_api_call("POST", "services/todo/add_item", data) + except ClientError as err: self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None: @@ -48,21 +57,22 @@ class HomeAssistant(Connector): if not self._config.on_shopping_list_entry_deleted_enabled: return - item, description = _format_shopping_list_entry(shopping_list_entry) + item, _ = _format_shopping_list_entry(shopping_list_entry) + + logging.debug(f"removing {item=} from {self._config.name}") + + data = { + "entity_id": self._config.todo_entity, + "item": item, + } - todo_domain = self._domains_cache.get('todo') try: - if todo_domain is None: - todo_domain = await self._client.async_get_domain('todo') - self._domains_cache['todo'] = todo_domain - - logging.debug(f"deleting {item} from {self._config.name}") - await todo_domain.remove_item(entity_id=self._config.todo_entity, item=item) - except HomeassistantAPIError as err: + await self.send_api_call("POST", "services/todo/remove_item", data) + except ClientError as err: self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") async def close(self) -> None: - await self._client.async_cache_session.close() + pass def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry): @@ -76,10 +86,10 @@ def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry): else: item += ")" - description = "Imported by TandoorRecipes" + description = "From TandoorRecipes" if shopping_list_entry.created_by.first_name and len(shopping_list_entry.created_by.first_name) > 0: - description += f", created by {shopping_list_entry.created_by.first_name}" + description += f", by {shopping_list_entry.created_by.first_name}" else: - description += f", created by {shopping_list_entry.created_by.username}" + description += f", by {shopping_list_entry.created_by.username}" return item, description diff --git a/requirements.txt b/requirements.txt index 1c4962076..3ef2dd2b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ django-auth-ldap==4.6.0 pyppeteer==2.0.0 validators==0.20.0 pytube==15.0.0 -homeassistant-api==4.1.1.post2 +aiohttp==3.9.3 # Development pytest==8.0.0 From 41a448578aceb4244b1ca80b1e4cabc205a93856 Mon Sep 17 00:00:00 2001 From: Mikhail Epifanov Date: Tue, 26 Mar 2024 23:21:23 +0100 Subject: [PATCH 2/2] extra check if we arent accidently doing a query --- cookbook/connectors/homeassistant.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index bdd52c8b1..f824a4262 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -1,6 +1,6 @@ import logging from logging import Logger -from typing import Dict +from typing import Dict, Tuple from urllib.parse import urljoin from aiohttp import ClientError, request @@ -17,10 +17,12 @@ class HomeAssistant(Connector): if not config.token or not config.url or not config.todo_entity: raise ValueError("config for HomeAssistantConnector in incomplete") + if config.url[-1] != "/": + config.url += "/" self._config = config self._logger = logging.getLogger("connector.HomeAssistant") - async def send_api_call(self, method: str, path: str, data: Dict) -> str: + async def homeassistant_api_call(self, method: str, path: str, data: Dict) -> str: headers = { "Authorization": f"Bearer {self._config.token}", "Content-Type": "application/json" @@ -44,7 +46,7 @@ class HomeAssistant(Connector): } try: - await self.send_api_call("POST", "services/todo/add_item", data) + await self.homeassistant_api_call("POST", "services/todo/add_item", data) except ClientError as err: self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") @@ -57,6 +59,11 @@ class HomeAssistant(Connector): if not self._config.on_shopping_list_entry_deleted_enabled: return + if not hasattr(shopping_list_entry._state.fields_cache, "food"): + # Sometimes the food foreign key is not loaded, and we cant load it from an async process + self._logger.debug("required property was not present in ShoppingListEntry") + return + item, _ = _format_shopping_list_entry(shopping_list_entry) logging.debug(f"removing {item=} from {self._config.name}") @@ -67,15 +74,16 @@ class HomeAssistant(Connector): } try: - await self.send_api_call("POST", "services/todo/remove_item", data) + await self.homeassistant_api_call("POST", "services/todo/remove_item", data) except ClientError as err: - self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") + # This error will always trigger if the item is not present/found + self._logger.debug(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") async def close(self) -> None: pass -def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry): +def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry) -> Tuple[str, str]: item = shopping_list_entry.food.name if shopping_list_entry.amount > 0: item += f" ({shopping_list_entry.amount:.2f}".rstrip('0').rstrip('.')