Compare commits

...

17 Commits

Author SHA1 Message Date
John Lancaster
d03a5e8a19 adjusted stages for stairs 2025-12-20 21:16:59 -06:00
John Lancaster
699d314862 added the blue button app 2025-12-15 22:12:12 -06:00
John Lancaster
610c2b68e2 updated patio stages 2025-12-12 17:42:51 -06:00
John Lancaster
27db3be4a0 rename 2025-12-12 17:42:42 -06:00
John Lancaster
dac73a8398 adjusted stages 2025-12-12 17:21:36 -06:00
John Lancaster
3f6688821a separated some functions 2025-12-12 08:20:24 -06:00
John Lancaster
25c529da60 added patio light 2025-12-11 22:14:56 -06:00
John Lancaster
9bbe28c1bb fixed 4th button 2025-12-09 08:14:14 -06:00
John Lancaster
3a8a0e48d0 set up other moes pad 2025-12-09 01:09:56 -06:00
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
11 changed files with 278 additions and 12 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,15 @@
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
blue-button:
module: media_button
class: Button

7
apps/drivers/moes.yaml Normal file
View File

@@ -0,0 +1,7 @@
moes_driver:
module: moes_pad
class: MoesBridge
moes_pad:
module: moes_pad
class: MoesPad

104
apps/drivers/moes_pad.py Normal file
View File

@@ -0,0 +1,104 @@
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):
"""Specfic implementation of a Moes Pad."""
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')
self.bedroom_offs = ('bedroom_stick', 'bedroom_john', 'bedroom_sydney')
self.main_offs = ('bar', 'server_lamp', 'living_room_stick')
# self.set_many(self.main_offs, state='on')
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)
case MoesActions.DOUBLE_PRESS:
self.handle_double_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.bedroom_stick')
case MoesButtons.BOTTOM_RIGHT:
any_on = any(self.adapi.get_state(f'light.{e}') == 'on' for e in self.bedroom_offs)
scene = {'entities': {f'light.{e}': {'state': 'off' if any_on else 'on'}} for e in self.bedroom_offs}
self.adapi.call_service('scene/apply', **scene)
def handle_double_press(self, button: MoesButtons) -> None:
match button:
case MoesButtons.BOTTOM_RIGHT:
all_lights = [*self.bedroom_offs, *self.main_offs]
any_on = self.check_many(all_lights, state='on')
new_state = 'off' if any_on else 'on'
self.log(f'New state from double press: {new_state}')
self.set_many(self.bedroom_offs, state=new_state)
self.set_many(self.main_offs, state=new_state)
def set_many(self, lights: set[str], state: str = 'off'):
entity_ids = [f'light.{e}' if not e.startswith('light') else e for e in lights]
scene = {'entities': {eid: {'state': state} for eid in entity_ids}}
return self.adapi.call_service('scene/apply', **scene)
def check_many(self, lights: set[str], state: str = 'off') -> bool:
entity_ids = [f'light.{e}' if not e.startswith('light') else e for e in lights]
return any(self.adapi.get_state(eid) == state for eid in entity_ids)

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

38
apps/media_button.py Normal file
View File

@@ -0,0 +1,38 @@
from enum import Enum
from typing import Any
from appdaemon.plugins.hass import Hass
class ButtonPress(str, Enum):
SINGLE = 'single'
DOUBLE = 'double'
HOLD = 'hold'
class Button(Hass):
def initialize(self):
self.set_log_level('DEBUG')
self.listen_state(
self.handle_button,
entity_id='sensor.blue_lamp_button_action',
new=lambda s: s.strip() != '',
)
def handle_button(self, entity: str, attribute: str, old: Any, new: Any, **kwargs: Any) -> None:
match new:
case ButtonPress.SINGLE:
self.log('Single press')
self.call_service('media_player/media_play_pause', entity_id='media_player.living_room_tv')
case ButtonPress.DOUBLE:
self.log('Double')
self.call_service(
'scene/apply',
entities={
'light.bar': {'state': 'on', 'brightness': 10},
'light.living_room_stick': {'state': 'on', 'brightness': 100},
'light.server_lamp': {'state': 'on', 'brightness': 25},
},
)
case ButtonPress.HOLD:
self.log('Hold down')

View File

@@ -69,15 +69,15 @@ class StagedLight(Hass):
### Transitions ### Transitions
def schedule_transition_checks(self, **kwargs: Any): def schedule_transition_checks(self, **_):
now = self.get_now() now = self.get_now()
for stage in self._stages: for stage in self._stages:
dt = self.parse_datetime(stage.start, aware=True, today=True) dt = self.parse_datetime(stage.start, aware=True, today=True)
if dt > now: if dt > now:
self.log(f'Scehduling transition at: {dt.isoformat()}', level='DEBUG') self.log(f'Scehduling transition at: {dt.strftime("%I:%M %p")}', level='DEBUG')
self.run_at(self._check_transition, start=dt) self.run_at(self._check_transition, start=dt)
def _check_transition(self, **kwargs: Any): def _check_transition(self, **_):
self.log('Firing transition event', level='DEBUG') self.log('Firing transition event', level='DEBUG')
self.fire_event( self.fire_event(
'stage_control', 'stage_control',

View File

@@ -1,4 +1,4 @@
bar_lights: main_floor:
module: light module: light
class: StagedLight class: StagedLight
activate-at-start: true activate-at-start: true
@@ -33,19 +33,46 @@ bar_lights:
light.bar: light.bar:
state: on state: on
color_temp_kelvin: 3000 color_temp_kelvin: 3000
brightness: 150 brightness: 75
light.server_lamp: light.server_lamp:
state: off state: off
- start: 'sunset - 60min'
scene:
light.bar:
state: on
color_temp_kelvin: 3000
brightness: 75
light.server_lamp:
state: on
brightness: 100
- 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:
@@ -56,6 +83,10 @@ 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:
@@ -63,4 +94,10 @@ bar_lights:
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 30 brightness: 30
light.server_lamp: light.server_lamp:
state: off state: on
rgb_color: [255, 112, 86]
brightness: 25
light.living_room_stick:
state: "on"
brightness: 75
effect: sunset

7
apps/stages/patio.py Normal file
View File

@@ -0,0 +1,7 @@
from light import StagedLight
class Patio(StagedLight):
def initialize(self):
super().initialize()
self.run_daily(lambda **kwargs: self.turn_on('light.patio', brightness=100), start='sunset - 31s')

34
apps/stages/patio.yaml Normal file
View File

@@ -0,0 +1,34 @@
patio:
module: patio
class: Patio
activate-at-start: true
transition: 30
stages:
- start: '01:00 am'
scene:
light.patio:
state: off
- start: sunset
scene:
light.patio:
state: on
brightness: 255
rgb_color: [0, 255, 0]
- start: '10:00 pm'
scene:
light.patio:
state: on
brightness: 175
rgb_color: [0, 255, 0]
- start: '11:00 pm'
scene:
light.patio:
state: on
brightness: 125
rgb_color: [0, 255, 0]
- start: '12:00 am'
scene:
light.patio:
state: on
brightness: 75
rgb_color: [0, 255, 0]

View File

@@ -1,6 +1,7 @@
upper_stairs: upper_stairs:
module: light module: light
class: StagedLight class: StagedLight
activate-at-start: True
stages: stages:
- start: '04:00' - start: '04:00'
scene: scene:
@@ -31,14 +32,14 @@ upper_stairs:
light.spire: light.spire:
state: "on" state: "on"
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 60 brightness: 180
- start: '11:00 pm' - start: '11:00 pm'
scene: scene:
light.bathroom_stairs: light.bathroom_stairs:
state: "on" state: "on"
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 40 brightness: 60
light.spire: light.spire:
state: "on" state: "on"
color_temp_kelvin: 2202 color_temp_kelvin: 2202
brightness: 40 brightness: 60