From b714221a7831d63fcc5d160ff6ffed3d63dcdc69 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 12 Jun 2024 19:19:22 +0200 Subject: [PATCH 1/2] manage garage as a cover instead of switch (without koleos docs) (#197) * Addition of a new sensor class to manage garage to manage garage as a cover * Evolution to manage garage as cover and add garage_door_horizontal that was not managed before. * Update MessageHandler.py * Managemnent two sensors sun and thermoPack Tysense Outdoor * Add management of automatic doors * Sensor name used in MQTT instead of Mosquito --------- Co-authored-by: koleos6 <41859744+koleos6@users.noreply.github.com> --- README.md | 27 ++++++++ app/mqtt/MqttClient.py | 39 ++++++++++- app/sensors/AutomaticDoor.py | 89 ++++++++++++++++++++++++ app/sensors/Garage.py | 127 +++++++++++++++++++++++++++++++++++ app/sensors/Sensor.py | 6 +- app/tydom/MessageHandler.py | 119 ++++++++++++++++++++++++++++---- 6 files changed, 390 insertions(+), 17 deletions(-) create mode 100644 README.md create mode 100644 app/sensors/AutomaticDoor.py create mode 100644 app/sensors/Garage.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..0c4dde8 --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# tydom2MQTT + +![Docker pulls](https://img.shields.io/docker/pulls/fmartinou/tydom2mqtt) +![License](https://img.shields.io/github/license/fmartinou/tydom2mqtt) +![Travis](https://img.shields.io/travis/fmartinou/tydom2mqtt/master) + +![](docs/tydom2mqtt_logo_250.png) + +> **Deltadore Tydom to MQTT Bridge** + +## Ready to go? +### Check out the [documentation](https://fmartinou.github.io/tydom2mqtt/) to get started! + +## Contact & Support + +- Create a [GitHub issue](https://github.com/fmartinou/tydom2mqtt/issues) for bug reports, feature requests, or questions +- Add a ⭐️ [star on GitHub](https://github.com/fmartinou/tydom2mqtt) to support the project! + +## Developer guide +[Please find here the developer guide](DEV.md) + +## Changelog +[Please find here the changelog](docs/changelog/README.md) + +## License + +This project is licensed under the [MIT license](https://github.com/fmartinou/tydom2mqtt/blob/master/LICENSE). \ No newline at end of file diff --git a/app/mqtt/MqttClient.py b/app/mqtt/MqttClient.py index e6f1bbe..639dab8 100644 --- a/app/mqtt/MqttClient.py +++ b/app/mqtt/MqttClient.py @@ -10,9 +10,11 @@ from sensors.Alarm import Alarm from sensors.Boiler import Boiler from sensors.Cover import Cover +from sensors.Garage import Garage from sensors.Light import Light from sensors.Switch import Switch from sensors.ShHvac import ShHvac +from sensors.AutomaticDoor import AutomaticDoor logger = logging.getLogger(__name__) @@ -141,6 +143,29 @@ async def on_message(self, client, topic, payload, qos, properties): device_id = (get_id.split("_"))[0] endpoint_id = (get_id.split("_"))[1] await Cover.put_position(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, position=str(value)) + + elif 'set_garageLevelCmd' in str(topic): + value = payload.decode() + logger.info( + 'set_garageLevelCmd message received (topic=%s, message=%s)', + topic, + value) + get_id = (topic.split("/"))[2] + device_id = (get_id.split("_"))[0] + endpoint_id = (get_id.split("_"))[1] + await Garage.put_garage_positionCmd(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, + positionCmd=str(value)) + + elif ('set_garageLevel' in str(topic)) and not ('set_garageLevelCmd' in str(topic)): + value = json.loads(payload) + logger.info( + 'set_garageLevel message received (topic=%s, message=%s)', + topic, + value) + get_id = (topic.split("/"))[2] + device_id = (get_id.split("_"))[0] + endpoint_id = (get_id.split("_"))[1] + await Garage.put_garage_position(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, position=str(value)) elif 'set_tilt' in str(topic): value = json.loads(payload) @@ -162,7 +187,7 @@ async def on_message(self, client, topic, payload, qos, properties): get_id = (topic.split("/"))[2] device_id = (get_id.split("_"))[0] endpoint_id = (get_id.split("_"))[1] - await Light.put_level_cmd(tydom_client=self.tydom, device_id=device_id, light_id=endpoint_id, + await Light.put_level_cmd(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, level_cmd=str(value)) elif ('set_level' in str(topic)) and not ('set_levelCmd' in str(topic)): @@ -174,8 +199,18 @@ async def on_message(self, client, topic, payload, qos, properties): get_id = (topic.split("/"))[2] device_id = (get_id.split("_"))[0] endpoint_id = (get_id.split("_"))[1] - await Light.put_level(tydom_client=self.tydom, device_id=device_id, light_id=endpoint_id, + await Light.put_level(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, level=str(value)) + elif 'open_automatic_door' in str(topic): + value = payload.decode() + logger.info( + 'open_automatic_door message received (topic=%s, message=%s)', + topic, + value) + get_id = (topic.split("/"))[2] + device_id = (get_id.split("_"))[0] + endpoint_id = (get_id.split("_"))[1] + await AutomaticDoor.put_podPosition(tydom_client=self.tydom, device_id=device_id, door_id=endpoint_id, position='OPEN') elif ('set_alarm_state' in str(topic)) and not ('homeassistant' in str(topic)): value = payload.decode() diff --git a/app/sensors/AutomaticDoor.py b/app/sensors/AutomaticDoor.py new file mode 100644 index 0000000..2fa032a --- /dev/null +++ b/app/sensors/AutomaticDoor.py @@ -0,0 +1,89 @@ +import json +import logging + +from .Sensor import Sensor + +logger = logging.getLogger(__name__) +button_config_topic = "homeassistant/button/tydom/{id}/config" +button_state_topic = "button/tydom/{id}/state" +button_command_topic = "button/tydom/{id}/open_automatic_door" + +class AutomaticDoor: + + def __init__(self, tydom_attributes, tydom_client=None, mqtt=None): + + self.config_topic = None + self.topic_to_func = None + self.config = None + self.device = None + self.attributes = tydom_attributes + self.device_id = self.attributes['device_id'] + self.endpoint_id = self.attributes['endpoint_id'] + self.id = self.attributes['id'] + self.name = self.attributes['name'] + self.mqtt = mqtt + self.tydom_client = tydom_client + + async def setup(self): + self.config = {} + self.device = { + 'manufacturer': 'Delta Dore', + 'name': self.name, + 'model': 'Automatic Door', + 'identifiers': self.id, + } + self.config['device'] = self.device + + self.config_topic = button_config_topic.format(id=self.id) + self.config = { + 'name': None, # set an MQTT entity's name to None to mark it as the main feature of a device + 'unique_id': self.id, + 'device': self.device, + 'button_state_topic': button_state_topic.format(id=self.id), + 'command_topic': button_command_topic.format(id=self.id), + 'icon': "mdi:lock-open-outline", + 'availability_topic': button_state_topic.format(id=self.id), + 'availability_template': '{% if value_json.podPosition == "CLOSE" -%}online{%- else -%}offline{%- endif %}', + 'payload_press': "OPEN" + } + if self.mqtt is not None: + self.mqtt.mqtt_client.publish( + self.config_topic, json.dumps( + self.config), qos=0, retain=True) + + async def update(self): + await self.setup() + + try: + await self.update_sensors() + except Exception as e: + logger.error("AutomaticDoor sensors Error :") + logger.error(e) + + if self.mqtt is not None: + self.mqtt.mqtt_client.publish( + self.config['button_state_topic'], + self.attributes, + qos=0, + retain=True) + + logger.info( + "AutomaticDoor created / updated : %s %s", + self.name, + self.id) + + async def update_sensors(self): + for i, j in self.attributes.items(): + if not i == 'device_type' and not i == 'id' and not i == 'device_id' and not i == 'endpoint_id': + new_sensor = Sensor( + elem_name=i, + tydom_attributes_payload=self.attributes, + mqtt=self.mqtt) + await new_sensor.update() + + @staticmethod + async def put_podPosition(tydom_client, device_id, door_id, position): + logger.info("put_podPosition: %s to %s", device_id, position) + if not (position == ''): + # ex: 'podPosition': 'OPEN' + await tydom_client.put_devices_data(device_id, door_id, 'podPosition', position) diff --git a/app/sensors/Garage.py b/app/sensors/Garage.py new file mode 100644 index 0000000..7b3a6a9 --- /dev/null +++ b/app/sensors/Garage.py @@ -0,0 +1,127 @@ +import json +import logging +from .Sensor import Sensor + +logger = logging.getLogger(__name__) +cover_command_topic = "cover/tydom/{id}/set_garageLevelCmd" +cover_config_topic = "homeassistant/cover/tydom/{id}/config" +cover_position_topic = "cover/tydom/{id}/current_position" +cover_state_topic = "cover/tydom/{id}/state" +cover_level_topic = "cover/tydom/{id}/current_level" +cover_set_level_topic = "cover/tydom/{id}/set_garageLevel" +cover_attributes_topic = "cover/tydom/{id}/attributes" + + +class Garage: + def __init__(self, tydom_attributes, set_level=None, mqtt=None): + self.device = None + self.config = None + self.config_topic = None + self.attributes = tydom_attributes + self.device_id = self.attributes['device_id'] + self.endpoint_id = self.attributes['endpoint_id'] + self.id = self.attributes['id'] + self.name = self.attributes['cover_name'] + try: + self.current_level = self.attributes['level'] + except Exception as e: + logger.error(e) + self.current_level = None + + self.set_level = set_level + self.current_position = set_level + + if 'position' in tydom_attributes: + self.current_position = self.attributes['position'] + + + self.mqtt = mqtt + + async def setup(self): + self.device = { + 'manufacturer': 'Delta Dore', + 'model': 'Garage Door Horizontal', + 'name': self.name, + 'identifiers': self.id} + self.config_topic = cover_config_topic.format(id=self.id) + self.config = { + 'name': None, # set an MQTT entity's name to None to mark it as the main feature of a device + 'unique_id': self.id, + 'command_topic': cover_command_topic.format( + id=self.id), + 'position_topic': cover_position_topic.format( + id=self.id), + 'level_topic': cover_level_topic.format( + id=self.id), + 'set_position_topic': cover_set_level_topic.format( + id=self.id), + 'payload_open': "ON", + 'payload_close': "OFF", + 'payload_stop': "STOP", + 'retain': 'false', + 'device': self.device, + 'device_class': self.attributes['cover_class']} + + self.config['json_attributes_topic'] = cover_attributes_topic.format( + id=self.id) + + if self.mqtt is not None: + self.mqtt.mqtt_client.publish( + self.config_topic, json.dumps( + self.config), qos=0, retain=True) + + async def update(self): + await self.setup() + + try: + await self.update_sensors() + except Exception as e: + logger.error("GarageDoor Horizontal sensors Error :") + logger.error(e) + + self.level_topic = cover_state_topic.format( + id=self.id, current_level=self.current_level) + + if self.mqtt is not None: + #and 'position' in self.attributes: + self.mqtt.mqtt_client.publish( + self.config['position_topic'], + self.current_level, + qos=0, retain=True) + + if self.mqtt is not None: + self.mqtt.mqtt_client.publish( + self.level_topic, + self.current_level, + qos=0, + retain=True) + self.mqtt.mqtt_client.publish( + self.config['json_attributes_topic'], + self.attributes, + qos=0, + retain=True) + + logger.info( + "GarageDoor Horizontal created / updated : %s %s %s", + self.name, + self.id, + self.current_level) + + async def update_sensors(self): + for i, j in self.attributes.items(): + if not i == 'device_type' and not i == 'id' and not i == 'device_id' and not i == 'endpoint_id': + new_sensor = Sensor( + elem_name=i, + tydom_attributes_payload=self.attributes, + mqtt=self.mqtt) + await new_sensor.update() + + async def put_garage_position(tydom_client, device_id, cover_id, position): + logger.info("%s %s %s", cover_id, 'level', position) + if not (position == ''): + await tydom_client.put_devices_data(device_id, cover_id, 'level', position) + + async def put_garage_positionCmd(tydom_client, device_id, cover_id, positionCmd): + logger.info("%s %s %s", cover_id, 'levelCmd', positionCmd) + if not (positionCmd == ''): + await tydom_client.put_devices_data(device_id, cover_id, 'levelCmd', positionCmd) diff --git a/app/sensors/Sensor.py b/app/sensors/Sensor.py index 1972c85..f8d00df 100644 --- a/app/sensors/Sensor.py +++ b/app/sensors/Sensor.py @@ -27,6 +27,7 @@ def __init__(self, elem_name, tydom_attributes_payload, mqtt=None): # extracted from json, but it will make sensor not in payload to be # considered offline.... self.parent_device_id = str(tydom_attributes_payload['id']) + self.parent_name = str(tydom_attributes_payload['name']) self.id = elem_name + '_tydom_' + str(tydom_attributes_payload['id']) self.name = elem_name if 'device_class' in tydom_attributes_payload.keys(): @@ -106,7 +107,10 @@ def __init__(self, elem_name, tydom_attributes_payload, mqtt=None): async def setup(self): self.device = { 'manufacturer': 'Delta Dore', - 'identifiers': self.parent_device_id} + 'identifiers': self.parent_device_id, + 'model':'Sensor', + 'name':self.parent_name + } self.config_sensor_topic = sensor_config_topic.format(id=self.id) diff --git a/app/tydom/MessageHandler.py b/app/tydom/MessageHandler.py index 617aa63..687d6e4 100644 --- a/app/tydom/MessageHandler.py +++ b/app/tydom/MessageHandler.py @@ -7,10 +7,12 @@ from sensors.Alarm import Alarm from sensors.Boiler import Boiler from sensors.Cover import Cover +from sensors.Garage import Garage from sensors.Light import Light from sensors.Sensor import Sensor from sensors.Switch import Switch from sensors.ShHvac import ShHvac +from sensors.AutomaticDoor import AutomaticDoor logger = logging.getLogger(__name__) @@ -97,14 +99,44 @@ 'onPresenceDetected', 'onDusk'] -deviceDoorKeywords = ['openState', 'intrusionDetect'] +deviceDoorKeywords = [ + 'openState', + 'intrusionDetect', + 'battDefect', + 'supervisionMode', + 'calibrationDefect'] deviceDoorDetailsKeywords = [ 'onFavPos', 'thermicDefect', 'obstacleDefect', - 'intrusion', - 'battDefect'] + 'intrusionDetect', + 'battDefect', + 'supervisionMode' + 'calibrationDefect'] +deviceAutomaticDoorKeywords = ['podPosition'] +deviceAutomaticDoorDetailsKeywords = ['podPosition'] + +deviceGaragelKeywords = [ + 'level', + 'onDusk', + 'onFavPos', + 'onPresenceDetected' + 'thermicDefect', + 'loadDefect', + 'cmdDefect', + 'battDefect'] + +deviceGarageDetailsKeywords = [ + 'level', + 'onDusk', + 'onFavPos', + 'onPresenceDetected' + 'thermicDefect', + 'loadDefect', + 'cmdDefect', + 'battDefect'] + deviceCoverKeywords = [ 'position', 'slope', @@ -121,7 +153,19 @@ 'battDefect', 'position', 'slope'] - + +deviceSensorsKeywords = [ + 'outTemperature', + 'lightPower', + 'configSensor', + 'configTemp', + 'battDefect'] +deviceSensorsDetailsKeywords = [ + 'outTemperature' + 'lightPower', + 'battDefect', + 'configSensor', + 'configTemp'] deviceBoilerKeywords = [ 'thermicLevel', 'delayThermicLevel', @@ -385,7 +429,7 @@ async def parse_config_data(parsed): if i["last_usage"] == 'shutter' or i["last_usage"] == 'klineShutter' or i["last_usage"] == 'light' or i["last_usage"] == 'window' or i["last_usage"] == 'windowFrench' or i["last_usage"] == 'windowSliding' or i[ "last_usage"] == 'belmDoor' or i["last_usage"] == 'klineDoor' or i["last_usage"] == 'klineWindowFrench' or i["last_usage"] == 'klineWindowSliding' or i["last_usage"] == 'garage_door' or i["last_usage"] == 'gate' or i[ - "last_usage"] == 'awning' or i["last_usage"] == 'others': + "last_usage"] == 'awning' or i["last_usage"] == 'garage_door_horizontal' or i["last_usage"] == 'others' or i["last_usage"] == 'sensorSun' or i["last_usage"] == 'sensorThermo': device_name[device_unique_id] = i["name"] device_type[device_unique_id] = i["last_usage"] device_endpoint[device_unique_id] = i["id_endpoint"] @@ -484,6 +528,7 @@ async def parse_endpoint_data(self, endpoint, device_id): try: attr_alarm = {} attr_cover = {} + attr_garage = {} attr_door = {} attr_ukn = {} attr_window = {} @@ -492,6 +537,9 @@ async def parse_endpoint_data(self, endpoint, device_id): attr_boiler = {} attr_sh_hvac = {} attr_smoke = {} + attr_sensor = {} + attr_automatic_door = {} + endpoint_id = endpoint["id"] unique_id = str(endpoint_id) + "_" + str(device_id) name_of_id = self.get_name_from_id(unique_id) @@ -549,6 +597,17 @@ async def parse_endpoint_data(self, endpoint, device_id): attr_door['element_name'] = element_name attr_door[element_name] = element_value + if type_of_id == 'belmDoor' or type_of_id == 'klineDoor': + if element_name in deviceAutomaticDoorKeywords and element_validity == 'upToDate': + attr_automatic_door['device_id'] = device_id + attr_automatic_door['endpoint_id'] = endpoint_id + attr_automatic_door['id'] = str( + device_id) + '_' + str(endpoint_id) + attr_automatic_door['name'] = print_id + attr_automatic_door['device_type'] = 'AutomaticDoor' + attr_automatic_door['element_name'] = element_name + attr_automatic_door[element_name] = element_value + if type_of_id == 'windowFrench' or type_of_id == 'window' or type_of_id == 'windowSliding' or type_of_id == 'klineWindowFrench' or type_of_id == 'klineWindowSliding': if element_name in deviceDoorKeywords and element_validity == 'upToDate': attr_window['device_id'] = device_id @@ -561,6 +620,17 @@ async def parse_endpoint_data(self, endpoint, device_id): attr_window['element_name'] = element_name attr_window[element_name] = element_value + if type_of_id == 'sensorThermo' or type_of_id == 'sensorSun': + if element_name in deviceSensorsKeywords and element_validity == 'upToDate': + attr_sensor['device_id'] = device_id + attr_sensor['endpoint_id'] = endpoint_id + attr_sensor['id'] = str( + device_id) + '_' + str(endpoint_id) + attr_sensor['name'] = print_id + attr_sensor['device_type'] = 'sensor' + attr_sensor['element_name'] = element_name + attr_sensor[element_name] = element_value + if type_of_id == 'boiler': if element_name in deviceBoilerKeywords and element_validity == 'upToDate': attr_boiler['device_id'] = device_id @@ -583,16 +653,20 @@ async def parse_endpoint_data(self, endpoint, device_id): attr_alarm['device_type'] = 'alarm_control_panel' attr_alarm[element_name] = element_value - if type_of_id == 'garage_door' or type_of_id == 'gate': - if element_name in deviceSwitchKeywords and element_validity == 'upToDate': - attr_gate['device_id'] = device_id - attr_gate['endpoint_id'] = endpoint_id - attr_gate['id'] = str( + if type_of_id == 'garage_door_horizontal' or type_of_id == 'garage_door' or type_of_id == 'gate': + if element_name in deviceGaragelKeywords and element_validity == 'upToDate': + attr_garage['device_id'] = device_id + attr_garage['endpoint_id'] = endpoint_id + attr_garage['id'] = str( device_id) + '_' + str(endpoint_id) - attr_gate['switch_name'] = print_id - attr_gate['name'] = print_id - attr_gate['device_type'] = 'switch' - attr_gate[element_name] = element_value + attr_garage['cover_name'] = print_id + attr_garage['name'] = print_id + attr_garage['device_type'] = 'garage' + if type_of_id == 'garage_door': + attr_garage['cover_class'] = 'garage' + else: + attr_garage['cover_class'] = 'gate' + attr_garage[element_name] = element_value if type_of_id == 'conso': if element_name in device_conso_keywords and element_validity == "upToDate": @@ -685,6 +759,11 @@ async def parse_endpoint_data(self, endpoint, device_id): tydom_attributes=attr_cover, mqtt=self.mqtt_client) await new_cover.update() + elif 'device_type' in attr_garage and attr_garage['device_type'] == 'garage': + new_garage = Garage( + tydom_attributes=attr_garage, + mqtt=self.mqtt_client) + await new_garage.update() elif 'device_type' in attr_door and attr_door['device_type'] == 'sensor': new_door = Sensor( elem_name=attr_door['element_name'], @@ -697,6 +776,12 @@ async def parse_endpoint_data(self, endpoint, device_id): tydom_attributes_payload=attr_window, mqtt=self.mqtt_client) await new_window.update() + elif 'device_type' in attr_sensor and attr_sensor['device_type'] == 'sensor': + new_sensor = Sensor( + elem_name=attr_sensor['element_name'], + tydom_attributes_payload=attr_sensor, + mqtt=self.mqtt_client) + await new_sensor.update() elif 'device_type' in attr_light and attr_light['device_type'] == 'light': new_light = Light( tydom_attributes=attr_light, @@ -731,6 +816,12 @@ async def parse_endpoint_data(self, endpoint, device_id): tydom_client=self.tydom_client, mqtt=self.mqtt_client) await new_sh_hvac.update() + elif 'device_type' in attr_automatic_door and attr_automatic_door['device_type'] == 'AutomaticDoor': + new_automatic_door = AutomaticDoor( + tydom_attributes=attr_automatic_door, + tydom_client=self.tydom_client, + mqtt=self.mqtt_client) + await new_automatic_door.update() # Get last known state (for alarm) # NEW METHOD elif 'device_type' in attr_alarm and attr_alarm['device_type'] == 'alarm_control_panel': From e2e8b451ff468128fa7e36e3f4868d6ed039c2a8 Mon Sep 17 00:00:00 2001 From: Romain Date: Wed, 9 Oct 2024 10:00:42 +0200 Subject: [PATCH 2/2] fix light command on feature #183 (#213) Apply BlacKTM1190 suggest and fix light command --- app/mqtt/MqttClient.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/mqtt/MqttClient.py b/app/mqtt/MqttClient.py index 639dab8..7a6a748 100644 --- a/app/mqtt/MqttClient.py +++ b/app/mqtt/MqttClient.py @@ -187,7 +187,7 @@ async def on_message(self, client, topic, payload, qos, properties): get_id = (topic.split("/"))[2] device_id = (get_id.split("_"))[0] endpoint_id = (get_id.split("_"))[1] - await Light.put_level_cmd(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, + await Light.put_level_cmd(tydom_client=self.tydom, device_id=device_id, light_id=endpoint_id, level_cmd=str(value)) elif ('set_level' in str(topic)) and not ('set_levelCmd' in str(topic)): @@ -199,8 +199,9 @@ async def on_message(self, client, topic, payload, qos, properties): get_id = (topic.split("/"))[2] device_id = (get_id.split("_"))[0] endpoint_id = (get_id.split("_"))[1] - await Light.put_level(tydom_client=self.tydom, device_id=device_id, cover_id=endpoint_id, + await Light.put_level(tydom_client=self.tydom, device_id=device_id, light_id=endpoint_id, level=str(value)) + elif 'open_automatic_door' in str(topic): value = payload.decode() logger.info(