Various enhancements

This commit is contained in:
Shy 2017-04-08 16:26:24 +02:00
parent 80186534dc
commit a4310ec1c7

200
c4ctrl.py
View file

@ -1,4 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
#
# Author: Shy
""" """
A command line client for Autoc4, the home automation system of the C4. A command line client for Autoc4, the home automation system of the C4.
@ -10,9 +12,6 @@ Dependencies:
(available from https://github.com/eclipse/paho.mqtt.python) (available from https://github.com/eclipse/paho.mqtt.python)
""" """
__date__ = "07 April 2017"
__author__ = "Shy"
import sys import sys
@ -25,39 +24,41 @@ class C4Interface():
retain = True retain = True
debug = False debug = False
def __init__(self, topic=None):
# Set a default topic
if topic: self.topic = topic
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.
message may a byte encoded payload or a list of byte encoded message may be a byte encoded payload or a list of either dict()s
payloads. """ or tuples()s. If message is a byte encoded payload, topic= must be
given. dict()s and tuple()s should lool like this:
dict("topic": str(topic), "payload": bytes(payload))
tuple(str(topic), bytes(payload)) """
from paho.mqtt import publish from paho.mqtt import publish
# Overwrite defaults # Skip empty messages
if topic: self.topic = topic if message == [] or message == "": return
if retain != None: self.retain = retain
# Set defaults
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
item["retain"] = self.retain item["retain"] = retain
elif type(item) == tuple: elif type(item) == tuple:
new_item = ( new_item = (
item[0] or self.topic, # topic item[0] or topic, # topic
item[1], # payload item[1], # payload
self.qos, # qos self.qos, # qos
self.retain # retain retain # retain
) )
message.remove(item) message.remove(item)
message.append(new_item) message.append(new_item)
if self.debug: return print("[DEBUG] inhibited messages:", message) if self.debug: return print("[DEBUG] inhibited messages:",
message, file=sys.stderr)
publish.multiple(message, publish.multiple(message,
hostname=self.broker, hostname=self.broker,
@ -66,29 +67,32 @@ class C4Interface():
else: # Message is not a list 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(
self.topic, message)) topic, message), file=sys.stderr)
publish.single(self.topic, publish.single(topic,
payload=message, payload=message,
qos=self.qos, qos=self.qos,
retain=self.retain, retain=retain,
hostname=self.broker, hostname=self.broker,
port=self.port) port=self.port)
def pull(self, topic=[]): def pull(self, topic=[]):
""" Return the state of a topic. """ Return the state of a topic.
topic may be a list of topics or a single topic given as string. """ topic may be a list of topics or a single topic given as string.
Returns a paho message object or list of message objects. """
from paho.mqtt import subscribe from paho.mqtt import subscribe
topic = topic or self.topic # topic must be a list
# <topic> must be a list
if type(topic) == str: if type(topic) == str:
topic = [topic] topic = [topic]
# Skip empty queries
if topic == []: return
if self.debug: if self.debug:
print("[DEBUG] inhibited query for:", topic) print("[DEBUG] inhibited query for:", topic, file=sys.stderr)
return [] return []
return subscribe.simple(topic, return subscribe.simple(topic,
@ -100,15 +104,16 @@ class C4Interface():
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. """
st = self.pull("club/status") club_status = self.pull("club/status")
# Produce 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 further execution!") print("[DEBUG] Warning: handing over fake data to allow for further execution!",
class st: pass file=sys.stderr)
st.payload = b'\x00' class club_status: pass
club_status.payload = b'\x00'
if st.payload == b'\x01': if club_status.payload == b'\x01':
return "open" return "open"
else: else:
return "closed" return "closed"
@ -131,6 +136,7 @@ class C4Interface():
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
template = "000000" template = "000000"
def __init__(self, topic, color=None): def __init__(self, topic, color=None):
@ -139,20 +145,20 @@ class Dmx:
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 value into hex template. """ Merge hex color values or payloads into the template.
Expand 4 bit hex code notation (eg. #f0f) and pad with template
to get a fitting payload for this kind of light. """
Expand 4 bit hex code notation (eg. #f0f) and pad with template. """
if len(color) > len(self.template): if len(color) > len(self.template):
# Silently truncate # 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.
# Yet, lets presume that a 6-char code should never be expanded. # Yet, let's presume that a 6-char code is alway meant to be
# interpreted as a color and should never be expanded.
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):
expanded = "" color = "".join(char*2 for char in color)
for c in color:
expanded += c*2
color = expanded
if len(color) == len(self.template): # Nothing more to do if len(color) == len(self.template): # Nothing more to do
return color return color
@ -165,6 +171,7 @@ class Dmx:
""" Set color (hex) for this instance. """ Set color (hex) for this instance.
The color is then available via its color variable. """ The color is then available via its color variable. """
color = self._pad_color(color) color = self._pad_color(color)
self.color = color self.color = color
@ -174,12 +181,15 @@ 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
template = "000000ff" template = "000000ff"
class Dmx7(Dmx): 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
# for brightness
template = "000000000000ff" template = "000000000000ff"
@ -191,7 +201,10 @@ class C4Room:
self.switch_state = "" # State of switches in the like of str("0010") self.switch_state = "" # State of switches in the like of str("0010")
def _interactive_light_switch(self): def _interactive_light_switch(self):
""" Interactively ask for input. Returns str(userinput). """ """ Interactively ask for input.
Returns str(userinput). Will not write to stdout if sys.stdin is
no tty. """
if sys.stdin.isatty(): if sys.stdin.isatty():
print("[{}]".format(self.name)) print("[{}]".format(self.name))
@ -211,18 +224,24 @@ class C4Room:
return userinput return userinput
def _get_state(self): def _get_state(self):
""" Returns current state of switches as a str() of 1s and 0s. """ """ Returns current state of switches as a string of 1s and 0s. """
state = "" state = ""
req = [] req = []
for topic in self.switches: for topic in self.switches:
req.append(topic[1]) req.append(topic[1])
responce = self.c4.pull(req) responce = self.c4.pull(req)
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, byteorder="little"))
if C4Interface.debug:
print("[DEBUG] Warning: handing over fake data to allow for further execution!",
file=sys.stderr)
state = '0' * len(self.switches)
return state return state
def light_switch(self, userinput=""): def light_switch(self, userinput=""):
@ -278,9 +297,13 @@ class C4Room:
command=[] command=[]
for i in range(len(self.switches)): for i in range(len(self.switches)):
# If we know their state, skip switches which are unchanged
if self.switch_state:
if self.switch_state[i] == userinput[i]: continue
command.append({ command.append({
"topic" : self.switches[i][1], "topic" : self.switches[i][1],
"payload" : bytearray([int(userinput[i])]) "payload" : bytes([int(userinput[i])])
}) })
return self.c4.push(command) return self.c4.push(command)
@ -288,7 +311,7 @@ class C4Room:
def set_colorscheme(self, colorscheme, magic): def set_colorscheme(self, colorscheme, magic):
""" Apply colorscheme to the LED Cans in this room. """ """ Apply colorscheme to the LED Cans in this room. """
cmd = [] command = []
for light in self.lights: for light in self.lights:
if colorscheme.color_for(light.topic): if colorscheme.color_for(light.topic):
@ -301,23 +324,24 @@ class C4Room:
# 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('/'):]
cmd.append({ command.append({
"topic" : ghost, "topic" : ghost,
"payload" : light.payload "payload" : light.payload
}) })
else: else:
# Send data to the real lanterns, not fluffyd. # Send data to the real lanterns, not fluffyd.
cmd.append({ command.append({
"topic" : light.topic, "topic" : light.topic,
"payload" : light.payload "payload" : light.payload
}) })
if cmd == []: return # Nothing to do. May happen if a preset defines no color for a room.
if command == []: return
if magic: # Do not retain "magic" messages if magic: # Do not retain "magic" messages
return self.c4.push(cmd, retain=False) return self.c4.push(command, retain=False)
else: else:
return self.c4.push(cmd) return self.c4.push(command)
class Wohnzimmer(C4Room): class Wohnzimmer(C4Room):
@ -400,22 +424,9 @@ class Keller(C4Room):
class Kitchenlight: class Kitchenlight:
""" The Kitchenlight. """ """ Interface to the Kitchenlight and its functions. """
_END = "little" # Endianess _END = "little" # Kitchenlight endianess
_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", def __init__(self, topic="kitchenlight/change_screen",
powertopic="power/wohnzimmer/kitchenlight", powertopic="power/wohnzimmer/kitchenlight",
@ -428,26 +439,42 @@ class Kitchenlight:
""" Send commands via a C4Interface to the MQTT broker. """ """ Send commands via a C4Interface to the MQTT broker. """
if self.autopower or poweron or poweroff: if self.autopower or poweron or poweroff:
c4 = C4Interface(self.topic) c4 = C4Interface()
cmd = [] command = []
cmd.append({ command.append({
"topic" : self.topic, "topic" : self.topic,
"payload" : data }) "payload" : data })
if poweroff: if poweroff:
cmd.append({ command.append({
"topic" : self.powertopic, "topic" : self.powertopic,
"payload" : b'\x00'}) "payload" : b'\x00'})
elif self.autopower or poweron: elif self.autopower or poweron:
cmd.append({ command.append({
"topic" : self.powertopic, "topic" : self.powertopic,
"payload" : b'\x01'}) "payload" : b'\x01'})
c4.push(cmd) c4.push(command)
else: else:
c4 = C4Interface(self.topic) c4 = C4Interface()
c4.push(data) c4.push(data, topic=self.topic)
def list_available(self):
""" Print a list of available Kitchenlight modes. """
print("Available Kitchenlight modes (options are optional):")
print("""
off turn off Kitchenlight
checker[,DELAY[,COLOR_1[,COLOR_2]]] Checker
matrix[,LINES] Matrix
mood[,1|2] (1=Colorwheel, 2=Random) Moodlight
oc[,DELAY] Open Chaos
pacman Pacman
sine Sine
text[,TEXT[,DELAY]] Text
flood Flood
clock Clock""")
def set_mode(self, mode, opts=[]): def set_mode(self, mode, opts=[]):
"""Switch to given mode.""" """ Switch to given mode. """
mode = mode.lower() mode = mode.lower()
if mode == "off": if mode == "off":
@ -458,7 +485,7 @@ class Kitchenlight:
return self.matrix(*opts) return self.matrix(*opts)
if mode == "mood": if mode == "mood":
return self.moodlight(*opts) return self.moodlight(*opts)
if mode == "oc" or mode == "openchaos": if mode == "oc":
return self.openchaos(*opts) return self.openchaos(*opts)
if mode == "pacman": if mode == "pacman":
return self.pacman() return self.pacman()
@ -836,7 +863,8 @@ class ColorScheme:
# First of all, refuse to override virtual presets # First of all, refuse to override virtual presets
if name in self._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)) 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 return False
if name == '-': if name == '-':
@ -1047,7 +1075,8 @@ class RemotePresets:
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 further execution!") print("[DEBUG] Warning: handing over fake data to allow for further execution!",
file=sys.stderr)
available = { available = {
"global" : [preset], "global" : [preset],
"wohnzimmer" : [preset], "wohnzimmer" : [preset],
@ -1079,7 +1108,8 @@ if __name__ == "__main__":
description="Command line client for AutoC4.") description="Command line client for AutoC4.")
parser.add_argument( parser.add_argument(
"-d", "--debug", action="store_true", "-d", "--debug", action="store_true",
help="display what would be send to the MQTT broker, but do not actually connect") help="display what would be send to the MQTT broker, but do not \
actually connect")
# Various club functions # Various club functions
group_fn = parser.add_argument_group(title="various functions") group_fn = parser.add_argument_group(title="various functions")
@ -1104,7 +1134,9 @@ if __name__ == "__main__":
# Ambient control # 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 (which may be abbreviated), '#' followed by a color value in hex notation (eg. \"#ff0066\") or '-' to read from stdin.") description="PRESET may be either a preset name (which may be \
abbreviated), '#' followed by a color value in hex notation (eg. \
\"#ff0066\") or '-' to read from stdin.")
group_cl.add_argument( group_cl.add_argument(
"-w", "--wohnzimmer", type=str, dest="w_color", metavar="PRESET", "-w", "--wohnzimmer", type=str, dest="w_color", metavar="PRESET",
help="apply local colorscheme PRESET to Wohnzimmer") help="apply local colorscheme PRESET to Wohnzimmer")
@ -1116,7 +1148,7 @@ if __name__ == "__main__":
help="apply local colorscheme PRESET to Fnordcenter") help="apply local colorscheme PRESET to Fnordcenter")
group_cl.add_argument( group_cl.add_argument(
"-m", "--magic", action="store_true", "-m", "--magic", action="store_true",
help="use fluffyd to change colors") help="EXPERIMENTAL: use fluffyd to change colors")
group_cl.add_argument( group_cl.add_argument(
"-l", "--list-presets", action="store_true", "-l", "--list-presets", action="store_true",
help="list locally available presets") help="list locally available presets")
@ -1126,7 +1158,9 @@ if __name__ == "__main__":
# Switch control # Switch control
group_sw = parser.add_argument_group(title="light switch control", group_sw = parser.add_argument_group(title="light switch control",
description="BINARY_CODE is a string of 0s or 1s for every light in the room. Accepts integers also. Will show some information and ask for input if omitted.") description="BINARY_CODE is a string of 0s or 1s for every light in the \
room. Accepts decimals also. May be prepended by '&' or '^' as AND or \
OR operators. Will show some information and ask for input if omitted.")
group_sw.add_argument( group_sw.add_argument(
"-W", nargs='?', dest="w_switch", const="", metavar="BINARY_CODE", "-W", nargs='?', dest="w_switch", const="", metavar="BINARY_CODE",
help="switch lights in Wohnzimmer on/off") help="switch lights in Wohnzimmer on/off")
@ -1142,13 +1176,16 @@ if __name__ == "__main__":
# Remote presets # Remote presets
group_rp = parser.add_argument_group(title="remote preset functions", group_rp = parser.add_argument_group(title="remote preset functions",
description="Available room names are \"wohnzimmer\", \"plenar\", \"fnord\" and \"keller\". Preset and room names may be abbreviated.") description="Available room names are \"wohnzimmer\", \"plenar\", \
\"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", type=str, metavar="PRESET[:ROOM[,ROOM,...]]",
help="activate remote PRESET for ROOM(s).") help="activate remote PRESET for ROOM(s). Activates preset globally \
if ROOM is omitted.")
group_rp.add_argument( group_rp.add_argument(
"-R", "--list-remote", nargs='?', const="global", metavar="ROOM", "-R", "--list-remote", nargs='?', const="global", metavar="ROOM",
help="list remote presets for ROOM") help="list remote presets for ROOM. Will list global presets if ROOM \
is omitted.")
args = parser.parse_args() args = parser.parse_args()
# Debug, gate, status and shutdown # Debug, gate, status and shutdown
@ -1167,8 +1204,7 @@ if __name__ == "__main__":
# Kitchenlight # Kitchenlight
if args.list_kl_modes: if args.list_kl_modes:
print("Available Kitchenlight modes (options are optional):") Kitchenlight().list_available()
print(Kitchenlight._available_modes)
if args.kl_mode: if args.kl_mode:
kl = Kitchenlight() kl = Kitchenlight()
mode = args.kl_mode.split(',') mode = args.kl_mode.split(',')