#!/bin/python # # c4ctrl: Command line client for AutoC4 import sys class C4Interface(): """Interaction with the C4 home automation system.""" port = 1883 broker = "autoc4.labor.koeln.ccc.de" qos = 0 retain = True debug = False def __init__(self, topic=None): # Set a default topic if topic: self.topic = topic def update(self, cmd, topic=None): """Send cmd to topic via the MQTT broker.""" from paho.mqtt import publish # Overwrite default topic if topic: self.topic = topic if type(cmd) == list: # Add and to every message for item in cmd.copy(): if type(item) == dict: item["qos"] = self.qos item["retain"] = self.retain elif type(item) == tuple: new_item = ( item[0] or self.topic, # topic item[1], # payload self.qos, # qos self.retain # retain ) cmd.remove(item) cmd.append(new_item) if self.debug: return print("[DEBUG] inhibited messages:", cmd) publish.multiple(cmd, hostname=self.broker, port=self.port) else: if self.debug: return print("[DEBUG] inhibited message to '{}': '{}'".format( self.topic, cmd)) publish.single(self.topic, payload=cmd, qos=self.qos, retain=self.retain, hostname=self.broker, port=self.port) def fetch(self, topic=[]): """Return current state of topic.""" from paho.mqtt import subscribe topic = topic or self.topic # must be a list if type(topic) == str: topic = [topic] if self.debug: print("[DEBUG] inhibited query for:", topic) return [] return subscribe.simple(topic, msg_count=len(topic), qos=self.qos, hostname=self.broker, port=self.port) 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: print("Club is closed") def open_gate(self): """Open the gate.""" self.update(cmd=b'\x01', topic="club/gate") def shutdown(self, force=False): """Invoke the shutdown routine.""" if force: payload = b'\x44' else: payload = b'\x00' self.update(cmd=payload, topic="club/shutdown") class Dmx: """Abstraction of the 3 Channel LED Cans in the Club.""" template = "000000" def __init__(self, topic, color=None): self.topic = topic self.set_color(color or self.template) def __repr__(self): return "".format(self.topic, self.color) def _pad_color(self, color): """Merge hex color value into hex template.""" # Expand shorthand hex codes (eg. #f0f) and pad with template if len(color) > len(self.template): # Truncate print("Warning: truncating color value {} to {}".format( color, color[:len(self.template)])) return color[:len(self.template)] # Expand 3 char codes and codes of half the required length. # Yet, lets presume that a 6-char code should never be expanded. if len(color) != 6 and len(color) == 3 or len(color) == (len(self.template) / 2): expanded = "" for c in color: expanded += c*2 color = expanded if len(color) == len(self.template): # Nothing to do return color # Add padding color = color + self.template[-(len(self.template) - len(color)):] return color def set_color(self, color): """Set color (hex) for this instance. The color is then available via the color variable.""" color = self._pad_color(color) self.color = color self.payload = bytearray.fromhex(color) class Dmx4(Dmx): """Abstraction of the 4 Channel LED Cans in the Club.""" template = "000000ff" def __repr__(self): return "".format(self.topic, self.color) class Dmx7(Dmx): """Abstraction of the 7 Channel LED Cans in the Club.""" template = "000000000000ff" def __repr__(self): return "".format(self.topic, self.color) class C4Room: """Base class for Club rooms.""" def interactive_switchctrl(self, userinput="NULL"): """Switch lamps in this rooms on and off.""" c4 = C4Interface() if userinput == "NULL": # Interactively ask for input print("[{}]".format(self.name)) print("Please enter 0 or 1 for every lamp:") for level in range(len(self.switches)): print((level * '|') + ",- " + self.switches[level][0]) # Current state of witches state = "" req = [] for t in self.switches: req.append(t[1]) responce = c4.fetch(req) for sw in self.switches: for r in responce: if r.topic == sw[1]: state = state + str(int.from_bytes(r.payload, byteorder="little")) print(state) try: userinput = sys.stdin.readline().rstrip('\n') except KeyboardInterrupt: print("\rInterrupted by user.") return False if len(userinput) != len(self.switches): print("Error: wrong number of digits (expected {}, got {})!".format( len(self.switches), len(userinput))) return False cmd=[] for si in range(len(self.switches)): if userinput[si] not in "01": print("Error: invalid digit: " + userinput[si]) return False cmd.append({ "topic" : self.switches[si][1], "payload" : bytearray([int(userinput[si])]) }) return c4.update(cmd) def set_colorscheme(self, colorscheme): """Apply colorscheme to the LED Cans in this room.""" cmd = [] # Todo: this stuff would make sense if the Sink Light would be slave # to a master #if colorscheme.single_color: # # Setting only master is more efficient here # if colorscheme.color_for(self.master): # self.master.set_color(colorscheme.color_for(self.master)) # cmd.append({ # "topic" : self.master.topic, # "payload" : self.master.payload # }) #else: # Iterate over every light (including master!) for light in self.lights: if colorscheme.color_for(light.topic): light.set_color(colorscheme.color_for(light.topic)) cmd.append({ "topic" : light.topic, "payload" : light.payload }) c4 = C4Interface() return c4.update(cmd) class Wohnzimmer(C4Room): """The Wohnzimmer.""" name = "Wohnzimmer" switches = [ ("Tür", "licht/wohnzimmer/tuer"), ("Mitte", "licht/wohnzimmer/mitte"), ("Flur", "licht/wohnzimmer/gang"), ("Küche", "licht/wohnzimmer/kueche") ] master = Dmx7("dmx/wohnzimmer/master") lights = [ Dmx7("dmx/wohnzimmer/master"), Dmx7("dmx/wohnzimmer/tuer1"), Dmx7("dmx/wohnzimmer/tuer2"), Dmx7("dmx/wohnzimmer/tuer3"), Dmx7("dmx/wohnzimmer/mitte1"), Dmx7("dmx/wohnzimmer/mitte2"), Dmx7("dmx/wohnzimmer/mitte3"), Dmx7("dmx/wohnzimmer/gang"), Dmx7("dmx/wohnzimmer/baellebad"), Dmx7("led/kitchen/sink") ] class Plenarsaal(C4Room): """The Plenarsaal.""" name = "Plenarsaal" switches = [ ("Vorne/Wand", "licht/plenar/vornewand"), ("Vorne/Fenster", "licht/plenar/vornefenster"), ("Hinten/Wand", "licht/plenar/hintenwand"), ("Hinten/Fenster", "licht/plenar/hintenfenster") ] master = Dmx7("dmx/plenar/master") lights = [ Dmx7("dmx/plenar/master"), Dmx7("dmx/plenar/vorne1"), Dmx7("dmx/plenar/vorne2"), Dmx7("dmx/plenar/vorne3"), Dmx7("dmx/plenar/hinten1"), Dmx7("dmx/plenar/hinten2"), Dmx7("dmx/plenar/hinten3"), Dmx7("dmx/plenar/hinten4") ] class Fnordcenter(C4Room): """The Fnordcenter.""" name = "Fnordcenter" switches = [ ("Links (Fairydust)", "licht/fnord/links"), ("Rechts (SCUMM)", "licht/fnord/rechts") ] master = Dmx4("dmx/fnord/master") lights = [ Dmx4("dmx/fnord/master"), Dmx4("dmx/fnord/scummfenster"), Dmx4("dmx/fnord/schranklinks"), Dmx4("dmx/fnord/fairyfenster"), Dmx4("dmx/fnord/schrankrechts") ] class Keller(C4Room): """The Keller.""" name = "Keller" switches = [ ("Außen", "licht/keller/aussen"), ("Innen", "licht/keller/innen"), ("Vorne", "licht/keller/vorne") ] master = "" lights = [] class Kitchenlight: """The Kitchenlight.""" _available_modes = """ off turn off checker[,DELAY[,COLOR_1[,COLOR_2]]] Checker matrix[,LINES] Matrix mood[,MODE] (1=Colorwheel, 2=Random) Moodlight oc[,DELAY] Open Chaos pacman Pacman sine Sine text[,TEXT[,DELAY]] Text flood Flood clock Clock""" def __init__(self, topic="kitchenlight/change_screen", powertopic="power/wohnzimmer/kitchenlight", autopower=True): self.topic = topic # Kitchenlight topic self.powertopic = powertopic # Topic for power on self.autopower = autopower # Power on on every mode change? def _switch(self, data, poweron=False, poweroff=False): """Send commands via a C4Interface to the MQTT broker.""" if self.autopower or poweron or poweroff: c4 = C4Interface(self.topic) cmd = [] cmd.append({ "topic" : self.topic, "payload" : data }) if poweroff: cmd.append({ "topic" : self.powertopic, "payload" : bytearray((0,))}) elif self.autopower or poweron: cmd.append({ "topic" : self.powertopic, "payload" : bytearray((1,))}) c4.update(cmd) else: c4 = C4Interface(self.topic) c4.update(data) def set_mode(self, mode, opts=[]): """Switch to given mode.""" mode = mode.lower() if mode == "off": return self.empty() if mode == "checker": return self.checker(*opts) if mode == "matrix": return self.matrix(*opts) if mode == "mood": return self.moodlight(*opts) if mode == "oc" or mode == "openchaos": return self.openchaos(*opts) if mode == "pacman": return self.pacman() if mode == "sine": return self.sine() if mode == "text": return self.text(*opts) if mode == "flood": return self.flood() if mode == "clock": return self.clock() print('Error: unknown mode "' + mode + '"!') return False def empty(self): """Set to mode "empty" and turn off Kitchenlight.""" # Screen 0 d = int(0).to_bytes(4, "little") self._switch(d, poweroff=True) def checker(self, delay=500, colA="0000ff", colB="00ff00"): """Set to mode "checker".""" # Kind of a hack: lets treat the two colors as DMX lights ca = Dmx("checker/a", colA.lstrip('#')) cb = Dmx("checker/b", colB.lstrip('#')) d = bytearray(20) v = memoryview(d) # Screen 1 v[0:4] = int(1).to_bytes(4, "little") # Delay v[4:8] = int(delay).to_bytes(4, "little") # ColorA R/G/B v[8:10] = int(ca.color[0:2], base=16).to_bytes(2, "little") v[10:12] = int(ca.color[2:4], base=16).to_bytes(2, "little") v[12:14] = int(ca.color[4:6], base=16).to_bytes(2, "little") # ColorB R/G/B v[14:16] = int(cb.color[0:2], base=16).to_bytes(2, "little") v[16:18] = int(cb.color[2:4], base=16).to_bytes(2, "little") v[18:20] = int(cb.color[4:6], base=16).to_bytes(2, "little") self._switch(d) def matrix(self, lines=8): """Set to mode "matrix".""" if int(lines) > 31: lines = 31 # Maximal line count d = bytearray(8) v = memoryview(d) # Screen 2 v[0:4] = int(2).to_bytes(4, "little") v[4:8] = int(lines).to_bytes(4, "little") self._switch(d) def moodlight(self, mode=1): """Set to mode "moodlight".""" if mode == 1: # Mode "Colorwheel" d = bytearray(19) v = memoryview(d) # Screen 3 v[0:4] = int(3).to_bytes(4, "little") # Mode v[4:5] = int(mode).to_bytes(1, "little") # Step v[5:9] = int(1).to_bytes(4, "little") # Fade delay v[9:13] = int(10).to_bytes(4, "little") # Pause v[13:17] = int(10000).to_bytes(4, "little") # Hue step v[17:19] = int(30).to_bytes(2, "little") else: # Mode "Random" d = bytearray(17) v = memoryview(d) # Screen 3 v[0:4] = int(3).to_bytes(4, "little") # Mode v[4:5] = int(mode).to_bytes(1, "little") # Step v[5:9] = int(1).to_bytes(4, "little") # Fade delay v[9:13] = int(10).to_bytes(4, "little") # Pause v[13:17] = int(10000).to_bytes(4, "little") self._switch(d) def openchaos(self, delay=1000): """Set to mode "openchaos".""" d = bytearray(8) v = memoryview(d) # Screen 4 v[0:4] = int(4).to_bytes(4, "little") v[4:8] = int(delay).to_bytes(4, "little") self._switch(d) def pacman(self): """Set to mode "pacman".""" # Screen 5 d = int(5).to_bytes(4, "little") self._switch(d) def sine(self): """Set to mode "sine".""" # Screen 6 d = int(6).to_bytes(4, "little") self._switch(d) # Screen 7 is Strobo, which is disabled because it seems to do harm to # the Kitchenlight. Evil strobo! def text(self, text="Hello World", delay=250): """Set to mode "text".""" text = text.encode("ascii", "ignore") d = bytearray(8 + len(text) + 1) v = memoryview(d) # Screen 8 v[0:4] = int(8).to_bytes(4, "little") v[4:8] = int(delay).to_bytes(4, "little") v[8:8 + len(text)] = text v[len(d) - 1:len(d)] = bytes(1) self._switch(d) def flood(self): """Set to mode "flood".""" # Screen 9 d = int(9).to_bytes(4, "little") self._switch(d) def clock(self): """Set to mode "clock".""" # Screen 11 d = int(11).to_bytes(4, "little") self._switch(d) class ColorScheme: """Abstraction of a colorscheme.""" # Virtual presets _virtual_presets = ["off", "random"] def __init__(self, autoinit=""): self.mapping = {} self.single_color = False self.return_random_color = False self.available = None # List of available presets if autoinit: # Load or generate preset if autoinit[0] == '#': if '/' in autoinit: return self.from_color(autoinit.split('/')) else: return self.from_color(autoinit) elif self._expand_preset(autoinit) == "off": # Special case. Sets masters to #000000 return self.from_color("000000") elif self._expand_preset(autoinit) == "random": return self.from_random() else: return self.from_file(autoinit) def __bool__(self): # Return true if color_for has a chance to present anything useful if self.mapping: return True if self.single_color: return True if self.return_random_color: return True else: return False def _get_cfg_dir(self, quiet=False, create=False): """Returns path of the config dir.""" import os # The name of our config directory XDG_NAME = "c4ctrl" # Get XDG_CONFIG_DIR from environment or set default if "XDG_CONFIG_DIR" in os.environ: XDG_CONFIG_DIR = os.environ["XDG_CONFIG_DIR"] else: XDG_CONFIG_DIR = os.path.expanduser("~/.config") # Does our config dir exist? cfg_dir = os.path.join(XDG_CONFIG_DIR, XDG_NAME) if not os.path.isdir(cfg_dir): if create: print("Creating config directory \"{}\"".format(cfg_dir)) os.mkdir(cfg_dir) elif quiet: return None else: print("Warning: config dir \"{}\" does not exist!".format(cfg_dir)) return None return cfg_dir def _expand_preset(self, preset): """Tries to expand given string to a valid preset name.""" import os if not self.available: cfg_dir = self._get_cfg_dir(quiet=True) if not cfg_dir: self.available = self._virtual_presets.copy() else: self.available = os.listdir(cfg_dir) self.available.extend(self._virtual_presets) # Search for an exact match first for a in self.available: if a == preset: return a # Return anything which begins with the name given for a in self.available: if a.find(preset) == 0: return a # Fallback return preset def _topic_is_master(self, topic): """Does the given topic look like a master topic?""" return topic.lower().rfind("/master") == len(topic)-7 # 7 = len("/master") def _random_color(self): """Returns a random 6 char hex color.""" from random import randint color = "" for i in range(6): # Dont return smaller values than 11 color = color + hex(randint(1, 15))[2:] return color def color_for(self, topic): """Returns the color (hex) this ColorScheme provides for the given topic.""" if self.mapping: if topic in self.mapping.keys(): return self.mapping[topic] elif self.single_color: return self.single_color elif self.return_random_color: # Returning a value for master would override all other settings if self._topic_is_master(topic): # 7 = len("/master") return None else: return self._random_color() else: return None def from_file(self, preset): """Load ColorScheme from file.""" import os cfg_dir = self._get_cfg_dir() if not cfg_dir: print("Error: could not load preset!") return None # Expand preset name preset = self._expand_preset(preset) # Try to open the preset file fn = os.path.join(cfg_dir, preset) try: fd = open(fn) except OSError: print("Error: could not load preset \"{}\" (file could not be accessed)!".format(preset)) return None # Parse the preset file self.mapping = {} self.name = preset for line in fd.readlines(): # Skip every line which does not begin with an alphabetic character try: if not line.lstrip()[0].isalpha(): continue except IndexError: continue # Empty line # Strip spaces and split k, v = line.replace(' ','').replace('\t','').split('=') # Convert #fff to fff and remove trailing comments, nl and cr chars vl = v.rstrip("\n\r").split('#') v = vl[0] or vl[1] # Validate hex code for c in v.lower(): if c not in "0123456789abcdef": print("Error: invalid color code \"{}\" in preset \"{}\"!".format(v, preset)) sys.exit(1) self.mapping[k] = v fd.close() def from_color(self, color): """Derive ColorScheme from a single hex color.""" self.single_color = color.lstrip('#') def from_random(self): """Derive ColorScheme from random colors.""" self.return_random_color = True def list_available(self): """List available presets.""" import os cfg_dir = self._get_cfg_dir() if not cfg_dir: self.available = self._virtual_presets.copy() if not self.available: self.available = os.listdir(cfg_dir) self.available.extend(self._virtual_presets) self.available.sort() print("Available presets:") for entry in self.available: if entry[0] == '.' or entry[-1:] == '~': continue print(" " + entry) def store(self, name): """Store the current state of all lights as preset.""" # First of all, refuse to override virtual presets if name in self._virtual_presets: print("I'm sorry Dave. I'm afraid I can't do that. The name \"{}\" is reserved. Please choose a different one.".format(name)) return False import os cfg_dir = self._get_cfg_dir(create=True) # Create config dir if missing fn = os.path.join(cfg_dir, name) try: fd = open(fn, 'xt') except FileExistsError: print("A preset with this name already exists, overwrite? [y/N]", end=' ', flush=True) if sys.stdin.read(1).lower() == 'y': fd = open(fn, 'wt') else: return False # Get current states c4 = C4Interface() fd.write("# Preset \"{}\" (auto generated)\n".format(name)) fd.write("# Note: \"/master\" topics override every other topic in the room.\n") for room in Wohnzimmer, Plenarsaal, Fnordcenter: topics = [] for light in room.lights: topics.append(light.topic) responce = c4.fetch(topics) fd.write("\n# {}\n".format(room.name)) for light in room.lights: for r in responce: if r.topic == light.topic: light.set_color(r.payload.hex()) # Out comment master, as it would overre everything else if self._topic_is_master(r.topic): fd.write("#{} = {}\n".format(light.topic, light.color)) else: fd.write("{} = {}\n".format(light.topic, light.color)) fd.close() 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 on 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 specified # Strip every "global" out of the room list. We take special care of # "global" later on. while "global" in rooms: rooms.remove("global") matchtable = {} if "global" not in rooms: for preset in available["global"]: # Candidate? if preset == name or preset.find(name) == 0: # Presets in "global" are available everywhere matchtable[preset] = len(rooms) 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 in all rooms if name in matchtable.keys() and matchtable[name] >= len(rooms): return name # Return first preset available in all rooms for match in matchtable.keys(): if matchtable[match] >= len(rooms): return match elif match in available["global"]: return match # Fallback return name def query_available(self, rooms=["global"]): """Returns a dict of remotely available presets for [rooms].""" import json # Presets in "global" are available everywhere and should always be included if "global" not in rooms: rooms.insert(0, "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) available = self.query_available([room]) 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((self.map[room]["set_topic"], preset)) c4 = C4Interface() return c4.update(cmd) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser( description="Command line client for AutoC4.") parser.add_argument( "-d", "--debug", action="store_true", help="display what would be send to the MQTT broker, but do not actually connect") # Various club functions group_fn = parser.add_argument_group(title="various functions") group_fn.add_argument( "-s", "--status", action="store_true", help="display club status") group_fn.add_argument( "-g", "--gate", action="store_true", help="open club gate") group_fn.add_argument( "-S", "--shutdown", action="count", help="shutdown (give twice to force shutdown)") # Kitchenlight control group_kl = parser.add_argument_group(title="Kitchenlight control") group_kl.add_argument( "-k", "--kl-mode", type=str, metavar="MODE[,OPTIONS]", help="set Kitchenlight to MODE") group_kl.add_argument( "-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", 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 local colorscheme PRESET to Wohnzimmer") group_cl.add_argument( "-p", "--plenarsaal", type=str, dest="p_color", metavar="PRESET", help="apply local colorscheme PRESET to Plenarsaal") group_cl.add_argument( "-f", "--fnordcenter", type=str, dest="f_color", metavar="PRESET", help="apply local colorscheme PRESET to Fnordcenter") group_cl.add_argument( "-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(s).") group_rp.add_argument( "-R", "--list-remote", nargs='?', const="global", metavar="ROOM", help="list remote presets for 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.") group_sw.add_argument( "-W", nargs='?', dest="w_switch", const="NULL", metavar="DIGIT_CODE", help="switch lights in Wohnzimmer on/off") group_sw.add_argument( "-P", nargs='?', dest="p_switch", const="NULL", metavar="DIGIT_CODE", help="switch lights in Plenarsaal on/off") group_sw.add_argument( "-F", nargs='?', dest="f_switch", const="NULL", metavar="DIGIT_CODE", help="switch lights in Fnordcentter on/off") group_sw.add_argument( "-K", nargs='?', dest="k_switch", const="NULL", metavar="DIGIT_CODE", help="switch lights in Keller on/off") args = parser.parse_args() # Debug, gate, status and shutdown if args.debug: C4Interface.debug = True if args.status: C4Interface().status() if args.gate: C4Interface().open_gate() if args.shutdown: if args.shutdown >= 2: C4Interface().shutdown(force=True) else: C4Interface().shutdown() # Kitchenlight if args.list_kl_modes: print("Available Kitchenlight modes (options are optional):") print(Kitchenlight._available_modes) if args.kl_mode: kl = Kitchenlight() mode = args.kl_mode.split(',') if len(mode) == 1: kl.set_mode(mode[0]) else: kl.set_mode(mode[0], mode[1:]) # Colorscheme if args.store_as: ColorScheme().store(args.store_as) if args.w_color: preset = ColorScheme(autoinit=args.w_color) if preset: Wohnzimmer().set_colorscheme(preset) if args.p_color: preset = ColorScheme(autoinit=args.p_color) if preset: Plenarsaal().set_colorscheme(preset) if args.f_color: preset = ColorScheme(autoinit=args.f_color) if preset: Fnordcenter().set_colorscheme(preset) 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].strip()) else: RemotePresets().apply_preset(remote_opts[0].strip(), remote_opts[1].lower().split(',')) # Light switches if args.w_switch: Wohnzimmer().interactive_switchctrl(args.w_switch) if args.p_switch: Plenarsaal().interactive_switchctrl(args.p_switch) if args.f_switch: Fnordcenter().interactive_switchctrl(args.f_switch) if args.k_switch: Keller().interactive_switchctrl(args.k_switch) # No command line options or only debug? if len(sys.argv) <= 1 or len(sys.argv) == 2 and args.debug: parser.print_help()