From 045e270a6b1c0f8b4b4ceea5e2272828d57e0be1 Mon Sep 17 00:00:00 2001 From: Dirk Alders Date: Tue, 10 Jan 2023 23:17:47 +0100 Subject: [PATCH] Radiator Functionality improved --- __simulation__/devices.py | 183 ++++++++++++++++++---------------- devices/__init__.py | 30 +++++- function/ground_floor_west.py | 7 +- function/modules.py | 69 +++++++++---- 4 files changed, 178 insertions(+), 111 deletions(-) diff --git a/__simulation__/devices.py b/__simulation__/devices.py index 5c9b261..5795245 100644 --- a/__simulation__/devices.py +++ b/__simulation__/devices.py @@ -22,21 +22,6 @@ def payload_filter(payload): return payload.decode("utf-8") -def percent_bar(value): - rv = "" - for i in range(0, 10): - rv += u"\u25ac" if (value - 5) > 10*i else u"\u25ad" - return rv - - -def green_led(): - return colored.fg('green') + "\u2b24" + colored.attr("reset") - - -def grey_led(): - return colored.fg('light_gray') + "\u2b24" + colored.attr("reset") - - def command_int_value(value): try: return int(value) @@ -51,6 +36,39 @@ def command_float_value(value): print("You need to give a integer parameter not '%s'" % str(value)) +def devicename(topic): + return " - ".join(topic.split('/')[1:]) + + +def percent_bar(value): + rv = "" + for i in range(0, 10): + rv += u"\u25ac" if (value - 5) > 10*i else u"\u25ad" + return rv + + +def print_light(color, state, topic, description, led=False): + if led is True: + if state is True: + icon = colored.fg('green') + "\u2b24" + color + else: + icon = colored.fg('light_gray') + "\u2b24" + color + else: + icon = u'\u2b24' if state is True else u'\u25ef' + print(color + 10 * ' ' + icon + 9 * ' ' + devicename(topic), description + colored.attr("reset")) + + +def print_switch(color, state, topic, description): + icon = u'\u25a0' if state is True else u'\u25a1' + print(color + 10 * ' ' + icon + 9 * ' ' + devicename(topic), description + colored.attr("reset")) + + +def print_percent(color, prefix, perc_value, value_str, topic, description): + if len(prefix) > 1 or len(value_str) > 7: + raise ValueError("Length of prefix (%d) > 1 or length of value_str (%d) > 7" % (len(prefix), len(value_str))) + print(color + prefix + percent_bar(perc_value), value_str + (8 - len(value_str)) * ' ' + devicename(topic), description + colored.attr("reset")) + + class base(object): AUTOSEND = True COMMANDS = [] @@ -226,11 +244,9 @@ class shelly(base): def print_formatted(self, device, key, value): if value is not None: - icon = u'\u2b24' if value == "on" else u'\u25ef' info = (" - %ds" % self.__output_0_auto_off__) if self.__output_0_auto_off__ is not None and value == "on" else "" channel = "(%s%s)" % (self.names.get(key, key), info) - devicename = " - ".join(self.topic.split('/')[1:]) - print(COLOR_SHELLY + 10 * ' ' + icon + 9 * ' ' + devicename + ' ' + channel + colored.attr("reset")) + print_light(COLOR_SHELLY, value == "on", self.topic, channel) class my_powerplug(base): @@ -293,10 +309,7 @@ class my_powerplug(base): def print_formatted(self, device, key, value): if value is not None: - icon = u'\u2b24' if value else u'\u25ef' - channel = "(%s)" % self.names.get(key, "Channel %d" % (int(key) + 1)) - devicename = " - ".join(self.topic.split('/')[1:]) - print(COLOR_POWERPLUG + 10 * ' ' + icon + 9 * ' ' + devicename + ' ' + channel + colored.attr("reset")) + print_light(COLOR_POWERPLUG, value, self.topic, "(%s)" % self.names.get(key, "Channel %d" % (int(key) + 1))) class silvercrest_powerplug(base): @@ -340,10 +353,7 @@ class silvercrest_powerplug(base): def print_formatted(self, device, key, value): if value is not None: - icon = u'\u2b24' if value == "on" else u'\u25ef' - channel = "(%s)" % self.names.get(key, key) - devicename = " - ".join(self.topic.split('/')[1:]) - print(COLOR_POWERPLUG + 10 * ' ' + icon + 9 * ' ' + devicename + ' ' + channel + colored.attr("reset")) + print_light(COLOR_POWERPLUG, value == "on", self.topic, "(%s)" % self.names.get(key, key)) class silvercrest_motion_sensor(base): @@ -372,10 +382,7 @@ class silvercrest_motion_sensor(base): def print_formatted(self, device, key, value): if value is not None: - if value: - print(COLOR_MOTION_SENSOR + 10 * ' ' + u'\u2b24' + 9 * ' ' + " - ".join(self.topic.split('/')[1:]) + colored.attr("reset")) - else: - print(COLOR_MOTION_SENSOR + 10 * ' ' + u'\u25ef' + 9 * ' ' + " - ".join(self.topic.split('/')[1:]) + colored.attr("reset")) + print_light(COLOR_MOTION_SENSOR, value, self.topic, "") class tradfri_light(base): @@ -480,24 +487,19 @@ class tradfri_light(base): if value is not None: color = COLOR_LIGHT_ACTIVE if key == self.KEY_STATE: - if value == "on": - print(color + 10 * ' ' + u'\u2b24' + 9 * ' ' + " - ".join(self.topic.split('/')[1:]) + colored.attr("reset")) - else: - print(color + 10 * ' ' + u'\u25ef' + 9 * ' ' + " - ".join(self.topic.split('/')[1:]) + colored.attr("reset")) + print_light(COLOR_LIGHT_ACTIVE, value == "on", self.topic, "") self.print_formatted(device, self.KEY_BRIGHTNESS, self.data.get(self.KEY_BRIGHTNESS)) self.print_formatted(device, self.KEY_COLOR_TEMP, self.data.get(self.KEY_COLOR_TEMP)) elif key in [self.KEY_BRIGHTNESS, self.KEY_COLOR_TEMP]: - if self.data.get(self.KEY_STATE) != "on": - color = COLOR_LIGHT_PASSIVE - if key == self.KEY_BRIGHTNESS: - value = round(value * 100 / 256, 0) - else: - value = round((value - 250) * 100 / 204, 0) - sys.stdout.write(color) - sys.stdout.write('B' if key == gui_light.KEY_BRIGHTNESS else 'C') - sys.stdout.write(percent_bar(value)) - sys.stdout.write("%3d%%" % value + 5 * " ") - print(" - ".join(self.topic.split('/')[1:]) + colored.attr("reset")) + perc_value = round(value * 100 / 256, 0) if key == self.KEY_BRIGHTNESS else round((value - 250) * 100 / 204, 0) + print_percent( + COLOR_LIGHT_PASSIVE if self.data.get(self.KEY_STATE) != "on" else COLOR_LIGHT_ACTIVE, + 'B' if key == gui_light.KEY_BRIGHTNESS else 'C', + perc_value, + "%3d%%" % perc_value, + self.topic, + "" + ) class gui_light(tradfri_light): @@ -576,24 +578,22 @@ class gui_light(tradfri_light): def print_formatted(self, device, key, value): if value is not None: - color = COLOR_GUI_ACTIVE device = " - ".join(self.topic.split('/')[1:]) if key == self.KEY_STATE: - if value == True: - print(color + 10 * ' ' + u'\u25a0' + 9 * ' ' + device + colored.attr("reset")) - else: - print(color + 10 * ' ' + u'\u25a1' + 9 * ' ' + device + colored.attr("reset")) + print_switch(COLOR_GUI_ACTIVE, value, self.topic, "") elif key == self.KEY_ENABLE: self.print_formatted(device, self.KEY_BRIGHTNESS, self.data.get(self.KEY_BRIGHTNESS)) self.print_formatted(device, self.KEY_COLOR_TEMP, self.data.get(self.KEY_COLOR_TEMP)) elif key in [self.KEY_BRIGHTNESS, self.KEY_COLOR_TEMP]: - if not self.data.get(self.KEY_ENABLE, False): - color = COLOR_GUI_PASSIVE - value = round(value * 10 if key == self.KEY_COLOR_TEMP else value, 0) - sys.stdout.write(color) - sys.stdout.write('B' if key == self.KEY_BRIGHTNESS else 'C') - sys.stdout.write(percent_bar(value)) - print("%3d%%" % value + 5 * " " + device + colored.attr("reset")) + perc_value = round(value * 10 if key == self.KEY_COLOR_TEMP else value, 0) + print_percent( + COLOR_GUI_PASSIVE if not self.data.get(self.KEY_ENABLE, False) else COLOR_GUI_ACTIVE, + 'B' if key == self.KEY_BRIGHTNESS else 'C', + perc_value, + "%3d%%" % perc_value, + self.topic, + "" + ) elif key == self.KEY_TIMER: if isinstance(value, (int, float, complex)) and not isinstance(value, bool): if self.maxvalue is None: @@ -606,13 +606,10 @@ class gui_light(tradfri_light): self.maxvalue = None self.last_printed = None if self.last_printed is None or abs(self.last_printed - perc) >= 4.95: - print(color + 't' + percent_bar(perc) + '%3d%%' % perc + 5 * ' ' + device + ' (%.1f)' % disp_value + colored.attr('reset')) + print_percent(COLOR_GUI_ACTIVE, 't', perc, '%3d%%' % perc, self.topic, '(%.1f)' % disp_value) self.last_printed = perc elif key.startswith(self.KEY_LED_X[:-2]): - led = green_led() if value else grey_led() - ledname = '(%s)' % self.led_names.get(key, key) - devicename = ' - '.join(self.topic.split('/')[1:]) - print(10 * ' ' + led + 9 * ' ' + COLOR_GUI_ACTIVE + devicename + ' ' + ledname + colored.attr("reset")) + print_light(COLOR_GUI_ACTIVE, value, self.topic, '(%s)' % self.led_names.get(key, key), True) class tradfri_button(base): @@ -686,10 +683,7 @@ class gui_led_array(base): print("Unknown key %s in %s" % (targetkey, self.__class__.__name__)) def print_formatted(self, device, key, value): - led = green_led() if value else grey_led() - channel = '(%s)' % self.names.get(key, key) - devicename = ' - '.join(self.topic.split('/')[1:]) - print(10 * ' ' + led + 9 * ' ' + COLOR_GUI_ACTIVE + devicename + ' ' + channel + colored.attr("reset")) + print_light(COLOR_GUI_ACTIVE, value, self.topic, '(%s)' % self.names.get(key, key), True) class remote(base): @@ -750,9 +744,7 @@ class brennenstuhl_radiator_valve(base): perc = 100 * (value - self.TEMP_RANGE[0]) / (self.TEMP_RANGE[1] - self.TEMP_RANGE[0]) perc = 100 if perc > 100 else perc perc = 0 if perc < 0 else perc - sys.stdout.write(COLOR_RADIATOR_VALVE + '\u03d1' + percent_bar(perc)) - sys.stdout.write("%4.1f°C" % value + 3 * " ") - print(devicename + colored.attr("reset")) + print_percent(COLOR_RADIATOR_VALVE, '\u03d1', perc, "%4.1f°C" % value, self.topic, "") class gui_radiator_valve(base): @@ -765,22 +757,31 @@ class gui_radiator_valve(base): KEY_SETPOINT_TEMP = "setpoint_temp" KEY_SETPOINT_TO_DEFAULT = "setpoint_to_default" KEY_BOOST = 'boost' + KEY_AWAY = "away" + KEY_SUMMER = "summer" + KEY_ENABLE = "enable" # COMMANDS = [ "get_temperature", "get_temperature_setpoint", "set_temperature_setpoint", - "trigger_boost", "trigger_setpoint_to_default" + "trigger_boost", "trigger_setpoint_to_default", + "toggle_away", "toggle_summer", ] def __init__(self, mqtt_client, topic): super().__init__(mqtt_client, topic) self.add_callback(self.KEY_SETPOINT_TEMP, self.print_formatted, None) self.add_callback(self.KEY_TIMER, self.print_formatted, None) + self.add_callback(self.KEY_AWAY, self.print_formatted, None) + self.add_callback(self.KEY_SUMMER, self.print_formatted, None) # self.store_data(**{ self.KEY_TEMPERATURE: 20.7, self.KEY_SETPOINT_TEMP: 20, self.KEY_TIMER: 0, + self.KEY_AWAY: False, + self.KEY_SUMMER: False, + self.KEY_ENABLE: True }) def __rx__(self, client, userdata, message): @@ -815,26 +816,32 @@ class gui_radiator_valve(base): self.send(self.KEY_BOOST, True) elif command == self.COMMANDS[4]: self.send(self.KEY_SETPOINT_TO_DEFAULT, True) + elif command == self.COMMANDS[5]: + self.send(self.KEY_AWAY, not self.data.get(self.KEY_AWAY)) + elif command == self.COMMANDS[6]: + self.send(self.KEY_SUMMER, not self.data.get(self.KEY_SUMMER)) def print_formatted(self, device, key, value): devicename = ' - '.join(self.topic.split('/')[1:]) - if key in [self.KEY_TEMPERATURE, self.KEY_SETPOINT_TEMP, self.KEY_TIMER]: - if key in [self.KEY_TEMPERATURE, self.KEY_SETPOINT_TEMP]: - perc = 100 * (value - self.TEMP_RANGE[0]) / (self.TEMP_RANGE[1] - self.TEMP_RANGE[0]) - info = " (Temperature)" if key == self.KEY_TEMPERATURE else " (Setpoint)" - else: - try: - perc = 100 * value / 60 - except TypeError: - value = 0 - perc = 0 - info = " (Timer)" + if key == self.KEY_TIMER: + try: + perc = 100 * value / 60 + except TypeError: + value = 0 + perc = 0 + print_percent(COLOR_GUI_ACTIVE, 'T', perc, "%4.1fmin" % value, self.topic, "(Timer)") + elif key == self.KEY_TEMPERATURE: + perc = 100 * (value - self.TEMP_RANGE[0]) / (self.TEMP_RANGE[1] - self.TEMP_RANGE[0]) perc = 100 if perc > 100 else perc perc = 0 if perc < 0 else perc - - sys.stdout.write(COLOR_GUI_ACTIVE) - if key == self.KEY_TIMER: - sys.stdout.write('T' + percent_bar(perc) + "%4.1fmin" % value + 2 * " ") - else: - sys.stdout.write('\u03d1' + percent_bar(perc) + "%4.1f°C" % value + 3 * " ") - print(devicename + info + colored.attr("reset")) + print_percent(COLOR_GUI_ACTIVE, '\u03d1', perc, "%4.1f°C" % value, self.topic, "(Temperature)") + elif key == self.KEY_SETPOINT_TEMP: + perc = 100 * (value - self.TEMP_RANGE[0]) / (self.TEMP_RANGE[1] - self.TEMP_RANGE[0]) + perc = 100 if perc > 100 else perc + perc = 0 if perc < 0 else perc + print_percent(COLOR_GUI_ACTIVE if self.data.get(self.KEY_ENABLE) else COLOR_GUI_PASSIVE, + '\u03d1', perc, "%4.1f°C" % value, self.topic, "(Setpoint)") + elif key == self.KEY_AWAY: + print_switch(COLOR_GUI_ACTIVE, value, self.topic, "(Away Mode)") + elif key == self.KEY_SUMMER: + print_switch(COLOR_GUI_ACTIVE, value, self.topic, "(Summer Mode)") diff --git a/devices/__init__.py b/devices/__init__.py index 9c4fc5c..42d08e4 100644 --- a/devices/__init__.py +++ b/devices/__init__.py @@ -738,8 +738,20 @@ class nodered_gui_radiator(nodered_gui_timer): KEY_SETPOINT_TEMP = "setpoint_temp" KEY_SETPOINT_TO_DEFAULT = "setpoint_to_default" KEY_BOOST = 'boost' + KEY_AWAY = "away" + KEY_SUMMER = "summer" + KEY_ENABLE = "enable" # - RX_KEYS = [KEY_TEMPERATURE, KEY_SETPOINT_TEMP, KEY_SETPOINT_TO_DEFAULT, KEY_BOOST] + RX_KEYS = [KEY_TEMPERATURE, KEY_SETPOINT_TEMP, KEY_SETPOINT_TO_DEFAULT, KEY_BOOST, KEY_AWAY, KEY_SUMMER] + + def __init__(self, mqtt_client, topic): + super().__init__(mqtt_client, topic) + self[self.KEY_ENABLE] = True + self.add_callback(self.KEY_AWAY, None, self.enable_gui) + self.add_callback(self.KEY_SUMMER, None, self.enable_gui) + + def enable_gui(self, device, key, data): + self.pack(self.KEY_ENABLE, not self.get(self.KEY_AWAY) and not self.get(self.KEY_SUMMER)) # # TX @@ -760,6 +772,22 @@ class nodered_gui_radiator(nodered_gui_timer): self.logger.debug("Sending %s with content %s", key, str(data)) self.set_setpoint_temperature(data) + def set_away(self, data): + """data: [True, False]""" + self.pack(self.KEY_AWAY, data) + + def set_away_mcb(self, device, key, data): + self.logger.debug("Sending %s with content %s", key, str(data)) + self.set_away(data) + + def set_summer(self, data): + """data: [True, False]""" + self.pack(self.KEY_SUMMER, data) + + def set_summer_mcb(self, device, key, data): + self.logger.debug("Sending %s with content %s", key, str(data)) + self.set_summer(data) + class brennenstuhl_heatingvalve(base): KEY_LINKQUALITY = "linkquality" diff --git a/function/ground_floor_west.py b/function/ground_floor_west.py index 23752e9..c8b2c7b 100644 --- a/function/ground_floor_west.py +++ b/function/ground_floor_west.py @@ -31,15 +31,14 @@ class ground_floor_west_floor(room_shelly_silvercrest_light): super().send_init_message_main_light() self.main_light_tradfri_2.mqtt_client.send(self.main_light_tradfri_2.topic + "/get", '{"state": ""}') - # radiator valve - self.radiator_function = radiator_function(mqtt_client, config.TOPIC_GFW_MARION_RADIATOR_VALVE_ZIGBEE, - config.TOPIC_GFW_MARION_RADIATOR_VALVE_GUI, config.DEFAULT_TEMPERATURE_GFW_MARION) - class ground_floor_west_marion(room_shelly): # http://shelly1l-E8DB84A1E067 def __init__(self, mqtt_client): super().__init__(mqtt_client, config.TOPIC_GFW_MARION_MAIN_LIGHT_SHELLY, config.TOPIC_GFW_MARION_MAIN_LIGHT_GUI) + # radiator valve + self.radiator_function = radiator_function(mqtt_client, config.TOPIC_GFW_MARION_RADIATOR_VALVE_ZIGBEE, + config.TOPIC_GFW_MARION_RADIATOR_VALVE_GUI, config.DEFAULT_TEMPERATURE_GFW_MARION) class ground_floor_west_dirk(room_shelly_tradfri_light): diff --git a/function/modules.py b/function/modules.py index 2df2c98..0cac65f 100644 --- a/function/modules.py +++ b/function/modules.py @@ -133,14 +133,16 @@ class circulation_pump(room_shelly): self.pump_timer -= self.ct.cycle_time / 60 -# TODO: -# - improve devices.brennenstuhl_heatingvalve (at least switch on for boost) and use boost in heating_function.boost_mcb -# - implement modules.heating_function -# * Central switch incl. central parameter storage for state (summer mode, away_mode - each switch activation disables the other switch) class radiator_function(object): + BOOST_TEMPERATURE = 30 + AWAY_REDUCTION = 5 + SUMMER_TEMPERATURE = 5 + def __init__(self, mqtt_client, topic_valve, topic_gui, default_temperature): self.default_temperature = default_temperature self.regular_temp_setpoint = self.default_temperature + self.__away_mode__ = False + self.__summer_mode__ = False # self.ct = task.periodic(1, self.cyclic_task) # @@ -149,14 +151,13 @@ class radiator_function(object): # self.heating_valve.set_heating_setpoint(self.default_temperature) self.heating_valve.add_callback(devices.brennenstuhl_heatingvalve.KEY_TEMPERATURE, None, self.gui_heating.set_temperature_mcb) - self.heating_valve.add_callback(devices.brennenstuhl_heatingvalve.KEY_HEATING_SETPOINT, None, self.gui_heating.set_setpoint_temperature_mcb) - self.heating_valve.add_callback(devices.brennenstuhl_heatingvalve.KEY_HEATING_SETPOINT, None, self.store_regular_setpoint) + self.heating_valve.add_callback(devices.brennenstuhl_heatingvalve.KEY_HEATING_SETPOINT, None, self.get_radiator_setpoint) # - self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SETPOINT_TEMP, None, self.heating_valve.set_heating_setpoint_mcb) - self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_BOOST, None, self.boost_mcb) - self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SETPOINT_TO_DEFAULT, None, self.setpoint_to_default_mcb) - self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SETPOINT_TEMP, None, self.cancel_boost) - self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SETPOINT_TO_DEFAULT, None, self.cancel_boost) + self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SETPOINT_TEMP, None, self.set_heating_setpoint) + self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_BOOST, None, self.boost) + self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SETPOINT_TO_DEFAULT, None, self.setpoint_to_default) + self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_AWAY, None, self.away_mode) + self.gui_heating.add_callback(devices.nodered_gui_radiator.KEY_SUMMER, None, self.summer_mode) # self.boost_timer = None # @@ -177,19 +178,51 @@ class radiator_function(object): self.boost_timer = None self.gui_heating.set_timer('-') - def store_regular_setpoint(self, device, key, data): - if self.boost_timer is None: - self.regular_temp_setpoint = data + def away_mode(self, device, key, value): + self.__away_mode__ = value + self.gui_heating.set_away(value) + if value is True: + self.__summer_mode__ = False + self.gui_heating.set_summer(False) + self.cancel_boost() + self.heating_valve.set_heating_setpoint(self.default_temperature - self.AWAY_REDUCTION) + else: + self.heating_valve.set_heating_setpoint(self.default_temperature) - def boost_mcb(self, device, key, data): + def summer_mode(self, device, key, value): + self.__summer_mode__ = value + self.gui_heating.set_summer(value) + if value is True: + self.__away_mode__ = False + self.gui_heating.set_away(False) + self.cancel_boost() + self.heating_valve.set_heating_setpoint(self.SUMMER_TEMPERATURE) + else: + self.heating_valve.set_heating_setpoint(self.default_temperature) + + def boost(self, device, key, data): + self.cancel_boost() if self.boost_timer is None: - self.heating_valve.logger.info('Starting boost mode with setpoint %.1f°C.', self.regular_temp_setpoint + 5) + self.heating_valve.logger.info('Starting boost mode with setpoint %.1f°C.', self.BOOST_TEMPERATURE) self.boost_timer = 15*60 - self.heating_valve.set_heating_setpoint(self.regular_temp_setpoint + 5) # TODO: Change setpoint to "on" if possible + self.heating_valve.set_heating_setpoint(self.BOOST_TEMPERATURE) else: self.boost_timer += 15 * 60 if self.boost_timer > 60 * 60: self.boost_timer = 60 * 60 - def setpoint_to_default_mcb(self, device, key, data): + def setpoint_to_default(self, device, key, data): self.heating_valve.set_heating_setpoint(self.default_temperature) + + def set_heating_setpoint(self, device, key, data): + self.cancel_boost() + self.heating_valve.set_heating_setpoint(data) + + def get_radiator_setpoint(self, device, key, data): + self.gui_heating.set_setpoint_temperature(data) + if self.__away_mode__: + self.away_mode(device, self.gui_heating.KEY_AWAY, True) + if self.__summer_mode__: + self.summer_mode(device, self.gui_heating.KEY_SUMMER, True) + if self.boost_timer is None and not self.__away_mode__ and not self.__summer_mode__: + self.regular_temp_setpoint = data