2017-03-14 14:57:02 +01:00
|
|
|
#!/usr/bin/env python3
|
2017-04-17 13:05:02 +02:00
|
|
|
# ____ __
|
|
|
|
# / __// /
|
|
|
|
# / / / /___ ____
|
|
|
|
# \ \__\_ / / __/____ __ _
|
|
|
|
# \___\ |_| / / |_ _| \ |
|
|
|
|
# \ \__ | || / |_
|
|
|
|
# \___\|_||_\_\___\
|
|
|
|
#
|
|
|
|
# c4ctrl: A command line client for Autoc4.
|
|
|
|
#
|
2017-04-08 16:26:24 +02:00
|
|
|
# Author: Shy
|
2017-04-14 17:20:41 +02:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify it under
|
|
|
|
# the terms of the GNU General Public License as published by the Free Software
|
|
|
|
# Foundation, either version 3 of the License, or (at your option) any later
|
|
|
|
# version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
|
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
|
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
|
|
# details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License along with
|
|
|
|
# this program. If not, see <http://www.gnu.org/licenses/>.
|
2017-04-06 16:33:32 +02:00
|
|
|
|
|
|
|
"""
|
2017-04-07 19:58:09 +02:00
|
|
|
A command line client for Autoc4, the home automation system of the C4.
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-04-13 17:48:25 +02:00
|
|
|
Run 'c4ctrl -h' for usage information.
|
2017-04-07 19:58:09 +02:00
|
|
|
|
|
|
|
Dependencies:
|
2017-04-17 13:05:02 +02:00
|
|
|
* Paho Python Client
|
|
|
|
(available from https://github.com/eclipse/paho.mqtt.python).
|
|
|
|
* Some parts will work on UNIX-like operating systems only.
|
2017-04-06 16:33:32 +02:00
|
|
|
"""
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
import sys
|
2017-05-19 19:56:03 +02:00
|
|
|
from random import choice # for client_id generation.
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class C4Interface: # {{{1
|
2017-04-07 19:58:09 +02:00
|
|
|
""" Interaction with AutoC4, the C4 home automation system. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2024-03-20 12:14:11 +01:00
|
|
|
broker = "autoc4.labor.koeln.ccc.de"
|
2017-04-07 19:58:09 +02:00
|
|
|
port = 1883
|
2017-04-18 22:06:18 +02:00
|
|
|
qos = 0
|
2017-03-11 14:04:02 +01:00
|
|
|
retain = True
|
2017-04-17 13:05:02 +02:00
|
|
|
# Generate a (sufficiently) unique client id.
|
2017-04-10 15:32:53 +02:00
|
|
|
client_id = "c4ctrl-" + "".join(
|
2017-04-10 21:50:11 +02:00
|
|
|
choice("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
|
|
for unused in range(16))
|
2017-03-11 14:04:02 +01:00
|
|
|
debug = False
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
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)
|
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
def push(self, message, topic=None, retain=None):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Send a message to the MQTT broker.
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
message may be a byte encoded payload or a list of either dict()s
|
|
|
|
or tuples()s. If message is a byte encoded payload, topic= must be
|
2017-04-13 17:48:25 +02:00
|
|
|
given. dict()s and tuple()s should look like:
|
2017-04-08 16:26:24 +02:00
|
|
|
dict("topic": str(topic), "payload": bytes(payload))
|
|
|
|
tuple(str(topic), bytes(payload)) """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
from paho.mqtt import publish
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Skip empty messages.
|
2017-04-08 16:26:24 +02:00
|
|
|
if message == [] or message == "": return
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Set defaults.
|
2017-04-08 16:26:24 +02:00
|
|
|
if retain == None: retain = self.retain
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
if type(message) == list:
|
2017-04-17 13:05:02 +02:00
|
|
|
# Add <qos> and <retain> to every message.
|
2017-04-07 19:58:09 +02:00
|
|
|
for item in message.copy():
|
2017-03-11 14:04:02 +01:00
|
|
|
if type(item) == dict:
|
|
|
|
item["qos"] = self.qos
|
2017-04-08 16:26:24 +02:00
|
|
|
item["retain"] = retain
|
2017-03-12 15:49:12 +01:00
|
|
|
elif type(item) == tuple:
|
2017-03-12 16:24:01 +01:00
|
|
|
new_item = (
|
2017-04-08 16:26:24 +02:00
|
|
|
item[0] or topic, # topic
|
2017-03-12 15:49:12 +01:00
|
|
|
item[1], # payload
|
|
|
|
self.qos, # qos
|
2017-04-08 16:26:24 +02:00
|
|
|
retain # retain
|
2017-03-12 15:49:12 +01:00
|
|
|
)
|
2017-04-07 19:58:09 +02:00
|
|
|
message.remove(item)
|
|
|
|
message.append(new_item)
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
if self.debug: return print("[DEBUG] inhibited messages:",
|
|
|
|
message, file=sys.stderr)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
try:
|
|
|
|
publish.multiple(message,
|
|
|
|
hostname=self.broker,
|
|
|
|
port=self.port,
|
|
|
|
client_id=self.client_id)
|
|
|
|
|
|
|
|
except PermissionError as error:
|
|
|
|
self.on_permission_error(error)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
except OSError as error:
|
|
|
|
self.on_os_error(error)
|
|
|
|
|
|
|
|
else: # Message is not a list.
|
2017-03-11 14:04:02 +01:00
|
|
|
if self.debug:
|
|
|
|
return print("[DEBUG] inhibited message to '{}': '{}'".format(
|
2017-04-08 16:26:24 +02:00
|
|
|
topic, message), file=sys.stderr)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
try:
|
|
|
|
publish.single(topic,
|
|
|
|
payload=message,
|
|
|
|
qos=self.qos,
|
|
|
|
retain=retain,
|
|
|
|
hostname=self.broker,
|
|
|
|
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)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-03-15 09:53:00 +01:00
|
|
|
def pull(self, topic=[]):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Return the state of a topic.
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
topic may be a list of topics or a single topic given as string.
|
|
|
|
Returns a paho message object or list of message objects. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
from paho.mqtt import subscribe
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Convert topics of type string to a single item list.
|
2017-03-13 12:35:14 +01:00
|
|
|
if type(topic) == str:
|
|
|
|
topic = [topic]
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Skip empty queries.
|
2017-04-08 16:26:24 +02:00
|
|
|
if topic == []: return
|
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
if self.debug:
|
2017-04-08 16:26:24 +02:00
|
|
|
print("[DEBUG] inhibited query for:", topic, file=sys.stderr)
|
2017-03-11 14:04:02 +01:00
|
|
|
return []
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
try:
|
|
|
|
return subscribe.simple(topic,
|
|
|
|
msg_count=len(topic),
|
|
|
|
qos=self.qos,
|
|
|
|
hostname=self.broker,
|
|
|
|
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)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def status(self):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Returns current status (string "open" or "closed") of the club. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
club_status = self.pull("club/status")
|
2017-03-12 15:13:41 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Create a fake result to prevent errors if in debug mode.
|
2017-03-12 15:13:41 +01:00
|
|
|
if C4Interface.debug:
|
2017-04-08 16:26:24 +02:00
|
|
|
print("[DEBUG] Warning: handing over fake data to allow for further execution!",
|
|
|
|
file=sys.stderr)
|
|
|
|
class club_status: pass
|
|
|
|
club_status.payload = b'\x00'
|
2017-03-12 15:13:41 +01:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
if club_status.payload == b'\x01':
|
2017-04-06 16:33:32 +02:00
|
|
|
return "open"
|
2017-03-11 14:04:02 +01:00
|
|
|
else:
|
2017-04-06 16:33:32 +02:00
|
|
|
return "closed"
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2018-11-07 21:34:19 +01:00
|
|
|
def cyberalert(self, alert):
|
|
|
|
""" Start/stop cyberalert. """
|
|
|
|
|
|
|
|
if alert == 0:
|
|
|
|
self.push(b'\x00', "club/cyber/alert", retain=False)
|
|
|
|
elif alert == 1:
|
|
|
|
self.push(b'\x01', "club/cyber/alert", retain=False)
|
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
def open_gate(self):
|
2017-04-13 17:48:25 +02:00
|
|
|
""" Open the gate. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
|
|
|
self.push(None, topic="club/gate", retain=False)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def shutdown(self, force=False):
|
2017-04-13 17:48:25 +02:00
|
|
|
""" Invoke the shutdown routine. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
if force:
|
|
|
|
payload = b'\x44'
|
|
|
|
else:
|
|
|
|
payload = b'\x00'
|
2017-04-07 19:58:09 +02:00
|
|
|
self.push(payload, topic="club/shutdown", retain=False)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Kitchenlight: # {{{1
|
2017-04-10 15:32:53 +02:00
|
|
|
""" Interface to the Kitchenlight and its functions. """
|
2017-08-23 08:14:27 +02:00
|
|
|
# TODO: use struct(?)
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-23 22:18:31 +02:00
|
|
|
# List of available modes.
|
|
|
|
modes = [
|
|
|
|
"off",
|
|
|
|
"checker",
|
|
|
|
"matrix",
|
|
|
|
"mood",
|
|
|
|
"oc",
|
|
|
|
"pacman",
|
|
|
|
"sine",
|
|
|
|
"text",
|
|
|
|
"flood",
|
|
|
|
"clock"
|
|
|
|
]
|
2017-04-17 13:05:02 +02:00
|
|
|
_END = "little" # Kitchenlight endianess.
|
2017-04-10 15:32:53 +02:00
|
|
|
|
|
|
|
def __init__(self, topic="kitchenlight/change_screen",
|
|
|
|
powertopic="power/wohnzimmer/kitchenlight",
|
|
|
|
autopower=True):
|
2017-04-17 13:05:02 +02:00
|
|
|
self.topic = topic # Kitchenlight topic.
|
|
|
|
self.powertopic = powertopic # Topic for power on.
|
2017-04-10 15:32:53 +02:00
|
|
|
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()
|
|
|
|
command = []
|
|
|
|
command.append({
|
|
|
|
"topic" : self.topic,
|
|
|
|
"payload" : data })
|
|
|
|
if poweroff:
|
|
|
|
command.append({
|
|
|
|
"topic" : self.powertopic,
|
|
|
|
"payload" : b'\x00'})
|
|
|
|
elif self.autopower or poweron:
|
|
|
|
command.append({
|
|
|
|
"topic" : self.powertopic,
|
|
|
|
"payload" : b'\x01'})
|
|
|
|
c4.push(command)
|
|
|
|
else:
|
|
|
|
c4 = C4Interface()
|
|
|
|
c4.push(data, topic=self.topic)
|
|
|
|
|
2017-04-23 22:18:31 +02:00
|
|
|
def _expand_mode_name(self, name):
|
|
|
|
# Return exact match.
|
|
|
|
if name in self.modes:
|
|
|
|
return name
|
|
|
|
|
|
|
|
for mode in self.modes:
|
|
|
|
if mode.find(name) == 0:
|
|
|
|
return mode
|
|
|
|
|
|
|
|
# Fallback.
|
|
|
|
return name
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
def list_available(self):
|
|
|
|
""" Print a list of available Kitchenlight modes. """
|
|
|
|
|
2017-05-19 19:56:03 +02:00
|
|
|
print(
|
|
|
|
"Available Kitchenlight modes (options are optional):\n\n"
|
|
|
|
" off turn off Kitchenlight\n"
|
|
|
|
" checker [DELAY] [COLOR_1] [COLOR_2] Checker\n"
|
|
|
|
" matrix [LINES] Matrix\n"
|
|
|
|
" mood [1|2] (1=Colorwheel, 2=Random) Moodlight\n"
|
|
|
|
" oc [DELAY] Open Chaos\n"
|
|
|
|
" pacman Pacman\n"
|
|
|
|
" sine Sine\n"
|
|
|
|
" text [TEXT] [DELAY] Text\n"
|
|
|
|
" flood Flood\n"
|
|
|
|
" clock Clock\n"
|
2017-08-23 08:14:27 +02:00
|
|
|
" life [SPEED] [LIFETIME] [0|1] (1=Rainbow)\n"
|
|
|
|
" Game of Life\n"
|
2017-05-19 19:56:03 +02:00
|
|
|
)
|
2017-04-10 15:32:53 +02:00
|
|
|
|
|
|
|
def set_mode(self, mode, opts=[]):
|
|
|
|
""" Switch to given mode. """
|
|
|
|
|
2017-04-23 22:18:31 +02:00
|
|
|
mode = self._expand_mode_name(mode.lower())
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
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":
|
|
|
|
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()
|
2017-08-23 08:14:27 +02:00
|
|
|
if mode == "life":
|
|
|
|
return self.life(*opts)
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
print("Error: unknown Kitchenlight mode {}!".format(mode))
|
|
|
|
return False
|
|
|
|
|
|
|
|
def empty(self):
|
|
|
|
""" Set to mode "empty" and turn off Kitchenlight. """
|
|
|
|
|
|
|
|
# Screen 0
|
|
|
|
d = int(0).to_bytes(4, self._END)
|
|
|
|
self._switch(d, poweroff=True)
|
|
|
|
|
|
|
|
def checker(self, delay=500, colA="0000ff", colB="00ff00"):
|
|
|
|
""" Set to mode "checker".
|
|
|
|
|
|
|
|
delay = delay in ms (default 500)
|
|
|
|
colA = first color (default 0000ff)
|
|
|
|
colB = second color (default 00ff00) """
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Kind of a hack: lets treat the two colors as DMX lights.
|
2017-04-10 15:32:53 +02:00
|
|
|
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, self._END)
|
|
|
|
# Delay
|
|
|
|
v[4:8] = int(delay).to_bytes(4, self._END)
|
|
|
|
# ColorA R/G/B
|
|
|
|
v[8:10] = int(ca.color[0:2], base=16).to_bytes(2, self._END)
|
|
|
|
v[10:12] = int(ca.color[2:4], base=16).to_bytes(2, self._END)
|
|
|
|
v[12:14] = int(ca.color[4:6], base=16).to_bytes(2, self._END)
|
|
|
|
# ColorB R/G/B
|
|
|
|
v[14:16] = int(cb.color[0:2], base=16).to_bytes(2, self._END)
|
|
|
|
v[16:18] = int(cb.color[2:4], base=16).to_bytes(2, self._END)
|
|
|
|
v[18:20] = int(cb.color[4:6], base=16).to_bytes(2, self._END)
|
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def matrix(self, lines=8):
|
|
|
|
""" Set to mode "matrix".
|
|
|
|
|
|
|
|
lines (>0, <32) = number of lines (default 8) """
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
if int(lines) > 31: lines = 31 # Maximal line count.
|
2017-04-10 15:32:53 +02:00
|
|
|
d = bytearray(8)
|
|
|
|
v = memoryview(d)
|
2017-08-23 08:14:27 +02:00
|
|
|
v[0:4] = int(2).to_bytes(4, self._END) # Screen 2
|
2017-04-10 15:32:53 +02:00
|
|
|
v[4:8] = int(lines).to_bytes(4, self._END)
|
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def moodlight(self, mode=1):
|
|
|
|
""" Set to mode "moodlight".
|
|
|
|
|
|
|
|
mode [1|2] = colorwheel(1) or random(2) """
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
if mode == 1: # Mode "Colorwheel".
|
2017-04-10 15:32:53 +02:00
|
|
|
d = bytearray(19)
|
|
|
|
v = memoryview(d)
|
|
|
|
# Screen 3
|
|
|
|
v[0:4] = int(3).to_bytes(4, self._END)
|
|
|
|
# Mode
|
|
|
|
v[4:5] = int(mode).to_bytes(1, self._END)
|
|
|
|
# Step
|
|
|
|
v[5:9] = int(1).to_bytes(4, self._END)
|
|
|
|
# Fade delay
|
|
|
|
v[9:13] = int(10).to_bytes(4, self._END)
|
|
|
|
# Pause
|
|
|
|
v[13:17] = int(10000).to_bytes(4, self._END)
|
|
|
|
# Hue step
|
|
|
|
v[17:19] = int(30).to_bytes(2, self._END)
|
2017-04-17 13:05:02 +02:00
|
|
|
else: # Mode "Random".
|
2017-04-10 15:32:53 +02:00
|
|
|
d = bytearray(17)
|
|
|
|
v = memoryview(d)
|
|
|
|
# Screen 3
|
|
|
|
v[0:4] = int(3).to_bytes(4, self._END)
|
|
|
|
# Mode
|
|
|
|
v[4:5] = int(mode).to_bytes(1, self._END)
|
|
|
|
# Step
|
|
|
|
v[5:9] = int(1).to_bytes(4, self._END)
|
|
|
|
# Fade delay
|
|
|
|
v[9:13] = int(10).to_bytes(4, self._END)
|
|
|
|
# Pause
|
|
|
|
v[13:17] = int(10000).to_bytes(4, self._END)
|
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def openchaos(self, delay=1000):
|
|
|
|
""" Set to mode "openchaos".
|
|
|
|
|
2017-04-13 17:48:25 +02:00
|
|
|
delay = delay in milliseconds (default 1000). """
|
2017-04-10 15:32:53 +02:00
|
|
|
|
|
|
|
d = bytearray(8)
|
|
|
|
v = memoryview(d)
|
2017-08-23 08:14:27 +02:00
|
|
|
v[0:4] = int(4).to_bytes(4, self._END) # Screen 4
|
2017-04-10 15:32:53 +02:00
|
|
|
v[4:8] = int(delay).to_bytes(4, self._END)
|
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def pacman(self):
|
|
|
|
""" Set to mode "pacman". """
|
|
|
|
|
2017-08-23 08:14:27 +02:00
|
|
|
d = int(5).to_bytes(4, self._END) # Screen 5
|
2017-04-10 15:32:53 +02:00
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def sine(self):
|
|
|
|
""" Set to mode "sine". """
|
|
|
|
|
2017-08-23 08:14:27 +02:00
|
|
|
d = int(6).to_bytes(4, self._END) # Screen 6
|
2017-04-10 15:32:53 +02:00
|
|
|
self._switch(d)
|
|
|
|
|
2017-08-23 08:14:27 +02:00
|
|
|
# Screen 7 is Strobo, which is disabled because it is said to do harm to
|
2017-04-10 15:32:53 +02:00
|
|
|
# the Kitchenlight. Evil strobo.
|
|
|
|
|
|
|
|
def text(self, text="Hello World", delay=250):
|
|
|
|
""" Set to mode "text".
|
|
|
|
|
2017-04-13 17:48:25 +02:00
|
|
|
text (str < 256 bytes) = text to display (default "Hello World").
|
|
|
|
delay = delay in milliseconds (default 250). """
|
2017-04-10 15:32:53 +02:00
|
|
|
|
|
|
|
text = text.encode("ascii", "ignore")
|
2017-04-17 13:05:02 +02:00
|
|
|
if len(text) > 255: # Maximum text length.
|
2017-04-13 17:48:25 +02:00
|
|
|
print("Warning: text length must not exceed 255 characters!", file=sys.stderr)
|
|
|
|
text = text[:255]
|
2017-04-10 15:32:53 +02:00
|
|
|
d = bytearray(8 + len(text) + 1)
|
|
|
|
v = memoryview(d)
|
2017-08-23 08:14:27 +02:00
|
|
|
v[0:4] = int(8).to_bytes(4, self._END) # Screen 8
|
2017-04-10 15:32:53 +02:00
|
|
|
v[4:8] = int(delay).to_bytes(4, self._END)
|
|
|
|
v[8:8 + len(text)] = text
|
|
|
|
v[len(d) - 1:len(d)] = bytes(1)
|
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def flood(self):
|
|
|
|
""" Set to mode "flood". """
|
2017-08-23 08:14:27 +02:00
|
|
|
|
|
|
|
d = int(9).to_bytes(4, self._END) # Screen 9
|
2017-04-10 15:32:53 +02:00
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def clock(self):
|
|
|
|
""" Set to mode "clock". """
|
2017-08-23 08:14:27 +02:00
|
|
|
|
|
|
|
d = int(11).to_bytes(4, self._END) # Screen 11
|
|
|
|
self._switch(d)
|
|
|
|
|
|
|
|
def life(self, generations=0, lifetime=0, rainbow=0):
|
|
|
|
""" Set to mode "life". """
|
|
|
|
|
|
|
|
d = bytearray(16)
|
|
|
|
v = memoryview(d)
|
|
|
|
v[0:4] = int(12).to_bytes(4, self._END) # Screen 12
|
|
|
|
v[4:8] = int(generations).to_bytes(4, self._END)
|
|
|
|
v[8:12] = int(lifetime).to_bytes(4, self._END)
|
|
|
|
v[12:16] = int(rainbow).to_bytes(4, self._END)
|
2017-04-10 15:32:53 +02:00
|
|
|
self._switch(d)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Dmx: # {{{1
|
2017-05-19 19:56:03 +02:00
|
|
|
""" Abstraction of the 3 channel LED cans. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# 3 bytes for color, one each for red, green and blue.
|
2017-03-13 12:35:14 +01:00
|
|
|
template = "000000"
|
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
def __init__(self, topic, color=None):
|
|
|
|
self.topic = topic
|
2017-03-13 12:35:14 +01:00
|
|
|
self.set_color(color or self.template)
|
2017-05-19 19:56:03 +02:00
|
|
|
self.is_master = topic.rfind("/master") == len(topic)-7 # 7 = len("/master")
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-03-13 12:35:14 +01:00
|
|
|
def _pad_color(self, color):
|
2017-04-08 16:26:24 +02:00
|
|
|
""" 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. """
|
2017-03-15 00:05:45 +01:00
|
|
|
|
2017-03-27 16:01:55 +02:00
|
|
|
if len(color) > len(self.template):
|
2017-04-17 13:05:02 +02:00
|
|
|
# Silently truncate bytes exceeding template length.
|
2017-03-13 12:35:14 +01:00
|
|
|
return color[:len(self.template)]
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
# Expand 3 char codes and codes of half the required length.
|
2017-04-08 16:26:24 +02:00
|
|
|
# Yet, let's presume that a 6-char code is alway meant to be
|
|
|
|
# interpreted as a color and should never be expanded.
|
2017-03-13 12:35:14 +01:00
|
|
|
if len(color) != 6 and len(color) == 3 or len(color) == (len(self.template) / 2):
|
2017-04-08 16:26:24 +02:00
|
|
|
color = "".join(char*2 for char in color)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
if len(color) == len(self.template): # Nothing more to do.
|
2017-03-11 14:04:02 +01:00
|
|
|
return color
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Add padding.
|
2017-03-15 00:05:45 +01:00
|
|
|
color = color + self.template[len(color):]
|
2017-03-11 14:04:02 +01:00
|
|
|
return color
|
|
|
|
|
|
|
|
def set_color(self, color):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Set color (hex) for this instance.
|
2017-03-12 16:24:01 +01:00
|
|
|
|
2017-04-06 16:33:32 +02:00
|
|
|
The color is then available via its color variable. """
|
2017-04-08 16:26:24 +02:00
|
|
|
|
2017-03-13 12:35:14 +01:00
|
|
|
color = self._pad_color(color)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
self.color = color
|
|
|
|
self.payload = bytearray.fromhex(color)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Dmx4(Dmx): # {{{1
|
2017-05-19 19:56:03 +02:00
|
|
|
""" Abstraction of the 4 channel LED cans. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# 3 bytes for color plus 1 byte for brightness.
|
2017-03-13 20:42:53 +01:00
|
|
|
template = "000000ff"
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-15 00:05:45 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Dmx7(Dmx): # {{{1
|
2017-05-19 19:56:03 +02:00
|
|
|
""" Abstraction of the 7 channel LED cans. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
# 3 bytes for color, another 3 bytes for special functions and 1 byte
|
2017-04-17 13:05:02 +02:00
|
|
|
# for brightness.
|
2017-03-13 12:35:14 +01:00
|
|
|
template = "000000000000ff"
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-15 00:05:45 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class C4Room: # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Methods of rooms in the club. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-03-15 00:05:45 +01:00
|
|
|
def __init__(self):
|
|
|
|
self.c4 = C4Interface()
|
2017-04-10 15:32:53 +02:00
|
|
|
# get_switch_state() will store its result and a timestamp to reduce
|
2017-04-17 13:05:02 +02:00
|
|
|
# requests to the broker.
|
2017-04-10 15:32:53 +02:00
|
|
|
self._switch_state = ("", 0.0)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-03-15 00:05:45 +01:00
|
|
|
def _interactive_light_switch(self):
|
2017-04-08 16:26:24 +02:00
|
|
|
""" Interactively ask for input.
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
Returns str(userinput). Will not write to stdout if sys.stdin is
|
|
|
|
no tty. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-25 23:45:42 +01:00
|
|
|
if sys.stdin.isatty():
|
|
|
|
print("[{}]".format(self.name))
|
|
|
|
print("Please enter 0 or 1 for every light:")
|
|
|
|
for level in range(len(self.switches)):
|
|
|
|
print((level * '|') + ",- " + self.switches[level][0])
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
switch_state = self.get_switch_state()
|
2017-04-17 13:05:02 +02:00
|
|
|
print(switch_state) # Present current state.
|
2017-03-15 00:05:45 +01:00
|
|
|
|
|
|
|
try:
|
|
|
|
userinput = sys.stdin.readline().rstrip('\n')
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
print("\rInterrupted by user.")
|
|
|
|
return ""
|
|
|
|
|
|
|
|
return userinput
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
def get_switch_state(self, max_age=5):
|
|
|
|
""" Returns current state of switches as a string of 1s and 0s.
|
|
|
|
|
2017-05-25 21:22:21 +02:00
|
|
|
max_age specifies how old (in seconds) a cached responce must be
|
|
|
|
before it is considered outdated. """
|
2017-04-10 15:32:53 +02:00
|
|
|
|
|
|
|
from time import time
|
|
|
|
|
|
|
|
# We store switch states in self._switch_state to reduce requests to
|
|
|
|
# the broker. If this variable is neither empty nor too old, use it!
|
|
|
|
if self._switch_state[0] != "":
|
|
|
|
if time() - self._switch_state[1] <= max_age:
|
|
|
|
return self._switch_state[0]
|
2017-04-07 19:58:09 +02:00
|
|
|
|
|
|
|
state = ""
|
|
|
|
req = []
|
|
|
|
for topic in self.switches:
|
|
|
|
req.append(topic[1])
|
|
|
|
responce = self.c4.pull(req)
|
2017-04-08 16:26:24 +02:00
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
for sw in self.switches:
|
|
|
|
for r in responce:
|
|
|
|
if r.topic == sw[1]:
|
2017-04-17 13:05:02 +02:00
|
|
|
state += str(int.from_bytes(r.payload, sys.byteorder))
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
if C4Interface.debug:
|
|
|
|
print("[DEBUG] Warning: handing over fake data to allow for further execution!",
|
|
|
|
file=sys.stderr)
|
|
|
|
state = '0' * len(self.switches)
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
self._switch_state = (state, time())
|
2017-04-07 19:58:09 +02:00
|
|
|
return state
|
|
|
|
|
2017-05-25 21:22:21 +02:00
|
|
|
def _parse_switch_input(self, userinput):
|
|
|
|
""" Parse user input to the switch command. """
|
2017-05-20 08:33:56 +02:00
|
|
|
|
|
|
|
# Let's support some binary operations!
|
|
|
|
ops = "" # Store operators.
|
|
|
|
while not userinput.isdecimal():
|
|
|
|
if userinput == "":
|
|
|
|
# Huh, no operand given. oO
|
|
|
|
if ops[-1:] == '~':
|
|
|
|
# The NOT operator may work on the current switch state.
|
|
|
|
userinput = self.get_switch_state()
|
|
|
|
else:
|
|
|
|
print("Error: missing operand after '{}'!".format(ops[-1]),
|
|
|
|
file=sys.stderr)
|
|
|
|
return
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-05-20 08:33:56 +02:00
|
|
|
elif userinput[0] in "&|^~":
|
|
|
|
ops += userinput[0]
|
2017-04-18 12:40:59 +02:00
|
|
|
userinput = userinput[1:].strip()
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
elif (userinput[:2] == ">>" or userinput[:2] == "<<") \
|
|
|
|
and (userinput[2:].strip() == "" or userinput[2:].strip().isdecimal()):
|
|
|
|
# Left or right shift
|
|
|
|
# How far shall we shift?
|
|
|
|
if userinput[2:].strip().isdecimal():
|
|
|
|
shift_by = int(userinput[2:])
|
|
|
|
else:
|
|
|
|
shift_by = 1
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Retrieve the current state of switches.
|
2017-04-10 15:32:53 +02:00
|
|
|
switch_state = self.get_switch_state()
|
|
|
|
if userinput[:2] == ">>":
|
2017-04-08 18:19:23 +02:00
|
|
|
# Right shift. '[2:]' removes the leading 'b0...'.
|
2017-04-10 15:32:53 +02:00
|
|
|
new_state = bin(int(switch_state, base=2) >> shift_by)[2:]
|
2017-04-08 18:19:23 +02:00
|
|
|
else:
|
|
|
|
# Left shift. '[2:]' removes the leading 'b0...'.
|
2017-04-10 15:32:53 +02:00
|
|
|
new_state = bin(int(switch_state, base=2) << shift_by)[2:]
|
2017-04-17 13:05:02 +02:00
|
|
|
# Cut any exceeding leftmost bits.
|
2017-04-08 18:19:23 +02:00
|
|
|
new_state = new_state[-len(self.switches):]
|
2017-04-17 13:05:02 +02:00
|
|
|
# Pad with leading zeroes.
|
2017-04-08 18:19:23 +02:00
|
|
|
userinput = new_state.rjust(len(self.switches), '0')
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
else:
|
2017-04-10 15:32:53 +02:00
|
|
|
# Oh no, input contained non-decimal characters which we could
|
|
|
|
# not parse. :(
|
|
|
|
print("Error: could not parse input!", file=sys.stderr)
|
2017-04-07 19:58:09 +02:00
|
|
|
return
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
if len(userinput) != len(self.switches):
|
2017-04-10 15:32:53 +02:00
|
|
|
# First try to convert from decimal if userinput's length doesn't
|
2017-04-17 13:05:02 +02:00
|
|
|
# match.
|
2017-03-25 23:45:42 +01:00
|
|
|
if len(bin(int(userinput))) <= len(self.switches)+2:
|
2017-04-17 13:05:02 +02:00
|
|
|
# ^ +2 because bin() returns strings like 'b0...'.
|
2017-05-19 19:56:03 +02:00
|
|
|
# Convert to binary and pad with leading zeroes.
|
|
|
|
userinput = "{{:0{}b}}".format(len(self.switches)).format(int(userinput))
|
2017-03-21 18:14:00 +01:00
|
|
|
else:
|
|
|
|
print("Error: wrong number of digits (expected {}, got {})!".format(
|
2017-04-10 15:32:53 +02:00
|
|
|
len(self.switches), len(userinput)), file=sys.stderr)
|
2017-03-21 18:14:00 +01:00
|
|
|
return False
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
# Now that everything special is expanded it's time to check if
|
2017-04-17 13:05:02 +02:00
|
|
|
# userinput really consists of 1s and 0s only.
|
2017-04-07 19:58:09 +02:00
|
|
|
for digit in userinput:
|
|
|
|
if digit not in "01":
|
|
|
|
print("Error: invalid digit: " + digit, file=sys.stderr)
|
2017-03-11 14:04:02 +01:00
|
|
|
return False
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-05-20 08:33:56 +02:00
|
|
|
while ops:
|
|
|
|
# Apply modifiers.
|
|
|
|
if ops[-1] == '~': # NOT operator.
|
|
|
|
userinput = "".join(map(lambda i: i == '0' and '1' or '0',
|
|
|
|
userinput))
|
|
|
|
elif ops[-1] == '&': # AND operator.
|
|
|
|
switch_state = self.get_switch_state()
|
|
|
|
userinput = "".join(map(lambda x, y: str(int(x) & int(y)),
|
|
|
|
userinput, switch_state))
|
|
|
|
elif ops[-1] == '|': # OR operator.
|
|
|
|
switch_state = self.get_switch_state()
|
|
|
|
userinput = "".join(map(lambda x, y: str(int(x) | int(y)),
|
|
|
|
userinput, switch_state))
|
|
|
|
elif ops[-1] == '^': # XOR operator.
|
|
|
|
switch_state = self.get_switch_state()
|
|
|
|
userinput = "".join(map(lambda x, y: str(int(x) ^ int(y)),
|
|
|
|
userinput, switch_state))
|
|
|
|
ops = ops[:-1]
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-05-25 21:22:21 +02:00
|
|
|
return userinput
|
|
|
|
|
|
|
|
def light_switch(self, userinput=""):
|
|
|
|
""" Switch lamps in a room on or off. """
|
|
|
|
|
|
|
|
if not userinput:
|
|
|
|
# Derive user input from stdin.
|
|
|
|
userinput = self._interactive_light_switch()
|
|
|
|
if userinput == "": return
|
|
|
|
|
|
|
|
if userinput == '-':
|
|
|
|
print(self.get_switch_state())
|
|
|
|
return
|
|
|
|
|
|
|
|
userinput = self._parse_switch_input(userinput)
|
|
|
|
if not userinput: sys.exit(1)
|
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
command=[]
|
|
|
|
for i in range(len(self.switches)):
|
2017-04-17 13:05:02 +02:00
|
|
|
# Skip unchanged switches if we happen to know their state.
|
2017-04-10 15:32:53 +02:00
|
|
|
if "switch_state" in dir():
|
|
|
|
if switch_state[i] == userinput[i]: continue
|
2017-04-08 16:26:24 +02:00
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
command.append({
|
|
|
|
"topic" : self.switches[i][1],
|
2017-04-08 16:26:24 +02:00
|
|
|
"payload" : bytes([int(userinput[i])])
|
2017-03-11 14:04:02 +01:00
|
|
|
})
|
2017-03-15 00:05:45 +01:00
|
|
|
|
2017-04-07 19:58:09 +02:00
|
|
|
return self.c4.push(command)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2018-10-27 07:56:14 +02:00
|
|
|
def set_colorscheme(self, colorscheme):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Apply colorscheme to the LED Cans in this room. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
command = []
|
2017-03-11 14:04:02 +01:00
|
|
|
for light in self.lights:
|
2017-04-10 15:32:53 +02:00
|
|
|
if colorscheme.get_color_for(light.topic):
|
2017-03-24 12:04:00 +01:00
|
|
|
|
2017-04-06 16:33:32 +02:00
|
|
|
# Update internal state of this Dmx object, so we can query
|
2017-04-17 13:05:02 +02:00
|
|
|
# <object>.payload later.
|
2017-04-10 15:32:53 +02:00
|
|
|
light.set_color(colorscheme.get_color_for(light.topic))
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2018-10-27 07:56:14 +02:00
|
|
|
# Send data to lanterns.
|
|
|
|
command.append({
|
|
|
|
"topic" : light.topic,
|
|
|
|
"payload" : light.payload
|
|
|
|
})
|
2017-03-17 23:27:54 +01:00
|
|
|
|
2017-04-08 16:26:24 +02:00
|
|
|
# Nothing to do. May happen if a preset defines no color for a room.
|
|
|
|
if command == []: return
|
2017-03-31 17:55:54 +02:00
|
|
|
|
2018-10-27 07:56:14 +02:00
|
|
|
return self.c4.push(command)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Wohnzimmer(C4Room): # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Description of the Wohnzimmer. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
name = "Wohnzimmer"
|
2017-03-21 14:23:04 +01:00
|
|
|
switches = (
|
2017-03-11 14:04:02 +01:00
|
|
|
("Tür", "licht/wohnzimmer/tuer"),
|
|
|
|
("Mitte", "licht/wohnzimmer/mitte"),
|
|
|
|
("Flur", "licht/wohnzimmer/gang"),
|
2017-12-11 18:46:27 +01:00
|
|
|
("Küche", "licht/wohnzimmer/kueche"),
|
|
|
|
("Leseleuchte", "socket/wohnzimmer/screen/a"),
|
2020-01-04 20:48:40 +01:00
|
|
|
("Infoscreen", "screen/wohnzimmer/infoscreen"),
|
|
|
|
("Verstärker", "socket/wohnzimmer/screen/b")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-03-11 14:04:02 +01:00
|
|
|
master = Dmx7("dmx/wohnzimmer/master")
|
2017-03-21 14:23:04 +01:00
|
|
|
lights = (
|
2017-03-11 14:04:02 +01:00
|
|
|
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"),
|
2024-03-15 01:42:30 +01:00
|
|
|
Dmx("dmx/wohnzimmer/spuele1"),
|
|
|
|
Dmx("dmx/wohnzimmer/spuele2"),
|
|
|
|
Dmx("dmx/wohnzimmer/tresen"),
|
|
|
|
Dmx("dmx/wohnzimmer/tresen2"),
|
2024-03-20 12:14:11 +01:00
|
|
|
Dmx("dmx/wohnzimmer/chaosknoten")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Plenarsaal(C4Room): # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Description of the Plenarsaal. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
name = "Plenarsaal"
|
2017-03-21 14:23:04 +01:00
|
|
|
switches = (
|
2017-03-11 14:04:02 +01:00
|
|
|
("Vorne/Wand", "licht/plenar/vornewand"),
|
|
|
|
("Vorne/Fenster", "licht/plenar/vornefenster"),
|
|
|
|
("Hinten/Wand", "licht/plenar/hintenwand"),
|
2024-02-13 21:13:11 +01:00
|
|
|
("Hinten/Fenster", "licht/plenar/hintenfenster"),
|
|
|
|
("Verstärker", "relais/plenar/amp")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-03-11 14:04:02 +01:00
|
|
|
master = Dmx7("dmx/plenar/master")
|
2017-03-21 14:23:04 +01:00
|
|
|
lights = (
|
2017-03-11 14:04:02 +01:00
|
|
|
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")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Fnordcenter(C4Room): # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Description of the Fnordcenter. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
name = "Fnordcenter"
|
2017-03-21 14:23:04 +01:00
|
|
|
switches = (
|
2017-03-11 14:04:02 +01:00
|
|
|
("Links (Fairydust)", "licht/fnord/links"),
|
2024-02-13 21:13:11 +01:00
|
|
|
("Rechts (SCUMM)", "licht/fnord/rechts"),
|
|
|
|
("Lautsprecher", "relais/fnord/audio")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-03-13 20:42:53 +01:00
|
|
|
master = Dmx4("dmx/fnord/master")
|
2017-03-21 14:23:04 +01:00
|
|
|
lights = (
|
2017-03-13 20:42:53 +01:00
|
|
|
Dmx4("dmx/fnord/master"),
|
|
|
|
Dmx4("dmx/fnord/scummfenster"),
|
|
|
|
Dmx4("dmx/fnord/schranklinks"),
|
|
|
|
Dmx4("dmx/fnord/fairyfenster"),
|
|
|
|
Dmx4("dmx/fnord/schrankrechts")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class Keller(C4Room): # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Description of the Keller. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
name = "Keller"
|
2017-03-21 14:23:04 +01:00
|
|
|
switches = (
|
2024-02-13 21:13:11 +01:00
|
|
|
("Vorne", "licht/keller/vorne"),
|
2017-05-19 20:09:03 +02:00
|
|
|
("Mitte", "licht/keller/mitte"),
|
2017-05-19 19:56:03 +02:00
|
|
|
("Lötplatz", "licht/keller/loet"),
|
2024-02-13 21:13:11 +01:00
|
|
|
("Werkbank (kalt)", "led/keller/werkbankkalt"),
|
|
|
|
("Werkbank (warm)", "led/keller/werkbankwarm"),
|
|
|
|
("Hinten (kalt)", "led/keller/hintenkalt"),
|
|
|
|
("Hinten (warm)", "led/keller/hintenwarm")
|
2017-03-21 14:23:04 +01:00
|
|
|
)
|
2017-03-31 17:14:03 +02:00
|
|
|
master = None
|
2017-03-24 12:04:00 +01:00
|
|
|
lights = ()
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class ColorScheme: # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Abstraction of a colorscheme. """
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-06 16:33:32 +02:00
|
|
|
# Names of virtual presets. These are always listed as available and the
|
|
|
|
# user may not save presets under this name.
|
2017-03-11 14:04:02 +01:00
|
|
|
_virtual_presets = ["off", "random"]
|
|
|
|
|
2017-04-10 21:50:11 +02:00
|
|
|
def __init__(self, init=""):
|
2017-03-11 14:04:02 +01:00
|
|
|
self.mapping = {}
|
|
|
|
self.single_color = False
|
|
|
|
self.return_random_color = False
|
2017-04-17 13:05:02 +02:00
|
|
|
self.available = None # List of available presets.
|
2017-04-10 21:50:11 +02:00
|
|
|
if init:
|
2017-04-17 13:05:02 +02:00
|
|
|
# Load or generate preset.
|
2017-04-10 21:50:11 +02:00
|
|
|
if init[0] == '#':
|
|
|
|
return self.from_color(init)
|
|
|
|
elif self._expand_preset(init) == "off":
|
2017-05-19 19:56:03 +02:00
|
|
|
# Virtual preset: set all to #000000.
|
2017-03-11 14:04:02 +01:00
|
|
|
return self.from_color("000000")
|
2017-04-10 21:50:11 +02:00
|
|
|
elif self._expand_preset(init) == "random":
|
2017-04-17 13:05:02 +02:00
|
|
|
# Virtual preset: return random color on every query.
|
2017-03-11 14:04:02 +01:00
|
|
|
return self.from_random()
|
|
|
|
else:
|
2017-04-17 13:05:02 +02:00
|
|
|
# Load preset file.
|
2017-04-10 21:50:11 +02:00
|
|
|
return self.from_file(init)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def __bool__(self):
|
2017-04-17 13:05:02 +02:00
|
|
|
# Return true if get_color_for has a chance to present anything useful.
|
2017-03-11 14:04:02 +01:00
|
|
|
if self.mapping: return True
|
|
|
|
if self.single_color: return True
|
|
|
|
if self.return_random_color: return True
|
|
|
|
else: return False
|
|
|
|
|
2017-04-10 21:50:11 +02:00
|
|
|
def _get_config_dir(self, ignore_missing=False, create=False):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Returns path of the config dir. """
|
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
import os
|
2017-04-17 13:05:02 +02:00
|
|
|
# The name of our config directory.
|
2017-04-17 22:20:45 +02:00
|
|
|
_NAME = "c4ctrl"
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 22:20:45 +02:00
|
|
|
# Get XDG_CONFIG_HOME from environment or set default.
|
|
|
|
if "XDG_CONFIG_HOME" in os.environ:
|
|
|
|
XDG_CONFIG_HOME = os.environ["XDG_CONFIG_HOME"]
|
2017-03-11 14:04:02 +01:00
|
|
|
else:
|
2017-04-17 22:20:45 +02:00
|
|
|
XDG_CONFIG_HOME = os.path.expanduser(os.path.join("~", ".config"))
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
# Does our config dir exist?
|
2017-04-17 22:20:45 +02:00
|
|
|
config_dir = os.path.join(XDG_CONFIG_HOME, _NAME)
|
2017-04-10 21:50:11 +02:00
|
|
|
if not os.path.isdir(config_dir):
|
2017-03-11 14:04:02 +01:00
|
|
|
if create:
|
2017-04-10 21:50:11 +02:00
|
|
|
print("Creating config directory \"{}\"".format(config_dir))
|
|
|
|
os.mkdir(config_dir)
|
|
|
|
elif ignore_missing:
|
2017-03-11 14:04:02 +01:00
|
|
|
return None
|
|
|
|
else:
|
2017-03-25 20:16:58 +01:00
|
|
|
print("Warning: config dir \"{}\" does not exist!".format(
|
2017-04-10 21:50:11 +02:00
|
|
|
config_dir), file=sys.stderr)
|
2017-03-11 14:04:02 +01:00
|
|
|
return None
|
|
|
|
|
2017-04-10 21:50:11 +02:00
|
|
|
return config_dir
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def _expand_preset(self, preset):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Tries to expand given string to a valid preset name. """
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
import os
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
if not self.available:
|
2017-04-10 21:50:11 +02:00
|
|
|
config_dir = self._get_config_dir(ignore_missing=True)
|
|
|
|
if not config_dir:
|
2017-03-11 14:04:02 +01:00
|
|
|
self.available = self._virtual_presets.copy()
|
|
|
|
else:
|
2017-04-10 21:50:11 +02:00
|
|
|
self.available = os.listdir(config_dir)
|
2017-03-11 14:04:02 +01:00
|
|
|
self.available.extend(self._virtual_presets)
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Search for an exact match first.
|
2017-04-23 22:50:34 +02:00
|
|
|
if preset in self.available: return preset
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Return anything which begins with the name given.
|
2017-03-11 14:04:02 +01:00
|
|
|
for a in self.available:
|
|
|
|
if a.find(preset) == 0: return a
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Fallback.
|
2017-03-11 14:04:02 +01:00
|
|
|
return preset
|
|
|
|
|
|
|
|
def _topic_is_master(self, topic):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Does the given topic look like a master topic? """
|
|
|
|
|
2017-05-19 19:56:03 +02:00
|
|
|
return topic.lower().rfind("/master") == len(topic)-7 # 7 = len("/master")
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def _random_color(self):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Returns a 3*4 bit pseudo random color in 6 char hex notation. """
|
|
|
|
|
2017-03-18 11:31:54 +01:00
|
|
|
from random import randint, sample
|
2017-04-23 22:18:31 +02:00
|
|
|
|
2017-12-11 18:46:27 +01:00
|
|
|
channels = [15, 0]
|
2017-04-10 21:50:11 +02:00
|
|
|
channels.append(randint(0,15))
|
2017-03-18 11:31:54 +01:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
color = ""
|
2017-04-10 21:50:11 +02:00
|
|
|
for ch in sample(channels, k=3):
|
2017-03-18 11:31:54 +01:00
|
|
|
color += hex(ch)[2:]*2
|
2017-03-11 14:04:02 +01:00
|
|
|
return color
|
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
def get_color_for(self, topic):
|
|
|
|
""" Returns color for topic.
|
2017-03-25 16:24:01 +01:00
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
Returns the color (in hexadecimal notation) this ColorScheme
|
|
|
|
associates with for the given topic. """
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
if self.mapping:
|
|
|
|
if topic in self.mapping.keys():
|
|
|
|
return self.mapping[topic]
|
|
|
|
elif self.single_color:
|
2017-04-10 21:50:11 +02:00
|
|
|
if not self._topic_is_master(topic):
|
|
|
|
return self.single_color
|
2017-03-11 14:04:02 +01:00
|
|
|
elif self.return_random_color:
|
2017-04-10 15:32:53 +02:00
|
|
|
# We need to take care not to return colors for both "normal" and
|
2017-04-17 13:05:02 +02:00
|
|
|
# master topics.
|
2017-03-31 17:55:54 +02:00
|
|
|
if not self._topic_is_master(topic):
|
2017-03-11 14:04:02 +01:00
|
|
|
return self._random_color()
|
2017-04-17 13:05:02 +02:00
|
|
|
# Fallback.
|
2017-03-15 00:05:45 +01:00
|
|
|
return None
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def from_file(self, preset):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Load ColorScheme from file. """
|
|
|
|
|
2017-03-25 14:54:54 +01:00
|
|
|
if preset == '-':
|
|
|
|
fd = sys.stdin
|
|
|
|
else:
|
|
|
|
import os
|
2017-04-10 21:50:11 +02:00
|
|
|
config_dir = self._get_config_dir()
|
|
|
|
if not config_dir:
|
2017-03-25 14:54:54 +01:00
|
|
|
print("Error: could not load preset!")
|
2017-03-25 23:45:42 +01:00
|
|
|
return
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Expand preset name.
|
2017-03-25 14:54:54 +01:00
|
|
|
preset = self._expand_preset(preset)
|
2017-04-17 13:05:02 +02:00
|
|
|
# Try to open the preset file.
|
2017-04-10 21:50:11 +02:00
|
|
|
fn = os.path.join(config_dir, preset)
|
2017-03-25 14:54:54 +01:00
|
|
|
try:
|
|
|
|
fd = open(fn)
|
|
|
|
except OSError:
|
|
|
|
print("Error: could not load preset \"{}\" (file could not be accessed)!".format(preset))
|
2017-03-25 23:45:42 +01:00
|
|
|
return
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Parse the preset file.
|
2017-03-11 14:04:02 +01:00
|
|
|
self.mapping = {}
|
|
|
|
self.name = preset
|
|
|
|
for line in fd.readlines():
|
2017-04-17 13:05:02 +02:00
|
|
|
# Skip every line which does not begin with an alphabetic character.
|
2017-03-11 14:04:02 +01:00
|
|
|
try:
|
|
|
|
if not line.lstrip()[0].isalpha(): continue
|
2017-04-17 13:05:02 +02:00
|
|
|
except IndexError: continue # Empty line.
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Strip spaces and split.
|
2017-03-11 14:04:02 +01:00
|
|
|
k, v = line.replace(' ','').replace('\t','').split('=')
|
2017-04-17 13:05:02 +02:00
|
|
|
# Convert #fff to fff and remove trailing comments, nl and cr chars.
|
2017-03-11 14:04:02 +01:00
|
|
|
vl = v.rstrip("\n\r").split('#')
|
|
|
|
v = vl[0] or vl[1]
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Validate hex code.
|
2017-03-11 14:04:02 +01:00
|
|
|
for c in v.lower():
|
|
|
|
if c not in "0123456789abcdef":
|
2017-03-28 17:47:03 +02:00
|
|
|
print("Error: invalid color code \"{}\" in preset \"{}\"!".format(v, preset), file=sys.stderr)
|
2017-03-11 14:04:02 +01:00
|
|
|
sys.exit(1)
|
|
|
|
self.mapping[k] = v
|
|
|
|
|
|
|
|
fd.close()
|
|
|
|
|
|
|
|
def from_color(self, color):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Derive ColorScheme from a single hex color. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
self.single_color = color.lstrip('#')
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def from_random(self):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Derive ColorScheme from random colors. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
self.return_random_color = True
|
|
|
|
|
|
|
|
def list_available(self):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" List available presets. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
import os
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-10 21:50:11 +02:00
|
|
|
config_dir = self._get_config_dir()
|
|
|
|
if not config_dir:
|
2017-03-11 14:04:02 +01:00
|
|
|
self.available = self._virtual_presets.copy()
|
|
|
|
|
|
|
|
if not self.available:
|
2017-04-10 21:50:11 +02:00
|
|
|
self.available = os.listdir(config_dir)
|
2017-03-11 14:04:02 +01:00
|
|
|
self.available.extend(self._virtual_presets)
|
|
|
|
self.available.sort()
|
2017-03-16 19:32:52 +01:00
|
|
|
print("Available presets:\n")
|
2017-03-11 14:04:02 +01:00
|
|
|
for entry in self.available:
|
|
|
|
if entry[0] == '.' or entry[-1:] == '~': continue
|
2017-03-12 15:13:41 +01:00
|
|
|
print(" " + entry)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
def store(self, name):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Store the current state of all lights as preset. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-04-10 15:32:53 +02:00
|
|
|
# Refuse to save under a name used by virtual presets. Let's also
|
|
|
|
# refuse to save as "config" or "c4ctrl.conf", as we may use one these
|
|
|
|
# file names in the future.
|
|
|
|
if name in self._virtual_presets or name in ["config", "c4ctrl.conf"]:
|
2017-04-08 16:26:24 +02:00
|
|
|
print("I'm sorry Dave. I'm afraid I can't do that. The name \"{}\" \
|
|
|
|
is reserved. Please choose a different one.".format(name))
|
2017-03-11 14:04:02 +01:00
|
|
|
return False
|
|
|
|
|
2017-03-25 14:54:54 +01:00
|
|
|
if name == '-':
|
|
|
|
fd = sys.stdout
|
|
|
|
else:
|
|
|
|
import os
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Put preset in our config directory, create it if necessary.
|
2017-04-10 21:50:11 +02:00
|
|
|
config_dir = self._get_config_dir(create=True)
|
2017-04-17 13:05:02 +02:00
|
|
|
# Strip any path elements.
|
2017-04-10 15:32:53 +02:00
|
|
|
name = os.path.split(name)[1]
|
2017-04-10 21:50:11 +02:00
|
|
|
fn = os.path.join(config_dir, name)
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-03-25 14:54:54 +01:00
|
|
|
try:
|
2017-04-17 13:05:02 +02:00
|
|
|
fd = open(fn, 'xt') # x = new file (writing), t = text mode.
|
2017-03-25 14:54:54 +01:00
|
|
|
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
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Get current states.
|
2017-03-11 14:04:02 +01:00
|
|
|
c4 = C4Interface()
|
|
|
|
|
2017-03-25 23:45:42 +01:00
|
|
|
if name == '-':
|
2017-03-28 17:47:03 +02:00
|
|
|
fd.write("# c4ctrl preset (auto generated)\n".format(name))
|
2017-03-25 23:45:42 +01:00
|
|
|
else:
|
2017-03-28 17:47:03 +02:00
|
|
|
fd.write("# c4ctrl preset \"{}\" (auto generated)\n".format(name))
|
|
|
|
fd.write("#\n")
|
|
|
|
fd.write("# Note: Topics ending with \"/master\" override all other topics in a room.\n")
|
|
|
|
fd.write("# All spaces will be stripped and lines beginning with \'#\' ignored.\n")
|
2017-03-11 14:04:02 +01:00
|
|
|
for room in Wohnzimmer, Plenarsaal, Fnordcenter:
|
|
|
|
topics = []
|
2017-03-28 17:47:03 +02:00
|
|
|
max_topic_len = 0
|
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
for light in room.lights:
|
|
|
|
topics.append(light.topic)
|
2017-03-28 17:47:03 +02:00
|
|
|
if len(light.topic) > max_topic_len:
|
|
|
|
max_topic_len = len(light.topic)
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-03-15 09:53:00 +01:00
|
|
|
responce = c4.pull(topics)
|
2017-03-11 14:04:02 +01:00
|
|
|
fd.write("\n# {}\n".format(room.name))
|
|
|
|
for light in room.lights:
|
|
|
|
for r in responce:
|
|
|
|
if r.topic == light.topic:
|
2017-03-12 16:24:01 +01:00
|
|
|
light.set_color(r.payload.hex())
|
2017-04-17 13:05:02 +02:00
|
|
|
# Format payload more nicely.
|
2017-03-28 17:47:03 +02:00
|
|
|
color = light.color
|
|
|
|
if len(color) > 6:
|
2024-03-20 12:14:11 +01:00
|
|
|
color = color[:6] + ' ' + color[6:]
|
2017-03-28 17:47:03 +02:00
|
|
|
topic = light.topic.ljust(max_topic_len)
|
2024-03-16 18:30:13 +01:00
|
|
|
# Comment out master, as it would override everything else.
|
2017-03-11 14:04:02 +01:00
|
|
|
if self._topic_is_master(r.topic):
|
2017-03-28 17:47:03 +02:00
|
|
|
fd.write("#{} = {}\n".format(topic, color))
|
2017-03-11 14:04:02 +01:00
|
|
|
else:
|
2017-03-28 17:47:03 +02:00
|
|
|
fd.write("{} = {}\n".format(topic, color))
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-05-19 19:56:03 +02:00
|
|
|
# Close opened files, but not stdout.
|
2017-03-25 14:54:54 +01:00
|
|
|
if name != '-':
|
|
|
|
fd.close()
|
|
|
|
print("Wrote preset \"{}\"".format(name))
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
class RemotePresets: # {{{1
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Remote preset control. """
|
2017-03-12 15:13:41 +01:00
|
|
|
|
|
|
|
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):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Returns a valid room name expanded from the given name. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-12 15:13:41 +01:00
|
|
|
if name in self.map.keys():
|
2017-04-17 13:05:02 +02:00
|
|
|
# Return on exact match.
|
2017-03-12 15:13:41 +01:00
|
|
|
return name
|
|
|
|
|
|
|
|
for room in self.map.keys():
|
|
|
|
if room.find(name) == 0:
|
|
|
|
return room
|
2017-04-17 13:05:02 +02:00
|
|
|
# Fallback.
|
2017-03-12 15:13:41 +01:00
|
|
|
return name
|
|
|
|
|
|
|
|
def _expand_preset_name(self, name, rooms, available):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Returns a valid preset name expanded from the given name.
|
2017-04-10 15:32:53 +02:00
|
|
|
|
2017-04-06 16:33:32 +02:00
|
|
|
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(). """
|
2017-03-13 14:09:38 +01:00
|
|
|
|
|
|
|
# Strip every "global" out of the room list. We take special care of
|
2017-03-13 14:34:54 +01:00
|
|
|
# "global" later on.
|
2017-03-13 14:09:38 +01:00
|
|
|
while "global" in rooms:
|
|
|
|
rooms.remove("global")
|
|
|
|
|
2017-03-12 15:13:41 +01:00
|
|
|
matchtable = {}
|
2017-03-13 14:09:38 +01:00
|
|
|
if "global" not in rooms:
|
|
|
|
for preset in available["global"]:
|
|
|
|
# Candidate?
|
|
|
|
if preset == name or preset.find(name) == 0:
|
2017-04-17 13:05:02 +02:00
|
|
|
# Presets in "global" are available everywhere.
|
2017-03-13 14:09:38 +01:00
|
|
|
matchtable[preset] = len(rooms)
|
|
|
|
|
2017-03-12 15:13:41 +01:00
|
|
|
for room in rooms:
|
|
|
|
for preset in available[room]:
|
2017-04-17 13:05:02 +02:00
|
|
|
# Candidate?
|
2017-03-12 15:13:41 +01:00
|
|
|
if preset == name or preset.find(name) == 0:
|
|
|
|
if preset in matchtable.keys():
|
|
|
|
matchtable[preset] += 1
|
|
|
|
else:
|
|
|
|
matchtable[preset] = 1
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# First check if there is an exact match in all rooms.
|
2017-03-13 14:09:38 +01:00
|
|
|
if name in matchtable.keys() and matchtable[name] >= len(rooms):
|
2017-03-12 16:24:01 +01:00
|
|
|
return name
|
2017-04-17 13:05:02 +02:00
|
|
|
# Return first preset available in all rooms.
|
2017-03-12 15:13:41 +01:00
|
|
|
for match in matchtable.keys():
|
2017-03-13 14:09:38 +01:00
|
|
|
if matchtable[match] >= len(rooms):
|
|
|
|
return match
|
|
|
|
elif match in available["global"]:
|
2017-03-12 15:13:41 +01:00
|
|
|
return match
|
2017-04-17 13:05:02 +02:00
|
|
|
# Fallback.
|
2017-03-12 15:13:41 +01:00
|
|
|
return name
|
|
|
|
|
|
|
|
def query_available(self, rooms=["global"]):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Returns a dict of remotely available presets for [rooms]. """
|
2017-04-07 19:58:09 +02:00
|
|
|
|
2017-03-12 15:13:41 +01:00
|
|
|
import json
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Presets in "global" are available everywhere and should always be included.
|
2017-03-12 15:13:41 +01:00
|
|
|
if "global" not in rooms:
|
2017-03-13 14:52:29 +01:00
|
|
|
rooms.insert(0, "global")
|
2017-03-12 15:13:41 +01:00
|
|
|
|
|
|
|
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()
|
2017-03-15 09:53:00 +01:00
|
|
|
responce = c4.pull(req)
|
2017-04-17 13:05:02 +02:00
|
|
|
# Make responce iterable.
|
2017-03-12 15:13:41 +01:00
|
|
|
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"):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Print a list of available Presets. """
|
|
|
|
|
2017-03-12 15:13:41 +01:00
|
|
|
room = self._expand_room_name(room)
|
2017-03-12 16:24:01 +01:00
|
|
|
available = self.query_available([room])
|
2017-03-12 15:13:41 +01:00
|
|
|
|
|
|
|
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"]):
|
2017-04-06 16:33:32 +02:00
|
|
|
""" Apply preset to given rooms. """
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Strip spaces and expand rooms names.
|
2017-03-12 15:13:41 +01:00
|
|
|
for i in range(len(rooms)):
|
|
|
|
rooms[i] = self._expand_room_name(rooms[i].strip())
|
|
|
|
|
|
|
|
available = self.query_available(rooms.copy())
|
2017-04-17 13:05:02 +02:00
|
|
|
# Produce some fake data to prevent KeyErrors if in debug mode.
|
2017-03-12 15:13:41 +01:00
|
|
|
if C4Interface.debug:
|
2017-04-08 16:26:24 +02:00
|
|
|
print("[DEBUG] Warning: handing over fake data to allow for further execution!",
|
|
|
|
file=sys.stderr)
|
2017-03-12 15:13:41 +01:00
|
|
|
available = {
|
|
|
|
"global" : [preset],
|
|
|
|
"wohnzimmer" : [preset],
|
|
|
|
"plenar" : [preset],
|
|
|
|
"fnord" : [preset],
|
|
|
|
"keller" : [preset]
|
|
|
|
}
|
2017-04-17 13:05:02 +02:00
|
|
|
# Expand preset name (stripping spaces).
|
2017-03-12 15:13:41 +01:00
|
|
|
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:
|
2017-03-12 16:24:01 +01:00
|
|
|
cmd.append((self.map[room]["set_topic"], preset))
|
2017-03-12 15:13:41 +01:00
|
|
|
|
|
|
|
c4 = C4Interface()
|
2017-03-15 09:53:00 +01:00
|
|
|
return c4.push(cmd)
|
2018-10-27 07:40:25 +02:00
|
|
|
|
2018-10-27 07:56:14 +02:00
|
|
|
def define_preset(self, name, domain="global"):
|
2018-10-27 07:40:25 +02:00
|
|
|
"""Define remote preset."""
|
|
|
|
|
|
|
|
if domain not in self.map.keys():
|
|
|
|
print("Error: \"{}\" is not a valid domain! Valid domains are: {}"
|
|
|
|
.format(domain, ", ".join(self.map.keys())))
|
|
|
|
return False
|
|
|
|
|
|
|
|
c4 = C4Interface()
|
|
|
|
return c4.push(name, topic=self.map[domain]["def_topic"])
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-12 15:13:41 +01:00
|
|
|
|
2017-04-23 21:23:58 +02:00
|
|
|
if __name__ == "__main__": # {{{1
|
2017-03-11 14:04:02 +01:00
|
|
|
import argparse
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description="Command line client for AutoC4.")
|
|
|
|
parser.add_argument(
|
|
|
|
"-d", "--debug", action="store_true",
|
2017-04-08 16:26:24 +02:00
|
|
|
help="display what would be send to the MQTT broker, but do not \
|
|
|
|
actually connect")
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
# 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)")
|
2018-11-07 21:34:19 +01:00
|
|
|
group_fn.add_argument(
|
|
|
|
"--cyberalert", nargs=1, type=int, metavar="0|1",
|
|
|
|
help="start/stop cyberalert")
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
# Kitchenlight control
|
|
|
|
group_kl = parser.add_argument_group(title="Kitchenlight control")
|
|
|
|
group_kl.add_argument(
|
2017-04-17 13:05:02 +02:00
|
|
|
"-k", "--kl-mode", nargs='+', type=str, metavar=("MODE", "OPTIONS"),
|
2017-04-23 22:18:31 +02:00
|
|
|
help="set Kitchenlight to MODE (MODE may be abbreviated)")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_kl.add_argument(
|
2017-03-12 15:13:41 +01:00
|
|
|
"-i", "--list-kl-modes", action="store_true",
|
|
|
|
help="list available Kitchenlight modes and their options")
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
# Ambient control
|
2017-03-12 15:13:41 +01:00
|
|
|
group_cl = parser.add_argument_group(title="ambient color control",
|
2017-04-08 16:26:24 +02:00
|
|
|
description="PRESET may be either a preset name (which may be \
|
2017-04-18 12:40:59 +02:00
|
|
|
abbreviated), '#' followed by a color value in hex notation (e.g. \
|
2017-04-08 16:26:24 +02:00
|
|
|
\"#ff0066\") or '-' to read from stdin.")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_cl.add_argument(
|
|
|
|
"-w", "--wohnzimmer", type=str, dest="w_color", metavar="PRESET",
|
2017-03-12 15:13:41 +01:00
|
|
|
help="apply local colorscheme PRESET to Wohnzimmer")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_cl.add_argument(
|
|
|
|
"-p", "--plenarsaal", type=str, dest="p_color", metavar="PRESET",
|
2017-03-12 15:13:41 +01:00
|
|
|
help="apply local colorscheme PRESET to Plenarsaal")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_cl.add_argument(
|
|
|
|
"-f", "--fnordcenter", type=str, dest="f_color", metavar="PRESET",
|
2017-03-12 15:13:41 +01:00
|
|
|
help="apply local colorscheme PRESET to Fnordcenter")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_cl.add_argument(
|
2017-03-12 15:13:41 +01:00
|
|
|
"-l", "--list-presets", action="store_true",
|
|
|
|
help="list locally available presets")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_cl.add_argument(
|
|
|
|
"-o", "--store-preset", type=str, dest="store_as", metavar="NAME",
|
2017-03-26 00:22:14 +01:00
|
|
|
help="store current state as preset NAME ('-' to write to stdout)")
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-03-11 14:04:02 +01:00
|
|
|
# Switch control
|
|
|
|
group_sw = parser.add_argument_group(title="light switch control",
|
2017-04-10 15:47:24 +02:00
|
|
|
description="BINARY_CODE is a string of 0s or 1s for every light in a \
|
2017-05-20 08:33:56 +02:00
|
|
|
room. May be given as decimal. May be prepended by '~', '&', '|' or \
|
|
|
|
'^' as NOT, AND, OR and XOR operators. Current switch states will be \
|
|
|
|
printed to stdout if BINARY_CODE is '-'. Will show usage information \
|
|
|
|
and ask for input if BINARY_CODE is omitted. Will read from stdin if \
|
2024-03-21 14:29:14 +01:00
|
|
|
BINARY_CODE is omitted and stdin is not connected to a TTY.")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_sw.add_argument(
|
2017-03-26 00:22:14 +01:00
|
|
|
"-W", nargs='?', dest="w_switch", const="", metavar="BINARY_CODE",
|
2017-12-11 18:46:27 +01:00
|
|
|
help="switch lights and sockets in Wohnzimmer on/off")
|
2017-03-11 14:04:02 +01:00
|
|
|
group_sw.add_argument(
|
2017-03-26 00:22:14 +01:00
|
|
|
"-P", nargs='?', dest="p_switch", const="", metavar="BINARY_CODE",
|
2017-03-11 14:04:02 +01:00
|
|
|
help="switch lights in Plenarsaal on/off")
|
|
|
|
group_sw.add_argument(
|
2017-03-26 00:22:14 +01:00
|
|
|
"-F", nargs='?', dest="f_switch", const="", metavar="BINARY_CODE",
|
2017-03-11 14:04:02 +01:00
|
|
|
help="switch lights in Fnordcentter on/off")
|
|
|
|
group_sw.add_argument(
|
2017-03-26 00:22:14 +01:00
|
|
|
"-K", nargs='?', dest="k_switch", const="", metavar="BINARY_CODE",
|
2017-03-11 14:04:02 +01:00
|
|
|
help="switch lights in Keller on/off")
|
2017-04-06 16:33:32 +02:00
|
|
|
|
2017-03-26 00:22:14 +01:00
|
|
|
# Remote presets
|
|
|
|
group_rp = parser.add_argument_group(title="remote preset functions",
|
2017-04-08 16:26:24 +02:00
|
|
|
description="Available room names are \"wohnzimmer\", \"plenar\", \
|
|
|
|
\"fnord\" and \"keller\". Preset and room names may be abbreviated.")
|
2017-03-26 00:22:14 +01:00
|
|
|
group_rp.add_argument(
|
2017-04-17 13:05:02 +02:00
|
|
|
"-r", "--remote-preset", nargs='+', type=str, metavar=("PRESET", "ROOM"),
|
2017-04-08 16:26:24 +02:00
|
|
|
help="activate remote PRESET for ROOM(s). Activates preset globally \
|
|
|
|
if ROOM is omitted.")
|
2017-03-26 00:22:14 +01:00
|
|
|
group_rp.add_argument(
|
|
|
|
"-R", "--list-remote", nargs='?', const="global", metavar="ROOM",
|
2017-04-08 16:26:24 +02:00
|
|
|
help="list remote presets for ROOM. Will list global presets if ROOM \
|
|
|
|
is omitted.")
|
2018-10-27 07:40:25 +02:00
|
|
|
group_rp.add_argument(
|
2018-10-27 07:56:14 +02:00
|
|
|
"--define-remote-preset", nargs=2, type=str, metavar=("NAME", "ROOM"),
|
|
|
|
help="define remote preset NAME for ROOM.")
|
2017-03-11 14:04:02 +01:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
2017-04-17 13:05:02 +02:00
|
|
|
# Debug, gate, status and shutdown.
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.debug:
|
|
|
|
C4Interface.debug = True
|
|
|
|
if args.status:
|
2017-04-06 16:33:32 +02:00
|
|
|
status = C4Interface().status()
|
|
|
|
print("Club is", status)
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.gate:
|
|
|
|
C4Interface().open_gate()
|
|
|
|
if args.shutdown:
|
|
|
|
if args.shutdown >= 2:
|
|
|
|
C4Interface().shutdown(force=True)
|
|
|
|
else:
|
|
|
|
C4Interface().shutdown()
|
2018-11-07 21:34:19 +01:00
|
|
|
if args.cyberalert:
|
|
|
|
C4Interface().cyberalert(args.cyberalert[0])
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
# Kitchenlight
|
2017-03-12 15:13:41 +01:00
|
|
|
if args.list_kl_modes:
|
2017-04-08 16:26:24 +02:00
|
|
|
Kitchenlight().list_available()
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.kl_mode:
|
|
|
|
kl = Kitchenlight()
|
2017-04-17 13:05:02 +02:00
|
|
|
if len(args.kl_mode) == 1:
|
|
|
|
kl.set_mode(args.kl_mode[0])
|
2017-03-11 14:04:02 +01:00
|
|
|
else:
|
2017-04-17 13:05:02 +02:00
|
|
|
kl.set_mode(args.kl_mode[0], args.kl_mode[1:])
|
2017-03-11 14:04:02 +01:00
|
|
|
|
|
|
|
# Colorscheme
|
|
|
|
if args.store_as:
|
|
|
|
ColorScheme().store(args.store_as)
|
2017-04-17 13:05:02 +02:00
|
|
|
presets = {} # Store and reuse initialized presets.
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.w_color:
|
2017-03-25 23:45:42 +01:00
|
|
|
if args.w_color not in presets:
|
2017-04-10 21:50:11 +02:00
|
|
|
presets[args.w_color] = ColorScheme(args.w_color)
|
2018-10-27 07:56:14 +02:00
|
|
|
Wohnzimmer().set_colorscheme(presets[args.w_color])
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.p_color:
|
2017-03-25 23:45:42 +01:00
|
|
|
if args.p_color not in presets:
|
2017-04-10 21:50:11 +02:00
|
|
|
presets[args.p_color] = ColorScheme(args.p_color)
|
2018-10-27 07:56:14 +02:00
|
|
|
Plenarsaal().set_colorscheme(presets[args.p_color])
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.f_color:
|
2017-03-25 23:45:42 +01:00
|
|
|
if args.f_color not in presets:
|
2017-04-10 21:50:11 +02:00
|
|
|
presets[args.f_color] = ColorScheme(args.f_color)
|
2018-10-27 07:56:14 +02:00
|
|
|
Fnordcenter().set_colorscheme(presets[args.f_color])
|
2017-03-11 14:04:02 +01:00
|
|
|
if args.list_presets:
|
|
|
|
ColorScheme().list_available()
|
|
|
|
|
2017-03-26 00:22:14 +01:00
|
|
|
# Light switches
|
|
|
|
if args.w_switch != None:
|
|
|
|
Wohnzimmer().light_switch(args.w_switch)
|
|
|
|
if args.p_switch != None:
|
|
|
|
Plenarsaal().light_switch(args.p_switch)
|
|
|
|
if args.f_switch != None:
|
|
|
|
Fnordcenter().light_switch(args.f_switch)
|
|
|
|
if args.k_switch != None:
|
|
|
|
Keller().light_switch(args.k_switch)
|
|
|
|
|
2017-03-12 15:13:41 +01:00
|
|
|
# Remote presets
|
|
|
|
if args.list_remote:
|
|
|
|
RemotePresets().list_available(args.list_remote.lower())
|
|
|
|
if args.remote_preset:
|
2017-04-17 13:05:02 +02:00
|
|
|
if len(args.remote_preset) == 1:
|
|
|
|
RemotePresets().apply_preset(args.remote_preset[0].strip())
|
2017-03-12 15:13:41 +01:00
|
|
|
else:
|
2017-04-17 13:05:02 +02:00
|
|
|
RemotePresets().apply_preset(args.remote_preset[0].strip(),
|
|
|
|
args.remote_preset[1:])
|
2018-10-27 07:56:14 +02:00
|
|
|
if args.define_remote_preset:
|
|
|
|
RemotePresets().define_preset(args.define_remote_preset[0].strip(),
|
|
|
|
args.define_remote_preset[1].strip())
|
2017-03-12 15:13:41 +01:00
|
|
|
|
2017-03-21 12:08:00 +01:00
|
|
|
# No or no useful command line options?
|
2017-03-11 14:04:02 +01:00
|
|
|
if len(sys.argv) <= 1 or len(sys.argv) == 2 and args.debug:
|
|
|
|
parser.print_help()
|
2017-04-23 21:23:58 +02:00
|
|
|
# }}}1
|
2017-03-11 14:04:02 +01:00
|
|
|
|