Set qos=2, adapted to new fluffyd version
This commit is contained in:
parent
66fa06c11a
commit
e64c157421
1 changed files with 118 additions and 121 deletions
167
c4ctrl.py
167
c4ctrl.py
|
@ -1,15 +1,24 @@
|
|||
#!/usr/bin/env python3
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
class C4Interface():
|
||||
""" Interaction with the C4 home automation system. """
|
||||
|
||||
port = 1883
|
||||
broker = "autoc4.labor.koeln.ccc.de"
|
||||
qos = 0
|
||||
qos = 2
|
||||
retain = True
|
||||
debug = False
|
||||
|
||||
|
@ -18,7 +27,10 @@ class C4Interface():
|
|||
if topic: self.topic = topic
|
||||
|
||||
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
|
||||
|
||||
# Overwrite defaults
|
||||
|
@ -60,7 +72,9 @@ class C4Interface():
|
|||
port=self.port)
|
||||
|
||||
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
|
||||
topic = topic or self.topic
|
||||
# <topic> must be a list
|
||||
|
@ -78,7 +92,7 @@ class C4Interface():
|
|||
port=self.port)
|
||||
|
||||
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")
|
||||
|
||||
# Produce fake result to prevent errors if in debug mode
|
||||
|
@ -88,9 +102,9 @@ class C4Interface():
|
|||
st.payload = b'\x00'
|
||||
|
||||
if st.payload == b'\x01':
|
||||
print("Club is open")
|
||||
return "open"
|
||||
else:
|
||||
print("Club is closed")
|
||||
return "closed"
|
||||
|
||||
def open_gate(self):
|
||||
"""Open the gate."""
|
||||
|
@ -115,9 +129,6 @@ class Dmx:
|
|||
self.set_color(color or self.template)
|
||||
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):
|
||||
""" Merge hex color value into hex template.
|
||||
|
||||
|
@ -156,21 +167,15 @@ class Dmx4(Dmx):
|
|||
|
||||
template = "000000ff"
|
||||
|
||||
def __repr__(self):
|
||||
return "<Dmx4 '{}: {}'>".format(self.topic, self.color)
|
||||
|
||||
|
||||
class Dmx7(Dmx):
|
||||
""" Abstraction of the 7 channel LED cans in the club. """
|
||||
|
||||
template = "000000000000ff"
|
||||
|
||||
def __repr__(self):
|
||||
return "<Dmx7 '{}: {}'>".format(self.topic, self.color)
|
||||
|
||||
|
||||
class C4Room:
|
||||
"""Base class for club rooms."""
|
||||
""" Methods of rooms in the club. """
|
||||
|
||||
def __init__(self):
|
||||
self.c4 = C4Interface()
|
||||
|
@ -205,7 +210,8 @@ class C4Room:
|
|||
return 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:
|
||||
userinput = self._interactive_light_switch()
|
||||
if userinput == "": return
|
||||
|
@ -237,40 +243,42 @@ class C4Room:
|
|||
|
||||
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. """
|
||||
cmd = []
|
||||
for light in self.lights:
|
||||
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))
|
||||
cmd.append({
|
||||
"topic" : Fluffy().ghostly_topic(light.topic),
|
||||
"payload" : Fluffy().ghostly_payload(light.payload, mode_id)
|
||||
})
|
||||
else:
|
||||
light.set_color(colorscheme.color_for(light.topic))
|
||||
|
||||
if no_magic:
|
||||
# Send data to the real lanterns, not fluffyd.
|
||||
cmd.append({
|
||||
"topic" : light.topic,
|
||||
"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 magic: # Do not retain "magic" messages
|
||||
return self.c4.push(cmd, retain=(not magic))
|
||||
else:
|
||||
if no_magic:
|
||||
return self.c4.push(cmd)
|
||||
else: # Do not retain "magic" messages
|
||||
return self.c4.push(cmd, retain=False)
|
||||
|
||||
|
||||
class Wohnzimmer(C4Room):
|
||||
"""The Wohnzimmer."""
|
||||
""" Description of the Wohnzimmer. """
|
||||
|
||||
name = "Wohnzimmer"
|
||||
switches = (
|
||||
|
@ -295,7 +303,7 @@ class Wohnzimmer(C4Room):
|
|||
|
||||
|
||||
class Plenarsaal(C4Room):
|
||||
"""The Plenarsaal."""
|
||||
""" Description of the Plenarsaal. """
|
||||
|
||||
name = "Plenarsaal"
|
||||
switches = (
|
||||
|
@ -318,7 +326,7 @@ class Plenarsaal(C4Room):
|
|||
|
||||
|
||||
class Fnordcenter(C4Room):
|
||||
"""The Fnordcenter."""
|
||||
""" Description of the Fnordcenter. """
|
||||
|
||||
name = "Fnordcenter"
|
||||
switches = (
|
||||
|
@ -336,7 +344,7 @@ class Fnordcenter(C4Room):
|
|||
|
||||
|
||||
class Keller(C4Room):
|
||||
"""The Keller."""
|
||||
""" Description of the Keller. """
|
||||
|
||||
name = "Keller"
|
||||
switches = (
|
||||
|
@ -384,11 +392,11 @@ class Kitchenlight:
|
|||
if poweroff:
|
||||
cmd.append({
|
||||
"topic" : self.powertopic,
|
||||
"payload" : bytearray((0,))})
|
||||
"payload" : b'\x00'})
|
||||
elif self.autopower or poweron:
|
||||
cmd.append({
|
||||
"topic" : self.powertopic,
|
||||
"payload" : bytearray((1,))})
|
||||
"payload" : b'\x01'})
|
||||
c4.push(cmd)
|
||||
else:
|
||||
c4 = C4Interface(self.topic)
|
||||
|
@ -544,7 +552,8 @@ class Kitchenlight:
|
|||
class 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"]
|
||||
|
||||
def __init__(self, autoinit=""):
|
||||
|
@ -575,6 +584,7 @@ class ColorScheme:
|
|||
|
||||
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"
|
||||
|
@ -621,10 +631,12 @@ class ColorScheme:
|
|||
|
||||
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 3*4 bit pseudo random color in 6 char hex notation. """
|
||||
|
||||
from random import randint, sample
|
||||
chls = [15]
|
||||
chls.append(randint(0,15))
|
||||
|
@ -660,9 +672,13 @@ class ColorScheme:
|
|||
return color
|
||||
|
||||
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
|
||||
# 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 topic in self.mapping.keys():
|
||||
return self.mapping[topic]
|
||||
|
@ -677,6 +693,7 @@ class ColorScheme:
|
|||
|
||||
def from_file(self, preset):
|
||||
""" Load ColorScheme from file. """
|
||||
|
||||
if preset == '-':
|
||||
fd = sys.stdin
|
||||
else:
|
||||
|
@ -809,37 +826,6 @@ class ColorScheme:
|
|||
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:
|
||||
""" Remote preset control. """
|
||||
|
||||
|
@ -878,7 +864,7 @@ class RemotePresets:
|
|||
}
|
||||
|
||||
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():
|
||||
# Return on exact match
|
||||
return name
|
||||
|
@ -890,12 +876,15 @@ class RemotePresets:
|
|||
return name
|
||||
|
||||
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.
|
||||
<available> 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
|
||||
Takes care to match only presets which are available for all rooms
|
||||
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
|
||||
# "global" later on.
|
||||
|
@ -962,6 +951,7 @@ class RemotePresets:
|
|||
|
||||
def list_available(self, room="global"):
|
||||
""" Print a list of available Presets. """
|
||||
|
||||
room = self._expand_room_name(room)
|
||||
available = self.query_available([room])
|
||||
|
||||
|
@ -975,6 +965,7 @@ class RemotePresets:
|
|||
|
||||
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())
|
||||
|
@ -1015,6 +1006,7 @@ if __name__ == "__main__":
|
|||
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(
|
||||
|
@ -1026,6 +1018,7 @@ if __name__ == "__main__":
|
|||
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(
|
||||
|
@ -1034,6 +1027,7 @@ if __name__ == "__main__":
|
|||
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 (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",
|
||||
help="apply local colorscheme PRESET to Fnordcenter")
|
||||
group_cl.add_argument(
|
||||
"-m", "--magic", type=str, default="fade", metavar="MODE",
|
||||
help="EXPERIMENTAL: blend into preset (needs a running instance of fluffyd on the network). MODE is either \"fade\", \"wave\", \"pulse\", \"emp\", \"flash\" or \"none\".")
|
||||
"-N", "--no-magic", action="store_true",
|
||||
help="Do not use fluffyd to change colors.")
|
||||
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 ('-' to write to stdout)")
|
||||
|
||||
# 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.")
|
||||
|
@ -1070,6 +1065,7 @@ if __name__ == "__main__":
|
|||
group_sw.add_argument(
|
||||
"-K", nargs='?', dest="k_switch", const="", metavar="BINARY_CODE",
|
||||
help="switch lights in Keller on/off")
|
||||
|
||||
# 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 may be abbreviated.")
|
||||
|
@ -1085,7 +1081,8 @@ if __name__ == "__main__":
|
|||
if args.debug:
|
||||
C4Interface.debug = True
|
||||
if args.status:
|
||||
C4Interface().status()
|
||||
status = C4Interface().status()
|
||||
print("Club is", status)
|
||||
if args.gate:
|
||||
C4Interface().open_gate()
|
||||
if args.shutdown:
|
||||
|
@ -1113,15 +1110,15 @@ if __name__ == "__main__":
|
|||
if args.w_color:
|
||||
if args.w_color not in presets:
|
||||
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 not in presets:
|
||||
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 not in presets:
|
||||
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:
|
||||
ColorScheme().list_available()
|
||||
|
||||
|
|
Loading…
Reference in a new issue