Argument parsing for -k and -r changed

This commit is contained in:
Shy 2017-04-17 13:05:02 +02:00
parent 44ae580dbf
commit 032ee2545c

306
c4ctrl.py
View file

@ -1,17 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# \__/ # ____ __
# / # / __// /
# _______/ # / / / /___ ____
# / ____\__ # \ \__\_ / / __/____ __ _
#__ / / __// / # \___\ |_| / / |_ _| \ |
# \___/ / / / /___ ____ # \ \__ | || / |_
#__/ \ \ \__\_ / / __/____ __ _ # \___\|_||_\_\___\
# \ \___\ |_| / / |_ _| \ | #
# \_______/ \ \__ | || / |_ # c4ctrl: A command line client for Autoc4.
# \ \___\|_||_\_\___\ #
# \__
# / \ c4ctrl: A command line
# \__/ client for Autoc4
# Author: Shy # Author: Shy
# #
# This program is free software: you can redistribute it and/or modify it under # This program is free software: you can redistribute it and/or modify it under
@ -33,12 +30,13 @@ A command line client for Autoc4, the home automation system of the C4.
Run 'c4ctrl -h' for usage information. Run 'c4ctrl -h' for usage information.
Dependencies: Dependencies:
Paho Python Client * Paho Python Client
(available from https://github.com/eclipse/paho.mqtt.python) (available from https://github.com/eclipse/paho.mqtt.python).
* Some parts will work on UNIX-like operating systems only.
""" """
import sys import sys
from random import choice # For client_id generation from random import choice # For client_id generation.
class C4Interface(): class C4Interface():
@ -48,12 +46,27 @@ class C4Interface():
port = 1883 port = 1883
qos = 2 qos = 2
retain = True retain = True
# Generate a (sufficiently) unique client id # Generate a (sufficiently) unique client id.
client_id = "c4ctrl-" + "".join( client_id = "c4ctrl-" + "".join(
choice("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") choice("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
for unused in range(16)) for unused in range(16))
debug = False debug = False
def on_permission_error(self, error):
""" Called when catching a PermissionDenied exception while connecting. """
print("Error: You don't have permission to connect to the broker.", file=sys.stderr)
print("Maybe you're not connected to the internal C4 network?", file=sys.stderr)
print(error, file=sys.stderr)
sys.exit(1)
def on_os_error(self, error):
""" Called when catching a OSError exception while connecting. """
print("Error: unable to open a network socket.", file=sys.stderr)
print(error, file=sys.stderr)
sys.exit(1)
def push(self, message, topic=None, retain=None): def push(self, message, topic=None, retain=None):
""" Send a message to the MQTT broker. """ Send a message to the MQTT broker.
@ -65,14 +78,14 @@ class C4Interface():
from paho.mqtt import publish from paho.mqtt import publish
# Skip empty messages # Skip empty messages.
if message == [] or message == "": return if message == [] or message == "": return
# Set defaults # Set defaults.
if retain == None: retain = self.retain if retain == None: retain = self.retain
if type(message) == list: if type(message) == list:
# Add <qos> and <retain> to every message # Add <qos> and <retain> to every message.
for item in message.copy(): for item in message.copy():
if type(item) == dict: if type(item) == dict:
item["qos"] = self.qos item["qos"] = self.qos
@ -90,23 +103,37 @@ class C4Interface():
if self.debug: return print("[DEBUG] inhibited messages:", if self.debug: return print("[DEBUG] inhibited messages:",
message, file=sys.stderr) message, file=sys.stderr)
publish.multiple(message, try:
hostname=self.broker, publish.multiple(message,
port=self.port, hostname=self.broker,
client_id=self.client_id) port=self.port,
client_id=self.client_id)
else: # Message is not a list except PermissionError as error:
self.on_permission_error(error)
except OSError as error:
self.on_os_error(error)
else: # Message is not a list.
if self.debug: if self.debug:
return print("[DEBUG] inhibited message to '{}': '{}'".format( return print("[DEBUG] inhibited message to '{}': '{}'".format(
topic, message), file=sys.stderr) topic, message), file=sys.stderr)
publish.single(topic, try:
payload=message, publish.single(topic,
qos=self.qos, payload=message,
retain=retain, qos=self.qos,
hostname=self.broker, retain=retain,
port=self.port, hostname=self.broker,
client_id=self.client_id) port=self.port,
client_id=self.client_id)
except PermissionError as error:
self.on_permission_error(error)
except OSError as error:
self.on_os_error(error)
def pull(self, topic=[]): def pull(self, topic=[]):
""" Return the state of a topic. """ Return the state of a topic.
@ -116,30 +143,37 @@ class C4Interface():
from paho.mqtt import subscribe from paho.mqtt import subscribe
# topic must be a list # Convert topics of type string to a single item list.
if type(topic) == str: if type(topic) == str:
topic = [topic] topic = [topic]
# Skip empty queries # Skip empty queries.
if topic == []: return if topic == []: return
if self.debug: if self.debug:
print("[DEBUG] inhibited query for:", topic, file=sys.stderr) print("[DEBUG] inhibited query for:", topic, file=sys.stderr)
return [] return []
return subscribe.simple(topic, try:
msg_count=len(topic), return subscribe.simple(topic,
qos=self.qos, msg_count=len(topic),
hostname=self.broker, qos=self.qos,
port=self.port, hostname=self.broker,
client_id=self.client_id) port=self.port,
client_id=self.client_id)
except PermissionError as error:
self.on_permission_error(error)
except OSError as error:
self.on_os_error(error)
def status(self): def status(self):
""" Returns current status (string "open" or "closed") of the club. """ """ Returns current status (string "open" or "closed") of the club. """
club_status = self.pull("club/status") club_status = self.pull("club/status")
# Create a fake result to prevent errors if in debug mode # Create a fake result to prevent errors if in debug mode.
if C4Interface.debug: if C4Interface.debug:
print("[DEBUG] Warning: handing over fake data to allow for further execution!", print("[DEBUG] Warning: handing over fake data to allow for further execution!",
file=sys.stderr) file=sys.stderr)
@ -169,13 +203,13 @@ class C4Interface():
class Kitchenlight: class Kitchenlight:
""" Interface to the Kitchenlight and its functions. """ """ Interface to the Kitchenlight and its functions. """
_END = "little" # Kitchenlight endianess _END = "little" # Kitchenlight endianess.
def __init__(self, topic="kitchenlight/change_screen", def __init__(self, topic="kitchenlight/change_screen",
powertopic="power/wohnzimmer/kitchenlight", powertopic="power/wohnzimmer/kitchenlight",
autopower=True): autopower=True):
self.topic = topic # Kitchenlight topic self.topic = topic # Kitchenlight topic.
self.powertopic = powertopic # Topic for power on self.powertopic = powertopic # Topic for power on.
self.autopower = autopower # Power on on every mode change? self.autopower = autopower # Power on on every mode change?
def _switch(self, data, poweron=False, poweroff=False): def _switch(self, data, poweron=False, poweroff=False):
@ -206,13 +240,13 @@ class Kitchenlight:
print("Available Kitchenlight modes (options are optional):") print("Available Kitchenlight modes (options are optional):")
print(""" print("""
off turn off Kitchenlight off turn off Kitchenlight
checker[,DELAY[,COLOR_1[,COLOR_2]]] Checker checker [DELAY] [COLOR_1] [COLOR_2] Checker
matrix[,LINES] Matrix matrix [LINES] Matrix
mood[,1|2] (1=Colorwheel, 2=Random) Moodlight mood [1|2] (1=Colorwheel, 2=Random) Moodlight
oc[,DELAY] Open Chaos oc [DELAY] Open Chaos
pacman Pacman pacman Pacman
sine Sine sine Sine
text[,TEXT[,DELAY]] Text text [TEXT] [DELAY] Text
flood Flood flood Flood
clock Clock""") clock Clock""")
@ -257,7 +291,7 @@ class Kitchenlight:
colA = first color (default 0000ff) colA = first color (default 0000ff)
colB = second color (default 00ff00) """ colB = second color (default 00ff00) """
# Kind of a hack: lets treat the two colors as DMX lights # Kind of a hack: lets treat the two colors as DMX lights.
ca = Dmx("checker/a", colA.lstrip('#')) ca = Dmx("checker/a", colA.lstrip('#'))
cb = Dmx("checker/b", colB.lstrip('#')) cb = Dmx("checker/b", colB.lstrip('#'))
d = bytearray(20) d = bytearray(20)
@ -281,7 +315,7 @@ class Kitchenlight:
lines (>0, <32) = number of lines (default 8) """ lines (>0, <32) = number of lines (default 8) """
if int(lines) > 31: lines = 31 # Maximal line count if int(lines) > 31: lines = 31 # Maximal line count.
d = bytearray(8) d = bytearray(8)
v = memoryview(d) v = memoryview(d)
# Screen 2 # Screen 2
@ -294,7 +328,7 @@ class Kitchenlight:
mode [1|2] = colorwheel(1) or random(2) """ mode [1|2] = colorwheel(1) or random(2) """
if mode == 1: # Mode "Colorwheel" if mode == 1: # Mode "Colorwheel".
d = bytearray(19) d = bytearray(19)
v = memoryview(d) v = memoryview(d)
# Screen 3 # Screen 3
@ -309,7 +343,7 @@ class Kitchenlight:
v[13:17] = int(10000).to_bytes(4, self._END) v[13:17] = int(10000).to_bytes(4, self._END)
# Hue step # Hue step
v[17:19] = int(30).to_bytes(2, self._END) v[17:19] = int(30).to_bytes(2, self._END)
else: # Mode "Random" else: # Mode "Random".
d = bytearray(17) d = bytearray(17)
v = memoryview(d) v = memoryview(d)
# Screen 3 # Screen 3
@ -360,7 +394,7 @@ class Kitchenlight:
delay = delay in milliseconds (default 250). """ delay = delay in milliseconds (default 250). """
text = text.encode("ascii", "ignore") text = text.encode("ascii", "ignore")
if len(text) > 255: # Maximum text length if len(text) > 255: # Maximum text length.
print("Warning: text length must not exceed 255 characters!", file=sys.stderr) print("Warning: text length must not exceed 255 characters!", file=sys.stderr)
text = text[:255] text = text[:255]
d = bytearray(8 + len(text) + 1) d = bytearray(8 + len(text) + 1)
@ -388,13 +422,13 @@ class Kitchenlight:
class Dmx: class Dmx:
""" Abstraction of the 3 channel LED cans in the club. """ """ Abstraction of the 3 channel LED cans in the club. """
# 3 bytes for color, one each for red, green and blue # 3 bytes for color, one each for red, green and blue.
template = "000000" template = "000000"
def __init__(self, topic, color=None): def __init__(self, topic, color=None):
self.topic = topic self.topic = topic
self.set_color(color or self.template) self.set_color(color or self.template)
self.is_master = topic.rfind("/master") == len(topic)-7 # 7 = len("/master") self.is_master = topic.rfind("/master") == len(topic)-7 # 7 = len("/master").
def _pad_color(self, color): def _pad_color(self, color):
""" Merge hex color values or payloads into the template. """ Merge hex color values or payloads into the template.
@ -403,7 +437,7 @@ class Dmx:
to get a fitting payload for this kind of light. """ to get a fitting payload for this kind of light. """
if len(color) > len(self.template): if len(color) > len(self.template):
# Silently truncate bytes exceeding template length # Silently truncate bytes exceeding template length.
return color[:len(self.template)] return color[:len(self.template)]
# Expand 3 char codes and codes of half the required length. # Expand 3 char codes and codes of half the required length.
@ -412,10 +446,10 @@ class Dmx:
if len(color) != 6 and len(color) == 3 or len(color) == (len(self.template) / 2): if len(color) != 6 and len(color) == 3 or len(color) == (len(self.template) / 2):
color = "".join(char*2 for char in color) color = "".join(char*2 for char in color)
if len(color) == len(self.template): # Nothing more to do if len(color) == len(self.template): # Nothing more to do.
return color return color
# Add padding # Add padding.
color = color + self.template[len(color):] color = color + self.template[len(color):]
return color return color
@ -433,7 +467,7 @@ class Dmx:
class Dmx4(Dmx): class Dmx4(Dmx):
""" Abstraction of the 4 channel LED cans in the club. """ """ Abstraction of the 4 channel LED cans in the club. """
# 3 bytes for color plus 1 byte for brightness # 3 bytes for color plus 1 byte for brightness.
template = "000000ff" template = "000000ff"
@ -441,7 +475,7 @@ class Dmx7(Dmx):
""" Abstraction of the 7 channel LED cans in the club. """ """ Abstraction of the 7 channel LED cans in the club. """
# 3 bytes for color, another 3 bytes for special functions and 1 byte # 3 bytes for color, another 3 bytes for special functions and 1 byte
# for brightness # for brightness.
template = "000000000000ff" template = "000000000000ff"
@ -451,7 +485,7 @@ class C4Room:
def __init__(self): def __init__(self):
self.c4 = C4Interface() self.c4 = C4Interface()
# get_switch_state() will store its result and a timestamp to reduce # get_switch_state() will store its result and a timestamp to reduce
# requests to the broker # requests to the broker.
self._switch_state = ("", 0.0) self._switch_state = ("", 0.0)
def _interactive_light_switch(self): def _interactive_light_switch(self):
@ -467,7 +501,7 @@ class C4Room:
print((level * '|') + ",- " + self.switches[level][0]) print((level * '|') + ",- " + self.switches[level][0])
switch_state = self.get_switch_state() switch_state = self.get_switch_state()
print(switch_state) # Present current state print(switch_state) # Present current state.
try: try:
userinput = sys.stdin.readline().rstrip('\n') userinput = sys.stdin.readline().rstrip('\n')
@ -500,7 +534,7 @@ class C4Room:
for sw in self.switches: for sw in self.switches:
for r in responce: for r in responce:
if r.topic == sw[1]: if r.topic == sw[1]:
state += str(int.from_bytes(r.payload, byteorder="little")) state += str(int.from_bytes(r.payload, sys.byteorder))
if C4Interface.debug: if C4Interface.debug:
print("[DEBUG] Warning: handing over fake data to allow for further execution!", print("[DEBUG] Warning: handing over fake data to allow for further execution!",
@ -514,20 +548,20 @@ class C4Room:
""" Switch lamps in a room on or off. """ """ Switch lamps in a room on or off. """
if not userinput: if not userinput:
# Derive user input from stdin # Derive user input from stdin.
userinput = self._interactive_light_switch() userinput = self._interactive_light_switch()
if userinput == "": return if userinput == "": return
# Let's support some geeky binary operations! # Let's support some geeky binary operations!
mode = 'n' # n = normal, a = AND, o = OR mode = 'n' # n = normal, a = AND, o = OR.
if not userinput.isdecimal(): if not userinput.isdecimal():
if userinput[0] == '&' and userinput[1:].strip().isdecimal(): if userinput[0] == '&' and userinput[1:].strip().isdecimal():
# AND operator, applied later after doing some more validation # AND operator, applied later after doing some more validation.
userinput = userinput[1:].strip() userinput = userinput[1:].strip()
mode = 'a' mode = 'a'
elif userinput[0] == '|' and userinput[1:].strip().isdecimal(): elif userinput[0] == '|' and userinput[1:].strip().isdecimal():
# OR operator, applied later after doing some more validation # OR operator, applied later after doing some more validation.
userinput = userinput[1:].strip() userinput = userinput[1:].strip()
mode = 'o' mode = 'o'
@ -540,7 +574,7 @@ class C4Room:
else: else:
shift_by = 1 shift_by = 1
# Retrieve the current state of switches # Retrieve the current state of switches.
switch_state = self.get_switch_state() switch_state = self.get_switch_state()
if userinput[:2] == ">>": if userinput[:2] == ">>":
# Right shift. '[2:]' removes the leading 'b0...'. # Right shift. '[2:]' removes the leading 'b0...'.
@ -548,9 +582,9 @@ class C4Room:
else: else:
# Left shift. '[2:]' removes the leading 'b0...'. # Left shift. '[2:]' removes the leading 'b0...'.
new_state = bin(int(switch_state, base=2) << shift_by)[2:] new_state = bin(int(switch_state, base=2) << shift_by)[2:]
# Cut any exceeding leftmost bits # Cut any exceeding leftmost bits.
new_state = new_state[-len(self.switches):] new_state = new_state[-len(self.switches):]
# Pad with leading zeroes # Pad with leading zeroes.
userinput = new_state.rjust(len(self.switches), '0') userinput = new_state.rjust(len(self.switches), '0')
else: else:
@ -561,11 +595,11 @@ class C4Room:
if len(userinput) != len(self.switches): if len(userinput) != len(self.switches):
# First try to convert from decimal if userinput's length doesn't # First try to convert from decimal if userinput's length doesn't
# match # match.
if len(bin(int(userinput))) <= len(self.switches)+2: if len(bin(int(userinput))) <= len(self.switches)+2:
# ^ +2 because bin() returns strings like 'b0...' # ^ +2 because bin() returns strings like 'b0...'.
binary = bin(int(userinput))[2:] # Strip leading 'b0' binary = bin(int(userinput))[2:] # Strip leading 'b0'.
# Pad with leading zeroes # Pad with leading zeroes.
userinput = binary.rjust(len(self.switches), '0') userinput = binary.rjust(len(self.switches), '0')
else: else:
print("Error: wrong number of digits (expected {}, got {})!".format( print("Error: wrong number of digits (expected {}, got {})!".format(
@ -573,24 +607,24 @@ class C4Room:
return False return False
# Now that everything special is expanded it's time to check if # Now that everything special is expanded it's time to check if
# userinput really consists of 1s and 0s only # userinput really consists of 1s and 0s only.
for digit in userinput: for digit in userinput:
if digit not in "01": if digit not in "01":
print("Error: invalid digit: " + digit, file=sys.stderr) print("Error: invalid digit: " + digit, file=sys.stderr)
return False return False
if mode == 'a': # AND operator if mode == 'a': # AND operator.
switch_state = self.get_switch_state() switch_state = self.get_switch_state()
userinput = "".join(map(lambda x, y: str(int(x) & int(y)), userinput = "".join(map(lambda x, y: str(int(x) & int(y)),
userinput, switch_state)) userinput, switch_state))
elif mode == 'o': # OR operator elif mode == 'o': # OR operator.
switch_state = self.get_switch_state() switch_state = self.get_switch_state()
userinput = "".join(map(lambda x, y: str(int(x) | int(y)), userinput = "".join(map(lambda x, y: str(int(x) | int(y)),
userinput, switch_state)) userinput, switch_state))
command=[] command=[]
for i in range(len(self.switches)): for i in range(len(self.switches)):
# Skip unchanged switches if we happen to know their state # Skip unchanged switches if we happen to know their state.
if "switch_state" in dir(): if "switch_state" in dir():
if switch_state[i] == userinput[i]: continue if switch_state[i] == userinput[i]: continue
@ -609,12 +643,12 @@ class C4Room:
if colorscheme.get_color_for(light.topic): if colorscheme.get_color_for(light.topic):
# Update internal state of this Dmx object, so we can query # Update internal state of this Dmx object, so we can query
# <object>.payload later # <object>.payload later.
light.set_color(colorscheme.get_color_for(light.topic)) light.set_color(colorscheme.get_color_for(light.topic))
if magic: if magic:
# Send color to ghost instead of the "real" light # Send color to ghost instead of the "real" light.
# Generate the ghost topic for topic # Generate the ghost topic for topic.
ghost = "ghosts" + light.topic[light.topic.find('/'):] ghost = "ghosts" + light.topic[light.topic.find('/'):]
command.append({ command.append({
@ -631,7 +665,7 @@ class C4Room:
# Nothing to do. May happen if a preset defines no color for a room. # Nothing to do. May happen if a preset defines no color for a room.
if command == []: return if command == []: return
if magic: # Do not retain "magic" messages if magic: # Do not retain "magic" messages.
return self.c4.push(command, retain=False) return self.c4.push(command, retain=False)
else: else:
return self.c4.push(command) return self.c4.push(command)
@ -727,23 +761,23 @@ class ColorScheme:
self.mapping = {} self.mapping = {}
self.single_color = False self.single_color = False
self.return_random_color = False self.return_random_color = False
self.available = None # List of available presets self.available = None # List of available presets.
if init: if init:
# Load or generate preset # Load or generate preset.
if init[0] == '#': if init[0] == '#':
return self.from_color(init) return self.from_color(init)
elif self._expand_preset(init) == "off": elif self._expand_preset(init) == "off":
# Virtual preset: set masters to #000000 # Virtual preset: set masters to #000000.
return self.from_color("000000") return self.from_color("000000")
elif self._expand_preset(init) == "random": elif self._expand_preset(init) == "random":
# Virtual preset: return random color on every query # Virtual preset: return random color on every query.
return self.from_random() return self.from_random()
else: else:
# Load preset file # Load preset file.
return self.from_file(init) return self.from_file(init)
def __bool__(self): def __bool__(self):
# Return true if get_color_for has a chance to present anything useful # Return true if get_color_for has a chance to present anything useful.
if self.mapping: return True if self.mapping: return True
if self.single_color: return True if self.single_color: return True
if self.return_random_color: return True if self.return_random_color: return True
@ -753,10 +787,10 @@ class ColorScheme:
""" Returns path of the config dir. """ """ Returns path of the config dir. """
import os import os
# The name of our config directory # The name of our config directory.
XDG_NAME = "c4ctrl" XDG_NAME = "c4ctrl"
# Get XDG_CONFIG_DIR from environment or set default # Get XDG_CONFIG_DIR from environment or set default.
if "XDG_CONFIG_DIR" in os.environ: if "XDG_CONFIG_DIR" in os.environ:
XDG_CONFIG_DIR = os.environ["XDG_CONFIG_DIR"] XDG_CONFIG_DIR = os.environ["XDG_CONFIG_DIR"]
else: else:
@ -787,19 +821,19 @@ class ColorScheme:
else: else:
self.available = os.listdir(config_dir) self.available = os.listdir(config_dir)
self.available.extend(self._virtual_presets) self.available.extend(self._virtual_presets)
# Search for an exact match first # Search for an exact match first.
for a in self.available: for a in self.available:
if a == preset: return a if a == preset: return a
# Return anything which begins with the name given # Return anything which begins with the name given.
for a in self.available: for a in self.available:
if a.find(preset) == 0: return a if a.find(preset) == 0: return a
# Fallback # Fallback.
return preset return preset
def _topic_is_master(self, topic): def _topic_is_master(self, topic):
""" Does the given topic look like a master topic? """ """ Does the given topic look like a master topic? """
return topic.lower().rfind("/master") == len(topic)-7 # 7 = len("/master") return topic.lower().rfind("/master") == len(topic)-7 # 7 = len("/master").
def _random_color(self): def _random_color(self):
""" Returns a 3*4 bit pseudo random color in 6 char hex notation. """ """ Returns a 3*4 bit pseudo random color in 6 char hex notation. """
@ -829,10 +863,10 @@ class ColorScheme:
return self.single_color return self.single_color
elif self.return_random_color: elif self.return_random_color:
# We need to take care not to return colors for both "normal" and # We need to take care not to return colors for both "normal" and
# master topics # master topics.
if not self._topic_is_master(topic): if not self._topic_is_master(topic):
return self._random_color() return self._random_color()
# Fallback # Fallback.
return None return None
def from_file(self, preset): def from_file(self, preset):
@ -847,9 +881,9 @@ class ColorScheme:
print("Error: could not load preset!") print("Error: could not load preset!")
return return
# Expand preset name # Expand preset name.
preset = self._expand_preset(preset) preset = self._expand_preset(preset)
# Try to open the preset file # Try to open the preset file.
fn = os.path.join(config_dir, preset) fn = os.path.join(config_dir, preset)
try: try:
fd = open(fn) fd = open(fn)
@ -857,22 +891,22 @@ class ColorScheme:
print("Error: could not load preset \"{}\" (file could not be accessed)!".format(preset)) print("Error: could not load preset \"{}\" (file could not be accessed)!".format(preset))
return return
# Parse the preset file # Parse the preset file.
self.mapping = {} self.mapping = {}
self.name = preset self.name = preset
for line in fd.readlines(): for line in fd.readlines():
# Skip every line which does not begin with an alphabetic character # Skip every line which does not begin with an alphabetic character.
try: try:
if not line.lstrip()[0].isalpha(): continue if not line.lstrip()[0].isalpha(): continue
except IndexError: continue # Empty line except IndexError: continue # Empty line.
# Strip spaces and split # Strip spaces and split.
k, v = line.replace(' ','').replace('\t','').split('=') k, v = line.replace(' ','').replace('\t','').split('=')
# Convert #fff to fff and remove trailing comments, nl and cr chars # Convert #fff to fff and remove trailing comments, nl and cr chars.
vl = v.rstrip("\n\r").split('#') vl = v.rstrip("\n\r").split('#')
v = vl[0] or vl[1] v = vl[0] or vl[1]
# Validate hex code # Validate hex code.
for c in v.lower(): for c in v.lower():
if c not in "0123456789abcdef": if c not in "0123456789abcdef":
print("Error: invalid color code \"{}\" in preset \"{}\"!".format(v, preset), file=sys.stderr) print("Error: invalid color code \"{}\" in preset \"{}\"!".format(v, preset), file=sys.stderr)
@ -925,14 +959,14 @@ is reserved. Please choose a different one.".format(name))
else: else:
import os import os
# Put preset in our config directory, create it if necessary # Put preset in our config directory, create it if necessary.
config_dir = self._get_config_dir(create=True) config_dir = self._get_config_dir(create=True)
# Strip any path elements # Strip any path elements.
name = os.path.split(name)[1] name = os.path.split(name)[1]
fn = os.path.join(config_dir, name) fn = os.path.join(config_dir, name)
try: try:
fd = open(fn, 'xt') # x = new file (writing), t = text mode fd = open(fn, 'xt') # x = new file (writing), t = text mode.
except FileExistsError: except FileExistsError:
print("A preset with this name already exists, overwrite? [y/N]", print("A preset with this name already exists, overwrite? [y/N]",
end=' ', flush=True) end=' ', flush=True)
@ -941,7 +975,7 @@ is reserved. Please choose a different one.".format(name))
else: else:
return False return False
# Get current states # Get current states.
c4 = C4Interface() c4 = C4Interface()
if name == '-': if name == '-':
@ -966,18 +1000,18 @@ is reserved. Please choose a different one.".format(name))
for r in responce: for r in responce:
if r.topic == light.topic: if r.topic == light.topic:
light.set_color(r.payload.hex()) light.set_color(r.payload.hex())
# Format payload more nicely # Format payload more nicely.
color = light.color color = light.color
if len(color) > 6: if len(color) > 6:
color = color[:6] + ' ' + color[6:] color = color[:6] + ' ' + color[6:]
topic = light.topic.ljust(max_topic_len) topic = light.topic.ljust(max_topic_len)
# Out comment master, as it would override everything else # Out comment master, as it would override everything else.
if self._topic_is_master(r.topic): if self._topic_is_master(r.topic):
fd.write("#{} = {}\n".format(topic, color)) fd.write("#{} = {}\n".format(topic, color))
else: else:
fd.write("{} = {}\n".format(topic, color)) fd.write("{} = {}\n".format(topic, color))
# Do not close stdout # Do not close stdout.
if name != '-': if name != '-':
fd.close() fd.close()
print("Wrote preset \"{}\"".format(name)) print("Wrote preset \"{}\"".format(name))
@ -1024,13 +1058,13 @@ class RemotePresets:
""" Returns a valid room name expanded from the given name. """ """ Returns a valid room name expanded from the given name. """
if name in self.map.keys(): if name in self.map.keys():
# Return on exact match # Return on exact match.
return name return name
for room in self.map.keys(): for room in self.map.keys():
if room.find(name) == 0: if room.find(name) == 0:
return room return room
# Fallback # Fallback.
return name return name
def _expand_preset_name(self, name, rooms, available): def _expand_preset_name(self, name, rooms, available):
@ -1054,28 +1088,28 @@ class RemotePresets:
for preset in available["global"]: for preset in available["global"]:
# Candidate? # Candidate?
if preset == name or preset.find(name) == 0: if preset == name or preset.find(name) == 0:
# Presets in "global" are available everywhere # Presets in "global" are available everywhere.
matchtable[preset] = len(rooms) matchtable[preset] = len(rooms)
for room in rooms: for room in rooms:
for preset in available[room]: for preset in available[room]:
# Candidate? # Candidate?
if preset == name or preset.find(name) == 0: if preset == name or preset.find(name) == 0:
if preset in matchtable.keys(): if preset in matchtable.keys():
matchtable[preset] += 1 matchtable[preset] += 1
else: else:
matchtable[preset] = 1 matchtable[preset] = 1
# First check if there is an exact match in all rooms # First check if there is an exact match in all rooms.
if name in matchtable.keys() and matchtable[name] >= len(rooms): if name in matchtable.keys() and matchtable[name] >= len(rooms):
return name return name
# Return first preset available in all rooms # Return first preset available in all rooms.
for match in matchtable.keys(): for match in matchtable.keys():
if matchtable[match] >= len(rooms): if matchtable[match] >= len(rooms):
return match return match
elif match in available["global"]: elif match in available["global"]:
return match return match
# Fallback # Fallback.
return name return name
def query_available(self, rooms=["global"]): def query_available(self, rooms=["global"]):
@ -1083,7 +1117,7 @@ class RemotePresets:
import json import json
# Presets in "global" are available everywhere and should always be included # Presets in "global" are available everywhere and should always be included.
if "global" not in rooms: if "global" not in rooms:
rooms.insert(0, "global") rooms.insert(0, "global")
@ -1097,7 +1131,7 @@ class RemotePresets:
c4 = C4Interface() c4 = C4Interface()
responce = c4.pull(req) responce = c4.pull(req)
# Make responce iterable # Make responce iterable.
if type(responce) != list: responce = [responce] if type(responce) != list: responce = [responce]
available = {} available = {}
@ -1125,12 +1159,12 @@ class RemotePresets:
def apply_preset(self, preset, rooms=["global"]): def apply_preset(self, preset, rooms=["global"]):
""" Apply preset to given rooms. """ """ Apply preset to given rooms. """
# Strip spaces and expand rooms names # Strip spaces and expand rooms names.
for i in range(len(rooms)): for i in range(len(rooms)):
rooms[i] = self._expand_room_name(rooms[i].strip()) rooms[i] = self._expand_room_name(rooms[i].strip())
available = self.query_available(rooms.copy()) available = self.query_available(rooms.copy())
# Produce some fake data to prevent KeyErrors if in debug mode # Produce some fake data to prevent KeyErrors if in debug mode.
if C4Interface.debug: if C4Interface.debug:
print("[DEBUG] Warning: handing over fake data to allow for further execution!", print("[DEBUG] Warning: handing over fake data to allow for further execution!",
file=sys.stderr) file=sys.stderr)
@ -1141,7 +1175,7 @@ class RemotePresets:
"fnord" : [preset], "fnord" : [preset],
"keller" : [preset] "keller" : [preset]
} }
# Expand preset name (stripping spaces) # Expand preset name (stripping spaces).
preset = self._expand_preset_name(preset, rooms.copy(), available.copy()) preset = self._expand_preset_name(preset, rooms.copy(), available.copy())
for room in rooms: for room in rooms:
@ -1183,7 +1217,7 @@ if __name__ == "__main__":
# Kitchenlight control # Kitchenlight control
group_kl = parser.add_argument_group(title="Kitchenlight control") group_kl = parser.add_argument_group(title="Kitchenlight control")
group_kl.add_argument( group_kl.add_argument(
"-k", "--kl-mode", type=str, metavar="MODE[,OPTIONS]", "-k", "--kl-mode", nargs='+', type=str, metavar=("MODE", "OPTIONS"),
help="set Kitchenlight to MODE") help="set Kitchenlight to MODE")
group_kl.add_argument( group_kl.add_argument(
"-i", "--list-kl-modes", action="store_true", "-i", "--list-kl-modes", action="store_true",
@ -1237,7 +1271,7 @@ if __name__ == "__main__":
description="Available room names are \"wohnzimmer\", \"plenar\", \ description="Available room names are \"wohnzimmer\", \"plenar\", \
\"fnord\" and \"keller\". Preset and room names may be abbreviated.") \"fnord\" and \"keller\". Preset and room names may be abbreviated.")
group_rp.add_argument( group_rp.add_argument(
"-r", "--remote-preset", type=str, metavar="PRESET[:ROOM[,ROOM,...]]", "-r", "--remote-preset", nargs='+', type=str, metavar=("PRESET", "ROOM"),
help="activate remote PRESET for ROOM(s). Activates preset globally \ help="activate remote PRESET for ROOM(s). Activates preset globally \
if ROOM is omitted.") if ROOM is omitted.")
group_rp.add_argument( group_rp.add_argument(
@ -1246,7 +1280,7 @@ if __name__ == "__main__":
is omitted.") is omitted.")
args = parser.parse_args() args = parser.parse_args()
# Debug, gate, status and shutdown # Debug, gate, status and shutdown.
if args.debug: if args.debug:
C4Interface.debug = True C4Interface.debug = True
if args.status: if args.status:
@ -1265,16 +1299,15 @@ if __name__ == "__main__":
Kitchenlight().list_available() Kitchenlight().list_available()
if args.kl_mode: if args.kl_mode:
kl = Kitchenlight() kl = Kitchenlight()
mode = args.kl_mode.split(',') if len(args.kl_mode) == 1:
if len(mode) == 1: kl.set_mode(args.kl_mode[0])
kl.set_mode(mode[0])
else: else:
kl.set_mode(mode[0], mode[1:]) kl.set_mode(args.kl_mode[0], args.kl_mode[1:])
# Colorscheme # Colorscheme
if args.store_as: if args.store_as:
ColorScheme().store(args.store_as) ColorScheme().store(args.store_as)
presets = {} # Store and reuse initialized presets presets = {} # Store and reuse initialized presets.
if args.w_color: if args.w_color:
if args.w_color not in presets: if args.w_color not in presets:
presets[args.w_color] = ColorScheme(args.w_color) presets[args.w_color] = ColorScheme(args.w_color)
@ -1304,12 +1337,11 @@ if __name__ == "__main__":
if args.list_remote: if args.list_remote:
RemotePresets().list_available(args.list_remote.lower()) RemotePresets().list_available(args.list_remote.lower())
if args.remote_preset: if args.remote_preset:
remote_opts = args.remote_preset.split(':') if len(args.remote_preset) == 1:
if len(remote_opts) == 1: RemotePresets().apply_preset(args.remote_preset[0].strip())
RemotePresets().apply_preset(remote_opts[0].strip())
else: else:
RemotePresets().apply_preset(remote_opts[0].strip(), RemotePresets().apply_preset(args.remote_preset[0].strip(),
remote_opts[1].lower().split(',')) args.remote_preset[1:])
# No or no useful command line options? # No or no useful command line options?
if len(sys.argv) <= 1 or len(sys.argv) == 2 and args.debug: if len(sys.argv) <= 1 or len(sys.argv) == 2 and args.debug: