diff --git a/__install__.py b/__install__.py index 69626dd..fe60147 100644 --- a/__install__.py +++ b/__install__.py @@ -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 ") + if __name__ == "__main__": if len(sys.argv) == 4: try: diff --git a/base.py b/base.py index 4339750..1897931 100644 --- a/base.py +++ b/base.py @@ -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): diff --git a/devices/__init__.py b/devices/__init__.py index e6efd87..5c4e62f 100644 --- a/devices/__init__.py +++ b/devices/__init__.py @@ -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)) diff --git a/function/modules.py b/function/modules.py index f5db6cb..c4cb32a 100644 --- a/function/modules.py +++ b/function/modules.py @@ -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]) diff --git a/function/videv.py b/function/videv.py index 5fbe6e6..a67140b 100644 --- a/function/videv.py +++ b/function/videv.py @@ -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) diff --git a/smart_brain.py b/smart_brain.py index 0d2d368..d7644df 100644 --- a/smart_brain.py +++ b/smart_brain.py @@ -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 = {