From 9233468a49bce8b4670a3c65823a1581af8184a5 Mon Sep 17 00:00:00 2001 From: Dirk Alders Date: Fri, 20 Jan 2023 08:03:06 +0100 Subject: [PATCH] videv implementation for lights --- base.py | 45 +++++++++++ devices/__init__.py | 5 ++ function/first_floor_east.py | 58 ++++++++++++++ function/first_floor_west.py | 23 ++++++ function/ground_floor_west.py | 40 +++++++++- function/modules.py | 17 ++++ function/rooms.py | 2 +- function/stairway.py | 8 ++ function/videv.py | 146 ++++++++++++++++++++++++++++++++++ 9 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 base.py create mode 100644 function/videv.py diff --git a/base.py b/base.py new file mode 100644 index 0000000..1798489 --- /dev/null +++ b/base.py @@ -0,0 +1,45 @@ +import logging + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' + + +class common_base(dict): + DEFAULT_VALUES = {} + + def __init__(self): + super().__init__(self.DEFAULT_VALUES) + self['__type__'] = self.__class__.__name__ + # + self.__callback_list__ = [] + self.logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) + + def add_callback(self, key, data, callback, on_change_only=True): + """ + 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 set(self, key, data): + 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 + callback(self, key, self[key]) + + +class mqtt_base(common_base): + def __init__(self, mqtt_client, topic): + super().__init__() + # + self.mqtt_client = mqtt_client + self.topic = topic + for entry in self.topic.split('/'): + self.logger = self.logger.getChild(entry) diff --git a/devices/__init__.py b/devices/__init__.py index 5411f3d..b505d43 100644 --- a/devices/__init__.py +++ b/devices/__init__.py @@ -26,6 +26,8 @@ devices (DEVICES) """ +# TODO: Usage of mqtt_base for all devices + __DEPENDENCIES__ = [] import json @@ -506,6 +508,9 @@ class tradfri_light(base): else: return super().pack_filter(key, data) + def request_data(self): + self.mqtt_client.send(self.topic + "/get", '{%s: ""}' % self.KEY_OUTPUT_0) + # # RX # diff --git a/function/first_floor_east.py b/function/first_floor_east.py index 5261749..09f4aa6 100644 --- a/function/first_floor_east.py +++ b/function/first_floor_east.py @@ -7,6 +7,7 @@ import devices from function.modules import brightness_choose_n_action, circulation_pump, radiator_function import logging from function.rooms import room_shelly, room_shelly_motion_sensor, room_shelly_tradfri_light +from function.videv import videv_switching, videv_switch_brightness, videv_switch_brightness_color_temp try: from config import APP_NAME as ROOT_LOGGER_NAME except ImportError: @@ -18,6 +19,11 @@ class first_floor_east_floor(room_shelly): def __init__(self, mqtt_client): # http://shelly1l-3C6105E4E629 super().__init__(mqtt_client, config.TOPIC_FFE_FLOOR_MAIN_LIGHT_SHELLY, config.TOPIC_FFE_FLOOR_MAIN_LIGHT_GUI) + # + self.main_light = videv_switching( + mqtt_client, config.TOPIC_FFE_FLOOR_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0 + ) class first_floor_east_kitchen(room_shelly): @@ -27,6 +33,15 @@ class first_floor_east_kitchen(room_shelly): # self.circulation_pump = circulation_pump(mqtt_client) self.circulation_pump.main_light_shelly.add_callback(devices.shelly.KEY_OUTPUT_0, None, self.flash_main_light) + # + self.main_light_videv = videv_switching( + mqtt_client, config.TOPIC_FFE_KITCHEN_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0 + ) + self.circulation_pump_videv = videv_switching( + mqtt_client, config.TOPIC_FFE_KITCHEN_CIRCULATION_PUMP_VIDEV, + self.circulation_pump.main_light_shelly, devices.shelly.KEY_OUTPUT_0 + ) def all_off(self, device=None, key=None, data=None): self.circulation_pump.all_off(device, key, data) @@ -48,6 +63,15 @@ class first_floor_east_dining(room_shelly): self.main_light_shelly.add_callback(devices.shelly.KEY_OUTPUT_0, None, self.floorlamp_powerplug.set_output_0_mcb, True) self.gui_floorlamp.add_callback(devices.nodered_gui_switch.KEY_STATE, None, self.floorlamp_powerplug.set_output_0_mcb) self.floorlamp_powerplug.add_callback(devices.silvercrest_powerplug.KEY_OUTPUT_0, None, self.gui_floorlamp.set_state_mcb) + # + self.main_light_videv = videv_switching( + mqtt_client, config.TOPIC_FFE_DININGROOM_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0 + ) + self.floorlamp_videv = videv_switching( + mqtt_client, config.TOPIC_FFE_DININGROOM_FLOOR_LAMP_VIDEV, + self.floorlamp_powerplug, devices.silvercrest_powerplug.KEY_OUTPUT_0 + ) def all_off(self, device=None, key=None, data=None): super().all_off(device, key, data) @@ -96,6 +120,22 @@ class first_floor_east_sleep(room_shelly_tradfri_light): self.gui_bed_light_di.add_callback(devices.nodered_gui_light.KEY_BRIGHTNESS, None, self.bed_light_di_tradfri.set_brightness_mcb) self.bed_light_di_tradfri.add_callback(devices.tradfri_light.KEY_OUTPUT_0, None, self.gui_bed_light_di.set_enable_mcb) self.bed_light_di_tradfri.add_callback(devices.tradfri_light.KEY_BRIGHTNESS, None, self.gui_bed_light_di.set_brightness_mcb) + # + self.main_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_FFE_SLEEP_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.main_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) + self.bed_light_di_videv = videv_switch_brightness( + mqtt_client, config.TOPIC_FFE_SLEEP_BED_LIGHT_DI_VIDEV, + self.bed_light_di_tradfri, devices.tradfri_light.KEY_OUTPUT_0, + self.bed_light_di_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + ) + self.bed_light_ma_videv = videv_switching( + mqtt_client, config.TOPIC_FFE_SLEEP_BED_LIGHT_MA_VIDEV, + self.bed_light_ma_powerplug, devices.silvercrest_powerplug.KEY_OUTPUT_0 + ) def all_off(self, device=None, key=None, data=None): super().all_off(device, key, data) @@ -139,6 +179,24 @@ class first_floor_east_living(room_shelly_tradfri_light): self.gui_xmas_tree.add_callback(devices.nodered_gui_switch.KEY_STATE, None, self.powerplug_xmas_tree.set_output_0_mcb) # self.powerplug_xmas_tree.add_callback(devices.silvercrest_powerplug.KEY_OUTPUT_0, None, self.powerplug_xmas_star.set_output_0_mcb) + # + self.main_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_FFE_LIVINGROOM_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.main_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) + self.floorlamp_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_FFE_LIVINGROOM_FLOOR_LAMP_VIDEV, + self.floorlamp_tradfri_1, devices.tradfri_light.KEY_OUTPUT_0, + self.floorlamp_tradfri_1, devices.tradfri_light.KEY_BRIGHTNESS, + self.floorlamp_tradfri_1, devices.tradfri_light.KEY_COLOR_TEMP + ) + if config.CHRISTMAS: + self.xmas_tree_videv = videv_switching( + mqtt_client, config.TOPIC_FFE_LIVINGROOM_XMAS_TREE_VIDEV, + self.powerplug_xmas_tree, devices.silvercrest_powerplug.KEY_OUTPUT_0 + ) def all_off(self, device=None, key=None, data=None): super().all_off(device, key, data) diff --git a/function/first_floor_west.py b/function/first_floor_west.py index 7135ba8..221363f 100644 --- a/function/first_floor_west.py +++ b/function/first_floor_west.py @@ -3,9 +3,11 @@ # import config +import devices import logging from function.modules import radiator_function from function.rooms import room_shelly, room_shelly_tradfri_light +from function.videv import videv_switch_brightness, videv_switch_brightness_color_temp try: @@ -19,6 +21,13 @@ class first_floor_west_julian(room_shelly_tradfri_light): # http://shelly1l-3C6105E43452 def __init__(self, mqtt_client): super().__init__(mqtt_client, config.TOPIC_FFW_JULIAN_MAIN_LIGHT_SHELLY, config.TOPIC_FFW_JULIAN_MAIN_LIGHT_GUI, config.TOPIC_FFW_JULIAN_MAIN_LIGHT_ZIGBEE) + # + self.main_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_FFW_JULIAN_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.main_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) class first_floor_west_bath(object): @@ -26,6 +35,7 @@ class first_floor_west_bath(object): # radiator valve self.radiator_function = radiator_function(mqtt_client, config.TOPIC_FFW_BATH_RADIATOR_VALVE_ZIGBEE, config.TOPIC_FFW_BATH_RADIATOR_VALVE_GUI, config.DEFAULT_TEMPERATURE_FFW_BATH) + def all_off(self): pass @@ -35,9 +45,22 @@ class first_floor_west_living(room_shelly_tradfri_light): def __init__(self, mqtt_client): super().__init__(mqtt_client, config.TOPIC_FFW_LIVINGROOM_MAIN_LIGHT_SHELLY, config.TOPIC_FFW_LIVINGROOM_MAIN_LIGHT_GUI, config.TOPIC_FFW_LIVINGROOM_MAIN_LIGHT_ZIGBEE) + # + self.main_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_FFW_LIVINGROOM_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.main_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) class first_floor_west_sleep(room_shelly_tradfri_light): # http://shelly1-3494546A51F2 def __init__(self, mqtt_client): super().__init__(mqtt_client, config.TOPIC_FFW_SLEEP_MAIN_LIGHT_SHELLY, config.TOPIC_FFW_SLEEP_MAIN_LIGHT_GUI, config.TOPIC_FFW_SLEEP_MAIN_LIGHT_ZIGBEE) + # + self.main_light_videv = videv_switch_brightness( + mqtt_client, config.TOPIC_FFW_SLEEP_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS + ) diff --git a/function/ground_floor_west.py b/function/ground_floor_west.py index c8b2c7b..f203f6b 100644 --- a/function/ground_floor_west.py +++ b/function/ground_floor_west.py @@ -7,6 +7,7 @@ import devices from function.modules import brightness_choose_n_action, radiator_function import logging from function.rooms import room_shelly, room_shelly_tradfri_light, room_shelly_silvercrest_light +from function.videv import videv_switching, videv_switch_brightness_color_temp import task try: @@ -26,10 +27,17 @@ class ground_floor_west_floor(room_shelly_silvercrest_light): self.main_light_tradfri_2 = devices.tradfri_light(mqtt_client, config.TOPIC_GFW_FLOOR_MAIN_LIGHT_2_ZIGBEE) self.main_light_tradfri.add_callback(devices.tradfri_light.KEY_BRIGHTNESS, None, self.main_light_tradfri_2.set_brightness_mcb) self.main_light_tradfri.add_callback(devices.tradfri_light.KEY_COLOR_TEMP, None, self.main_light_tradfri_2.set_color_temp_mcb) + # + self.main_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_GFW_FLOOR_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.main_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) def send_init_message_main_light(self): super().send_init_message_main_light() - self.main_light_tradfri_2.mqtt_client.send(self.main_light_tradfri_2.topic + "/get", '{"state": ""}') + self.main_light_tradfri_2.request_data() class ground_floor_west_marion(room_shelly): @@ -39,6 +47,11 @@ class ground_floor_west_marion(room_shelly): # 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) + # + self.main_light_videv = videv_switching( + mqtt_client, config.TOPIC_GFW_MARION_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0 + ) class ground_floor_west_dirk(room_shelly_tradfri_light): @@ -136,6 +149,31 @@ class ground_floor_west_dirk(room_shelly_tradfri_light): self.powerplug_common.toggle_output_3_mcb) self.gui_pc_dock.add_callback(devices.nodered_gui_switch.KEY_STATE, None, self.powerplug_common.set_output_3_mcb) self.powerplug_common.add_callback(self.KEY_POWERPLUG_PC_DOCK, None, self.gui_pc_dock.set_state_mcb) + # + self.main_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_GFW_DIRK_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0, + self.main_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.main_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) + self.desk_light_videv = videv_switch_brightness_color_temp( + mqtt_client, config.TOPIC_GFW_DIRK_DESK_LIGHT_VIDEV, + self.powerplug_common, self.KEY_POWERPLUG_DESK_LIGHT, + self.desk_light_tradfri, devices.tradfri_light.KEY_BRIGHTNESS, + self.desk_light_tradfri, devices.tradfri_light.KEY_COLOR_TEMP + ) + self.amplifier_videv = videv_switching( + mqtt_client, config.TOPIC_GFW_DIRK_AMPLIFIER_VIDEV, + self.powerplug_common, self.KEY_POWERPLUG_AMPLIFIER + ) + self.cd_player_videv = videv_switching( + mqtt_client, config.TOPIC_GFW_DIRK_CD_PLAYER_VIDEV, + self.powerplug_common, self.KEY_POWERPLUG_CD_PLAYER + ) + self.pc_dock_videv = videv_switching( + mqtt_client, config.TOPIC_GFW_DIRK_PC_DOCK_VIDEV, + self.powerplug_common, self.KEY_POWERPLUG_PC_DOCK + ) def all_off(self, device=None, key=None, data=None): super().all_off(device, key, data) diff --git a/function/modules.py b/function/modules.py index 8a2ab48..5981c38 100644 --- a/function/modules.py +++ b/function/modules.py @@ -1,6 +1,15 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- # +""" +Functional Modules + +Targets: + * Device like structure to be compatible with videv + - KEY_* as part of the class for all parameters which needs to be accessed from videv + - Method *.set(key, data) to pass data from videv to Module + - Method .add_calback(key, data, callback, on_change_only=False) to register videv actualisation on changes +""" import config import devices @@ -16,6 +25,14 @@ except ImportError: logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__) +class base(object): + def set(key, data): + pass + + def add_calback(self, key, data, callback, on_change_only=False): + pass + + class brightness_choose_n_action(object): def __init__(self, mqtt_client, button_tradfri, topic_led): self.gui_led_active_device = devices.nodered_gui_leds(mqtt_client, topic_led) diff --git a/function/rooms.py b/function/rooms.py index a4ef4ca..a8acfe2 100644 --- a/function/rooms.py +++ b/function/rooms.py @@ -145,4 +145,4 @@ class room_shelly_silvercrest_light(room_shelly_tradfri_light): self.main_light_shelly_last = data def send_init_message_main_light(self): - self.main_light_tradfri.mqtt_client.send(self.main_light_tradfri.topic + "/get", '{"state": ""}') + self.main_light_tradfri.request_data() diff --git a/function/stairway.py b/function/stairway.py index 8de38db..adf2e4d 100644 --- a/function/stairway.py +++ b/function/stairway.py @@ -3,9 +3,12 @@ # import config +import devices from function.modules import brightness_choose_n_action import logging from function.rooms import room_shelly_motion_sensor +from function.videv import videv_switching + try: from config import APP_NAME as ROOT_LOGGER_NAME except ImportError: @@ -19,3 +22,8 @@ class stairway(room_shelly_motion_sensor): super().__init__(mqtt_client, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_SHELLY, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_GUI, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_MOTION_SENSOR_GF, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_MOTION_SENSOR_FF, timer_value=config.USER_ON_TIME_STAIRWAYS) + # + self.main_light_videv = videv_switching( + mqtt_client, config.TOPIC_STW_STAIRWAY_MAIN_LIGHT_VIDEV, + self.main_light_shelly, devices.shelly.KEY_OUTPUT_0 + ) diff --git a/function/videv.py b/function/videv.py new file mode 100644 index 0000000..d131ffc --- /dev/null +++ b/function/videv.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +""" +Virtual Device(s) + +Targets: + * MQTT-Interface to control joined devices as one virtual device. + * Primary signal routing + * No functionality should be implemented here +""" + +from base import mqtt_base +import devices +import json +import logging + +BASETOPIC = "videv" + + +try: + from config import APP_NAME as ROOT_LOGGER_NAME +except ImportError: + ROOT_LOGGER_NAME = 'root' + + +class base(mqtt_base): + DEFAULT_VALUES = {} + + def __init__(self, mqtt_client, topic, *args): + super().__init__(mqtt_client, topic) + self.__device_list__ = {} + for videv_key, device in [reduced[:2] for reduced in args]: + self.__device_list__[videv_key] = device + # send initial state + for key in self.keys(): + self.__tx__(key, self[key]) + # add receive topics + mqtt_client.add_callback(self.topic + "/#", self.__rx__) + + def __tx__(self, key, data): + if type(data) not in (str, ): + data = json.dumps(data) + if key in self.keys(): + self.mqtt_client.send(self.topic + '/' + key, data) + else: + self.logger.warning("Ignoring send request for key %s (not available for this class)", key) + + def __rx__(self, client, userdata, message): + key = message.topic.split('/')[-1] + if key in self.keys(): + try: + data = json.loads(message.payload) + except json.decoder.JSONDecodeError: + data = message.payload + if data != self[key]: + self.__rx_functionality__(key, data) + self.set(key, data) + else: + self.logger.info("Ignoring rx message with topic %s", message.topic) + + def __rx_functionality__(self, key, data): + raise NotImplemented("Method __rx_functionality__ needs to be implemented in child class") + + def __device_data__(self, device, key, data): + raise NotImplemented("Method __device_data__ needs to be implemented in child class") + + +class base_routing(base): + def __init__(self, mqtt_client, topic, *args): + super().__init__(mqtt_client, topic, *args) + # + self.__device_key__ = {} + index = 0 + for videv_key, device, device_key in args: + if self.__device_list__[videv_key] != device: + raise ReferenceError("Parent class generated a deviating device list") + self.__device_key__[videv_key] = device_key + index += 1 + # add callbacks + for key in self.__device_list__: + self.__device_list__[key].add_callback(self.__device_key__[key], None, self.__device_data__, True) + + def __rx_functionality__(self, key, data): + try: + self.__device_list__[key].set(self.__device_key__[key], data) + except KeyError: + self.logger.warning("RX passthrough not possible for key %s", key) + + def __device_data__(self, device, key, data): + l1 = [k for k, v in self.__device_list__.items() if v == device] + l2 = [k for k, v in self.__device_key__.items() if v == key] + try: + videv_key = [k for k in l1 if k in l2][0] + except IndexError: + self.logger.warning("videv_key not available for %s::%s", device.__class__.__name__, device.topic) + else: + self.set(videv_key, data) + self.__tx__(videv_key, data) + + +class videv_switching(base_routing): + KEY_STATE = 'state' + # + DEFAULT_VALUES = { + KEY_STATE: False, + } + + def __init__(self, mqtt_client, topic, sw_device, sw_key): + # + super().__init__(mqtt_client, topic, (self.KEY_STATE, sw_device, sw_key)) + + +class videv_switch_brightness(base_routing): + KEY_STATE = 'state' + KEY_BRIGHTNESS = 'brightness' + # + DEFAULT_VALUES = { + KEY_STATE: False, + KEY_BRIGHTNESS: 0 + } + + def __init__(self, mqtt_client, topic, sw_device, sw_key, br_device, br_key): + # + super().__init__(mqtt_client, topic, (self.KEY_STATE, sw_device, sw_key), (self.KEY_BRIGHTNESS, br_device, br_key)) + + +class videv_switch_brightness_color_temp(base_routing): + KEY_STATE = 'state' + KEY_BRIGHTNESS = 'brightness' + KEY_COLOR_TEMP = 'color_temp' + # + DEFAULT_VALUES = { + KEY_STATE: False, + KEY_BRIGHTNESS: 0, + KEY_COLOR_TEMP: 0, + } + + def __init__(self, mqtt_client, topic, sw_device, sw_key, br_device, br_key, ct_device, ct_key): + # + super().__init__( + mqtt_client, topic, + (self.KEY_STATE, sw_device, sw_key), + (self.KEY_BRIGHTNESS, br_device, br_key), + (self.KEY_COLOR_TEMP, ct_device, ct_key) + )