From 96343b9cec1937d4be27810d1c70a67fd1a1adc6 Mon Sep 17 00:00:00 2001 From: Shy Date: Sun, 12 Mar 2017 15:13:41 +0100 Subject: [PATCH] Added support for remote presets --- c4ctrl.py | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 202 insertions(+), 16 deletions(-) diff --git a/c4ctrl.py b/c4ctrl.py index 9946b49..136e165 100755 --- a/c4ctrl.py +++ b/c4ctrl.py @@ -31,8 +31,9 @@ class C4Interface(): item["qos"] = self.qos item["retain"] = self.retain - if self.debug: return print("[DEBUG] inhibited message:", cmd) + if self.debug: return print("[DEBUG] inhibited messages:", cmd) + print(cmd) publish.multiple(cmd, hostname=self.broker, port=self.port) @@ -68,6 +69,13 @@ class C4Interface(): def status(self): """Print current status (open or closed) of C4.""" st = self.fetch("club/status") + + # Produce fake result to prevent errors if in debug mode + if C4Interface.debug: + print("[DEBUG] Warning: handing over fake data to allow further execution!") + class st: pass + st.payload = b'\x00' + if st.payload == b'\x01': print("Club is open") else: @@ -163,7 +171,6 @@ class C4Room: def interactive_switchctrl(self, userinput="NULL"): """Switch lamps in this rooms on and off.""" - import sys c4 = C4Interface() if userinput == "NULL": @@ -318,7 +325,7 @@ class Kitchenlight: """The Kitchenlight.""" _available_modes = """ - off disable + off turn off checker[,DELAY[,COLOR_1[,COLOR_2]]] Checker matrix[,LINES] Matrix @@ -676,8 +683,7 @@ class ColorScheme: print("Available presets:") for entry in self.available: if entry[0] == '.' or entry[-1:] == '~': continue - print(entry) - print("PRESET may also be a color in hex notation (eg. #f06 or #ff0066).") + print(" " + entry) def store(self, name): """Store the current state of all lights as preset.""" @@ -727,8 +733,165 @@ class ColorScheme: print("Wrote preset \"{}\"".format(name)) +class RemotePresets: + """Remote preset control.""" + + def __init__(self): + self.map = { + "global" : { + "name" : "AutoC4", + "list_topic" : "preset/list", + "set_topic" : "preset/set", + "def_topic" : "preset/def" + }, + "wohnzimmer" : { + "name" : "Wohnzimmer", + "list_topic" : "preset/wohnzimmer/list", + "set_topic" : "preset/wohnzimmer/set", + "def_topic" : "preset/wohnzimmer/def" + }, + "plenar" : { + "name" : "Plenarsaal", + "list_topic" : "preset/plenar/list", + "set_topic" : "preset/plenar/set", + "def_topic" : "preset/plenar/def" + }, + "fnord" : { + "name" : "Fnordcenter", + "list_topic" : "preset/fnord/list", + "set_topic" : "preset/fnord/set", + "def_topic" : "preset/fnord/def" + }, + "keller" : { + "name" : "Keller", + "list_topic" : "preset/keller/list", + "set_topic" : "preset/keller/set", + "def_topic" : "preset/keller/def" + } + } + + def _expand_room_name(self, name): + """Try to expand partial names.""" + if name in self.map.keys(): + # Return exact match + return name + + for room in self.map.keys(): + if room.find(name) == 0: + return room + # Fallback + return name + + def _expand_preset_name(self, name, rooms, available): + """Try to expand partial preset names. + + must be a list of rooms to consider. + must be a dict as returned by query_available().""" + # We need to take care to match only presets which are available for + # every room + matchtable = {} + for room in rooms: + for preset in available[room]: + # Candidate? + if preset == name or preset.find(name) == 0: + if preset in matchtable.keys(): + matchtable[preset] += 1 + else: + matchtable[preset] = 1 + + # First check if there is an exact match + if name in matchtable.keys() and matchtable[name] == len(rooms): + return name # Exact match + + # Return first preset available in all rooms + for match in matchtable.keys(): + if matchtable[match] == len(rooms): + return match + # Fallback + return name + + def query_available(self, rooms=["global"]): + """Returns a dict of remotely available presets for [rooms].""" + import json + + # "global" is available everywhere and should always be included + if "global" not in rooms: + rooms.append("global") + + req = [] + for room in rooms: + if room not in self.map.keys(): + print("Error: unknown room \"{}\"".format(room)) + return {} + + req.append(self.map[room]["list_topic"]) + + c4 = C4Interface() + responce = c4.fetch(req) + # Make responce iterable + if type(responce) != list: responce = [responce] + + available = {} + for room in rooms: + for r in responce: + if r.topic == self.map[room]["list_topic"]: + available[room] = json.decoder.JSONDecoder().decode(r.payload.decode()) + + return available + + def list_available(self, room="global"): + """Print a list of available Presets.""" + room = self._expand_room_name(room) + if room == "global": + rooms = [room] + else: # "global" is available in every room, thus append + rooms = ["global", room] + available = self.query_available(rooms.copy()) + + if not available: + print("No presets available for {}".format(self.map[room]["name"])) + else: + print("Available presets for {}:".format(self.map[room]["name"])) + for r in available.keys(): + for preset in available[r]: + print( " " + preset) + + def apply_preset(self, preset, rooms=["global"]): + """Apply preset to given rooms.""" + # Strip spaces and expand rooms names + for i in range(len(rooms)): + rooms[i] = self._expand_room_name(rooms[i].strip()) + + available = self.query_available(rooms.copy()) + # Produce some fake data to prevent KeyErrors if in debug mode + if C4Interface.debug: + print("[DEBUG] Warning: handing over fake data to allow further execution!") + available = { + "global" : [preset], + "wohnzimmer" : [preset], + "plenar" : [preset], + "fnord" : [preset], + "keller" : [preset] + } + # Expand preset name (stripping spaces) + preset = self._expand_preset_name(preset, rooms.copy(), available.copy()) + + for room in rooms: + if preset not in available[room] and preset not in available["global"]: + print("Error: preset \"{}\" not available for room \"{}\"!".format( + preset, self.map[room]["name"])) + return False + + cmd = [] + for room in rooms: + cmd.append({"topic" : self.map[room]["set_topic"], + "payload" : preset}) + + c4 = C4Interface() + return c4.update(cmd) + + if __name__ == "__main__": - import sys import argparse parser = argparse.ArgumentParser( @@ -753,28 +916,38 @@ if __name__ == "__main__": "-k", "--kl-mode", type=str, metavar="MODE[,OPTIONS]", help="set Kitchenlight to MODE") group_kl.add_argument( - "-l", "--kl-list", action="store_true", - help="list available Kitchenlight modes") + "-i", "--list-kl-modes", action="store_true", + help="list available Kitchenlight modes and their options") # Ambient control - group_cl = parser.add_argument_group(title="ambient color control") + group_cl = parser.add_argument_group(title="ambient color control", + description="PRESET may be either a preset name or a color value in hex notation (eg. \"#ff0066\").") group_cl.add_argument( "-w", "--wohnzimmer", type=str, dest="w_color", metavar="PRESET", - help="apply colorscheme PRESET to Wohnzimmer") + help="apply local colorscheme PRESET to Wohnzimmer") group_cl.add_argument( "-p", "--plenarsaal", type=str, dest="p_color", metavar="PRESET", - help="apply colorscheme PRESET to Plenarsaal") + help="apply local colorscheme PRESET to Plenarsaal") group_cl.add_argument( "-f", "--fnordcenter", type=str, dest="f_color", metavar="PRESET", - help="apply colorscheme PRESET to Fnordcenter") + help="apply local colorscheme PRESET to Fnordcenter") group_cl.add_argument( - "-i", "--list-presets", action="store_true", - help="list available presets") + "-l", "--list-presets", action="store_true", + help="list locally available presets") group_cl.add_argument( "-o", "--store-preset", type=str, dest="store_as", metavar="NAME", help="store current state as preset NAME") + # Remote presets + group_rp = parser.add_argument_group(title="remote preset functions", + description="Available room names are \"wohnzimmer\", \"plenar\", \"fnord\" and \"keller\". Preset and room names can be abbreviated.") + group_rp.add_argument( + "-r", "--remote-preset", type=str, metavar="PRESET[:ROOM[,ROOM,...]]", + help="activate remote PRESET for ROOM.") + group_rp.add_argument( + "-R", "--list-remote", nargs='?', const="global", metavar="ROOM", + help="list remote presets for room ROOM") # Switch control group_sw = parser.add_argument_group(title="light switch control", - description="The optional [DIGIT_CODE] is a string of 0s or 1s for every light in the room. Works interactivly if missing.") + description="The optional DIGIT_CODE is a string of 0s or 1s for every light in the room. Works interactivly if missing.") group_sw.add_argument( "-W", nargs='?', dest="w_switch", const="NULL", metavar="DIGIT_CODE", help="switch lights in Wohnzimmer on/off") @@ -803,7 +976,7 @@ if __name__ == "__main__": C4Interface().shutdown() # Kitchenlight - if args.kl_list: + if args.list_kl_modes: print("Available Kitchenlight modes (options are optional):") print(Kitchenlight._available_modes) if args.kl_mode: @@ -829,6 +1002,19 @@ if __name__ == "__main__": if args.list_presets: ColorScheme().list_available() + # Remote presets + if args.list_remote: + RemotePresets().list_available(args.list_remote.lower()) + if args.remote_preset: + # Lets try to preserve spaces + #remote_opts = ' '.join(args.remote_preset).split(':') + remote_opts = args.remote_preset.split(':') + if len(remote_opts) == 1: + RemotePresets().apply_preset(remote_opts[0].lower().strip()) + else: + RemotePresets().apply_preset(remote_opts[0].lower().strip(), + remote_opts[1].split(',')) + # Light switches if args.w_switch: Wohnzimmer().interactive_switchctrl(args.w_switch)