Compare commits
26 Commits
98d9ad0556
...
moes-pad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ae7c2b8e9 | ||
|
|
01c6faa362 | ||
|
|
56ee0c3d3e | ||
|
|
657d01a724 | ||
|
|
e21eca4f42 | ||
|
|
61d5d99dee | ||
|
|
29759692b2 | ||
|
|
fbd60ab6ac | ||
|
|
2f52febf38 | ||
|
|
d8476e9fdf | ||
|
|
43beb1e950 | ||
|
|
81b5d5afa5 | ||
|
|
b33016089f | ||
|
|
25892e1de8 | ||
|
|
05bad81571 | ||
|
|
697b09a7a1 | ||
|
|
d90f2b28fa | ||
|
|
c1313409db | ||
|
|
678cd75ae1 | ||
|
|
b3d0993ee4 | ||
|
|
8a889355aa | ||
|
|
76027f6fc5 | ||
|
|
d6941f8c7c | ||
|
|
a85a06b207 | ||
|
|
4e8f069123 | ||
|
|
852dbcfd8f |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +0,0 @@
|
|||||||
[submodule "apps/room_control"]
|
|
||||||
path = apps/room_control
|
|
||||||
url = https://gitea.john-stream.com/john/room_control
|
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
appdaemon:
|
appdaemon:
|
||||||
latitude: 0
|
uvloop: True
|
||||||
longitude: 0
|
import_method: expert
|
||||||
elevation: 30
|
latitude: 30.250968
|
||||||
|
longitude: -97.748193
|
||||||
|
elevation: 150
|
||||||
time_zone: US/Central
|
time_zone: US/Central
|
||||||
|
exclude_dirs:
|
||||||
|
- room_control
|
||||||
plugins:
|
plugins:
|
||||||
HASS:
|
HASS:
|
||||||
type: hass
|
type: hass
|
||||||
@@ -13,3 +17,9 @@ http:
|
|||||||
admin:
|
admin:
|
||||||
api:
|
api:
|
||||||
hadashboard:
|
hadashboard:
|
||||||
|
|
||||||
|
logs:
|
||||||
|
main_log:
|
||||||
|
date_format: '%Y-%m-%d %I:%M:%S %p'
|
||||||
|
error_log:
|
||||||
|
date_format: '%Y-%m-%d %I:%M:%S %p'
|
||||||
|
|||||||
@@ -1,33 +1,11 @@
|
|||||||
# hello_world:
|
hello-world:
|
||||||
# module: hello
|
module: hello
|
||||||
# class: HelloWorld
|
class: HelloWorld
|
||||||
|
|
||||||
|
gone:
|
||||||
bar_lights:
|
module: gone
|
||||||
module: motion
|
class: Gone
|
||||||
class: StagedMotionLight
|
entities:
|
||||||
stages:
|
- light.bar
|
||||||
- start: '06:00 am'
|
- light.h6076
|
||||||
scene:
|
- light.h6076_2
|
||||||
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
|
|
||||||
|
|||||||
7
apps/drivers/moes.yaml
Normal file
7
apps/drivers/moes.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
moes_driver:
|
||||||
|
module: moes_pad
|
||||||
|
class: MoesBridge
|
||||||
|
|
||||||
|
moes2:
|
||||||
|
module: moes_pad
|
||||||
|
class: MoesPad
|
||||||
73
apps/drivers/moes_pad.py
Normal file
73
apps/drivers/moes_pad.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from appdaemon.adapi import ADAPI
|
||||||
|
from appdaemon.adbase import ADBase
|
||||||
|
|
||||||
|
|
||||||
|
class MoesButtons(int, Enum):
|
||||||
|
TOP_LEFT = 1
|
||||||
|
TOP_RIGHT = 2
|
||||||
|
BOTTOM_LEFT = 3
|
||||||
|
BOTTOM_RIGHT = 4
|
||||||
|
|
||||||
|
|
||||||
|
class MoesActions(str, Enum):
|
||||||
|
SINGLE_PRESS = 'single'
|
||||||
|
DOUBLE_PRESS = 'double'
|
||||||
|
LONG_PRESS = 'hold'
|
||||||
|
|
||||||
|
|
||||||
|
class MoesBridge(ADBase):
|
||||||
|
"""Class for an app that listens for state changes generated by 4 button Moes Pads and makes the events more
|
||||||
|
sensible with enums."""
|
||||||
|
|
||||||
|
adapi: ADAPI
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.adapi = self.get_ad_api()
|
||||||
|
self.log = self.adapi.log
|
||||||
|
self.event_handle = self.adapi.listen_state(self.handle_state_change)
|
||||||
|
|
||||||
|
def handle_state_change(self, entity: str, attribute: str, old: Any, new: Any, **kwargs: Any) -> None:
|
||||||
|
if new == '' or not new[0].isdigit():
|
||||||
|
return
|
||||||
|
|
||||||
|
domain, ent = entity.split('.', 1)
|
||||||
|
if domain == 'sensor' and ent.startswith('moes'):
|
||||||
|
button, action = new.split('_')
|
||||||
|
button = MoesButtons(int(button))
|
||||||
|
button_str = button.name.lower().replace('_', ' ')
|
||||||
|
action = MoesActions(action)
|
||||||
|
self.log(f'Moes Pad action detected: {action.value} on button {button_str}')
|
||||||
|
self.adapi.fire_event(
|
||||||
|
'moes_pad',
|
||||||
|
device=entity,
|
||||||
|
button=button,
|
||||||
|
action=action,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MoesPad(ADBase):
|
||||||
|
def initialize(self):
|
||||||
|
self.adapi = self.get_ad_api()
|
||||||
|
self.log = self.adapi.log
|
||||||
|
self.event_handle = self.adapi.listen_event(self.handle_moes_event, 'moes_pad')
|
||||||
|
|
||||||
|
def handle_moes_event(self, event_name: str, data: dict[str, Any], **kwargs: Any) -> None:
|
||||||
|
button = MoesButtons(data.get('button'))
|
||||||
|
action = MoesActions(data.get('action'))
|
||||||
|
self.log(f'Received Moes Pad event: {action} on button {button}')
|
||||||
|
|
||||||
|
match action:
|
||||||
|
case MoesActions.SINGLE_PRESS:
|
||||||
|
self.handle_single_press(button)
|
||||||
|
|
||||||
|
def handle_single_press(self, button: MoesButtons) -> None:
|
||||||
|
match button:
|
||||||
|
case MoesButtons.TOP_LEFT:
|
||||||
|
self.adapi.call_service('light/toggle', entity_id='light.bedroom_sydney')
|
||||||
|
case MoesButtons.TOP_RIGHT:
|
||||||
|
self.adapi.call_service('light/toggle', entity_id='light.bedroom_john')
|
||||||
|
case MoesButtons.BOTTOM_LEFT:
|
||||||
|
self.adapi.call_service('light/toggle', entity_id='light.h6076_2')
|
||||||
27
apps/gone.py
Normal file
27
apps/gone.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from appdaemon.plugins.hass import Hass
|
||||||
|
|
||||||
|
|
||||||
|
class Gone(Hass):
|
||||||
|
def initialize(self):
|
||||||
|
people = self.get_state('person')
|
||||||
|
# self.log(json.dumps(people, indent=2))
|
||||||
|
|
||||||
|
self.log(list(people.keys()))
|
||||||
|
for person in self.get_state('person'):
|
||||||
|
self.listen_state(self.handle_state, entity_id=person, new='not_home')
|
||||||
|
|
||||||
|
self.log(f'No one home: {self.no_one_home()}')
|
||||||
|
|
||||||
|
def no_one_home(self) -> bool:
|
||||||
|
return all(state.get('state') != 'home' for state in self.get_state('person', copy=False).values())
|
||||||
|
|
||||||
|
def handle_state(self, entity: str, attribute: str, old: Any, new: Any, **kwargs: Any) -> None:
|
||||||
|
if self.no_one_home():
|
||||||
|
for ent in self.args['entities']:
|
||||||
|
try:
|
||||||
|
self.turn_off(ent)
|
||||||
|
except Exception:
|
||||||
|
self.log(f'Failed to turn off {ent}', level='ERROR')
|
||||||
|
continue
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from appdaemon.adapi import ADAPI
|
from appdaemon.plugins.hass import Hass
|
||||||
|
|
||||||
|
|
||||||
class HelloWorld(ADAPI):
|
class HelloWorld(Hass):
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
self.log("Hello from AppDaemon")
|
self.log('Hello from AppDaemon')
|
||||||
self.log("You are now ready to run Apps!")
|
self.log('You are now ready to run Apps!')
|
||||||
|
|
||||||
|
# self.get_history('ball1')
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
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)
|
|
||||||
Submodule apps/room_control deleted from b1638be692
@@ -1,36 +0,0 @@
|
|||||||
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]
|
|
||||||
94
apps/stages/bar.yaml
Normal file
94
apps/stages/bar.yaml
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
bar_lights:
|
||||||
|
module: light
|
||||||
|
class: StagedLight
|
||||||
|
activate-at-start: true
|
||||||
|
# transition: 3
|
||||||
|
stages:
|
||||||
|
- start: '03:00 am'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 4500
|
||||||
|
brightness: 25
|
||||||
|
light.server_lamp:
|
||||||
|
state: off
|
||||||
|
- start: '07:00 am'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 3500
|
||||||
|
brightness: 75
|
||||||
|
light.server_lamp:
|
||||||
|
state: off
|
||||||
|
- start: '09:00 am'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 3500
|
||||||
|
brightness: 125
|
||||||
|
light.server_lamp:
|
||||||
|
state: off
|
||||||
|
- start: '01:00 pm'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 3000
|
||||||
|
brightness: 150
|
||||||
|
light.server_lamp:
|
||||||
|
state: off
|
||||||
|
- start: 'sunset'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 100
|
||||||
|
light.server_lamp:
|
||||||
|
state: "on"
|
||||||
|
rgb_color: [255, 112, 86]
|
||||||
|
brightness: 175
|
||||||
|
light.living_room_stick:
|
||||||
|
state: "on"
|
||||||
|
brightness: 150
|
||||||
|
effect: sunset
|
||||||
|
- start: 'sunset + 1:00'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 75
|
||||||
|
light.server_lamp:
|
||||||
|
state: "on"
|
||||||
|
rgb_color: [255, 112, 86]
|
||||||
|
brightness: 175
|
||||||
|
light.living_room_stick:
|
||||||
|
state: "on"
|
||||||
|
brightness: 100
|
||||||
|
effect: sunset
|
||||||
|
- start: '10:00 pm'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 75
|
||||||
|
light.server_lamp:
|
||||||
|
state: on
|
||||||
|
rgb_color: [255, 112, 86]
|
||||||
|
brightness: 75
|
||||||
|
light.living_room_stick:
|
||||||
|
state: "on"
|
||||||
|
brightness: 175
|
||||||
|
effect: sunset
|
||||||
|
- start: '11:30 pm'
|
||||||
|
scene:
|
||||||
|
light.bar:
|
||||||
|
state: on
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 30
|
||||||
|
light.server_lamp:
|
||||||
|
state: on
|
||||||
|
rgb_color: [255, 112, 86]
|
||||||
|
brightness: 25
|
||||||
|
light.living_room_stick:
|
||||||
|
state: "on"
|
||||||
|
brightness: 75
|
||||||
|
effect: sunset
|
||||||
140
apps/stages/light.py
Normal file
140
apps/stages/light.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
from collections.abc import Generator
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from itertools import count
|
||||||
|
from itertools import cycle
|
||||||
|
from itertools import pairwise
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from appdaemon.plugins.hass import Hass
|
||||||
|
from pydantic import TypeAdapter
|
||||||
|
from stages import Stage
|
||||||
|
|
||||||
|
|
||||||
|
class StagedControlEvent(str, Enum):
|
||||||
|
"""Enum to define the different types of valid stage control events"""
|
||||||
|
|
||||||
|
ACTIVATE = 'activate'
|
||||||
|
DEACTIVATE = 'deactivate'
|
||||||
|
|
||||||
|
|
||||||
|
class StageCondition(str, Enum):
|
||||||
|
"""Enum to define the different types of conditions for an event"""
|
||||||
|
|
||||||
|
ANY_ON = 'any_on'
|
||||||
|
ALL_OFF = 'all_off'
|
||||||
|
|
||||||
|
|
||||||
|
class StagedLight(Hass):
|
||||||
|
def initialize(self):
|
||||||
|
self.set_log_level('DEBUG')
|
||||||
|
self._type_adapter = TypeAdapter(list[Stage])
|
||||||
|
self._stages = self._type_adapter.validate_python(self.args['stages'])
|
||||||
|
|
||||||
|
self.log(f'Initialized Motion Sensor with {len(self._stages)} stages')
|
||||||
|
self.listen_event(self.handle_event, 'stage_control', app=self.name)
|
||||||
|
|
||||||
|
if self.args.get('activate-at-start', False):
|
||||||
|
self.activate()
|
||||||
|
|
||||||
|
# self._check_transition()
|
||||||
|
self.schedule_transition_checks()
|
||||||
|
self.run_daily(self.schedule_transition_checks, start='00:00:00')
|
||||||
|
|
||||||
|
### Stages
|
||||||
|
|
||||||
|
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 an infinite progression of start and end times for the stages."""
|
||||||
|
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}', level='DEBUG')
|
||||||
|
stage.assign_start(start)
|
||||||
|
return stage
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def current_scene(self):
|
||||||
|
return self.current_stage().scene_json()
|
||||||
|
|
||||||
|
### Transitions
|
||||||
|
|
||||||
|
def schedule_transition_checks(self, **_):
|
||||||
|
now = self.get_now()
|
||||||
|
for stage in self._stages:
|
||||||
|
dt = self.parse_datetime(stage.start, aware=True, today=True)
|
||||||
|
if dt > now:
|
||||||
|
self.log(f'Scehduling transition at: {dt.strftime("%I:%M %p")}', level='DEBUG')
|
||||||
|
self.run_at(self._check_transition, start=dt)
|
||||||
|
|
||||||
|
def _check_transition(self, **_):
|
||||||
|
self.log('Firing transition event', level='DEBUG')
|
||||||
|
self.fire_event(
|
||||||
|
'stage_control',
|
||||||
|
app=self.name,
|
||||||
|
condition=StageCondition.ANY_ON,
|
||||||
|
action=StagedControlEvent.ACTIVATE,
|
||||||
|
)
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
def handle_event(self, event_type: str, data: dict[str, Any], **kwargs: Any) -> None:
|
||||||
|
self.log(f'Event handler: {event_type}', level='DEBUG')
|
||||||
|
stage = self.current_stage()
|
||||||
|
scene = stage.scene_json()
|
||||||
|
|
||||||
|
match data:
|
||||||
|
case {'condition': StageCondition.ANY_ON}:
|
||||||
|
any_on = any(self.get_state(e) == 'on' for e in scene)
|
||||||
|
if not any_on:
|
||||||
|
self.log('Nothing is on, skipping', level='DEBUG')
|
||||||
|
return
|
||||||
|
case {'condition': StageCondition.ALL_OFF}:
|
||||||
|
all_off = all(self.get_state(e) == 'off' for e in scene)
|
||||||
|
if not all_off:
|
||||||
|
self.log('Everything is not off, skipping', level='DEBUG')
|
||||||
|
return
|
||||||
|
|
||||||
|
match data:
|
||||||
|
case {'action': StagedControlEvent.ACTIVATE}:
|
||||||
|
self.activate(stage)
|
||||||
|
case {'action': StagedControlEvent.DEACTIVATE}:
|
||||||
|
self.deactivate(stage)
|
||||||
|
case _:
|
||||||
|
self.log(str(data), level='DEBUG')
|
||||||
|
self.log(str(kwargs), level='DEBUG')
|
||||||
|
|
||||||
|
### Actions
|
||||||
|
|
||||||
|
def activate(self, stage: Stage | None = None, **kwargs: Any):
|
||||||
|
if stage is None:
|
||||||
|
stage = self.current_stage()
|
||||||
|
kwargs['entities'] = stage.scene_json()
|
||||||
|
else:
|
||||||
|
kwargs['entities'] = stage.scene_json()
|
||||||
|
|
||||||
|
if t := self.args.get('transition'):
|
||||||
|
kwargs['transition'] = t
|
||||||
|
|
||||||
|
start_time_str = stage.formatted_start('%I:%M %p')
|
||||||
|
self.log(f'Activating current stage: {start_time_str}')
|
||||||
|
return self.call_service('scene/apply', **kwargs)
|
||||||
|
|
||||||
|
def deactivate(self, stage: Stage | None = None, **kwargs):
|
||||||
|
stage = stage if stage is not None else self.current_stage()
|
||||||
|
|
||||||
|
start_time_str = stage.formatted_start('%I:%M %p')
|
||||||
|
self.log(f'Deactivating current stage: {start_time_str}')
|
||||||
|
|
||||||
|
for entity in stage.scene:
|
||||||
|
self.turn_off(entity)
|
||||||
37
apps/stages/stages.py
Normal file
37
apps/stages/stages.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import PrivateAttr
|
||||||
|
from pydantic import field_serializer
|
||||||
|
|
||||||
|
|
||||||
|
class EntityState(BaseModel, extra='allow'):
|
||||||
|
state: bool = True
|
||||||
|
color_temp_kelvin: int | None = None
|
||||||
|
rgb_color: list[int] | None = None
|
||||||
|
brightness: int | None = None
|
||||||
|
|
||||||
|
@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
|
||||||
|
scene: dict[str, EntityState]
|
||||||
|
_start: datetime = PrivateAttr()
|
||||||
|
transition: int | None = None
|
||||||
|
|
||||||
|
def assign_start(self, dt: datetime):
|
||||||
|
self._start = dt
|
||||||
|
|
||||||
|
def formatted_start(self, fmt: str) -> str:
|
||||||
|
return self._start.strftime(fmt)
|
||||||
|
|
||||||
|
def scene_json(self):
|
||||||
|
return self.model_dump(mode='json')['scene']
|
||||||
44
apps/stages/upper_stairs.yaml
Normal file
44
apps/stages/upper_stairs.yaml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
upper_stairs:
|
||||||
|
module: light
|
||||||
|
class: StagedLight
|
||||||
|
stages:
|
||||||
|
- start: '04:00'
|
||||||
|
scene:
|
||||||
|
light.bathroom_stairs:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 4000
|
||||||
|
brightness: 30
|
||||||
|
light.spire:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 4000
|
||||||
|
brightness: 30
|
||||||
|
- start: '07:00'
|
||||||
|
scene:
|
||||||
|
light.bathroom_stairs:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 4000
|
||||||
|
brightness: 60
|
||||||
|
light.spire:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 4000
|
||||||
|
brightness: 60
|
||||||
|
- start: 'sunset'
|
||||||
|
scene:
|
||||||
|
light.bathroom_stairs:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 60
|
||||||
|
light.spire:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 60
|
||||||
|
- start: '11:00 pm'
|
||||||
|
scene:
|
||||||
|
light.bathroom_stairs:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 40
|
||||||
|
light.spire:
|
||||||
|
state: "on"
|
||||||
|
color_temp_kelvin: 2202
|
||||||
|
brightness: 40
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
services:
|
services:
|
||||||
appdaemon:
|
appdaemon:
|
||||||
container_name: appdaemon
|
container_name: appdaemon
|
||||||
image: acockburn/appdaemon:dev
|
image: acockburn/appdaemon:dev
|
||||||
|
# image: acockburn/appdaemon:local-dev
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
tty: true
|
tty: true
|
||||||
|
dns:
|
||||||
|
- 192.168.1.150
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
|||||||
Reference in New Issue
Block a user