Compare commits

...

16 Commits

Author SHA1 Message Date
John Lancaster
3ae7c2b8e9 fewer lines 2025-12-04 23:56:18 -06:00
John Lancaster
01c6faa362 stage adjustments 2025-12-04 23:52:29 -06:00
John Lancaster
56ee0c3d3e adjusted for matter 2025-12-04 18:51:02 -06:00
John Lancaster
657d01a724 pruning 2025-12-02 22:08:27 -06:00
John Lancaster
e21eca4f42 started gone app 2025-12-02 22:08:15 -06:00
John Lancaster
61d5d99dee changed time format in logs 2025-12-02 22:07:20 -06:00
John Lancaster
29759692b2 added the govee stick to the bar lights stages 2025-12-02 22:07:07 -06:00
John Lancaster
fbd60ab6ac started moes pad driver and app 2025-12-02 10:50:30 -06:00
John Lancaster
2f52febf38 additional bar lights stage 2025-12-02 10:45:55 -06:00
John Lancaster
d8476e9fdf added explicit hermes dns 2025-12-02 10:45:38 -06:00
John Lancaster
43beb1e950 commented get_history call 2025-12-02 10:45:25 -06:00
John Lancaster
81b5d5afa5 app tweaks 2025-11-30 23:05:37 -06:00
John Lancaster
b33016089f added conditions and transtions 2025-11-30 23:05:21 -06:00
John Lancaster
25892e1de8 added transition config 2025-11-30 16:41:46 -06:00
John Lancaster
05bad81571 added server being off to some stages 2025-11-23 13:59:19 -06:00
John Lancaster
697b09a7a1 made activation at startup optional 2025-11-23 13:56:22 -06:00
11 changed files with 242 additions and 19 deletions

View File

@@ -1,6 +1,5 @@
appdaemon: appdaemon:
uvloop: True uvloop: True
use_dictionary_unpacking: True
import_method: expert import_method: expert
latitude: 30.250968 latitude: 30.250968
longitude: -97.748193 longitude: -97.748193

View File

@@ -1,3 +1,11 @@
hello-world: hello-world:
module: hello module: hello
class: HelloWorld class: HelloWorld
gone:
module: gone
class: Gone
entities:
- light.bar
- light.h6076
- light.h6076_2

7
apps/drivers/moes.yaml Normal file
View 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
View 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
View 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

View File

@@ -6,4 +6,4 @@ class HelloWorld(Hass):
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') # self.get_history('ball1')

View File

@@ -1,6 +1,8 @@
bar_lights: bar_lights:
module: light module: light
class: StagedLight class: StagedLight
activate-at-start: true
# transition: 3
stages: stages:
- start: '03:00 am' - start: '03:00 am'
scene: scene:
@@ -8,28 +10,60 @@ bar_lights:
state: on state: on
color_temp_kelvin: 4500 color_temp_kelvin: 4500
brightness: 25 brightness: 25
light.server_lamp:
state: off
- start: '07:00 am' - start: '07:00 am'
scene: scene:
light.bar: light.bar:
state: on state: on
color_temp_kelvin: 3500 color_temp_kelvin: 3500
brightness: 100 brightness: 75
- start: '13:00' light.server_lamp:
state: off
- start: '09:00 am'
scene: scene:
light.bar: light.bar:
state: on state: on
color_temp_kelvin: 2500 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 brightness: 150
light.server_lamp:
state: off
- start: 'sunset' - 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: scene:
light.bar: light.bar:
state: on state: on
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 100 brightness: 75
light.server_lamp: light.server_lamp:
state: on state: "on"
rgb_color: [255, 112, 86] rgb_color: [255, 112, 86]
brightness: 175 brightness: 175
light.living_room_stick:
state: "on"
brightness: 100
effect: sunset
- start: '10:00 pm' - start: '10:00 pm'
scene: scene:
light.bar: light.bar:
@@ -40,9 +74,21 @@ bar_lights:
state: on state: on
rgb_color: [255, 112, 86] rgb_color: [255, 112, 86]
brightness: 75 brightness: 75
light.living_room_stick:
state: "on"
brightness: 175
effect: sunset
- start: '11:30 pm' - start: '11:30 pm'
scene: scene:
light.bar: light.bar:
state: on state: on
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 30 brightness: 30
light.server_lamp:
state: on
rgb_color: [255, 112, 86]
brightness: 25
light.living_room_stick:
state: "on"
brightness: 75
effect: sunset

View File

@@ -12,10 +12,19 @@ from stages import Stage
class StagedControlEvent(str, Enum): class StagedControlEvent(str, Enum):
"""Enum to define the different types of valid stage control events"""
ACTIVATE = 'activate' ACTIVATE = 'activate'
DEACTIVATE = 'deactivate' 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): class StagedLight(Hass):
def initialize(self): def initialize(self):
self.set_log_level('DEBUG') self.set_log_level('DEBUG')
@@ -24,7 +33,13 @@ class StagedLight(Hass):
self.log(f'Initialized Motion Sensor with {len(self._stages)} stages') self.log(f'Initialized Motion Sensor with {len(self._stages)} stages')
self.listen_event(self.handle_event, 'stage_control', app=self.name) self.listen_event(self.handle_event, 'stage_control', app=self.name)
self.activate()
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 ### Stages
@@ -52,30 +67,74 @@ class StagedLight(Hass):
def current_scene(self): def current_scene(self):
return self.current_stage().scene_json() 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 ### Events
def handle_event(self, event_type: str, data: dict[str, Any], **kwargs: Any) -> None: def handle_event(self, event_type: str, data: dict[str, Any], **kwargs: Any) -> None:
self.log(f'Event handler: {event_type}', level='DEBUG') self.log(f'Event handler: {event_type}', level='DEBUG')
stage = self.current_stage() stage = self.current_stage()
start_time_str = stage.formatted_start('%I:%M %p') 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: match data:
case {'action': StagedControlEvent.ACTIVATE}: case {'action': StagedControlEvent.ACTIVATE}:
self.log(f'Activating current stage: {start_time_str}') self.activate(stage)
self.activate(scene=stage.scene_json())
case {'action': StagedControlEvent.DEACTIVATE}: case {'action': StagedControlEvent.DEACTIVATE}:
self.log(f'Deactivating current stage: {start_time_str}') self.deactivate(stage)
self.deactivate()
case _: case _:
self.log(str(data), level='DEBUG') self.log(str(data), level='DEBUG')
self.log(str(kwargs), level='DEBUG') self.log(str(kwargs), level='DEBUG')
### Actions ### Actions
def activate(self, scene: dict | None = None, **kwargs): def activate(self, stage: Stage | None = None, **kwargs: Any):
scene = scene if scene is not None else self.current_scene() if stage is None:
return self.call_service('scene/apply', entities=scene, transition=5) 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): def deactivate(self, stage: Stage | None = None, **kwargs):
stage = stage if stage is not None else self.current_stage() 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: for entity in stage.scene:
self.turn_off(entity) self.turn_off(entity)

View File

@@ -23,8 +23,9 @@ class EntityState(BaseModel, extra='allow'):
class Stage(BaseModel): class Stage(BaseModel):
# start: Annotated[time, BeforeValidator(lambda v: parser(v).time())] # start: Annotated[time, BeforeValidator(lambda v: parser(v).time())]
start: str start: str
_start: datetime = PrivateAttr()
scene: dict[str, EntityState] scene: dict[str, EntityState]
_start: datetime = PrivateAttr()
transition: int | None = None
def assign_start(self, dt: datetime): def assign_start(self, dt: datetime):
self._start = dt self._start = dt

View File

@@ -32,7 +32,7 @@ upper_stairs:
state: "on" state: "on"
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 60 brightness: 60
- start: '11:00' - start: '11:00 pm'
scene: scene:
light.bathroom_stairs: light.bathroom_stairs:
state: "on" state: "on"

View File

@@ -2,8 +2,11 @@ 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