19 Commits
main ... async

Author SHA1 Message Date
John Lancaster
dda9d2b501 small bugfix 2024-01-02 21:54:20 -06:00
John Lancaster
f088a23da7 maybe this time? 2023-12-08 16:36:22 -06:00
John Lancaster
4f11d9bdcc fixed states? 2023-12-08 16:29:25 -06:00
John Lancaster
cf9f0f3244 moved toggle logic 2023-12-08 08:22:57 -06:00
John Lancaster
e0caaedc15 cancelling all callbacks on a state change 2023-12-06 20:58:56 -06:00
John Lancaster
0d70649bb8 fixed async all_off/any_on 2023-12-04 14:17:22 -06:00
John Lancaster
145aeca667 changed on callback to trigger on brightness 2023-12-02 12:48:34 -06:00
John Lancaster
8b00faceb6 improved logging 2023-11-25 22:36:00 -06:00
John Lancaster
148645094a toggle stuff 2023-11-25 22:04:10 -06:00
John Lancaster
e659629c71 logging tweaks 2023-11-25 21:19:59 -06:00
John Lancaster
4886eb29d6 deactivate cause 2023-11-25 21:12:34 -06:00
John Lancaster
7bd46ffc42 deactivate cause 2023-11-25 21:12:24 -06:00
John Lancaster
945abc91c3 removed sync state 2023-11-25 21:12:15 -06:00
John Lancaster
af28cda9a5 logging improvements 2023-11-25 19:29:28 -06:00
John Lancaster
afc5e45642 bug fix from scene detector 2023-11-25 18:41:49 -06:00
John Lancaster
8a5431a72b better use of sync wrapper 2023-11-25 18:00:12 -06:00
John Lancaster
50b79c8d13 streamlined initialize 2023-11-25 17:52:25 -06:00
John Lancaster
5f9218311c removed obsolete 2023-11-25 17:45:58 -06:00
John Lancaster
7de5dfa3a8 tweaked button init 2023-11-25 17:44:27 -06:00
4 changed files with 99 additions and 108 deletions

View File

@@ -5,12 +5,10 @@ from appdaemon.plugins.mqtt.mqttapi import Mqtt
from room_control import RoomController from room_control import RoomController
class ButtonController(Mqtt): class Button(Mqtt):
def initialize(self): async def initialize(self):
task = self.get_app(self.args['app']) self.app: RoomController = await self.get_app(self.args['app'])
self.app: RoomController = asyncio.get_event_loop().run_until_complete(task)
self.setup_buttons(self.args['button']) self.setup_buttons(self.args['button'])
# self.log(f'Done')
def setup_buttons(self, buttons): def setup_buttons(self, buttons):
if isinstance(buttons, list): if isinstance(buttons, list):
@@ -26,29 +24,27 @@ class ButtonController(Mqtt):
self.log(f'"{topic}" controls app {self.app.name}') self.log(f'"{topic}" controls app {self.app.name}')
async def handle_button(self, event_name, data, kwargs): async def handle_button(self, event_name, data, kwargs):
topic = data['topic']
self.log(f'Button event for: {topic}')
try: try:
payload = json.loads(data['payload']) payload = json.loads(data['payload'])
action = payload['action'] action = payload['action']
button = kwargs['button']
except json.JSONDecodeError: except json.JSONDecodeError:
self.log(f'Error decoding JSON from {data["payload"]}', level='ERROR') self.log(f'Error decoding JSON from {data["payload"]}', level='ERROR')
except KeyError as e: except KeyError as e:
return return
else: else:
self.log(f'{button}: {action}') if action != '':
await self.handle_action(action) await self.handle_action(action)
async def handle_action(self, action: str): async def handle_action(self, action: str):
if action == '': if action == 'single':
return self.log(f' {action.upper()} '.center(50, '='))
elif action == 'single': state = await self.get_state(self.args['ref_entity'])
cause = 'button single click' kwargs = {
state = await self.get_state(entity_id=self.args['ref_entity']) 'kwargs': {'cause': f'button single click: toggle while {state}'}
}
if state == 'on': if state == 'on':
self.app.deactivate(cause=cause) self.app.deactivate(**kwargs)
else: else:
await self.app.activate(cause=cause) await self.app.activate(**kwargs)
else: else:
pass pass

View File

@@ -4,8 +4,5 @@ from room_control import RoomController
class Door(Hass): class Door(Hass):
async def initialize(self): async def initialize(self):
await self.listen_state(self.door_open, entity_id=self.args['door'], new='on')
async def door_open(self, entity, attribute, old, new, kwargs):
app: RoomController = await self.get_app(self.args['app']) app: RoomController = await self.get_app(self.args['app'])
await app.activate_all_off() await self.listen_state(app.activate_all_off, entity_id=self.args['door'], new='on', cause='door open')

View File

@@ -1,28 +1,12 @@
import asyncio
from datetime import timedelta
import re import re
from datetime import timedelta
from appdaemon.entity import Entity from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass from appdaemon.plugins.hass.hassapi import Hass
from room_control import RoomController from room_control import RoomController
from appdaemon import utils
class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
def get_event_loop(self):
try:
# Try to get the current event loop
loop = super().get_event_loop()
except RuntimeError as ex:
if "There is no current event loop" in str(ex):
# If there's no current loop, create a new one and set it
loop = self.new_event_loop()
self.set_event_loop(loop)
else:
raise
return loop
# Set the custom event loop policy
asyncio.set_event_loop_policy(CustomEventLoopPolicy())
class Motion(Hass): class Motion(Hass):
@property @property
@@ -38,50 +22,39 @@ class Motion(Hass):
return self.get_entity(self.args['ref_entity']) return self.get_entity(self.args['ref_entity'])
@property @property
def ref_entity_state(self) -> bool: async def ref_entity_state(self) -> bool:
return self.ref_entity.get_state() == 'on' return (await self.ref_entity.get_state()) == 'on'
def initialize(self): async def initialize(self):
try: self.app: RoomController = await self.get_app(self.args['app'])
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
self.app: RoomController = loop.run_until_complete(self.get_app(self.args['app']))
self.log(f'Connected to app {self.app.name}') self.log(f'Connected to app {self.app.name}')
self.listen_state(self.callback_light_on, self.ref_entity.entity_id, new='on') base_kwargs = dict(
self.listen_state(self.callback_light_off, self.ref_entity.entity_id, new='off') entity_id=self.ref_entity.entity_id,
immediate=True, # avoids needing to sync the state
loop.run_until_complete(self.sync_state()) )
# don't need to await these because they'll already get turned into a task by the utils.sync_wrapper decorator
async def sync_state(self): self.listen_state(**base_kwargs, attribute='brightness', callback=self.callback_light_on)
"""Synchronizes the callbacks with the state of the light. self.listen_state(**base_kwargs, new='off', callback=self.callback_light_off)
Essentially mimics the `state_change` callback based on the current state of the light.
"""
if self.ref_entity_state:
await self.callback_light_on()
else:
await self.callback_light_off()
async def listen_motion_on(self): async def listen_motion_on(self):
"""Sets up the motion on callback to activate the room """Sets up the motion on callback to activate the room
""" """
self.log(f'Waiting for motion on {self.sensor.friendly_name}') await self.cancel_motion_callback()
self.motion_on_handle = await self.listen_state( await self.listen_state(
callback=self.app.activate_all_off, callback=self.app.activate_all_off,
entity_id=self.sensor.entity_id, entity_id=self.sensor.entity_id,
new='on', new='on',
oneshot=True, oneshot=True,
cause='motion on' cause='motion on'
) )
self.log(f'Waiting for motion on {self.sensor.friendly_name}')
async def listen_motion_off(self, duration: timedelta): async def listen_motion_off(self, duration: timedelta):
"""Sets up the motion off callback to deactivate the room """Sets up the motion off callback to deactivate the room
""" """
self.log(f'Waiting for motion to stop on {self.sensor.friendly_name}') await self.cancel_motion_callback()
self.motion_off_handle = await self.listen_state( await self.listen_state(
callback=self.app.deactivate, callback=self.app.deactivate,
entity_id=self.sensor.entity_id, entity_id=self.sensor.entity_id,
new='off', new='off',
@@ -89,19 +62,22 @@ class Motion(Hass):
oneshot=True, oneshot=True,
cause='motion off' cause='motion off'
) )
self.log(f'Waiting for motion to stop on {self.sensor.friendly_name} for {duration}')
@utils.sync_wrapper
async def callback_light_on(self, entity=None, attribute=None, old=None, new=None, kwargs=None): async def callback_light_on(self, entity=None, attribute=None, old=None, new=None, kwargs=None):
"""Called when the light turns on """Called when the light turns on
""" """
self.log('Light on callback') if new is not None:
await self.cancel_motion_callback(new='on') self.log(f'{entity} turned on')
await self.listen_motion_off(await self.app.off_duration()) duration = await self.app.off_duration()
await self.listen_motion_off(duration)
@utils.sync_wrapper
async def callback_light_off(self, entity=None, attribute=None, old=None, new=None, kwargs=None): async def callback_light_off(self, entity=None, attribute=None, old=None, new=None, kwargs=None):
"""Called when the light turns off """Called when the light turns off
""" """
self.log('Light off callback') self.log(f'{entity} turned off')
await self.cancel_motion_callback(new='off')
await self.listen_motion_on() await self.listen_motion_on()
async def get_app_callbacks(self, name: str = None): async def get_app_callbacks(self, name: str = None):
@@ -123,13 +99,13 @@ class Motion(Hass):
if info['entity'] == self.sensor.entity_id if info['entity'] == self.sensor.entity_id
} }
async def cancel_motion_callback(self, new: str): async def cancel_motion_callback(self):
callbacks = await self.get_sensor_callbacks() callbacks = await self.get_sensor_callbacks()
# self.log(f'Found {len(callbacks)}') # self.log(f'Found {len(callbacks)} callbacks for {self.sensor.entity_id}')
for handle, info in callbacks.items(): for handle, info in callbacks.items():
entity = info["entity"] entity = info["entity"]
new_match = re.match('new=(?P<new>.*?)\s', info['kwargs']) kwargs = info['kwargs']
# self.log(f'{handle}: {info["entity"]}: {info["kwargs"]}') if (m := re.match('new=(?P<new>.*?)\s', kwargs)) is not None:
if new_match is not None and new_match.group("new") == new: new = m.group('new')
await self.cancel_listen_state(handle) await self.cancel_listen_state(handle)
self.log(f'cancelled: {await self.friendly_name(entity)}: {new}') self.log(f'cancelled callback for sensor {entity} turning {new}')

View File

@@ -1,7 +1,7 @@
import asyncio import asyncio
from copy import deepcopy from copy import deepcopy
from datetime import time, timedelta from datetime import datetime, time, timedelta
from typing import List from typing import Dict, List
import appdaemon.utils as utils import appdaemon.utils as utils
import astral import astral
@@ -22,7 +22,7 @@ class RoomController(Hass, Mqtt):
async def initialize(self): async def initialize(self):
self.app_entities = await self.gather_app_entities() self.app_entities = await self.gather_app_entities()
self.log(f'entities: {self.app_entities}') # self.log(f'entities: {self.app_entities}')
await self.refresh_state_times() await self.refresh_state_times()
await self.run_daily(callback=self.refresh_state_times, start='00:00:00') await self.run_daily(callback=self.refresh_state_times, start='00:00:00')
@@ -62,15 +62,16 @@ class RoomController(Hass, Mqtt):
# schedule the transitions # schedule the transitions
for state in self.states: for state in self.states:
dt = str(state['time'])[:8] t: time = state['time']
self.log(f'Scheduling transition at: {dt}')
try: try:
await self.run_at(callback=self.activate_any_on, start=dt) await self.run_at(callback=self.activate_any_on, start=t.strftime('%H:%M:%S'), cause='scheduled transition')
except ValueError: except ValueError:
# happens when the callback time is in the past # happens when the callback time is in the past
pass pass
except Exception as e: except Exception as e:
self.log(f'Failed with {type(e)}: {e}') self.log(f'Failed with {type(e)}: {e}')
else:
self.log(f'Scheduled transition at: {t.strftime("%I:%M:%S %p")}')
async def parse_states(self): async def parse_states(self):
async def gen(): async def gen():
@@ -98,47 +99,61 @@ class RoomController(Hass, Mqtt):
yield state yield state
states = [s async for s in gen()] states = [s async for s in gen()]
states = sorted(states, key=lambda s: s['time']) states = sorted(states, key=lambda s: s['time'], reverse=True)
return states return states
async def current_state(self, time: time = None): async def current_state(self, time: time = None):
if (await self.sleep_bool()): if (await self.sleep_bool()):
self.log(f'sleep: active')
if (state := self.args.get('sleep_state')): if (state := self.args.get('sleep_state')):
return state return state
else: else:
return {} return {}
else: else:
now = await self.get_now() # now: datetime = await self.get_now()
self.log(f'Getting state for datetime: {now}') # self.log(f'Getting state for datetime: {now.strftime("%I:%M:%S %p")}')
time = time or (await self.get_now()).time() time = time or (await self.get_now()).time()
for state in self.states[::-1]: time_fmt = "%I:%M %p"
self.log(f'Getting state before: {time.strftime(time_fmt)}')
for state in self.states:
time_str = state["time"].strftime(time_fmt)
if state['time'] <= time: if state['time'] <= time:
self.log(f'Selected state from {state["time"]}') self.log(f'Selected state from {time_str}')
return state return state
else: else:
return self.states[-1] self.log(f'Not {time_str}')
else:
self.log(f'Defaulting to first state')
return self.states[0]
async def current_scene(self, time: time = None): async def current_scene(self, time: time = None):
if (state := (await self.current_state(time=time))) is not None: if (state := (await self.current_state(time=time))) is not None:
return state['scene'] return state['scene']
@property async def app_entity_states(self) -> Dict[str, str]:
def all_off(self) -> bool: states = {
entity: (await self.get_state(entity))
for entity in self.app_entities
}
return states
async def all_off(self) -> bool:
""""All off" is the logic opposite of "any on" """"All off" is the logic opposite of "any on"
Returns: Returns:
bool: Whether all the lights associated with the app are off bool: Whether all the lights associated with the app are off
""" """
return all(self.get_state(entity) != 'on' for entity in self.app_entities) states = await self.app_entity_states()
return all(state != 'on' for entity, state in states.items())
@property async def any_on(self) -> bool:
def any_on(self) -> bool:
""""Any on" is the logic opposite of "all off" """"Any on" is the logic opposite of "all off"
Returns: Returns:
bool: Whether any of the lights associated with the app are on bool: Whether any of the lights associated with the app are on
""" """
return any(self.get_state(entity) == 'on' for entity in self.app_entities) states = await self.app_entity_states()
return any(state == 'on' for entity, state in states.items())
async def sleep_bool(self) -> bool: async def sleep_bool(self) -> bool:
if (sleep_var := self.args.get('sleep')): if (sleep_var := self.args.get('sleep')):
@@ -178,7 +193,13 @@ class RoomController(Hass, Mqtt):
except Exception: except Exception:
return timedelta() return timedelta()
async def activate(self, *args, cause: str = 'unknown', **kwargs): @utils.sync_wrapper
async def activate(self, entity = None, attribute = None, old = None, new = None, kwargs = None):
if kwargs is not None:
cause = kwargs.get('cause', 'unknown')
else:
cause = 'unknown'
self.log(f'Activating: {cause}') self.log(f'Activating: {cause}')
scene = await self.current_scene() scene = await self.current_scene()
@@ -202,26 +223,27 @@ class RoomController(Hass, Mqtt):
else: else:
self.log(f'ERROR: unknown scene: {scene}') self.log(f'ERROR: unknown scene: {scene}')
@utils.sync_wrapper
async def activate_all_off(self, *args, **kwargs): async def activate_all_off(self, *args, **kwargs):
"""Activate if all of the entities are off """Activate if all of the entities are off. Args and kwargs are passed directly to self.activate()
""" """
if self.all_off: if (await self.all_off()):
self.log(f'Activate all off kwargs: {kwargs}') self.activate(*args, **kwargs)
await self.activate(*args, **kwargs)
else: else:
self.log(f'Skipped activating - everything is not off') self.log(f'Skipped activating - everything is not off')
@utils.sync_wrapper
async def activate_any_on(self, *args, **kwargs): async def activate_any_on(self, *args, **kwargs):
"""Activate if any of the entities are on """Activate if any of the entities are on. Args and kwargs are passed directly to self.activate()
""" """
if self.any_on: if (await self.any_on()):
await self.activate(*args, **kwargs) self.activate(*args, **kwargs)
else: else:
self.log(f'Skipped activating - everything is off') self.log(f'Skipped activating - everything is off')
def deactivate(self, *args, cause: str = 'unknown', **kwargs): def deactivate(self, entity = None, attribute = None, old = None, new = None, kwargs = None):
cause = kwargs.get('cause', 'unknown')
self.log(f'Deactivating: {cause}') self.log(f'Deactivating: {cause}')
for entity in self.app_entities: for e in self.app_entities:
self.turn_off(entity) self.turn_off(e)
self.log(f'Turned off {entity}') self.log(f'Turned off {e}')