diff --git a/apps/apps.yaml b/apps/apps.yaml index 4c84034..1fb955d 100644 --- a/apps/apps.yaml +++ b/apps/apps.yaml @@ -1,3 +1,33 @@ -hello_world: - module: hello - class: HelloWorld +# hello_world: +# module: hello +# class: HelloWorld + + +bar_lights: + module: motion + class: StagedMotionLight + stages: + - start: '06:00 am' + scene: + light.bar: + state: on + color_temp_kelvin: 4500 + brightness: 25 + - start: '09:00 am' + scene: + light.bar: + state: on + color_temp_kelvin: 3500 + brightness: 100 + - start: '13:00' + scene: + light.bar: + state: on + color_temp_kelvin: 2500 + brightness: 150 + - start: 'sunset' + scene: + light.bar: + state: on + color_temp_kelvin: 2000 + brightness: 100 diff --git a/apps/motion.py b/apps/motion.py new file mode 100644 index 0000000..9b7b958 --- /dev/null +++ b/apps/motion.py @@ -0,0 +1,55 @@ +import json +from collections.abc import Generator +from datetime import datetime +from itertools import count, cycle, pairwise +from pathlib import Path + +from appdaemon.adapi import ADAPI +from pydantic import BaseModel, TypeAdapter + +from stages import Stage + +adapter = TypeAdapter(list[Stage]) + + +class StagedMotionLight(ADAPI): + def initialize(self): + self.set_log_level("DEBUG") + self._stages = adapter.validate_python(self.args["stages"]) + + self.log(f"Initialized Motion Sensor with {len(self._stages)} stages") + self.activate() + + def _stage_starts(self) -> Generator[datetime]: + for offset in count(start=-1): + for stage in self._stages: + dt = self.parse_datetime( + stage.start, + days_offset=offset, + aware=True, + today=True, + ) + dt = dt.replace(microsecond=0) + yield dt + + def start_pairs(self): + yield from pairwise(self._stage_starts()) + + def current_stage(self) -> Stage: + for stage, (t1, t2) in zip(cycle(self._stages), self.start_pairs()): + start, end = sorted([t1, t2]) + if self.now_is_between(start, end): + self.log(f"Current stage start time: {stage.start}") + return stage + else: + raise ValueError + + def current_scene(self): + return self.current_stage().model_dump(mode="json")["scene"] + + def activate(self, **kwargs): + return self.call_service("scene/apply", entities=self.current_scene()) + + # @property + # def stages(self) -> GeneratorExit: + # yield from pairwise(self._stage_starts) diff --git a/apps/stages.py b/apps/stages.py new file mode 100644 index 0000000..29c6fc4 --- /dev/null +++ b/apps/stages.py @@ -0,0 +1,36 @@ +import functools +from datetime import datetime, time +from pathlib import Path +from typing import Annotated, Any + +from appdaemon.adapi import ADAPI +from pydantic import ( + BaseModel, + BeforeValidator, + Field, + PrivateAttr, + TypeAdapter, + field_serializer, + field_validator, +) +from rich import print as rprint + + +class EntityState(BaseModel): + state: bool = True + color_temp_kelvin: int + brightness: int + + @field_serializer("state") + def convert_state(self, val: Any): + if val: + return "on" + else: + return "off" + + +class Stage(BaseModel): + # start: Annotated[time, BeforeValidator(lambda v: parser(v).time())] + start: str + _start: time = PrivateAttr() + scene: dict[str, EntityState]