Restructured devices

This commit is contained in:
Dirk Alders 2023-02-10 10:37:35 +01:00
parent 0b74ff04c9
commit 8a3bbf77a4
6 changed files with 143 additions and 193 deletions

View File

@ -11,6 +11,7 @@ Wants=network-online.target
[Service]
User=%(UID)d
Group=%(GID)d
WorkingDirectory=%(MY_PATH)s
ExecStart=%(MY_PATH)s/smart_brain.sh
Type=simple
[Install]
@ -21,6 +22,7 @@ WantedBy=default.target
def help():
print("Usage: prog <UID> <GID> <TARGET_PATH>")
if __name__ == "__main__":
if len(sys.argv) == 4:
try:

21
base.py
View File

@ -14,7 +14,7 @@ class common_base(dict):
self['__type__'] = self.__class__.__name__
#
self.__callback_list__ = []
self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild("devices")
def add_callback(self, key, data, callback, on_change_only=False):
"""
@ -26,14 +26,17 @@ class common_base(dict):
self.__callback_list__.append(cb_tup)
def set(self, key, data, block_callback=[]):
value_changed = self[key] != data
self[key] = data
for cb_key, cb_data, callback, on_change_only in self.__callback_list__:
if cb_key is None or key == cb_key: # key fits to callback definition
if cb_data is None or cb_data == self[key]: # data fits to callback definition
if value_changed or not on_change_only: # change status fits to callback definition
if not callback in block_callback: # block given callbacks
callback(self, key, self[key])
if key in self.keys():
value_changed = self[key] != data
self[key] = data
for cb_key, cb_data, callback, on_change_only in self.__callback_list__:
if cb_key is None or key == cb_key: # key fits to callback definition
if cb_data is None or cb_data == self[key]: # data fits to callback definition
if value_changed or not on_change_only: # change status fits to callback definition
if not callback in block_callback: # block given callbacks
callback(self, key, self[key])
else:
self.logger.warning("Unexpected key %s", key)
class mqtt_base(common_base):

View File

@ -26,6 +26,8 @@ devices (DEVICES)
"""
from base import mqtt_base
from function.videv import base as videv_base
import json
import logging
import task
@ -93,7 +95,7 @@ class group(object):
return rv
class base(dict):
class base(mqtt_base):
TX_TOPIC = "set"
TX_VALUE = 0
TX_DICT = 1
@ -104,84 +106,60 @@ class base(dict):
RX_IGNORE_TOPICS = []
RX_IGNORE_KEYS = []
RX_FILTER_DATA_KEYS = []
#
KEY_WARNING = '__WARNING__'
def __init__(self, mqtt_client, topic):
super().__init__(mqtt_client, topic, default_values=dict.fromkeys(self.RX_KEYS))
# data storage
self.mqtt_client = mqtt_client
self.topic = topic
self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
for entry in self.topic.split('/'):
self.logger = self.logger.getChild(entry)
# initialisations
dict.__init__(self)
mqtt_client.add_callback(
topic=self.topic, callback=self.receive_callback)
mqtt_client.add_callback(
topic=self.topic+"/#", callback=self.receive_callback)
#
self.callback_list = []
self.warning_callback = None
#
self.__previous__ = {}
mqtt_client.add_callback(topic=self.topic, callback=self.receive_callback)
mqtt_client.add_callback(topic=self.topic+"/#", callback=self.receive_callback)
def set(self, key, data, block_callback=[]):
if key in self.RX_IGNORE_KEYS:
pass # ignore these keys
elif key in self.RX_KEYS:
return super().set(key, data, block_callback)
else:
self.logger.warning("Unexpected key %s", key)
def receive_callback(self, client, userdata, message):
self.unpack(message)
def unpack_filter(self, key):
if key in self.RX_FILTER_DATA_KEYS:
if self.get(key) == 1 or self.get(key) == 'on' or self.get(key) == 'ON':
self[key] = True
elif self.get(key) == 0 or self.get(key) == 'off' or self.get(key) == 'OFF':
self[key] = False
def unpack_single_value(self, key, data):
prev_value = self.get(key)
if key in self.RX_KEYS:
self[key] = data
self.__previous__[key] = prev_value
# Filter, if needed
self.unpack_filter(key)
self.logger.debug("Received data %s - %s", key, str(self.get(key)))
self.callback_caller(key, self[key], self.get(key) != self.__previous__.get(key))
elif key not in self.RX_IGNORE_KEYS:
self.logger.warning('Got a message with unparsed content: "%s - %s"', key, str(data))
else:
self.logger.debug("Ignoring key %s", key)
def unpack(self, message):
content_key = message.topic[len(self.topic) + 1:]
if content_key not in self.RX_IGNORE_TOPICS and (not message.topic.endswith(self.TX_TOPIC) or len(self.TX_TOPIC) == 0):
self.logger.debug("Unpacking content_key \"%s\" from message.", content_key)
if is_json(message.payload):
data = json.loads(message.payload)
if type(data) is dict:
for key in data:
self.unpack_single_value(key, data[key])
if message.topic != self.topic + '/' + videv_base.KEY_INFO:
content_key = message.topic[len(self.topic) + 1:]
if content_key not in self.RX_IGNORE_TOPICS and (not message.topic.endswith(self.TX_TOPIC) or len(self.TX_TOPIC) == 0):
self.logger.debug("Unpacking content_key \"%s\" from message.", content_key)
if is_json(message.payload):
data = json.loads(message.payload)
if type(data) is dict:
for key in data:
self.set(key, self.__device_to_instance_filter__(key, data[key]))
else:
self.set(content_key, self.__device_to_instance_filter__(content_key, data))
# String
else:
self.unpack_single_value(content_key, data)
# String
self.set(content_key, self.__device_to_instance_filter__(content_key, message.payload.decode('utf-8')))
else:
self.unpack_single_value(
content_key, message.payload.decode('utf-8'))
self.warning_caller()
else:
self.logger.debug("Ignoring topic %s", content_key)
self.logger.debug("Ignoring topic %s", content_key)
def pack_filter(self, key, data):
def __device_to_instance_filter__(self, key, data):
if key in self.RX_FILTER_DATA_KEYS:
if data in [1, 'on', 'ON']:
return True
elif data in [0, 'off', 'OFF']:
return False
return data
def __instance_to_device_filter__(self, key, data):
if key in self.TX_FILTER_DATA_KEYS:
if data is True:
return "on"
elif data is False:
return "off"
else:
return data
return data
def set(self, key, data):
self.pack(key, data)
def pack(self, key, data):
data = self.pack_filter(key, data)
def send_command(self, key, data):
data = self.__instance_to_device_filter__(key, data)
if self.TX_TOPIC is not None:
if self.TX_TYPE < 0:
self.logger.error("Unknown tx type. Set TX_TYPE of class to a known value")
@ -196,40 +174,6 @@ class base(dict):
else:
self.logger.error("Unknown tx toptic. Set TX_TOPIC of class to a known value")
def add_callback(self, key, data, callback, on_change_only=False):
"""
key: key or None for all keys
data: data or None for all data
"""
cb_tup = (key, data, callback, on_change_only)
if cb_tup not in self.callback_list:
self.callback_list.append(cb_tup)
def add_warning_callback(self, callback):
self.warning_callback = callback
def warning_call_condition(self):
return False
def callback_caller(self, key, data, value_changed):
for cb_key, cb_data, callback, on_change_only in self.callback_list:
if (cb_key == key or cb_key is None) and (cb_data == data or cb_data is None) and callback is not None:
if not on_change_only or value_changed:
callback(self, key, data)
def warning_caller(self):
if self.warning_call_condition():
warn_txt = self.warning_text()
self.logger.warning(warn_txt)
if self.warning_callback is not None:
self.warning_callback(self, warn_txt)
def warning_text(self):
return "default warning text - replace parent warning_text function"
def previous_value(self, key):
return self.__previous__.get(key)
class shelly(base):
KEY_OUTPUT_0 = "relay/0"
@ -264,11 +208,13 @@ class shelly(base):
self.delayed_flash_task = task.delayed(0.3, self.flash_task)
self.delayed_off_task = task.delayed(0.3, self.off_task)
#
self.add_callback(self.KEY_OVERTEMPERATURE, True, self.__warning__, True)
#
self.all_off_requested = False
def flash_task(self, *args):
if self.flash_active:
self.pack(self.output_key_delayed, not self.get(self.output_key_delayed))
self.send_command(self.output_key_delayed, not self.get(self.output_key_delayed))
self.output_key_delayed = None
if self.all_off_requested:
self.delayed_off_task.run()
@ -283,15 +229,8 @@ class shelly(base):
#
# WARNING CALL
#
def warning_call_condition(self):
return self.get(self.KEY_OVERTEMPERATURE)
def warning_text(self):
if self.overtemperature:
if self.temperature is not None:
return "Overtemperature detected for %s. Temperature was %.1f°C." % (self.topic, self.temperature)
else:
return "Overtemperature detected for %s." % self.topic
def __warning__(self, client, key, data):
pass # TODO: implement warning feedback (key: KEY_OVERTEMPERATURE - info: KEY_TEMPERATURE)
#
# RX
@ -335,8 +274,8 @@ class shelly(base):
# TX
#
def set_output_0(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_0, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_0, state)
def set_output_0_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_0 else logging.DEBUG, "Changing output 0 to %s", str(data))
@ -344,11 +283,11 @@ class shelly(base):
def toggle_output_0_mcb(self, device, key, data):
self.logger.info("Toggeling output 0")
self.set_output_0('toggle')
self.set_output_0(not self.output_0)
def set_output_1(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_1, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_1, state)
def set_output_1_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_1 else logging.DEBUG, "Changing output 1 to %s", str(data))
@ -356,7 +295,7 @@ class shelly(base):
def toggle_output_1_mcb(self, device, key, data):
self.logger.info("Toggeling output 1")
self.set_output_1('toggle')
self.set_output_1(not self.output_1)
def flash_0_mcb(self, device, key, data):
self.output_key_delayed = self.KEY_OUTPUT_0
@ -408,8 +347,8 @@ class silvercrest_powerplug(base):
# TX
#
def set_output_0(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_0, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_0, state)
def set_output_0_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_0 else logging.DEBUG, "Changing output 0 to %s", str(data))
@ -417,7 +356,7 @@ class silvercrest_powerplug(base):
def toggle_output_0_mcb(self, device, key, data):
self.logger.info("Toggeling output 0")
self.set_output_0('toggle')
self.set_output_0(not self.output_0)
def all_off(self):
if self.output_0:
@ -436,17 +375,18 @@ class silvercrest_motion_sensor(base):
def __init__(self, mqtt_client, topic):
super().__init__(mqtt_client, topic)
#
self.add_callback(self.KEY_BATTERY_LOW, True, self.__warning__, True)
def warning_call_condition(self):
return self.get(self.KEY_BATTERY_LOW)
def warning_text(self, data):
return "Battery low: level=%d" % self.get(self.KEY_BATTERY)
#
# WARNING CALL
#
def __warning__(self, client, key, data):
pass # TODO: implement warning feedback (key: KEY_BATTERY_LOW - info: KEY_BATTERY)
#
# RX
#
@property
def linkquality(self):
"""rv: numeric value"""
@ -496,13 +436,13 @@ class my_powerplug(base):
#
def set_output(self, key, state):
if key in self.KEY_OUTPUT_LIST:
self.pack(key, state)
self.send_command(key, state)
else:
logging.error("Unknown key to set the output!")
def set_output_0(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_0, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_0, state)
def set_output_0_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_0 else logging.DEBUG, "Changing output 0 to %s", str(data))
@ -510,11 +450,11 @@ class my_powerplug(base):
def toggle_output_0_mcb(self, device, key, data):
self.logger.info("Toggeling output 0")
self.set_output_0('toggle')
self.set_output_0(not self.output_0)
def set_output_1(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_1, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_1, state)
def set_output_1_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_1 else logging.DEBUG, "Changing output 1 to %s", str(data))
@ -522,11 +462,11 @@ class my_powerplug(base):
def toggle_output_1_mcb(self, device, key, data):
self.logger.info("Toggeling output 1")
self.set_output_1('toggle')
self.set_output_1(not self.output_1)
def set_output_2(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_2, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_2, state)
def set_output_2_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_2 else logging.DEBUG, "Changing output 2 to %s", str(data))
@ -534,11 +474,11 @@ class my_powerplug(base):
def toggle_output_2_mcb(self, device, key, data):
self.logger.info("Toggeling output 2")
self.set_output_2('toggle')
self.set_output_2(not self.output_2)
def set_output_3(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_3, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_3, state)
def set_output_3_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_3 else logging.DEBUG, "Changing output 3 to %s", str(data))
@ -546,20 +486,16 @@ class my_powerplug(base):
def toggle_output_3_mcb(self, device, key, data):
self.logger.info("Toggeling output 3")
self.set_output_3('toggle')
self.set_output_3(not self.output_3)
def set_output_all(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_ALL, state)
self.send_command(self.KEY_OUTPUT_ALL, state)
def set_output_all_mcb(self, device, key, data):
self.logger.info("Changing all outputs to %s", str(data))
self.set_output_all(data)
def toggle_output_all_mcb(self, device, key, data):
self.logger.info("Toggeling all outputs")
self.set_output_0('toggle')
def all_off(self):
self.set_output_all(False)
@ -581,21 +517,19 @@ class tradfri_light(base):
def __init__(self, mqtt_client, topic):
super().__init__(mqtt_client, topic)
def unpack_filter(self, key):
def __device_to_instance_filter__(self, key, data):
if key == self.KEY_BRIGHTNESS:
self[key] = round((self[key] - 1) * 100 / 253, 0)
return int(round((data - 1) * 100 / 253, 0))
elif key == self.KEY_COLOR_TEMP:
self[key] = round((self[key] - 250) * 10 / 204, 0)
else:
super().unpack_filter(key)
return int(round((data - 250) * 10 / 204, 0))
return super().__device_to_instance_filter__(key, data)
def pack_filter(self, key, data):
def __instance_to_device_filter__(self, key, data):
if key == self.KEY_BRIGHTNESS:
return round(data * 253 / 100 + 1, 0)
return int(round(data * 253 / 100 + 1, 0))
elif key == self.KEY_COLOR_TEMP:
return round(data * 204 / 10 + 250, 0)
else:
return super().pack_filter(key, data)
return int(round(data * 204 / 10 + 250, 0))
return super().__instance_to_device_filter__(key, data)
#
# RX
@ -627,8 +561,8 @@ class tradfri_light(base):
self.mqtt_client.send(self.topic + "/get", '{"%s": ""}' % self.KEY_OUTPUT_0)
def set_output_0(self, state):
"""state: [True, False, 'toggle']"""
self.pack(self.KEY_OUTPUT_0, state)
"""state: [True, False]"""
self.send_command(self.KEY_OUTPUT_0, state)
def set_output_0_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.output_0 else logging.DEBUG, "Changing output 0 to %s", str(data))
@ -636,18 +570,18 @@ class tradfri_light(base):
def toggle_output_0_mcb(self, device, key, data):
self.logger.info("Toggeling output 0")
self.set_output_0('toggle')
self.set_output_0(not self.output_0)
def set_brightness(self, brightness):
"""brightness: [0, ..., 100]"""
self.pack(self.KEY_BRIGHTNESS, brightness)
self.send_command(self.KEY_BRIGHTNESS, brightness)
def set_brightness_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.brightness else logging.DEBUG, "Changing brightness to %s", str(data))
self.set_brightness(data)
def default_inc(self, speed=40):
self.pack(self.KEY_BRIGHTNESS_FADE, speed)
self.send_command(self.KEY_BRIGHTNESS_FADE, speed)
def default_dec(self, speed=-40):
self.default_inc(speed)
@ -657,7 +591,7 @@ class tradfri_light(base):
def set_color_temp(self, color_temp):
"""color_temp: [0, ..., 10]"""
self.pack(self.KEY_COLOR_TEMP, color_temp)
self.send_command(self.KEY_COLOR_TEMP, color_temp)
def set_color_temp_mcb(self, device, key, data):
self.logger.log(logging.INFO if data != self.color_temp else logging.DEBUG, "Changing color temperature to %s", str(data))
@ -693,6 +627,20 @@ class tradfri_button(base):
def __init__(self, mqtt_client, topic):
super().__init__(mqtt_client, topic)
#
self.add_callback(self.KEY_BATTERY, None, self.__warning__, True)
self.__battery_warning__ = False
#
# WARNING CALL
#
def __warning__(self, client, key, data):
if data <= BATTERY_WARN_LEVEL:
if not self.__battery_warning__:
self.__battery_warning__ = True
pass # TODO: implement warning feedback (key: KEY_BATTERY_LOW - info: KEY_BATTERY)
else:
self.__battery_warning__ = False
#
# RX
@ -702,15 +650,6 @@ class tradfri_button(base):
"""rv: action_txt"""
return self.get(self.KEY_ACTION)
#
# WARNING CALL
#
def warning_call_condition(self):
return self.get(self.KEY_BATTERY) is not None and self.get(self.KEY_BATTERY) <= BATTERY_WARN_LEVEL
def warning_text(self):
return "Low battery level detected for %s. Battery level was %.0f%%." % (self.topic, self.get(self.KEY_BATTERY))
class brennenstuhl_heatingvalve(base):
KEY_LINKQUALITY = "linkquality"
@ -734,12 +673,20 @@ class brennenstuhl_heatingvalve(base):
super().__init__(mqtt_client, topic)
self.mqtt_client.send(self.topic + '/' + self.TX_TOPIC, json.dumps({self.KEY_WINDOW_DETECTION: "ON",
self.KEY_CHILD_LOCK: "UNLOCK", self.KEY_VALVE_DETECTION: "ON", self.KEY_SYSTEM_MODE: "heat", self.KEY_PRESET: "manual"}))
#
self.add_callback(self.KEY_BATTERY, None, self.__warning__, True)
self.__battery_warning__ = False
def warning_call_condition(self):
return self.get(self.KEY_BATTERY, 100) <= BATTERY_WARN_LEVEL
def warning_text(self):
return "Low battery level detected for %s. Battery level was %.0f%%." % (self.topic, self.get(self.KEY_BATTERY))
#
# WARNING CALL
#
def __warning__(self, client, key, data):
if data <= BATTERY_WARN_LEVEL:
if not self.__battery_warning__:
self.__battery_warning__ = True
pass # TODO: implement warning feedback (key: KEY_BATTERY_LOW - info: KEY_BATTERY)
else:
self.__battery_warning__ = False
#
# RX
@ -760,7 +707,7 @@ class brennenstuhl_heatingvalve(base):
# TX
#
def set_heating_setpoint(self, setpoint):
self.pack(self.KEY_HEATING_SETPOINT, setpoint)
self.send_command(self.KEY_HEATING_SETPOINT, setpoint)
def set_heating_setpoint_mcb(self, device, key, data):
self.logger.info("Changing heating setpoint to %s", str(data))
@ -782,27 +729,27 @@ class remote(base):
RX_IGNORE_TOPICS = [KEY_CD, KEY_LINE1, KEY_LINE3, KEY_MUTE, KEY_POWER, KEY_VOLUP, KEY_VOLDOWN]
def set_cd(self, device=None, key=None, data=None):
self.pack(self.KEY_CD, None)
self.send_command(self.KEY_CD, None)
def set_line1(self, device=None, key=None, data=None):
self.pack(self.KEY_LINE1, None)
self.send_command(self.KEY_LINE1, None)
def set_line3(self, device=None, key=None, data=None):
self.pack(self.KEY_LINE3, None)
self.send_command(self.KEY_LINE3, None)
def set_mute(self, device=None, key=None, data=None):
self.pack(self.KEY_MUTE, None)
self.send_command(self.KEY_MUTE, None)
def set_power(self, device=None, key=None, data=None):
self.pack(self.KEY_POWER, None)
self.send_command(self.KEY_POWER, None)
def set_volume_up(self, data=False):
"""data: [True, False]"""
self.pack(self.KEY_VOLUP, data)
self.send_command(self.KEY_VOLUP, data)
def set_volume_down(self, data=False):
"""data: [True, False]"""
self.pack(self.KEY_VOLDOWN, data)
self.send_command(self.KEY_VOLDOWN, data)
def default_inc(self, device=None, key=None, data=None):
self.set_volume_up(True)
@ -823,7 +770,7 @@ class status(base):
def set_state(self, num, data):
"""data: [True, False]"""
self.pack(self.KEY_STATE + "/" + str(num), data)
self.send_command(self.KEY_STATE + "/" + str(num), data)
def set_state_mcb(self, device, key, data):
self.logger.info("Changing state to %s", str(data))

View File

@ -199,7 +199,7 @@ class heating_function(common_base):
def cancel_boost(self):
self.set(self.KEY_BOOST_TIMER, 0, block_callback=[self.timer_expired])
def set(self, key, data, block_callback=[]):
def send_command(self, key, data, block_callback=[]):
rv = super().set(key, data, block_callback)
set_radiator_data(self.heating_valve.topic, self[self.KEY_AWAY_MODE], self[self.KEY_SUMMER_MODE],
self[self.KEY_USER_TEMPERATURE_SETPOINT], self[self.KEY_TEMPERATURE_SETPOINT])

View File

@ -88,7 +88,7 @@ class base(mqtt_base):
ext_device, ext_key, on_change_only = self.__control_dict__[my_key]
if my_key in self.keys():
if data != self[my_key] or not on_change_only:
ext_device.set(ext_key, data)
ext_device.send_command(ext_key, data)
self.set(my_key, data)
else:
self.logger.info("Ignoring rx message with topic %s", message.topic)

View File

@ -10,14 +10,12 @@ import time
logger = logging.getLogger(config.APP_NAME)
# TODO: Restructure nodered gui (own heating page - with circulation pump)
# TODO: Rework devices to base.mqtt (pack -> set, ...)
# TODO: Implement handling of warnings (videv element to show in webapp?)
# TODO: implement garland (incl. day events like sunset, sunrise, ...)
VERS_MAJOR = 1
VERS_MINOR = 0
VERS_PATCH = 1
VERS_PATCH = 2
INFO_TOPIC = "__info__"
INFO_DATA = {