Add ConnectorManager component which allows for Connectors to listen to triggers and do actions on them. Also add HomeAssistantConfig which stores the configuration for the HomeAssistantConnector

This commit is contained in:
Mikhail Epifanov
2024-01-11 22:05:34 +01:00
parent d493ba72a1
commit e5f0c19cdc
18 changed files with 566 additions and 70 deletions

View File

View File

@@ -0,0 +1,41 @@
from abc import ABC, abstractmethod
from cookbook.models import ShoppingListEntry, Space
class Connector(ABC):
@abstractmethod
async def on_shopping_list_entry_created(self, space: Space, instance: ShoppingListEntry) -> None:
pass
@abstractmethod
async def on_shopping_list_entry_updated(self, space: Space, instance: ShoppingListEntry) -> None:
pass
@abstractmethod
async def on_shopping_list_entry_deleted(self, space: Space, instance: ShoppingListEntry) -> None:
pass
# @abstractmethod
# def on_recipe_created(self, instance: Recipe, **kwargs) -> None:
# pass
#
# @abstractmethod
# def on_recipe_updated(self, instance: Recipe, **kwargs) -> None:
# pass
#
# @abstractmethod
# def on_recipe_deleted(self, instance: Recipe, **kwargs) -> None:
# pass
#
# @abstractmethod
# def on_meal_plan_created(self, instance: MealPlan, **kwargs) -> None:
# pass
#
# @abstractmethod
# def on_meal_plan_updated(self, instance: MealPlan, **kwargs) -> None:
# pass
#
# @abstractmethod
# def on_meal_plan_deleted(self, instance: MealPlan, **kwargs) -> None:
# pass

View File

@@ -0,0 +1,98 @@
import asyncio
from enum import Enum
from types import UnionType
from typing import List, Any, Dict
from django_scopes import scope
from cookbook.connectors.connector import Connector
from cookbook.connectors.homeassistant import HomeAssistant
from cookbook.models import ShoppingListEntry, Recipe, MealPlan, Space
class ActionType(Enum):
CREATED = 1
UPDATED = 2
DELETED = 3
class ConnectorManager:
_connectors: Dict[str, List[Connector]]
_listening_to_classes: UnionType = ShoppingListEntry | Recipe | MealPlan | Connector
max_concurrent_tasks = 2
def __init__(self):
self._connectors = dict()
def __call__(self, instance: Any, **kwargs) -> None:
if not isinstance(instance, self._listening_to_classes):
return
# If a Connector was changed/updated, refresh connector from the database for said space
purge_connector_cache = isinstance(instance, Connector)
space: Space = instance.space
if space.name in self._connectors and not purge_connector_cache:
connectors: List[Connector] = self._connectors[space.name]
else:
with scope(space=space):
connectors: List[Connector] = [HomeAssistant(config) for config in space.homeassistantconfig_set.all()]
self._connectors[space.name] = connectors
if len(connectors) == 0 or purge_connector_cache:
return
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(self.run_connectors(connectors, space, instance, **kwargs))
loop.close()
@staticmethod
async def run_connectors(connectors: List[Connector], space: Space, instance: Any, **kwargs):
action_type: ActionType
if "created" in kwargs and kwargs["created"]:
action_type = ActionType.CREATED
elif "created" in kwargs and not kwargs["created"]:
action_type = ActionType.UPDATED
elif "origin" in kwargs:
action_type = ActionType.DELETED
else:
return
tasks: List[asyncio.Task] = list()
if isinstance(instance, ShoppingListEntry):
shopping_list_entry: ShoppingListEntry = instance
match action_type:
case ActionType.CREATED:
for connector in connectors:
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_created(space, shopping_list_entry)))
case ActionType.UPDATED:
for connector in connectors:
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_updated(space, shopping_list_entry)))
case ActionType.DELETED:
for connector in connectors:
tasks.append(asyncio.create_task(connector.on_shopping_list_entry_deleted(space, shopping_list_entry)))
try:
await asyncio.gather(*tasks, return_exceptions=False)
except BaseException as e:
print("received an exception from one of the tasks: ", e)
# if isinstance(instance, Recipe):
# if "created" in kwargs and kwargs["created"]:
# for connector in self._connectors:
# connector.on_recipe_created(instance, **kwargs)
# return
# for connector in self._connectors:
# connector.on_recipe_updated(instance, **kwargs)
# return
#
# if isinstance(instance, MealPlan):
# if "created" in kwargs and kwargs["created"]:
# for connector in self._connectors:
# connector.on_meal_plan_created(instance, **kwargs)
# return
# for connector in self._connectors:
# connector.on_meal_plan_updated(instance, **kwargs)
# return

View File

@@ -0,0 +1,62 @@
import logging
from homeassistant_api import Client, HomeassistantAPIError
from cookbook.connectors.connector import Connector
from cookbook.models import ShoppingListEntry, HomeAssistantConfig, Space
class HomeAssistant(Connector):
_config: HomeAssistantConfig
def __init__(self, config: HomeAssistantConfig):
self._config = config
self._logger = logging.getLogger("connector.HomeAssistant")
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:
return
item, description = _format_shopping_list_entry(shopping_list_entry)
async with Client(self._config.url, self._config.token, use_async=True) as client:
try:
todo_domain = await client.async_get_domain('todo')
await todo_domain.add_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)=}")
async def on_shopping_list_entry_updated(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
if not self._config.on_shopping_list_entry_updated_enabled:
return
pass
async def on_shopping_list_entry_deleted(self, space: Space, shopping_list_entry: ShoppingListEntry) -> None:
if not self._config.on_shopping_list_entry_deleted_enabled:
return
item, description = _format_shopping_list_entry(shopping_list_entry)
async with Client(self._config.url, self._config.token, use_async=True) as client:
try:
todo_domain = await client.async_get_domain('todo')
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)=}")
def _format_shopping_list_entry(shopping_list_entry: ShoppingListEntry):
item = shopping_list_entry.food.name
if shopping_list_entry.amount > 0:
if shopping_list_entry.unit and shopping_list_entry.unit.base_unit and len(shopping_list_entry.unit.base_unit) > 0:
item += f" ({shopping_list_entry.amount} {shopping_list_entry.unit.base_unit})"
elif shopping_list_entry.unit and shopping_list_entry.unit.name and len(shopping_list_entry.unit.name) > 0:
item += f" ({shopping_list_entry.amount} {shopping_list_entry.unit.name})"
else:
item += f" ({shopping_list_entry.amount})"
description = "Imported by 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}"
else:
description += f", created by {shopping_list_entry.created_by.username}"
return item, description