commit 8f0412d712e3bee3e40811c6a07f1e44583e28ea Author: Dessa Simpson Date: Mon May 16 07:00:43 2022 +0000 Initial commit diff --git a/wallmote.py b/wallmote.py new file mode 100644 index 0000000..8eb6c31 --- /dev/null +++ b/wallmote.py @@ -0,0 +1,356 @@ +#!/usr/bin/python3 +import hassapi as hass +from threading import Timer + +############### +# Action Tree # +############### + +""" +1: States +1: Lights + 1: Kitchen Lights + 1: Off + 2: 10% + 3: 55% + 4: 100% + 2: Main Room Lights + [...] + 3: Living Room Lights + [...] + 4: Bedroom Lights + [...] +2: A/C + 2: Main Room AC + 1: Power + 1: On + 2: On + 3: Off + 4: Off + 2: Mode + 1: Cool + 2: Heat + 3: Fan + 3: Fan Level + 1: Auto + 2: Quiet + 3: Medium + 4: High + 4: Temperature + 1: 72 + 2: 74 + 3: 76 + 4: 78 + 3: Living Room AC + [...] + 4: Bedroom AC + [...] +3: Fans + 1: Kitchen Fan + 1: Off + 2: 1 + 3: 2 + 4: 3 + 3: Living Room Fan + [...] + 4: Bedroom Fan + [...] +2: Miscellaneous +1: Effects + 1: Clear light RGB strip notifications +2: Music + 1: Join/unjoin speakers + 1. Unjoin speakers + # Select master + 3. Living Room Sonos + 4. Bedroom Sonos + 2: Play Music + 3: Play on Living Room Sonos + 1: Spotify: Liked Songs + 2: Spotify: Getting Things Done + 4: Play on Bedroom Sonos + 1: Spotify: Liked Songs + 2: Spotify: Chill Sleep + 3: Media Controls + 3: Living Room Sonos + 1. Play + 2. Pause + 3. Previous + 4. Next + 4: Bedroom Sonos + [...] + 4. Volume: TODO + 3: Living Room Sonos + 4: Bedroom Sonos +3: Reserved +4: Configuration +""" + +tree = { + 1: { # States + 1: { # Lights + 1: { # Kitchen + 1: { "function": lambda hass: set_brightness(hass,"light.kitchen_lights",0) }, + 2: { "function": lambda hass: set_brightness(hass,"light.kitchen_lights",10) }, + 3: { "function": lambda hass: set_brightness(hass,"light.kitchen_lights",55) }, + 4: { "function": lambda hass: set_brightness(hass,"light.kitchen_lights",100) } + }, + 2: { # Main Room + 1: { "function": lambda hass: set_brightness(hass,"light.mainroom_lights",0) }, + 2: { "function": lambda hass: set_brightness(hass,"light.mainroom_lights",10) }, + 3: { "function": lambda hass: set_brightness(hass,"light.mainroom_lights",55) }, + 4: { "function": lambda hass: set_brightness(hass,"light.mainroom_lights",100) } + }, + 3: { # Living Room + 1: { "function": lambda hass: set_brightness(hass,"light.livingroom_lights",0) }, + 2: { "function": lambda hass: set_brightness(hass,"light.livingroom_lights",10) }, + 3: { "function": lambda hass: set_brightness(hass,"light.livingroom_lights",55) }, + 4: { "function": lambda hass: set_brightness(hass,"light.livingroom_lights",100) } + }, + 4: { # Bedroom + 1: { "function": lambda hass: set_brightness(hass,"light.bedroom_lights",0) }, + 2: { "function": lambda hass: set_brightness(hass,"light.bedroom_lights",10) }, + 3: { "function": lambda hass: set_brightness(hass,"light.bedroom_lights",55) }, + 4: { "function": lambda hass: set_brightness(hass,"light.bedroom_lights",100) } + } + }, + 2: { # A/C + 2: { # Main Room + 1: { # Power + 1: { "function": lambda hass: hass.turn_on("climate.mainroom_ac") }, + 2: { "function": lambda hass: hass.turn_on("climate.mainroom_ac") }, + 3: { "function": lambda hass: hass.turn_off("climate.mainroom_ac") }, + 4: { "function": lambda hass: hass.turn_off("climate.mainroom_ac") }, + }, + 2: { # Mode + 1: { "function": lambda hass: gree_set_mode(hass,"mainroom_ac","cool") }, + 2: { "function": lambda hass: gree_set_mode(hass,"mainroom_ac","heat") }, + 3: { "function": lambda hass: gree_set_mode(hass,"mainroom_ac","fan_only") } + }, + 3: { # Fan Level + 1: { "function": lambda hass: gree_set_fan_level(hass,"mainroom_ac","auto") }, + 2: { "function": lambda hass: gree_set_fan_level(hass,"mainroom_ac","quiet") }, + 3: { "function": lambda hass: gree_set_fan_level(hass,"mainroom_ac","medium") }, + 4: { "function": lambda hass: gree_set_fan_level(hass,"mainroom_ac","high") } + }, + 4: { # Temperature + 1: { "function": lambda hass: set_temperature(hass,"mainroom_ac",72) }, + 2: { "function": lambda hass: set_temperature(hass,"mainroom_ac",74) }, + 3: { "function": lambda hass: set_temperature(hass,"mainroom_ac",76) }, + 4: { "function": lambda hass: set_temperature(hass,"mainroom_ac",78) } + } + }, + 3: { # Living Room + 1: { # Power + 1: { "function": lambda hass: hass.turn_on("climate.livingroom_ac") }, + 2: { "function": lambda hass: hass.turn_on("climate.livingroom_ac") }, + 3: { "function": lambda hass: hass.turn_off("climate.livingroom_ac") }, + 4: { "function": lambda hass: hass.turn_off("climate.livingroom_ac") }, + }, + 2: { # Mode + 1: { "function": lambda hass: gree_set_mode(hass,"livingroom_ac","cool") }, + 2: { "function": lambda hass: gree_set_mode(hass,"livingroom_ac","heat") }, + 3: { "function": lambda hass: gree_set_mode(hass,"livingroom_ac","fan_only") } + }, + 3: { # Fan Level + 1: { "function": lambda hass: gree_set_fan_level(hass,"livingroom_ac","auto") }, + 2: { "function": lambda hass: gree_set_fan_level(hass,"livingroom_ac","quiet") }, + 3: { "function": lambda hass: gree_set_fan_level(hass,"livingroom_ac","medium") }, + 4: { "function": lambda hass: gree_set_fan_level(hass,"livingroom_ac","high") } + }, + 4: { # Temperature + 1: { "function": lambda hass: set_temperature(hass,"livingroom_ac",72) }, + 2: { "function": lambda hass: set_temperature(hass,"livingroom_ac",74) }, + 3: { "function": lambda hass: set_temperature(hass,"livingroom_ac",76) }, + 4: { "function": lambda hass: set_temperature(hass,"livingroom_ac",78) } + } + }, + 4: { # Bedroom + 1: { # Power + 1: { "function": lambda hass: hass.turn_on("climate.bedroom_ac") }, + 2: { "function": lambda hass: hass.turn_on("climate.bedroom_ac") }, + 3: { "function": lambda hass: hass.turn_off("climate.bedroom_ac") }, + 4: { "function": lambda hass: hass.turn_off("climate.bedroom_ac") }, + }, + 2: { # Mode + 1: { "function": lambda hass: gree_set_mode(hass,"bedroom_ac","cool") }, + 2: { "function": lambda hass: gree_set_mode(hass,"bedroom_ac","heat") }, + 3: { "function": lambda hass: gree_set_mode(hass,"bedroom_ac","fan_only") } + }, + 3: { # Fan Level + 1: { "function": lambda hass: gree_set_fan_level(hass,"bedroom_ac","auto") }, + 2: { "function": lambda hass: gree_set_fan_level(hass,"bedroom_ac","quiet") }, + 3: { "function": lambda hass: gree_set_fan_level(hass,"bedroom_ac","medium") }, + 4: { "function": lambda hass: gree_set_fan_level(hass,"bedroom_ac","high") } + }, + 4: { # Temperature + 1: { "function": lambda hass: set_temperature(hass,"bedroom_ac",72) }, + 2: { "function": lambda hass: set_temperature(hass,"bedroom_ac",74) }, + 3: { "function": lambda hass: set_temperature(hass,"bedroom_ac",76) }, + 4: { "function": lambda hass: set_temperature(hass,"bedroom_ac",78) } + } + } + }, + 3: { # Fans + 1: { # Kitchen + 1: { "function": lambda hass: hass.set_value("input_number.kitchen_fan",0) }, + 2: { "function": lambda hass: hass.set_value("input_number.kitchen_fan",1) }, + 3: { "function": lambda hass: hass.set_value("input_number.kitchen_fan",2) }, + 4: { "function": lambda hass: hass.set_value("input_number.kitchen_fan",3) } + }, + 3: { # Living Room + 1: { "function": lambda hass: hass.set_value("input_number.livingroom_fan",0) }, + 2: { "function": lambda hass: hass.set_value("input_number.livingroom_fan",1) }, + 3: { "function": lambda hass: hass.set_value("input_number.livingroom_fan",2) }, + 4: { "function": lambda hass: hass.set_value("input_number.livingroom_fan",3) } + }, + 4: { # Bedroom + 1: { "function": lambda hass: hass.set_value("input_number.bedroom_fan",0) }, + 2: { "function": lambda hass: hass.set_value("input_number.bedroom_fan",1) }, + 3: { "function": lambda hass: hass.set_value("input_number.bedroom_fan",2) }, + 4: { "function": lambda hass: hass.set_value("input_number.bedroom_fan",3) } + } + } + }, + 2: { # Miscellaneous + 1: { # Effects + 1: { "service": "script/clear_lightswitch_leds" } + }, + 2: { # Music + 1: { # Join/unjoin + 1: { "function": lambda hass: hass.call_service("sonos/unjoin", + entity_id=["media_player.livingroom_sonos","media_player.bedroom_sonos"]) }, + 3: { "function": lambda hass: hass.call_service("sonos/join", + master="media_player.livingroom_sonos",entity_id="media_player.bedroom_sonos")}, + 4: { "function": lambda hass: hass.call_service("sonos/join", + master="media_player.livingroom_sonos",entity_id="media_player.bedroom_sonos")}, + }, + 2: { # Play Music + 3: { # Living Room Sonos + 1: { "function": lambda hass: select_source(hass,"media_player.livingroom_sonos","Songs") }, + 2: { "function": lambda hass: select_source(hass,"media_player.livingroom_sonos","Getting Things Done") } + }, + 4: { # Bedroom Sonos + 1: { "function": lambda hass: select_source(hass,"media_player.bedroom_sonos","Songs") }, + 2: { "function": lambda hass: select_source(hass,"media_player.bedroom_sonos","Chill sleep") } + } + }, + 3: { # Media Controls + 3: { # Living Room Sonos + 1: { "function": lambda hass: hass.call_service("media_player/media_pause", + entity_id="media_player.livingroom_sonos")}, + 2: { "function": lambda hass: hass.call_service("media_player/media_play", + entity_id="media_player.livingroom_sonos")}, + 3: { "function": lambda hass: hass.call_service("media_player/media_previous_track", + entity_id="media_player.livingroom_sonos")}, + 4: { "function": lambda hass: hass.call_service("media_player/media_next_track", + entity_id="media_player.livingroom_sonos")} + }, + 4: { # Bedroom Sonos + 1: { "function": lambda hass: hass.call_service("media_player/media_pause", + entity_id="media_player.bedroom_sonos")}, + 2: { "function": lambda hass: hass.call_service("media_player/media_play", + entity_id="media_player.bedroom_sonos")}, + 3: { "function": lambda hass: hass.call_service("media_player/media_previous_track", + entity_id="media_player.bedroom_sonos")}, + 4: { "function": lambda hass: hass.call_service("media_player/media_next_track", + entity_id="media_player.bedroom_sonos")} + } + } + } + } +} + +########################### +# Action Helper Functions # +########################### + +def set_brightness(hass, device, brightness): + # Convert brightness from percent to 0-255 + brightness = brightness * 255 / 100; + hass.turn_on(device, brightness=brightness); + +def set_temperature(hass, device, temperature): + hass.call_service("climate/set_temperature",entity_id="climate."+device,temperature=temperature); + +def gree_set_mode(hass, device, mode): + hass.call_service("climate/set_hvac_mode",entity_id="climate."+device,hvac_mode=mode); + +def gree_set_fan_level(hass, device, level): + if (level == "quiet"): + hass.turn_on(f"switch.{device}_quiet"); + else: + hass.turn_off(f"switch.{device}_quiet"); + hass.call_service("climate/set_fan_mode",entity_id="climate."+device,fan_mode=level); + +def select_source(hass, device, source): + hass.call_service("media_player/select_source",entity_id=device,source=source) + +################## +# Sequence Logic # +################## + +# Long press cancels any existing sequence and begins a new one. The first element in a +# sequence is always a long press, and all others are always short presses. +# While a sequence is being entered, normal actions from short presses in hass are +# disabled. A sequence can be ended either immediately by recognizing a full sequence +# or by TIMEOUT seconds passing. + +# - self.currentSequence contains an array of keys representing a sequence that is +# actively being entered. +# - self.tot contains a timer for the process_sequence function, ending sequence entry. + +TIMEOUT=2 + +class WallmotePlumbing(hass.Hass): + def initialize(self): + self.turn_on("automation.wallmote"); + self.currentSequence = []; + self.listen_event(self.onevent, "zwave_js_value_notification"); + + def onevent(self, _, event, kwargs): + if (event.get('value') == 'KeyHeldDown'): return; + if (event.get('device_id') != '3f7add36f94839b1446886b5dab35e6d'): return; + key = int(event.get('property_key')); + # We don't get a KeyPressed event at all for long presses + evt = event.get('value'); + if (evt == "KeyPressed"): + self.shortpress(key); + elif (evt == "KeyReleased"): + self.longpress(key); + + def shortpress(self, key): + # Single presses are handled in hass; sequences must start with a long press + if (len(self.currentSequence) == 0): return; + self.currentSequence.append(key); + self.reset_tot(); + + def longpress(self, key): + self.turn_off("automation.wallmote"); + self.currentSequence = [key]; + self.reset_tot(); + + def reset_tot(self): + if (hasattr(self,"tot")): self.tot.cancel(); + self.tot = Timer(TIMEOUT,self.process_sequence); + self.tot.start(); + + def process_sequence(self): + self.tot.cancel(); # So process_sequence can be called directly + self.turn_on("automation.wallmote"); + # Clear self.currentSequence so future shortpresses are not + # treated as part of a sequence + sequence = self.currentSequence; + self.currentSequence = []; + self.log(sequence); + self.run_action(sequence); + + def run_action(self, sequence, tree=tree): + if (len(sequence) > 0): + # Deeper into the rabbit hole + category = sequence.pop(0); + if (category in tree): self.run_action(sequence,tree[category]); + else: + # Perform an action from here + if ("function" in tree): tree["function"](self); + elif ("service" in tree): self.call_service(tree["service"]);