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
from appdaemon.plugins.mqtt.mqttapi import Mqtt
@@ -7,8 +7,10 @@ from room_control import RoomController
class ButtonController(Mqtt):
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.app: RoomController = self.get_app(self.args['app'])
# self.log(f'Done')
def setup_buttons(self, buttons):
if isinstance(buttons, list):
@@ -21,11 +23,11 @@ class ButtonController(Mqtt):
topic = f'zigbee2mqtt/{name}'
self.mqtt_subscribe(topic, namespace='mqtt')
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']
# self.log(f'Button event for: {topic}')
self.log(f'Button event for: {topic}')
try:
payload = json.loads(data['payload'])
action = payload['action']
@@ -36,16 +38,17 @@ class ButtonController(Mqtt):
return
else:
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 == '':
return
elif action == 'single':
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)
else:
self.app.activate(cause=cause)
await self.app.activate(cause=cause)
else:
pass

12
door.py
View File

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

View File

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

View File

@@ -1,8 +1,11 @@
import asyncio
from copy import deepcopy
from datetime import time, timedelta
from typing import List
import appdaemon.utils as utils
import astral
from appdaemon.entity import Entity
from appdaemon.plugins.hass.hassapi import Hass
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.
"""
def initialize(self):
self.app_entities = self.gather_app_entities()
self.refresh_state_times()
self.run_daily(callback=self.refresh_state_times, start='00:00:00')
async def initialize(self):
self.app_entities = await self.gather_app_entities()
self.log(f'entities: {self.app_entities}')
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')):
self.log(f'Setting up input button: {self.friendly_name(ha_button)}')
self.listen_state(callback=self.activate_any_on, entity_id=ha_button)
# if (ha_button := self.args.get('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)
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.
Parsed states have an absolute time for a certain 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
for state in self.states:
dt = str(state['time'])[:8]
self.log(f'Scheduling transition at: {dt}')
try:
self.run_at(callback=self.activate_any_on, start=dt)
await self.run_at(callback=self.activate_any_on, start=dt)
except ValueError:
# happens when the callback time is in the past
pass
except Exception as e:
self.log(f'Failed with {type(e)}: {e}')
def parse_states(self):
def gen():
async def parse_states(self):
async def gen():
for state in deepcopy(self.args['states']):
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)):
assert 'direction' in state, f'State needs a direction if it has an elevation'
@@ -71,17 +97,20 @@ class RoomController(Hass, Mqtt):
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
def current_state(self, time: time = None):
if self.sleep_bool:
async def current_state(self, time: time = None):
if (await self.sleep_bool()):
if (state := self.args.get('sleep_state')):
return state
else:
return {}
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]:
if state['time'] <= time:
self.log(f'Selected state from {state["time"]}')
@@ -89,26 +118,10 @@ class RoomController(Hass, Mqtt):
else:
return self.states[-1]
def current_scene(self, time: time = None):
if (state := self.current_state(time=time)) is not None:
async def current_scene(self, time: time = None):
if (state := (await self.current_state(time=time))) is not None:
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
def all_off(self) -> bool:
""""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)
@property
def sleep_bool(self) -> bool:
async def sleep_bool(self) -> bool:
if (sleep_var := self.args.get('sleep')):
return self.get_state(sleep_var) == 'on'
return (await self.get_state(sleep_var)) == 'on'
else:
# self.log('WARNING')
return False
@sleep_bool.setter
def sleep_bool(self, val) -> bool:
if (sleep_var := self.args.get('sleep')):
if isinstance(val, str):
self.set_state(sleep_var, state=val)
elif isinstance(val, bool):
self.set_state(sleep_var, state='on' if val else 'off')
else:
raise ValueError('Sleep variable is undefined')
# @sleep_bool.setter
# def sleep_bool(self, val) -> bool:
# if (sleep_var := self.args.get('sleep')):
# if isinstance(val, str):
# self.set_state(sleep_var, state=val)
# elif isinstance(val, bool):
# self.set_state(sleep_var, state='on' if val else 'off')
# else:
# raise ValueError('Sleep variable is undefined')
@property
def off_duration(self) -> timedelta:
async def off_duration(self) -> timedelta:
"""Determines the time that the motion sensor has to be clear before deactivating
Priority:
@@ -156,8 +166,8 @@ class RoomController(Hass, Mqtt):
- Sleep - 0
"""
duration_str = self.current_state().get(
current_state = await self.current_state()
duration_str = current_state.get(
'off_duration',
self.args.get('off_duration', '00:00:00')
)
@@ -168,9 +178,9 @@ class RoomController(Hass, Mqtt):
except Exception:
return timedelta()
def activate(self, *args, cause: str = 'unknown', **kwargs):
async def activate(self, *args, cause: str = 'unknown', **kwargs):
self.log(f'Activating: {cause}')
scene = self.current_scene()
scene = await self.current_scene()
if isinstance(scene, str):
self.turn_on(scene)
@@ -188,24 +198,24 @@ class RoomController(Hass, Mqtt):
elif scene is None:
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?)
self.callback_light_off()
# self.callback_light_off()
else:
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
"""
if self.all_off:
self.log(f'Activate all off kwargs: {kwargs}')
self.activate(*args, **kwargs)
await self.activate(*args, **kwargs)
else:
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
"""
if self.any_on:
self.activate(*args, **kwargs)
await self.activate(*args, **kwargs)
else:
self.log(f'Skipped activating - everything is off')