diff --git a/src/room_control/room_control.py b/src/room_control/room_control.py index df14104..ce27a39 100755 --- a/src/room_control/room_control.py +++ b/src/room_control/room_control.py @@ -2,12 +2,14 @@ import datetime import json import logging import logging.config +import traceback from copy import deepcopy -from typing import Dict, List +from functools import wraps +from typing import Any, Dict, List from appdaemon.entity import Entity from appdaemon.plugins.hass.hassapi import Hass -from appdaemon.plugins.mqtt.mqttapi import Mqtt +from astral.location import Location from . import console from .model import ControllerStateConfig, RoomControllerConfig @@ -15,7 +17,7 @@ from .model import ControllerStateConfig, RoomControllerConfig logger = logging.getLogger(__name__) -class RoomController(Hass, Mqtt): +class RoomController(Hass): """Class for linking room's lights with a motion sensor. - Separate the turning on and turning off functions. @@ -34,12 +36,30 @@ class RoomController(Hass, Mqtt): assert all(isinstance(s, ControllerStateConfig) for s in new), f'Invalid: {new}' self._room_config.states = new + @property + @wraps(Location.time_at_elevation) + def time_at_elevation(self): + return self.AD.sched.location.time_at_elevation + + @property + def state_entity(self) -> Entity: + return self.get_entity(f'{self.name}.state') + def initialize(self): + self.set_namespace('controller') + self.logger = console.load_rich_config(self.name) + self.set_log_level('DEBUG') + + self.register_service(f'{self.name}/activate', self.service_activate) + self.register_service(f'{self.name}/deactivate', self.service_deactivate) + self.app_entities = self.gather_app_entities() # self.log(f'entities: {self.app_entities}') + self.refresh_state_times() self.run_daily(callback=self.refresh_state_times, start='00:00:00') + self.log(f'Initialized [bold green]{type(self).__name__}[/]') def terminate(self): @@ -82,46 +102,60 @@ class RoomController(Hass, Mqtt): for state in self._room_config.states: if state.time is None and state.elevation is not None: - state.time = self.AD.sched.location.time_at_elevation( + transition_time = self.time_at_elevation( elevation=state.elevation, direction=state.direction ).time() elif isinstance(state.time, str): - state.time = self.parse_time(state.time) + transition_time = self.parse_time(state.time) assert isinstance(state.time, datetime.time), f'Invalid time: {state.time}' - self.states = sorted(self.states, key=lambda s: s.time, reverse=True) - - # schedule the transitions - for state in self.states[::-1]: - # t: datetime.time = state['time'] - t: datetime.time = state.time try: self.run_at( - callback=self.activate_any_on, - start=t.strftime('%H:%M:%S'), - cause='scheduled transition', + callback=lambda cb_args: self.set_controller_scene(cb_args['state']), + start=transition_time.strftime('%H:%M:%S'), + state=state, ) - except ValueError: - # happens when the callback time is in the past - pass except Exception as e: self.log(f'Failed with {type(e)}: {e}') - def current_state(self, now: datetime.time = None) -> ControllerStateConfig: + def set_controller_scene(self, state: ControllerStateConfig): + try: + self.state_entity.set_state(attributes=state.model_dump()) + except Exception: + self.logger.error(traceback.format_exc()) + else: + self.log(f'Set controller state of {self.name}: {state.model_dump()}', level='DEBUG') + + def current_state(self) -> ControllerStateConfig: if self.sleep_bool(): - self.log('sleep: active') + self.log('sleep: active', level='DEBUG') if state := self.args.get('sleep_state'): return ControllerStateConfig(**state) else: - return ControllerStateConfig(scene={}) + return ControllerStateConfig() else: - now = now or self.get_now().time().replace(microsecond=0) - self.log(f'Getting state for {now.strftime("%I:%M:%S %p")}', level='DEBUG') + attrs = self.state_entity.get_state('all')['attributes'] + return ControllerStateConfig.model_validate(attrs) - state = self._room_config.current_state(now) - self.log(f'Current state: {state.time}', level='DEBUG') - return state + def current_scene(self, transition: int = None) -> Dict[str, Any]: + state = self.current_state() + if isinstance(state.scene, str): + return state.scene + elif isinstance(state.scene, dict): + return state.to_apply_kwargs(transition) + + def service_activate(self, namespace: str, domain: str, service: str, kwargs: Dict[str, Any]): + scene = self.current_scene(transition=0) + + if isinstance(scene, str): + self.turn_on(scene) + elif isinstance(scene, dict): + self.call_service('scene/apply', namespace='default', **scene) + + def service_deactivate(self, namespace: str, domain: str, service: str, kwargs: Dict[str, Any]): + for e in self.app_entities: + self.turn_off(e) def app_entity_states(self) -> Dict[str, str]: states = {entity: self.get_state(entity) for entity in self.app_entities}