started stages logic

This commit is contained in:
John Lancaster
2025-11-21 07:54:49 -06:00
parent 19d1f9bdfe
commit 98d9ad0556
3 changed files with 124 additions and 3 deletions

View File

@@ -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

55
apps/motion.py Normal file
View File

@@ -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)

36
apps/stages.py Normal file
View File

@@ -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]