diff --git a/.gitmodules b/.gitmodules index 4fd1c93..fa2204f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "report"] path = report url = https://git.mount-mockery.de/pylib/report.git +[submodule "task"] + path = task + url = https://git.mount-mockery.de/pylib/task.git diff --git a/devices/__init__.py b/devices/__init__.py index f62af80..a204780 100644 --- a/devices/__init__.py +++ b/devices/__init__.py @@ -60,7 +60,6 @@ class base(dict): TX_TYPE = -1 TX_FILTER_DATA_KEYS = [] # - RX_LOG_INFO_ALWAYS_KEYS = [] RX_KEYS = [] RX_IGNORE_TOPICS = [] RX_IGNORE_KEYS = [] @@ -96,8 +95,10 @@ class base(dict): self[key] = data # Filter, if needed self.unpack_filter(key) - logger.log(logging.INFO if key in self.RX_LOG_INFO_ALWAYS_KEYS or prev_value != self.get(key) else logging.DEBUG, - "Received data for (%s) %s - %s", self.topic, key, str(self.get(key))) + if prev_value != self.get(key): + logger.info("Received new data for (%s) %s - %s", self.topic, key, str(self.get(key))) + else: + logger.debug("Received data for (%s) %s - %s", self.topic, key, str(self.get(key))) self.callback_caller(key, self[key]) elif key not in self.RX_IGNORE_KEYS: logger.warning('Got a message from \"%s\" with unparsed content "%s"', self.topic, key) @@ -143,6 +144,7 @@ class base(dict): logger.error( "Unknown tx type. Set TX_TYPE of class to a known value") else: + logger.info("Sending data for (%s) %s - %s", self.topic, key, str(data)) if self.TX_TYPE == self.TX_DICT: self.mqtt_client.send('/'.join([self.topic, self.TX_TOPIC]), json.dumps({key: data})) else: @@ -452,7 +454,6 @@ class tradfri_button(base): KEY_BATTERY = "battery" KEY_ACTION = "action" # - RX_LOG_INFO_ALWAYS_KEYS = [KEY_ACTION] RX_KEYS = [KEY_LINKQUALITY, KEY_BATTERY, KEY_ACTION] RX_IGNORE_TOPICS = [] RX_IGNORE_KEYS = ['update'] @@ -485,13 +486,14 @@ class nodered_gui(base): KEY_STATE = "state" KEY_BRIGHTNESS = "brightness" KEY_COLOR_TEMP = "color_temp" + KEY_HEATING_BOOST = "heating_boost" + KEY_HEATING_SETPOINT = "heating_setpoint" # TX_TOPIC = 'set' TX_TYPE = base.TX_VALUE TX_FILTER_DATA_KEYS = [] # - RX_LOG_INFO_ALWAYS_KEYS = [] - RX_KEYS = [KEY_STATE, KEY_BRIGHTNESS, KEY_COLOR_TEMP] + RX_KEYS = [KEY_STATE, KEY_BRIGHTNESS, KEY_COLOR_TEMP, KEY_HEATING_BOOST, KEY_HEATING_SETPOINT] RX_IGNORE_TOPICS = [KEY_FEEDBACK + '/' + TX_TOPIC, KEY_ENABLE + '/' + TX_TOPIC] RX_FILTER_DATA_KEYS = [] @@ -516,6 +518,16 @@ class nodered_gui(base): """rv: [0, ..., 100]""" return self.get(self.KEY_COLOR_TEMP) + @property + def heating_boost(self): + """rv: [True, False]""" + return self.get(self.KEY_HEATING_BOOST) + + @property + def heating_(self): + """rv: [5, ..., 30]""" + return self.get(self.KEY_HEATING_SETPOINT) + # # TX # @@ -525,3 +537,61 @@ class nodered_gui(base): def enable(self, data): """data: [True, False]""" self.pack(self.KEY_ENABLE, data) + + +class brennenstuhl_heatingvalve(base): + KEY_LINKQUALITY = "linkquality" + KEY_BATTERY = "battery" + KEY_HEATING_SETPOINT = "current_heating_setpoint" + KEY_TEMPERATURE = "local_temperature" + # + KEY_AWAY_MODE = "away_mode" + KEY_CHILD_LOCK = "child_lock" + KEY_PRESET = "preset" + KEY_SYSTEM_MODE = "system_mode" + KEY_VALVE_DETECTION = "valve_detection" + KEY_WINDOW_DETECTION = "window_detection" + # + TX_TOPIC = 'set' + TX_VALUE = 0 + TX_DICT = 1 + TX_TYPE = base.TX_DICT + TX_FILTER_DATA_KEYS = [] + # + RX_KEYS = [KEY_LINKQUALITY, KEY_BATTERY, KEY_HEATING_SETPOINT, KEY_TEMPERATURE] + RX_IGNORE_TOPICS = [TX_TOPIC] + RX_IGNORE_KEYS = [KEY_AWAY_MODE, KEY_CHILD_LOCK, KEY_PRESET, + KEY_SYSTEM_MODE, KEY_VALVE_DETECTION, KEY_WINDOW_DETECTION] + RX_FILTER_DATA_KEYS = [] + + def __init__(self, mqtt_client, topic): + 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"})) + + def warning_call_condition(self): + return 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)) + + # + # RX + # + @property + def linkqulity(self): + return self.get(self.KEY_LINKQUALITY) + + @property + def heating_setpoint(self): + return self.get(self.KEY_HEATING_SETPOINT) + + @property + def temperature(self): + return self.get(self.KEY_TEMPERATURE) + + # + # TX + # + def set_heating_setpoint(self, setpoint): + self.pack(self.KEY_HEATING_SETPOINT, setpoint) diff --git a/function/__init__.py b/function/__init__.py index 6ce1a72..1dcf698 100644 --- a/function/__init__.py +++ b/function/__init__.py @@ -6,8 +6,8 @@ from function.ground_floor_west import ground_floor_west_floor, ground_floor_wes from function.first_floor_west import first_floor_west_julian, first_floor_west_living from function.first_floor_east import first_floor_east_floor, first_floor_east_kitchen, first_floor_east_dining, first_floor_east_sleep_madi, first_floor_east_living import inspect +from function import modules -# TODO: implement heating function sleep_madi # TODO: implement circulation pump # TODO: implement switch off functionality (except of switch off button transportation) # TODO: implement garland (incl. day events like sunset, sunrise, ...) @@ -40,6 +40,7 @@ class all_functions(object): # additional functionality # self.init_input_device_sleep_madi_functionality() + self.init_heating_functionality() def init_input_device_sleep_madi_functionality(self): # @@ -57,6 +58,10 @@ class all_functions(object): self.ffe_button_tradfri_sleep.add_callback(devices.tradfri_button.KEY_ACTION, None, self.ffe_sleep_madi.fade_light) + def init_heating_functionality(self): + self.ffe_heating_sleep_madi = modules.heating_function_brennenstuhl( + self.mqtt_client, "zigbee_og_e/radiator/sleep_madi", 20, "gui/ffe_bo_sleep_madi", "gui/ffe_ts_sleep_madi", "gui/ffe_bl_sleep_madi") + def devicelist(self): if self.__devices__ is None: self.__devices__ = [] diff --git a/function/modules.py b/function/modules.py new file mode 100644 index 0000000..57f1db2 --- /dev/null +++ b/function/modules.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# + +import devices +import logging +import task + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' +logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) + + +class heating_function_brennenstuhl(object): + RETURN_TO_DEFAULT_TIME = 45 * 60 + BOOST_TEMP_OFFSET = 5 + + def __init__(self, mqtt_client, topic_valve, default_temperature, topic_boost, topic_setpoint, topic_led): + self.ct = task.periodic(1, self.cyclic_task) + # + self.topic = topic_valve + self.default_temperature = default_temperature + # + self.heating_valve = devices.brennenstuhl_heatingvalve(mqtt_client, topic_valve) + self.heating_valve.set_heating_setpoint(self.default_temperature) + self.heating_valve.add_callback( + devices.brennenstuhl_heatingvalve.KEY_HEATING_SETPOINT, None, self.heating_setpoint_actions) + + self.gui_value_temp_setp = devices.nodered_gui(mqtt_client, topic_setpoint) + self.gui_value_temp_setp.add_callback( + devices.nodered_gui.KEY_HEATING_SETPOINT, None, self.heating_setpoint_actions) + + self.gui_button_boost = devices.nodered_gui(mqtt_client, topic_boost) + self.gui_button_boost.add_callback(None, None, self.boost_actions) + + self.gui_led_boost = devices.nodered_gui(mqtt_client, topic_led) + + # + self.return_to_default_timer = None + self.return_to_default_setpoint = None + self.gui_led_boost.set_feedback(False) + # + self.ct.run() + + def heating_setpoint_actions(self, device, key, data): + if device.topic == self.heating_valve.topic: + # valve setpoint action + self.gui_value_temp_setp.set_feedback(data) + if data > self.default_temperature: + if data != self.return_to_default_setpoint: + logger.info('Got heating setpoint (%.1f°C) \"%s\" with deviation to the default value (%.1f°C). Starting timer for returning to default.', + data, self.topic, self.default_temperature) + self.return_to_default_timer = self.RETURN_TO_DEFAULT_TIME + self.return_to_default_setpoint = data + self.gui_led_boost.set_feedback(True) + else: + if self.return_to_default_timer is not None: + logger.info('Deleting timer \"%s\" for returning to default.', self.topic) + self.return_to_default_timer = None + self.return_to_default_setpoint = None + self.gui_led_boost.set_feedback(False) + elif device.topic == self.gui_value_temp_setp.topic: + # user setpoint action + logger.info('Setpoint change \"%s\" to %.1f°C', self.topic, data) + self.default_temperature = data + self.heating_valve.set_heating_setpoint(self.default_temperature) + self.return_to_default_timer = None + self.return_to_default_setpoint = None + self.gui_led_boost.set_feedback(False) + + def boost_actions(self, davice, key, data): + logger.info('Starting boost mode \"%s\" with setpoint %.1f°C.', + self.topic, self.default_temperature + self.BOOST_TEMP_OFFSET) + self.heating_valve.set_heating_setpoint(self.default_temperature + self.BOOST_TEMP_OFFSET) + + def cyclic_task(self, rt): + if self.return_to_default_timer is not None: + self.return_to_default_timer -= self.ct.cycle_time + if self.return_to_default_timer <= 0: + logger.info('Return to default timer expired \"%s\".', self.topic) + self.heating_valve.set_heating_setpoint(self.default_temperature) + self.return_to_default_timer = None + self.return_to_default_setpoint = None + self.gui_led_boost.set_feedback(False) diff --git a/function/rooms.py b/function/rooms.py index c979cd9..8d0beab 100644 --- a/function/rooms.py +++ b/function/rooms.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- # -import config import devices import logging diff --git a/mqtt b/mqtt index 1921bc6..79ac04f 160000 --- a/mqtt +++ b/mqtt @@ -1 +1 @@ -Subproject commit 1921bc619a9c4af682a7707d6fe58069478c59cd +Subproject commit 79ac04ffdb61ea61334b2bb90bee565472957657 diff --git a/smart_brain.py b/smart_brain.py index 9bda580..6b2a062 100644 --- a/smart_brain.py +++ b/smart_brain.py @@ -10,7 +10,8 @@ logger = logging.getLogger(config.APP_NAME) if __name__ == "__main__": if config.DEBUG: - report.stdoutLoggingConfigure(([config.APP_NAME, logging.DEBUG], ), report.LONG_FMT) + report.appLoggingConfigure(None, None, ((config.APP_NAME, logging.DEBUG), ), + fmt=report.SHORT_FMT, host='localhost', port=19996) else: report.stdoutLoggingConfigure(((config.APP_NAME, logging.INFO), (config.APP_NAME+'.devices', logging.WARNING)), report.SHORT_FMT) diff --git a/task b/task new file mode 160000 index 0000000..af35e83 --- /dev/null +++ b/task @@ -0,0 +1 @@ +Subproject commit af35e83d1f07fd4cb9070bdb77cf1f3bdda3a463