Files
room_control/model.py
2024-03-10 18:17:14 -05:00

78 lines
2.4 KiB
Python

from datetime import datetime, timedelta
from pathlib import Path
from typing import Annotated, Dict, List, Self
import yaml
from astral import SunDirection
from pydantic import BaseModel, BeforeValidator, ValidationError, conint, root_validator
def str_to_timedelta(input_str: str) -> timedelta:
try:
hours, minutes, seconds = map(int, input_str.split(':'))
return timedelta(hours=hours, minutes=minutes, seconds=seconds)
except Exception:
return timedelta()
def str_to_direction(input_str: str) -> SunDirection:
if input_str.lower() == 'setting':
return SunDirection.SETTING
elif input_str == 'rising':
return SunDirection.RISING
else:
raise ValidationError(f'Invalid sun direction: {input_str}')
OffDuration = Annotated[timedelta, BeforeValidator(str_to_timedelta)]
class State(BaseModel):
state: bool = True
brightness: conint(ge=1, le=255) = None
color_temp: conint(ge=200, le=650) = None
class ApplyKwargs(BaseModel):
"""Arguments to call with the 'scene/apply' service"""
entities: Dict[str, State]
transition: int = None
class ControllerStateConfig(BaseModel):
time: str | datetime = None
elevation: float = None
direction: Annotated[SunDirection, BeforeValidator(str_to_direction)] = None
off_duration: OffDuration = None
scene: Dict[str, State]
@root_validator(pre=True)
def check_args(cls, values):
time, elevation = values.get('time'), values.get('elevation')
if time is None and elevation is None:
raise ValueError('Either time or elevation must be set.')
elif time is not None and elevation is not None:
raise ValueError('Only one of time or elevation can be set.')
elif elevation is not None:
assert 'direction' in values
return values
def to_apply_kwargs(self, transition: int = 0):
return ApplyKwargs(entities=self.scene, transition=transition).model_dump(exclude_none=True)
class RoomConfig(BaseModel):
states: List[ControllerStateConfig]
off_duration: OffDuration = None
@classmethod
def from_yaml(cls: Self, yaml_path: Path):
yaml_path = Path(yaml_path)
with yaml_path.open('r') as f:
for appname, app_cfg in yaml.load(f, Loader=yaml.SafeLoader).items():
if app_cfg['class'] == 'RoomController':
break
print(app_cfg)
return cls(**app_cfg)