Set qos=2, adapted to new fluffyd version

This commit is contained in:
Shy 2017-04-06 16:33:32 +02:00
parent 66fa06c11a
commit e64c157421

167
c4ctrl.py
View file

@ -1,15 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# #
# c4ctrl: Command line client for AutoC4 # c4ctrl: Command line client for AutoC4
#
# Author: Shy
"""
This is a command line client for Autoc4, the home automation system of the C4.
Some parts of it **may** be useful as python module for simple tasks.
"""
import sys import sys
class C4Interface(): class C4Interface():
""" Interaction with the C4 home automation system. """ """ Interaction with the C4 home automation system. """
port = 1883 port = 1883
broker = "autoc4.labor.koeln.ccc.de" broker = "autoc4.labor.koeln.ccc.de"
qos = 0 qos = 2
retain = True retain = True
debug = False debug = False
@ -18,7 +27,10 @@ class C4Interface():
if topic: self.topic = topic if topic: self.topic = topic
def push(self, cmd, topic=None, retain=None): def push(self, cmd, topic=None, retain=None):
"""Send cmd to topic via the MQTT broker.""" """ Send a message to the MQTT broker.
cmd may a byte encoded payload or a list of byte encoded
payloads. """
from paho.mqtt import publish from paho.mqtt import publish
# Overwrite defaults # Overwrite defaults
@ -60,7 +72,9 @@ class C4Interface():
port=self.port) port=self.port)
def pull(self, topic=[]): def pull(self, topic=[]):
"""Return current state of topic.""" """ Return the state of a topic.
topic may be a list of topics or a single topic given as string. """
from paho.mqtt import subscribe from paho.mqtt import subscribe
topic = topic or self.topic topic = topic or self.topic
# <topic> must be a list # <topic> must be a list
@ -78,7 +92,7 @@ class C4Interface():
port=self.port) port=self.port)
def status(self): def status(self):
"""Print current status (open or closed) of C4.""" """ Returns current status (string "open" or "closed") of the club. """
st = self.pull("club/status") st = self.pull("club/status")
# Produce fake result to prevent errors if in debug mode # Produce fake result to prevent errors if in debug mode
@ -88,9 +102,9 @@ class C4Interface():
st.payload = b'\x00' st.payload = b'\x00'
if st.payload == b'\x01': if st.payload == b'\x01':
print("Club is open") return "open"
else: else:
print("Club is closed") return "closed"
def open_gate(self): def open_gate(self):
"""Open the gate.""" """Open the gate."""
@ -115,9 +129,6 @@ class Dmx:
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 __repr__(self):
return "<Dmx '{}: {}'>".format(self.topic, self.color)
def _pad_color(self, color): def _pad_color(self, color):
""" Merge hex color value into hex template. """ Merge hex color value into hex template.
@ -156,21 +167,15 @@ class Dmx4(Dmx):
template = "000000ff" template = "000000ff"
def __repr__(self):
return "<Dmx4 '{}: {}'>".format(self.topic, self.color)
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. """
template = "000000000000ff" template = "000000000000ff"
def __repr__(self):
return "<Dmx7 '{}: {}'>".format(self.topic, self.color)
class C4Room: class C4Room:
"""Base class for club rooms.""" """ Methods of rooms in the club. """
def __init__(self): def __init__(self):
self.c4 = C4Interface() self.c4 = C4Interface()
@ -205,7 +210,8 @@ class C4Room:
return userinput return userinput
def light_switch(self, userinput=""): def light_switch(self, userinput=""):
"""Switch lamps in this rooms on and off.""" """ Switch lamps in a room on or off. """
if not userinput: if not userinput:
userinput = self._interactive_light_switch() userinput = self._interactive_light_switch()
if userinput == "": return if userinput == "": return
@ -237,40 +243,42 @@ class C4Room:
return self.c4.push(cmd) return self.c4.push(cmd)
def set_colorscheme(self, colorscheme, magic): def set_colorscheme(self, colorscheme, no_magic):
""" Apply colorscheme to the LED Cans in this room. """ """ Apply colorscheme to the LED Cans in this room. """
cmd = [] cmd = []
for light in self.lights: for light in self.lights:
if colorscheme.color_for(light.topic): if colorscheme.color_for(light.topic):
if magic != "none" and magic != '0':
# Send color to ghost instead of the "real" light
mode_id, error = Fluffy().mode_id(magic)
if error:
print("Warning: unknown mode \"{}\". Using default.".format(
magic), file=sys.stderr)
# Update internal state of this Dmx object, so we can query
# <object>.payload later
light.set_color(colorscheme.color_for(light.topic)) light.set_color(colorscheme.color_for(light.topic))
cmd.append({
"topic" : Fluffy().ghostly_topic(light.topic), if no_magic:
"payload" : Fluffy().ghostly_payload(light.payload, mode_id) # Send data to the real lanterns, not fluffyd.
})
else:
light.set_color(colorscheme.color_for(light.topic))
cmd.append({ cmd.append({
"topic" : light.topic, "topic" : light.topic,
"payload" : light.payload "payload" : light.payload
}) })
else:
# Send color to ghost instead of the "real" light
# Generate the ghost topic for topic
ghost = "ghosts" + light.topic[light.topic.find('/'):]
cmd.append({
"topic" : ghost,
"payload" : light.payload
})
if cmd == []: return if cmd == []: return
if magic: # Do not retain "magic" messages if no_magic:
return self.c4.push(cmd, retain=(not magic))
else:
return self.c4.push(cmd) return self.c4.push(cmd)
else: # Do not retain "magic" messages
return self.c4.push(cmd, retain=False)
class Wohnzimmer(C4Room): class Wohnzimmer(C4Room):
"""The Wohnzimmer.""" """ Description of the Wohnzimmer. """
name = "Wohnzimmer" name = "Wohnzimmer"
switches = ( switches = (
@ -295,7 +303,7 @@ class Wohnzimmer(C4Room):
class Plenarsaal(C4Room): class Plenarsaal(C4Room):
"""The Plenarsaal.""" """ Description of the Plenarsaal. """
name = "Plenarsaal" name = "Plenarsaal"
switches = ( switches = (
@ -318,7 +326,7 @@ class Plenarsaal(C4Room):
class Fnordcenter(C4Room): class Fnordcenter(C4Room):
"""The Fnordcenter.""" """ Description of the Fnordcenter. """
name = "Fnordcenter" name = "Fnordcenter"
switches = ( switches = (
@ -336,7 +344,7 @@ class Fnordcenter(C4Room):
class Keller(C4Room): class Keller(C4Room):
"""The Keller.""" """ Description of the Keller. """
name = "Keller" name = "Keller"
switches = ( switches = (
@ -384,11 +392,11 @@ class Kitchenlight:
if poweroff: if poweroff:
cmd.append({ cmd.append({
"topic" : self.powertopic, "topic" : self.powertopic,
"payload" : bytearray((0,))}) "payload" : b'\x00'})
elif self.autopower or poweron: elif self.autopower or poweron:
cmd.append({ cmd.append({
"topic" : self.powertopic, "topic" : self.powertopic,
"payload" : bytearray((1,))}) "payload" : b'\x01'})
c4.push(cmd) c4.push(cmd)
else: else:
c4 = C4Interface(self.topic) c4 = C4Interface(self.topic)
@ -544,7 +552,8 @@ class Kitchenlight:
class ColorScheme: class ColorScheme:
""" Abstraction of a colorscheme. """ """ Abstraction of a colorscheme. """
# Names of virtual presets # Names of virtual presets. These are always listed as available and the
# user may not save presets under this name.
_virtual_presets = ["off", "random"] _virtual_presets = ["off", "random"]
def __init__(self, autoinit=""): def __init__(self, autoinit=""):
@ -575,6 +584,7 @@ class ColorScheme:
def _get_cfg_dir(self, quiet=False, create=False): def _get_cfg_dir(self, quiet=False, create=False):
""" 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"
@ -621,10 +631,12 @@ class ColorScheme:
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. """
from random import randint, sample from random import randint, sample
chls = [15] chls = [15]
chls.append(randint(0,15)) chls.append(randint(0,15))
@ -660,9 +672,13 @@ class ColorScheme:
return color return color
def color_for(self, topic): def color_for(self, topic):
"""Returns the color (hex) this ColorScheme provides for the given topic.""" """ Returns the color (in hexadecimal notation) this ColorScheme assumes
for the given topic. """
# We need to take care not to return colors for both "normal" topics # We need to take care not to return colors for both "normal" topics
# and masters, as setting masters would override other settings # and masters, as setting masters would override other settings.
# If this ColorScheme has been read from a file though, we asssume that
# the user has taken care of this and apply what we are told to apply.
if self.mapping: if self.mapping:
if topic in self.mapping.keys(): if topic in self.mapping.keys():
return self.mapping[topic] return self.mapping[topic]
@ -677,6 +693,7 @@ class ColorScheme:
def from_file(self, preset): def from_file(self, preset):
""" Load ColorScheme from file. """ """ Load ColorScheme from file. """
if preset == '-': if preset == '-':
fd = sys.stdin fd = sys.stdin
else: else:
@ -809,37 +826,6 @@ class ColorScheme:
print("Wrote preset \"{}\"".format(name)) print("Wrote preset \"{}\"".format(name))
class Fluffy:
"""Fluffyd functions."""
modes = {
# Fluffy modes and their id's
"fade" : 1,
"wave" : 4,
"pulse" : 8,
"emp" : 9,
"flash" : 12
}
def ghostly_topic(self, topic):
# Return the ghost topic of topic
return "ghosts" + topic[topic.find('/'):]
def ghostly_payload(self, payload, mode_id):
return payload + int(mode_id).to_bytes(1, "little")
def mode_id(self, name):
if name.isdecimal() and int(name) <= 255:
# Let's trust the user with this
return (int(name), False)
else:
if name.lower() in self.modes.keys():
return (self.modes[name.lower()], False)
# Fallback
return (0, True)
class RemotePresets: class RemotePresets:
""" Remote preset control. """ """ Remote preset control. """
@ -878,7 +864,7 @@ class RemotePresets:
} }
def _expand_room_name(self, name): def _expand_room_name(self, name):
"""Try to expand partial names.""" """ 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
@ -890,12 +876,15 @@ class RemotePresets:
return name return name
def _expand_preset_name(self, name, rooms, available): def _expand_preset_name(self, name, rooms, available):
"""Try to expand partial preset names. """ Returns a valid preset name expanded from the given name.
<rooms> must be a list of rooms to consider. Takes care to match only presets which are available for all rooms
<available> must be a dict as returned by query_available().""" specified.
# We need to take care to match only presets which are available for
# every room specified rooms is a list of rooms for which the preset should be a valid
option.
available is a dict containing valid presets for rooms as returned
by query_available(). """
# Strip every "global" out of the room list. We take special care of # Strip every "global" out of the room list. We take special care of
# "global" later on. # "global" later on.
@ -962,6 +951,7 @@ class RemotePresets:
def list_available(self, room="global"): def list_available(self, room="global"):
""" Print a list of available Presets. """ """ Print a list of available Presets. """
room = self._expand_room_name(room) room = self._expand_room_name(room)
available = self.query_available([room]) available = self.query_available([room])
@ -975,6 +965,7 @@ 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())
@ -1015,6 +1006,7 @@ if __name__ == "__main__":
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")
group_fn.add_argument( group_fn.add_argument(
@ -1026,6 +1018,7 @@ if __name__ == "__main__":
group_fn.add_argument( group_fn.add_argument(
"-S", "--shutdown", action="count", "-S", "--shutdown", action="count",
help="shutdown (give twice to force shutdown)") help="shutdown (give twice to force shutdown)")
# 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(
@ -1034,6 +1027,7 @@ if __name__ == "__main__":
group_kl.add_argument( group_kl.add_argument(
"-i", "--list-kl-modes", action="store_true", "-i", "--list-kl-modes", action="store_true",
help="list available Kitchenlight modes and their options") help="list available Kitchenlight modes and their options")
# 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.")
@ -1047,14 +1041,15 @@ if __name__ == "__main__":
"-f", "--fnordcenter", type=str, dest="f_color", metavar="PRESET", "-f", "--fnordcenter", type=str, dest="f_color", metavar="PRESET",
help="apply local colorscheme PRESET to Fnordcenter") help="apply local colorscheme PRESET to Fnordcenter")
group_cl.add_argument( group_cl.add_argument(
"-m", "--magic", type=str, default="fade", metavar="MODE", "-N", "--no-magic", action="store_true",
help="EXPERIMENTAL: blend into preset (needs a running instance of fluffyd on the network). MODE is either \"fade\", \"wave\", \"pulse\", \"emp\", \"flash\" or \"none\".") help="Do not 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")
group_cl.add_argument( group_cl.add_argument(
"-o", "--store-preset", type=str, dest="store_as", metavar="NAME", "-o", "--store-preset", type=str, dest="store_as", metavar="NAME",
help="store current state as preset NAME ('-' to write to stdout)") help="store current state as preset NAME ('-' to write to stdout)")
# 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 integers also. Will show some information and ask for input if omitted.")
@ -1070,6 +1065,7 @@ if __name__ == "__main__":
group_sw.add_argument( group_sw.add_argument(
"-K", nargs='?', dest="k_switch", const="", metavar="BINARY_CODE", "-K", nargs='?', dest="k_switch", const="", metavar="BINARY_CODE",
help="switch lights in Keller on/off") help="switch lights in Keller on/off")
# 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.")
@ -1085,7 +1081,8 @@ if __name__ == "__main__":
if args.debug: if args.debug:
C4Interface.debug = True C4Interface.debug = True
if args.status: if args.status:
C4Interface().status() status = C4Interface().status()
print("Club is", status)
if args.gate: if args.gate:
C4Interface().open_gate() C4Interface().open_gate()
if args.shutdown: if args.shutdown:
@ -1113,15 +1110,15 @@ if __name__ == "__main__":
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(autoinit=args.w_color) presets[args.w_color] = ColorScheme(autoinit=args.w_color)
if presets[args.w_color]: Wohnzimmer().set_colorscheme(presets[args.w_color], args.magic) if presets[args.w_color]: Wohnzimmer().set_colorscheme(presets[args.w_color], args.no_magic)
if args.p_color: if args.p_color:
if args.p_color not in presets: if args.p_color not in presets:
presets[args.p_color] = ColorScheme(autoinit=args.p_color) presets[args.p_color] = ColorScheme(autoinit=args.p_color)
if presets[args.p_color]: Plenarsaal().set_colorscheme(presets[args.p_color], args.magic) if presets[args.p_color]: Plenarsaal().set_colorscheme(presets[args.p_color], args.no_magic)
if args.f_color: if args.f_color:
if args.f_color not in presets: if args.f_color not in presets:
presets[args.f_color] = ColorScheme(autoinit=args.f_color) presets[args.f_color] = ColorScheme(autoinit=args.f_color)
if presets[args.f_color]: Fnordcenter().set_colorscheme(presets[args.f_color], args.magic) if presets[args.f_color]: Fnordcenter().set_colorscheme(presets[args.f_color], args.no_magic)
if args.list_presets: if args.list_presets:
ColorScheme().list_available() ColorScheme().list_available()