diff --git a/cookbook/connectors/homeassistant.py b/cookbook/connectors/homeassistant.py index e653c3f3de..f824a4262a 100644 --- a/cookbook/connectors/homeassistant.py +++ b/cookbook/connectors/homeassistant.py @@ -1,26 +1,35 @@ import logging from logging import Logger +from typing import Dict, Tuple +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() + if config.url[-1] != "/": + config.url += "/" 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 homeassistant_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 +37,17 @@ async def on_shopping_list_entry_created(self, space: Space, shopping_list_entry 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}") + + data = { + "entity_id": self._config.todo_entity, + "item": item, + "description": description, + } - 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: + try: + 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)=}") async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None: @@ -48,24 +59,31 @@ async def on_shopping_list_entry_deleted(self, space: Space, shopping_list_entry if not self._config.on_shopping_list_entry_deleted_enabled: return - item, description = _format_shopping_list_entry(shopping_list_entry) + 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 - 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 + item, _ = _format_shopping_list_entry(shopping_list_entry) - 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: - self._logger.warning(f"[HomeAssistant {self._config.name}] Received an exception from the api: {err=}, {type(err)=}") + logging.debug(f"removing {item=} from {self._config.name}") + + data = { + "entity_id": self._config.todo_entity, + "item": item, + } + + try: + await self.homeassistant_api_call("POST", "services/todo/remove_item", data) + except ClientError as 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: - await self._client.async_cache_session.close() + 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('.') @@ -76,10 +94,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 1c49620761..3ef2dd2b62 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