mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2026-01-01 12:18:45 -05:00
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:
0
cookbook/connectors/__init__.py
Normal file
0
cookbook/connectors/__init__.py
Normal file
41
cookbook/connectors/connector.py
Normal file
41
cookbook/connectors/connector.py
Normal 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
|
||||
98
cookbook/connectors/connector_manager.py
Normal file
98
cookbook/connectors/connector_manager.py
Normal 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
|
||||
62
cookbook/connectors/homeassistant.py
Normal file
62
cookbook/connectors/homeassistant.py
Normal 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
|
||||
Reference in New Issue
Block a user