async working...?

This commit is contained in:
John Lancaster
2023-11-25 17:37:47 -06:00
parent c9cc841d58
commit a00f10a967
4 changed files with 133 additions and 101 deletions

View File

@@ -1,4 +1,4 @@
import asyncio
import json import json
from appdaemon.plugins.mqtt.mqttapi import Mqtt from appdaemon.plugins.mqtt.mqttapi import Mqtt
@@ -7,8 +7,10 @@ from room_control import RoomController
class ButtonController(Mqtt): class ButtonController(Mqtt):
def initialize(self): def initialize(self):
task = 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.app: RoomController = self.get_app(self.args['app']) # self.log(f'Done')
def setup_buttons(self, buttons): def setup_buttons(self, buttons):
if isinstance(buttons, list): if isinstance(buttons, list):
@@ -21,11 +23,11 @@ class ButtonController(Mqtt):
topic = f'zigbee2mqtt/{name}' topic = f'zigbee2mqtt/{name}'
self.mqtt_subscribe(topic, namespace='mqtt') self.mqtt_subscribe(topic, namespace='mqtt')
self.listen_event(self.handle_button, "MQTT_MESSAGE", topic=topic, namespace='mqtt', button=name) self.listen_event(self.handle_button, "MQTT_MESSAGE", topic=topic, namespace='mqtt', button=name)
self.log(f'{name} controls app {self.args["app"]}') self.log(f'"{topic}" controls app {self.app.name}')
def handle_button(self, event_name, data, kwargs): async def handle_button(self, event_name, data, kwargs):
topic = data['topic'] topic = data['topic']
# self.log(f'Button event for: {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']
@@ -36,16 +38,17 @@ class ButtonController(Mqtt):
return return
else: else:
self.log(f'{button}: {action}') self.log(f'{button}: {action}')
self.handle_action(action) await self.handle_action(action)
def handle_action(self, action: str): async def handle_action(self, action: str):
if action == '': if action == '':
return return
elif action == 'single': elif action == 'single':
cause = 'button single click' cause = 'button single click'
if self.get_state(entity_id=self.args['ref_entity']) == 'on': state = await self.get_state(entity_id=self.args['ref_entity'])
if state == 'on':
self.app.deactivate(cause=cause) self.app.deactivate(cause=cause)
else: else:
self.app.activate(cause=cause) await self.app.activate(cause=cause)
else: else:
pass pass

12
door.py
View File

@@ -2,10 +2,10 @@ from appdaemon.plugins.hass.hassapi import Hass
from room_control import RoomController from room_control import RoomController
class DoorControl(Hass): class Door(Hass):
def initialize(self): async def initialize(self):
self.listen_state(self.door_open, entity_id=self.args['door'], new='on') await self.listen_state(self.door_open, entity_id=self.args['door'], new='on')
def door_open(self, entity, attribute, old, new, kwargs): async def door_open(self, entity, attribute, old, new, kwargs):
app: RoomController = self.get_app(self.args['app']) app: RoomController = await self.get_app(self.args['app'])
app.activate_all_off() await app.activate_all_off()

View File

@@ -1,3 +1,4 @@
import asyncio
from datetime import timedelta from datetime import timedelta
import re import re
@@ -6,6 +7,23 @@ from appdaemon.plugins.hass.hassapi import Hass
from room_control import RoomController from room_control import RoomController
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
def sensor(self) -> Entity: def sensor(self) -> Entity:
@@ -23,34 +41,35 @@ class Motion(Hass):
def ref_entity_state(self) -> bool: def ref_entity_state(self) -> bool:
return self.ref_entity.get_state() == 'on' return self.ref_entity.get_state() == 'on'
@property
def off_duration(self) -> timedelta:
return self.app.off_duration
def initialize(self): def initialize(self):
self.app: RoomController = self.get_app(self.args['app']) try:
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') self.listen_state(self.callback_light_on, self.ref_entity.entity_id, new='on')
self.listen_state(self.callback_light_off, self.ref_entity.entity_id, new='off') self.listen_state(self.callback_light_off, self.ref_entity.entity_id, new='off')
self.sync_state() loop.run_until_complete(self.sync_state())
def sync_state(self): async def sync_state(self):
"""Synchronizes the callbacks with the state of the light. """Synchronizes the callbacks with the state of the light.
Essentially mimics the `state_change` callback based on the current state of the light. Essentially mimics the `state_change` callback based on the current state of the light.
""" """
if self.ref_entity_state: if self.ref_entity_state:
self.callback_light_on() await self.callback_light_on()
else: else:
self.callback_light_off() await self.callback_light_off()
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}') self.log(f'Waiting for motion on {self.sensor.friendly_name}')
self.motion_on_handle = self.listen_state( self.motion_on_handle = 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',
@@ -58,11 +77,11 @@ class Motion(Hass):
cause='motion on' cause='motion on'
) )
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}') self.log(f'Waiting for motion to stop on {self.sensor.friendly_name}')
self.motion_off_handle = self.listen_state( self.motion_off_handle = 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',
@@ -71,46 +90,46 @@ class Motion(Hass):
cause='motion off' cause='motion off'
) )
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') self.log('Light on callback')
self.cancel_motion_callback(new='on') await self.cancel_motion_callback(new='on')
self.listen_motion_off(self.off_duration) await self.listen_motion_off(await self.app.off_duration())
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('Light off callback')
self.cancel_motion_callback(new='off') await self.cancel_motion_callback(new='off')
self.listen_motion_on() await self.listen_motion_on()
def get_app_callbacks(self, name: str = None): async def get_app_callbacks(self, name: str = None):
"""Gets all the callbacks associated with the app """Gets all the callbacks associated with the app
""" """
name = name or self.name name = name or self.name
callbacks = { callbacks = {
handle: info handle: info
for app_name, callbacks in self.get_callback_entries().items() for app_name, callbacks in (await self.get_callback_entries()).items()
for handle, info in callbacks.items() for handle, info in callbacks.items()
if app_name == name if app_name == name
} }
return callbacks return callbacks
def get_sensor_callbacks(self): async def get_sensor_callbacks(self):
return { return {
handle: info handle: info
for handle, info in self.get_app_callbacks().items() for handle, info in (await self.get_app_callbacks()).items()
if info['entity'] == self.sensor.entity_id if info['entity'] == self.sensor.entity_id
} }
def cancel_motion_callback(self, new: str): async def cancel_motion_callback(self, new: str):
callbacks = self.get_sensor_callbacks() callbacks = await self.get_sensor_callbacks()
# self.log(f'Found {len(callbacks)}') # self.log(f'Found {len(callbacks)}')
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']) new_match = re.match('new=(?P<new>.*?)\s', info['kwargs'])
# self.log(f'{handle}: {info["entity"]}: {info["kwargs"]}') # self.log(f'{handle}: {info["entity"]}: {info["kwargs"]}')
if new_match is not None and new_match.group("new") == new: if new_match is not None and new_match.group("new") == new:
self.cancel_listen_state(handle) await self.cancel_listen_state(handle)
self.log(f'cancelled: {self.friendly_name(entity)}: {new}') self.log(f'cancelled: {await self.friendly_name(entity)}: {new}')

View File

@@ -1,8 +1,11 @@
import asyncio
from copy import deepcopy from copy import deepcopy
from datetime import time, timedelta from datetime import time, timedelta
from typing import List from typing import List
import appdaemon.utils as utils
import astral import astral
from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass from appdaemon.plugins.hass.hassapi import Hass
from appdaemon.plugins.mqtt.mqttapi import Mqtt from appdaemon.plugins.mqtt.mqttapi import Mqtt
@@ -17,40 +20,63 @@ class RoomController(Hass, Mqtt):
- When the light comes on, check if it's attributes match what they should, given the time. - When the light comes on, check if it's attributes match what they should, given the time.
""" """
def initialize(self): async def initialize(self):
self.app_entities = self.gather_app_entities() self.app_entities = await self.gather_app_entities()
self.refresh_state_times() self.log(f'entities: {self.app_entities}')
self.run_daily(callback=self.refresh_state_times, start='00:00:00') await self.refresh_state_times()
await self.run_daily(callback=self.refresh_state_times, start='00:00:00')
if (ha_button := self.args.get('ha_button')): # if (ha_button := self.args.get('ha_button')):
self.log(f'Setting up input button: {self.friendly_name(ha_button)}') # self.log(f'Setting up input button: {self.friendly_name(ha_button)}')
self.listen_state(callback=self.activate_any_on, entity_id=ha_button) # self.listen_state(callback=self.activate_any_on, entity_id=ha_button)
def refresh_state_times(self, *args, **kwargs): async def gather_app_entities(self) -> List[str]:
"""Returns a list of all the entities involved in any of the states
"""
async def async_generator():
for settings in deepcopy(self.args['states']):
if (scene := settings.get('scene')):
if isinstance(scene, str):
assert scene.startswith('scene.'), f"Scene definition must start with 'scene.' for app {self.name}"
entity: Entity = self.get_entity(scene)
entity_state = await entity.get_state('all')
attributes = entity_state['attributes']
for entity in attributes['entity_id']:
yield entity
else:
for key in scene.keys():
yield key
else:
yield self.args['entity']
entities = [e async for e in async_generator()]
return set(entities)
async def refresh_state_times(self, *args, **kwargs):
"""Resets the `self.states` attribute to a newly parsed version of the states. """Resets the `self.states` attribute to a newly parsed version of the states.
Parsed states have an absolute time for a certain day. Parsed states have an absolute time for a certain day.
""" """
# re-parse the state strings into times for the current day # re-parse the state strings into times for the current day
self.states = self.parse_states() self.states = await self.parse_states()
# schedule the transitions # schedule the transitions
for state in self.states: for state in self.states:
dt = str(state['time'])[:8] dt = str(state['time'])[:8]
self.log(f'Scheduling transition at: {dt}') self.log(f'Scheduling transition at: {dt}')
try: try:
self.run_at(callback=self.activate_any_on, start=dt) await self.run_at(callback=self.activate_any_on, start=dt)
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}')
def parse_states(self): async def parse_states(self):
def gen(): async def gen():
for state in deepcopy(self.args['states']): for state in deepcopy(self.args['states']):
if (time := state.get('time')): if (time := state.get('time')):
state['time'] = self.parse_time(time) state['time'] = await self.parse_time(time)
elif isinstance((elevation := state.get('elevation')), (int, float)): elif isinstance((elevation := state.get('elevation')), (int, float)):
assert 'direction' in state, f'State needs a direction if it has an elevation' assert 'direction' in state, f'State needs a direction if it has an elevation'
@@ -71,17 +97,20 @@ class RoomController(Hass, Mqtt):
yield state yield state
states = sorted(gen(), key=lambda s: s['time']) states = [s async for s in gen()]
states = sorted(states, key=lambda s: s['time'])
return states return states
def current_state(self, time: time = None): async def current_state(self, time: time = None):
if self.sleep_bool: if (await self.sleep_bool()):
if (state := self.args.get('sleep_state')): if (state := self.args.get('sleep_state')):
return state return state
else: else:
return {} return {}
else: else:
time = time or self.get_now().time() now = await self.get_now()
self.log(f'Getting state for datetime: {now}')
time = time or (await self.get_now()).time()
for state in self.states[::-1]: for state in self.states[::-1]:
if state['time'] <= time: if state['time'] <= time:
self.log(f'Selected state from {state["time"]}') self.log(f'Selected state from {state["time"]}')
@@ -89,26 +118,10 @@ class RoomController(Hass, Mqtt):
else: else:
return self.states[-1] return self.states[-1]
def current_scene(self, time: time = None): async def current_scene(self, time: time = None):
if (state := 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']
def gather_app_entities(self) -> List[str]:
"""Returns a list of all the entities involved in any of the states
"""
def gen():
for settings in deepcopy(self.args['states']):
# dt = self.parse_time(settings.pop('time'))
if (scene := settings.get('scene')):
if isinstance(scene, str):
yield from self.get_entity(scene).get_state('all')['attributes']['entity_id']
else:
yield from scene.keys()
else:
yield self.args['entity']
return list(set(gen()))
@property @property
def all_off(self) -> bool: def all_off(self) -> bool:
""""All off" is the logic opposite of "any on" """"All off" is the logic opposite of "any on"
@@ -127,26 +140,23 @@ class RoomController(Hass, Mqtt):
""" """
return any(self.get_state(entity) == 'on' for entity in self.app_entities) return any(self.get_state(entity) == 'on' for entity in self.app_entities)
@property async def sleep_bool(self) -> bool:
def sleep_bool(self) -> bool:
if (sleep_var := self.args.get('sleep')): if (sleep_var := self.args.get('sleep')):
return self.get_state(sleep_var) == 'on' return (await self.get_state(sleep_var)) == 'on'
else: else:
# self.log('WARNING')
return False return False
@sleep_bool.setter # @sleep_bool.setter
def sleep_bool(self, val) -> bool: # def sleep_bool(self, val) -> bool:
if (sleep_var := self.args.get('sleep')): # if (sleep_var := self.args.get('sleep')):
if isinstance(val, str): # if isinstance(val, str):
self.set_state(sleep_var, state=val) # self.set_state(sleep_var, state=val)
elif isinstance(val, bool): # elif isinstance(val, bool):
self.set_state(sleep_var, state='on' if val else 'off') # self.set_state(sleep_var, state='on' if val else 'off')
else: # else:
raise ValueError('Sleep variable is undefined') # raise ValueError('Sleep variable is undefined')
@property async def off_duration(self) -> timedelta:
def off_duration(self) -> timedelta:
"""Determines the time that the motion sensor has to be clear before deactivating """Determines the time that the motion sensor has to be clear before deactivating
Priority: Priority:
@@ -156,8 +166,8 @@ class RoomController(Hass, Mqtt):
- Sleep - 0 - Sleep - 0
""" """
current_state = await self.current_state()
duration_str = self.current_state().get( duration_str = current_state.get(
'off_duration', 'off_duration',
self.args.get('off_duration', '00:00:00') self.args.get('off_duration', '00:00:00')
) )
@@ -168,9 +178,9 @@ class RoomController(Hass, Mqtt):
except Exception: except Exception:
return timedelta() return timedelta()
def activate(self, *args, cause: str = 'unknown', **kwargs): async def activate(self, *args, cause: str = 'unknown', **kwargs):
self.log(f'Activating: {cause}') self.log(f'Activating: {cause}')
scene = self.current_scene() scene = await self.current_scene()
if isinstance(scene, str): if isinstance(scene, str):
self.turn_on(scene) self.turn_on(scene)
@@ -188,24 +198,24 @@ class RoomController(Hass, Mqtt):
elif scene is None: elif scene is None:
self.log(f'No scene, ignoring...') self.log(f'No scene, ignoring...')
# Need to act as if the light had just turned off to reset the motion (and maybe other things?) # Need to act as if the light had just turned off to reset the motion (and maybe other things?)
self.callback_light_off() # self.callback_light_off()
else: else:
self.log(f'ERROR: unknown scene: {scene}') self.log(f'ERROR: unknown scene: {scene}')
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
""" """
if self.all_off: if self.all_off:
self.log(f'Activate all off kwargs: {kwargs}') 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')
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
""" """
if self.any_on: if self.any_on:
self.activate(*args, **kwargs) await self.activate(*args, **kwargs)
else: else:
self.log(f'Skipped activating - everything is off') self.log(f'Skipped activating - everything is off')