From 82d8136e9745f74740b6f48cbf3ceb805dc6431b Mon Sep 17 00:00:00 2001 From: root Date: Sun, 30 Jul 2023 13:51:46 -0500 Subject: [PATCH] added room_control as submodule --- .gitmodules | 3 + apps/room_control | 1 + apps/rooms/basic_motion.py | 382 ------------------------------------ apps/rooms/bathroom.yaml | 36 ++++ apps/rooms/bedroom.yaml | 65 ++++++ apps/rooms/closet.yaml | 27 +++ apps/rooms/kitchen.yaml | 39 ++++ apps/rooms/living_room.yaml | 70 +++++++ apps/rooms/rooms.yaml | 248 ----------------------- docker-compose.yml | 3 +- 10 files changed, 243 insertions(+), 631 deletions(-) create mode 100644 .gitmodules create mode 160000 apps/room_control delete mode 100755 apps/rooms/basic_motion.py create mode 100644 apps/rooms/bathroom.yaml create mode 100644 apps/rooms/bedroom.yaml create mode 100644 apps/rooms/closet.yaml create mode 100755 apps/rooms/kitchen.yaml create mode 100644 apps/rooms/living_room.yaml delete mode 100755 apps/rooms/rooms.yaml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7125fa9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "apps/room_control"] + path = apps/room_control + url = https://github.com/jsl12/room_control diff --git a/apps/room_control b/apps/room_control new file mode 160000 index 0000000..221bd81 --- /dev/null +++ b/apps/room_control @@ -0,0 +1 @@ +Subproject commit 221bd81fe3306ab5fd44fab473e4c6ab98b7c0a6 diff --git a/apps/rooms/basic_motion.py b/apps/rooms/basic_motion.py deleted file mode 100755 index bfefe04..0000000 --- a/apps/rooms/basic_motion.py +++ /dev/null @@ -1,382 +0,0 @@ -from copy import deepcopy -from datetime import time, timedelta -from typing import Dict, List, Tuple, Union - -import astral -from appdaemon.plugins.hass.hassapi import Hass -from continuous import Continuous - - -class MotionLight(Hass): - """Class for linking an light with a motion sensor. - - - Separate the turning on and turning off functions. - - Use the state change of the light to set up the event for changing to the other state - - `handle_on` - - `handle_off` - - When the light comes on, check if it's attributes match what they should, given the time. - """ - - def initialize(self): - self.state_change_handle = self.listen_state(self.handle_state_change, self.entity) - self.refresh_state_times() - self.sync_state() - self.run_daily(callback=self.refresh_state_times, start='00:00:00') - - self.app_entities = self.gather_app_entities() - - if (button := self.args.get('button')): - if not isinstance(button, list): - button = [button] - for button in button: - self.log(f'Setting up button: {button}') - self.listen_event(self.handle_button, event='deconz_event', id=button) - - if (door := self.args.get('door')): - door_entity = self.get_entity(door) - self.log(f'Setting up door: {door_entity.friendly_name}') - self.listen_state( - callback=self.activate_all_off, - entity_id=door, - new='on' - ) - - @property - def sensor(self) -> str: - return self.args['sensor'] - - @property - def entity(self) -> str: - return self.args['entity'] - - @property - def off_duration(self) -> timedelta: - """Determines the time that the motion sensor has to be clear before deactivating - - Priority: - - Value in scene definition - - Default value - - Normal - value in app definition - - Sleep - 0 - - """ - - duration_str = self.current_state().get( - 'off_duration', - self.args.get('off_duration', '00:00:00') - ) - - try: - hours, minutes, seconds = map(int, duration_str.split(':')) - return timedelta(hours=hours, minutes=minutes, seconds=seconds) - except Exception: - return timedelta() - - @property - def entity_state(self) -> bool: - return self.get_state(self.entity) == 'on' - - @entity_state.setter - def entity_state(self, new): - if isinstance(new, str): - if new == 'on': - self.turn_on(self.entity) - elif new == 'off': - self.turn_on(self.entity) - else: - raise ValueError(f'Invalid value for entity state: {new}') - elif isinstance(new, bool): - if new: - self.turn_on(self.entity) - self.log(f'Turned on {self.friendly_name(self.entity)}') - else: - self.turn_off(self.entity) - self.log(f'Turned off {self.friendly_name(self.entity)}') - elif isinstance(new, dict): - if any(isinstance(val, dict) for val in new.values()): - # self.log(f'Setting scene with nested dict: {new}') - for entity, state in new.items(): - if state.pop('state', 'on') == 'on': - # self.log(f'Setting {entity} state with: {state}') - self.turn_on(entity_id=entity, **state) - else: - self.turn_off(entity) - else: - if new.pop('state', 'on') == 'on': - self.turn_on(self.entity, **new) - else: - self.turn_off(self.entity) - - else: - raise TypeError(f'Invalid type: {type(new)}: {new}') - - @property - def is_stateful(self): - return 'scene' in self.args and isinstance(self.args['scene'], (list, dict)) - - def parse_states(self): - def gen(): - for state in deepcopy(self.args['scene']): - if (time := state.get('time')): - state['time'] = 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' - - if state['direction'] == 'rising': - dir = astral.SunDirection.RISING - elif state['direction'] == 'setting': - dir = astral.SunDirection.SETTING - else: - raise ValueError(f'Invalid sun direction: {state["direction"]}') - - state['time'] = self.AD.sched.location.time_at_elevation( - elevation=elevation, direction=dir - ).time() - - else: - raise ValueError(f'Missing time') - - yield state - - states = sorted(gen(), key=lambda s: s['time']) - return states - - def current_state(self, time: time = None): - if self.sleep_bool: - if (state := self.args.get('sleep_state')): - return state - else: - time = time or self.get_now().time() - for state in self.states[::-1]: - if state['time'] <= time: - return state - else: - return self.states[-1] - - def current_scene(self, time: time = None): - return self.current_state(time=time)['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['scene']): - # 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" - - Returns: - bool: Whether all the lights associated with the app are off - """ - return all(self.get_state(entity) != 'on' for entity in self.app_entities) - - @property - def any_on(self) -> bool: - """"Any on" is the logic opposite of "all off" - - Returns: - 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) - - @property - def delay(self) -> timedelta: - try: - hours, minutes, seconds = map(int, self.args['delay'].split(':')) - return timedelta(hours=hours, minutes=minutes, seconds=seconds) - except Exception: - return timedelta() - - @property - def sleep_bool(self) -> bool: - if (sleep_var := self.args.get('sleep')): - return 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') - - 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.entity_state: - self.callback_light_on() - else: - self.callback_light_off() - - def listen_motion_on(self): - """Sets up the motion on callback to activate the room - """ - self.log(f'Waiting for motion on {self.friendly_name(self.sensor)} to turn on {self.friendly_name(self.entity)}') - self.motion_on_handle = self.listen_state( - callback=self.activate, - entity_id=self.sensor, - new='on', - oneshot=True - ) - - 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.friendly_name(self.sensor)} for {duration} to turn off {self.friendly_name(self.entity)}') - self.motion_off_handle = self.listen_state( - callback=self.deactivate, - entity_id=self.sensor, - new='off', - duration=duration.total_seconds(), - oneshot=True - ) - - def handle_state_change(self, entity=None, attribute=None, old=None, new=None, kwargs=None): - """Callback attached to the state change of the light. - """ - if new == 'on': - self.callback_light_on(entity, attribute, old, new, kwargs) - elif new == 'off': - self.callback_light_off(entity, attribute, old, new, kwargs) - else: - self.log(f'Unknown state: {new}') - - 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) - - 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() - - def activate(self, *args, **kwargs): - self.log('Activating') - scene = self.current_scene() - - if isinstance(scene, str): - self.turn_on(scene) - self.log(f'Turned on scene: {scene}') - - elif isinstance(scene, dict): - # makes setting the state to 'on' optional in the yaml definition - for entity, settings in scene.items(): - if 'state' not in settings: - scene[entity]['state'] = 'on' - - self.call_service('scene/apply', entities=scene, transition=0) - self.log(f'Applied scene: {scene}') - - 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() - else: - self.log(f'ERROR: unknown scene: {scene}') - - def activate_all_off(self, *args, **kwargs): - if self.all_off: - self.activate() - else: - self.log(f'Skipped activating - everything is not off') - - def activate_any_on(self, kwargs): - if self.any_on: - self.activate() - else: - self.log(f'Skipped activating - everything is not off') - - def deactivate(self, *args, **kwargs): - self.log('Deactivating') - for entity in self.app_entities: - self.turn_off(entity) - self.log(f'Turned off {entity}') - - 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() - - # 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) - 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 handle_button(self, event_name, data, kwargs): - # event 1002 is a single button press - if data['event'] == 1002: - self.log(f'{data["id"]} single click') - if self.entity_state: - self.deactivate() - else: - self.activate() - - # event 1001 is a long press start - elif data['event'] == 1001: - self.log(f'{data["id"]} long press down') - if 'delay' in self.args and self.entity_state: - self.cancel_motion_callback(new='off') - self.listen_motion_off(self.delay) - self.turn_on(self.entity, brightness_pct=100) - - # event 1004 is a double click - elif data['event'] == 1004: - self.log(f'{data["id"]} double click') - if 'sleep' in self.args: - self.sleep_bool = not self.sleep_bool - # self.cancel_motion_callback(new='off') - # self.callback_light_on() - self.activate() - - def get_app_callbacks(self, name: str = None): - name = name or self.name - for app_name, callbacks in self.get_callback_entries().items(): - if app_name == name: - return callbacks - - def get_motion_callback(self): - return { - handle: info - for handle, info in self.get_app_callbacks().items() - if info['entity'] == self.sensor - } - - def cancel_motion_callback(self, new: str): - for handle, info in self.get_motion_callback().items(): - if f'new={new}' in info['kwargs']: - self.log(f'Cancelling callback for {info}') - self.cancel_listen_state(handle) diff --git a/apps/rooms/bathroom.yaml b/apps/rooms/bathroom.yaml new file mode 100644 index 0000000..d3a7a44 --- /dev/null +++ b/apps/rooms/bathroom.yaml @@ -0,0 +1,36 @@ +bathroom: + module: room_control + class: RoomController + entity: light.bathroom + sensor: binary_sensor.bathroom_motion_occupancy + off_duration: '00:05:00' + button: bathroom + delay: '00:20:00' + scene: + - time: '05:00:00' + scene: + light.bathroom: + brightness_pct: 40 + color_temp: 250 + - time: '12:00:00' + scene: + light.bathroom: + brightness_pct: 70 + color_temp: 300 + - time: sunset + scene: + light.bathroom: + brightness_pct: 50 + color_temp: 350 + - time: '23:00:00' + scene: + light.bathroom: + brightness_pct: 20 + color_temp: 350 + sleep: input_boolean.sleeping + sleep_state: + off_duration: '00:02:00' + scene: + light.bathroom: + brightness_pct: 10 + color_temp: 250 \ No newline at end of file diff --git a/apps/rooms/bedroom.yaml b/apps/rooms/bedroom.yaml new file mode 100644 index 0000000..0677397 --- /dev/null +++ b/apps/rooms/bedroom.yaml @@ -0,0 +1,65 @@ +bedroom: + module: room_control + class: RoomController + entity: light.bedroom + sensor: binary_sensor.bedroom_motion_occupancy + off_duration: '00:05:00' + button: + - bedroom + - bedroom2 + scene: + - time: 'sunrise - 03:00:00' + scene: + light.bedroom: + state: on + color_temp: 200 + brightness_pct: 20 + light.globe: + state: on + color_temp: 200 + brightness_pct: 20 + light.overhead: + state: off + - time: '06:00:00' + scene: + light.bedroom: + state: on + color_temp: 250 + brightness_pct: 20 + light.globe: + state: on + color_temp: 250 + brightness_pct: 20 + light.overhead: + state: on + color_temp: 250 + brightness_pct: 15 + - time: '12:00:00' + scene: + light.bedroom: + state: on + color_temp: 325 + brightness_pct: 30 + light.globe: + state: on + color_temp: 325 + brightness_pct: 30 + light.overhead: + state: on + color_temp: 325 + brightness_pct: 70 + - time: 'sunset' + scene: + light.bedroom: + state: on + color_temp: 325 + brightness_pct: 50 + light.globe: + state: on + color_temp: 325 + brightness_pct: 50 + light.overhead: + state: on + color_temp: 350 + brightness_pct: 10 + sleep: input_boolean.sleeping \ No newline at end of file diff --git a/apps/rooms/closet.yaml b/apps/rooms/closet.yaml new file mode 100644 index 0000000..cd12757 --- /dev/null +++ b/apps/rooms/closet.yaml @@ -0,0 +1,27 @@ +closet: + module: room_control + class: RoomController + entity: light.closet + sensor: binary_sensor.closet_motion_occupancy + off_duration: '00:02:00' + scene: + - time: 'sunrise - 03:00:00' + scene: + light.closet: + brightness_pct: 10 + color_temp: 200 + - time: 'sunrise' + scene: + light.closet: + brightness_pct: 30 + color_temp: 200 + - time: '12:00:00' + scene: + light.closet: + brightness_pct: 70 + color_temp: 300 + - time: sunset + scene: + light.closet: + brightness_pct: 40 + color_temp: 400 \ No newline at end of file diff --git a/apps/rooms/kitchen.yaml b/apps/rooms/kitchen.yaml new file mode 100755 index 0000000..4879c52 --- /dev/null +++ b/apps/rooms/kitchen.yaml @@ -0,0 +1,39 @@ +kitchen: + module: room_control + class: RoomController + entity: light.kitchen + sensor: binary_sensor.kitchen_motion_occupancy + off_duration: '00:10:00' + button: kitchen + scene: + - time: sunrise + scene: + light.kitchen: + state: on + color_temp: 200 + brightness_pct: 10 + - time: '12:00:00' + scene: + light.kitchen: + state: on + color_temp: 300 + brightness_pct: 30 + - time: sunset + scene: + light.kitchen: + state: on + color_temp: 450 + brightness_pct: 40 + - time: '22:00:00' + off_duration: '00:02:00' + scene: + light.kitchen: + state: on + color_temp: 650 + brightness_pct: 10 + sleep: input_boolean.sleeping + sleep_state: + scene: + light.kitchen: + state: on + brightness_pct: 1 diff --git a/apps/rooms/living_room.yaml b/apps/rooms/living_room.yaml new file mode 100644 index 0000000..ec9dfc0 --- /dev/null +++ b/apps/rooms/living_room.yaml @@ -0,0 +1,70 @@ +living_room: + module: room_control + class: RoomController + sensor: binary_sensor.living_room_motion_occupancy + off_duration: 00:30:00 + entity: light.living_room + button: living_room + door: binary_sensor.door_front + scene: + - time: sunrise + scene: + light.living_room: + state: on + color_temp: 200 + brightness_pct: 30 + light.couch_corner: + state: on + color_temp: 200 + brightness_pct: 7 + - time: '09:00:00' + scene: + light.living_room: + state: on + color_temp: 250 + brightness_pct: 50 + light.couch_corner: + state: on + color_temp: 250 + brightness_pct: 25 + - time: '12:00:00' + scene: + light.living_room: + state: on + color_temp: 300 + brightness_pct: 100 + light.couch_corner: + state: on + color_temp: 450 + brightness_pct: 50 + - time: sunset + off_duration: 01:00:00 + scene: + light.living_room: + state: on + color_temp: 350 + brightness_pct: 70 + light.couch_corner: + state: on + color_temp: 650 + brightness_pct: 10 + - elevation: -20 + direction: setting + off_duration: 00:30:00 + scene: + light.living_room: + state: on + color_temp: 350 + brightness_pct: 50 + light.couch_corner: + state: on + color_temp: 650 + brightness_pct: 5 + sleep: input_boolean.sleeping + sleep_state: + off_duration: '00:02:00' + scene: + light.living_room: + state: 'on' + color_name: 'red' + brightness_pct: 50 \ No newline at end of file diff --git a/apps/rooms/rooms.yaml b/apps/rooms/rooms.yaml deleted file mode 100755 index 9be6655..0000000 --- a/apps/rooms/rooms.yaml +++ /dev/null @@ -1,248 +0,0 @@ -bathroom: - module: basic_motion - class: MotionLight - entity: light.bathroom - sensor: binary_sensor.bathroom_motion_occupancy - off_duration: '00:05:00' - button: bathroom - delay: '00:20:00' - scene: - - time: '05:00:00' - scene: - light.bathroom: - brightness_pct: 40 - color_temp: 250 - - time: '12:00:00' - scene: - light.bathroom: - brightness_pct: 70 - color_temp: 300 - - time: sunset - scene: - light.bathroom: - brightness_pct: 50 - color_temp: 350 - - time: '23:00:00' - scene: - light.bathroom: - brightness_pct: 20 - color_temp: 350 - sleep: input_boolean.sleeping - sleep_state: - off_duration: '00:02:00' - scene: - light.bathroom: - brightness_pct: 10 - color_temp: 250 - -closet: - module: basic_motion - class: MotionLight - entity: light.closet - sensor: binary_sensor.closet_motion_occupancy - off_duration: '00:02:00' - scene: - - time: 'sunrise - 03:00:00' - scene: - light.closet: - brightness_pct: 10 - color_temp: 200 - - time: 'sunrise' - scene: - light.closet: - brightness_pct: 30 - color_temp: 200 - - time: '12:00:00' - scene: - light.closet: - brightness_pct: 70 - color_temp: 300 - - time: sunset - scene: - light.closet: - brightness_pct: 40 - color_temp: 400 - -bedroom: - module: basic_motion - class: MotionLight - entity: light.bedroom - sensor: binary_sensor.bedroom_motion_occupancy - off_duration: '00:05:00' - button: - - bedroom - - bedroom2 - scene: - - time: 'sunrise - 03:00:00' - scene: - light.bedroom: - state: on - color_temp: 200 - brightness_pct: 20 - light.globe: - state: on - color_temp: 200 - brightness_pct: 20 - light.overhead: - state: off - - time: '06:00:00' - scene: - light.bedroom: - state: on - color_temp: 250 - brightness_pct: 20 - light.globe: - state: on - color_temp: 250 - brightness_pct: 20 - light.overhead: - state: on - color_temp: 250 - brightness_pct: 15 - - time: '12:00:00' - scene: - light.bedroom: - state: on - color_temp: 325 - brightness_pct: 30 - light.globe: - state: on - color_temp: 325 - brightness_pct: 30 - light.overhead: - state: on - color_temp: 325 - brightness_pct: 70 - - time: 'sunset' - scene: - light.bedroom: - state: on - color_temp: 325 - brightness_pct: 50 - light.globe: - state: on - color_temp: 325 - brightness_pct: 50 - light.overhead: - state: on - color_temp: 350 - brightness_pct: 10 - sleep: input_boolean.sleeping - -living_room: - module: basic_motion - class: MotionLight - sensor: binary_sensor.living_room_motion_occupancy - off_duration: 00:30:00 - entity: light.living_room - button: living_room - door: binary_sensor.door_front - scene: - - time: sunrise - scene: - light.living_room: - state: on - color_temp: 200 - brightness_pct: 30 - light.couch_corner: - state: on - color_temp: 200 - brightness_pct: 7 - - time: '09:00:00' - scene: - light.living_room: - state: on - color_temp: 250 - brightness_pct: 50 - light.couch_corner: - state: on - color_temp: 250 - brightness_pct: 25 - - time: '12:00:00' - scene: - light.living_room: - state: on - color_temp: 300 - brightness_pct: 100 - light.couch_corner: - state: on - color_temp: 450 - brightness_pct: 50 - - time: sunset - off_duration: 01:00:00 - scene: - light.living_room: - state: on - color_temp: 350 - brightness_pct: 70 - light.couch_corner: - state: on - color_temp: 650 - brightness_pct: 10 - - elevation: -20 - direction: setting - off_duration: 00:30:00 - scene: - light.living_room: - state: on - color_temp: 350 - brightness_pct: 50 - light.couch_corner: - state: on - color_temp: 650 - brightness_pct: 5 - sleep: input_boolean.sleeping - sleep_state: - off_duration: '00:02:00' - scene: - light.living_room: - state: 'on' - color_name: 'red' - brightness_pct: 50 - -kitchen: - module: basic_motion - class: MotionLight - entity: light.kitchen - sensor: binary_sensor.kitchen_motion_occupancy - off_duration: '00:10:00' - button: kitchen - scene: - - time: sunrise - scene: - light.kitchen: - state: on - color_temp: 200 - brightness_pct: 10 - - time: '12:00:00' - scene: - light.kitchen: - state: on - color_temp: 300 - brightness_pct: 30 - - time: sunset - scene: - light.kitchen: - state: on - color_temp: 450 - brightness_pct: 40 - - time: '22:00:00' - off_duration: '00:02:00' - scene: - light.kitchen: - state: on - color_temp: 650 - brightness_pct: 10 - sleep: input_boolean.sleeping - sleep_state: - scene: - light.kitchen: - state: on - brightness_pct: 1 - -# patio: -# module: patio -# class: PatioLight -# light: light.patio -# linked: light.living_room -# door: binary_sensor.back_door diff --git a/docker-compose.yml b/docker-compose.yml index f5262bc..1ca0127 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,10 +11,11 @@ services: - 5050:5050 restart: unless-stopped + volumes: config: driver: local driver_opts: o: bind type: none - device: ./ \ No newline at end of file + device: ./