diff --git a/zigbee2mqtt/__init__.py b/zigbee2mqtt/__init__.py index eeb800dc8..6006a2cf9 100755 --- a/zigbee2mqtt/__init__.py +++ b/zigbee2mqtt/__init__.py @@ -2,6 +2,7 @@ # vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab ######################################################################### # Copyright 2021- Michael Wenzel wenzel_michael@web.de +# Copyright 2023- Sebastian Helms morg @ knx-Forum ######################################################################### # This file is part of SmartHomeNG. # @@ -23,110 +24,118 @@ ######################################################################### from datetime import datetime -import logging +import json -from lib.model.mqttplugin import * -from lib.utils import Utils +from lib.model.mqttplugin import MqttPlugin + +from .rgbxy import Converter from .webif import WebInterface +Z2M_TOPIC = 'z2m_topic' +Z2M_ATTR = 'z2m_attr' +Z2M_RO = 'z2m_readonly' +Z2M_WO = 'z2m_writeonly' +Z2M_BVAL = 'z2m_bool_values' +MSEP = '#' + +HANDLE_IN_PREFIX = '_handle_in_' +HANDLE_OUT_PREFIX = '_handle_out_' +HANDLE_DEV = 'dev_' +HANDLE_ATTR = 'attr_' + class Zigbee2Mqtt(MqttPlugin): - """ - Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items - """ + """ Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items """ - PLUGIN_VERSION = '1.1.2' + PLUGIN_VERSION = '2.0.0' - def __init__(self, sh): - """ - Initializes the plugin. - """ + def __init__(self, sh, **kwargs): + """ Initializes the plugin. """ # Call init code of parent class (MqttPlugin) super().__init__() - if not self._init_complete: - return - - self.logger.info('Init Zigbee2Mqtt Plugin') - # Enable / Disable debug log generation depending on log level - if self.logger.isEnabledFor(logging.DEBUG): - self.debug_log = True - else: - self.debug_log = False + self.logger.info(f'Init {self.get_shortname()} plugin {self.PLUGIN_VERSION}') # get the parameters for the plugin (as defined in metadata plugin.yaml): - self.topic_level1 = self.get_parameter_value('base_topic').lower() + self.z2m_base = self.get_parameter_value('base_topic').lower() self.cycle = self.get_parameter_value('poll_period') self.read_at_init = self.get_parameter_value('read_at_init') - self.webif_pagelength = self.get_parameter_value('webif_pagelength') - - # Initialization code goes here - self.zigbee2mqtt_devices = {} # to hold device information for web interface; contains data of all found devices - self.zigbee2mqtt_plugin_devices = {} # to hold device information for web interface; contains data of all devices addressed in items - self.zigbee2mqtt_items = [] # to hold item information for web interface; contains list of all items + self.bool_values = self.get_parameter_value('bool_values') + self._z2m_gui = self.get_parameter_value('z2m_gui') + + self._items_read = [] + self._items_write = [] + self._devices = {'bridge': {}} + # { + # 'dev1': { + # 'lastseen': , + # 'meta': {[...]}, # really needed anymore? + # 'data': {[...]}, + # 'exposes': {[...]}, + # 'scenes': {'name1': id1, 'name2': id2, [...]}, + # 'attr1': { + # 'item': item1, + # 'read': bool, + # 'write': bool, + # 'bool_values': ['OFF', 'ON'], + # 'value': ... + # }, + # 'attr2': ... + # }, + # 'dev2': ... + # } # Add subscription to get bridge announces - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'devices', '', '', 'list', callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'state', '', '', 'bool', bool_values=['offline', 'online'], callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'info', '', '', 'dict', callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'log', '', '', 'dict', callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'extensions', '', '', 'list', callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'config', '', '', 'dict', callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'groups', '', '', 'list', callback=self.on_mqtt_announce) - self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'response', '', '', 'dict', callback=self.on_mqtt_announce) + bridge_subs = [ + ['devices', 'list', None], + ['state', 'bool', ['offline', 'online']], + ['info', 'dict', None], + ['log', 'dict', None], + ['extensions', 'list', None], + ['config', 'dict', None], + ['groups', 'list', None], + ['response', 'dict', None] + ] + for attr, dtype, blist in bridge_subs: + self.add_z2m_subscription('bridge', attr, '', '', dtype, callback=self.on_mqtt_msg, bool_values=blist) # Add subscription to get device announces - self.add_zigbee2mqtt_subscription(self.topic_level1, '+', '', '', '', 'dict', callback=self.on_mqtt_announce) - - self.local_ip = '' - self.alive = None + self.add_z2m_subscription('+', '', '', '', 'dict', callback=self.on_mqtt_msg) - # if plugin should start even without web interface + # try to load webif self.init_webinterface(WebInterface) - return - - # ToDo: Verarbeiten der bridge_devices bridge/log/device def run(self): - """ - Run method for the plugin - """ + """ Run method for the plugin """ self.logger.debug("Run method called") - # get local ip - self.local_ip = Utils.get_local_ipv4_address() - self.logger.info(f"local ip adress is {self.local_ip}") + self.alive = True # start subscription to all topics self.start_subscriptions() - self.scheduler_add('poll_bridge', self.poll_bridge, cycle=self.cycle) - self.publish_zigbee2mqtt_topic(self.topic_level1, 'bridge', 'config', 'devices', 'get', '') + self.scheduler_add('z2m_cycle', self.poll_bridge, cycle=self.cycle) + self.publish_z2m_topic('bridge', 'config', 'devices', 'get') if self.read_at_init: - self.publish_zigbee2mqtt_topic(self.topic_level1, 'bridge', 'request', 'restart', '', '') - - self.alive = True + self.publish_z2m_topic('bridge', 'request', 'restart') try: - self._get_current_status_of_all_devices_linked_to_items() + self._read_all_data() except Exception: pass def stop(self): - """ - Stop method for the plugin - """ + """ Stop method for the plugin """ self.alive = False self.logger.debug("Stop method called") - self.scheduler_remove('poll_bridge') + self.scheduler_remove('z2m_c') # stop subscription to all topics self.stop_subscriptions() - return def parse_item(self, item): """ @@ -142,32 +151,94 @@ def parse_item(self, item): can be sent to the knx with a knx write function within the knx plugin. """ - if self.has_iattr(item.conf, 'zigbee2mqtt_attr'): - if self.debug_log: - self.logger.debug(f"parsing item: {item.id()}") + # remove this block when its included in smartplugin.py, + # replace with super().parse_item(item) + # check for suspend item + if item.property.path == self._suspend_item_path: + self.logger.debug(f'suspend item {item.property.path} registered') + self._suspend_item = item + self.add_item(item, updating=True) + return self.update_item + # end block - zigbee2mqtt_attr = self.get_iattr_value(item.conf, 'zigbee2mqtt_attr').lower() - topic_level2 = self._get_zigbee2mqtt_topic_from_item(item) + if self.has_iattr(item.conf, Z2M_ATTR): + self.logger.debug(f"parsing item: {item}") - if not topic_level2: + device = self._get_z2m_topic_from_item(item) + if not device: + self.logger.warning(f"parsed item {item} has no {Z2M_TOPIC} set, ignoring") return - if not self.zigbee2mqtt_plugin_devices.get(topic_level2): - self.zigbee2mqtt_plugin_devices[topic_level2] = {} - self.zigbee2mqtt_plugin_devices[topic_level2]['connected_to_item'] = False - self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items'] = {} + attr = self.get_iattr_value(item.conf, Z2M_ATTR).lower() + + if item.type() == 'bool': + bval = self.get_iattr_value(item.conf, Z2M_BVAL) + if bval == []: + bval = None + if bval is None or type(bval) is not list: + bval = self.bool_values + + # invert read-only/write-only logic to allow read/write + write = not self.get_iattr_value(item.conf, Z2M_RO, False) + read = not self.get_iattr_value(item.conf, Z2M_WO, False) or not write + + if device not in self._devices: + self._devices[device] = {} + + if attr not in self._devices[device]: + self._devices[device][attr] = {} + + data = { + 'value': None, + 'item': item, + 'read': read, + 'write': write, + } + if item.type() == 'bool': + data['bval'] = bval + + self._devices[device][attr].update(data) + + if read and item not in self._items_read: + self._items_read.append(item) + if write and item not in self._items_write: + self._items_write.append(item) + + # use new smartplugin method of registering items + # # needed as mapping, as device or attr + # by themselves are not unique + self.add_item(item, {}, device + MSEP + attr, write) + if write: + return self.update_item + + def remove_item(self, item): + if item not in self._plg_item_dict: + return + + mapping = self.get_item_mapping(item) + if mapping: + device, attr = mapping.split(MSEP) - self.zigbee2mqtt_plugin_devices[topic_level2]['connected_to_item'] = True - self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items']['item_' + zigbee2mqtt_attr] = item - if zigbee2mqtt_attr == 'online': - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = False - # append to list used for web interface - if item not in self.zigbee2mqtt_items: - self.zigbee2mqtt_items.append(item) + # remove references in plugin-internal storage + try: + del self._devices[device][attr] + except KeyError: + pass + if not self._devices[device]: + del self._devices[device] - return self.update_item + try: + self._items_read.remove(item) + except ValueError: + pass + try: + self._items_write.remove(item) + except ValueError: + pass - def update_item(self, item, caller=None, source=None, dest=None): + super().remove_item(item) + + def update_item(self, item, caller='', source=None, dest=None): """ Item has been updated @@ -176,505 +247,563 @@ def update_item(self, item, caller=None, source=None, dest=None): :param source: if given it represents the source :param dest: if given it represents the dest """ + self.logger.debug(f"update_item: {item} called by {caller} and source {source}") + + if self.alive and not self.suspended and not caller.startswith(self.get_shortname()): - if self.debug_log: - self.logger.debug(f"update_item: {item.id()} called by {caller} and source {source}") + if item in self._items_write: - if self.alive and self.get_shortname() not in caller: - # code to execute if the plugin is not stopped AND only, if the item has not been changed for this plugin + mapping = self.get_item_mapping(item) + if not mapping: + self.logger.error(f"update_item called for item {item}, but no z2m associations are stored. This shouldn't happen...") + return - # get zigbee2mqtt attributes of caller item - topic_level2 = self.get_iattr_value(item.conf, 'zigbee2mqtt_topic') - topic_level2 = self._handle_hex_in_topic_level2(topic_level2, item) + self.logger.info(f"update_item: {item}, item has been changed outside of this plugin in {caller} with value {item()}") - zigbee2mqtt_attr = self.get_iattr_value(item.conf, 'zigbee2mqtt_attr') + device, attr = mapping.split(MSEP) - if zigbee2mqtt_attr in ['bridge_permit_join', 'bridge_health_check', 'bridge_restart', 'bridge_networkmap_raw', 'device_remove', - 'device_ota_update_check', 'device_ota_update_update', 'device_configure', 'device_options', 'device_rename', - 'device_bind', 'device_unbind', 'device_configure_reporting', 'state', 'color_temp', 'brightness', 'hue', 'saturation']: + # make access easier and code more readable + _device = self._devices[device] + _attr = _device[attr] - self.logger.info(f"update_item: {item.id()}, item has been changed in SmartHomeNG outside of this plugin in {caller} with value {item()}") + # pre-set values + topic_3 = 'set' + topic_4 = topic_5 = '' payload = None - bool_values = None - topic_level3 = topic_level4 = topic_level5 = '' - - if zigbee2mqtt_attr == 'bridge_permit_join': - topic_level3 = 'request' - topic_level4 = 'permit_join' - payload = item() - bool_values = ['false', 'true'] - elif zigbee2mqtt_attr == 'bridge_health_check': - topic_level3 = 'request' - topic_level4 = 'health_check' - payload = '' - elif zigbee2mqtt_attr == 'bridge_restart': - topic_level3 = 'request' - topic_level4 = 'restart' - payload = '' - elif zigbee2mqtt_attr == 'bridge_networkmap_raw': - topic_level3 = 'request' - topic_level4 = 'networkmap' - payload = 'raw' - elif zigbee2mqtt_attr == 'device_remove': - topic_level3 = 'request' - topic_level4 = 'device' - topic_level5 = 'remove' - payload = str(item()) - # elif zigbee2mqtt_attr == 'device_ota_update_check': - # topic_level3 = 'request' - # topic_level4 = 'device' - # payload = 'raw' - # bool_values = None - # elif zigbee2mqtt_attr == 'device_ota_update_update': - # topic_level3 = 'request' - # topic_level4 = 'device' - # payload = 'raw' - # bool_values = None - elif zigbee2mqtt_attr == 'device_configure': - topic_level3 = 'request' - topic_level4 = 'device' - topic_level5 = 'configure' - payload = str(item()) - elif zigbee2mqtt_attr == 'device_options': - topic_level3 = 'request' - topic_level4 = 'device' - topic_level5 = 'options' - payload = str(item()) - elif zigbee2mqtt_attr == 'device_rename': - topic_level3 = 'request' - topic_level4 = 'device' - topic_level5 = 'rename' - payload = str(item()) - elif zigbee2mqtt_attr == 'state': - topic_level3 = 'set' - payload = '{' + f'"state" : "{self._bool2str(item(), 1)}"' + '}' - elif zigbee2mqtt_attr == 'brightness': - topic_level3 = 'set' - value = int(round(item() * 255 / 100, 0)) # Umrechnung von 0-100% in 0-254 - if value < 0 or value > 255: - self.logger.warning(f'commanded value for brightness not within allowed range; set to next valid value') - value = 0 if value < 0 else 255 - payload = '{' + f'"brightness" : "{value}"' + '}' - elif zigbee2mqtt_attr == 'color_temp': - topic_level3 = 'set' - value = int(round(1000000 / item(), 0)) - # mired scale - if value < 150 or value > 500: - self.logger.warning(f' commanded value for brightness not within allowed range; set to next valid value') - value = 150 if value < 150 else 500 - payload = '{' + f'"color_temp" : "{value}"' + '}' - elif zigbee2mqtt_attr == 'hue': - topic_level3 = 'set' - hue = item() - saturation_item = self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items']['item_saturation'] - saturation = saturation_item() - if hue < 0 or hue > 359: - self.logger.warning(f'commanded value for hue not within allowed range; set to next valid value') - hue = 0 if hue < 0 else 359 - payload = '{"color":{' + f'"hue":{hue}, "saturation":{saturation}' + '}}' - elif zigbee2mqtt_attr == 'saturation': - topic_level3 = 'set' - saturation = item() - hue_item = self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items']['item_hue'] - hue = hue_item() - if saturation < 0 or saturation > 100: - self.logger.warning(f'commanded value for hue not within allowed range; set to next valid value') - saturation = 0 if saturation < 0 else 100 - payload = '{"color":{' + f'"hue":{hue}, "saturation":{saturation}' + '}}' - else: - self.logger.warning(f"update_item: {item.id()}, attribut {zigbee2mqtt_attr} not implemented yet (by {caller})") + bool_values = _attr.get('bool_values', self.bool_values) + scenes = _device.get('scenes') + value = item() + + # apply bool_values if present and applicable + if bool_values and isinstance(value, bool): + value = bool_values[value] + + # replace scene with index + if attr == 'scene_recall' and scenes: + try: + value = scenes[value] + except KeyError: + self.logger.warning(f'scene {value} not defined for {device}') + return + + # check device handler + if hasattr(self, HANDLE_OUT_PREFIX + HANDLE_DEV + device): + value, topic_3, topic_4, topic_5, abort = getattr(self, HANDLE_OUT_PREFIX + HANDLE_DEV + device)(item, value, topic_3, topic_4, topic_5, device, attr) + if abort: + self.logger.debug(f'processing of item {item} stopped due to abort statement from handler {HANDLE_OUT_PREFIX + HANDLE_DEV + device}') + return + + # check attribute handler + if hasattr(self, HANDLE_OUT_PREFIX + HANDLE_ATTR + attr): + value, topic_3, topic_4, topic_5, abort = getattr(self, HANDLE_OUT_PREFIX + HANDLE_ATTR + attr)(item, value, topic_3, topic_4, topic_5, device, attr) + if abort: + self.logger.debug(f'processing of item {item} stopped due to abort statement from handler {HANDLE_OUT_PREFIX + HANDLE_ATTR + attr}') + return + + # create payload + payload = json.dumps({ + attr: value + }) if payload is not None: - self.publish_zigbee2mqtt_topic(self.topic_level1, topic_level2, topic_level3, topic_level4, topic_level5, payload, item, bool_values=bool_values) + self.publish_z2m_topic(device, topic_3, topic_4, topic_5, payload, item, bool_values=bool_values) else: - self.logger.warning(f"update_item: {item.id()}, no value/payload defined (by {caller})") + self.logger.warning(f"update_item: {item}, no payload defined (by {caller})") else: - self.logger.warning(f"update_item: {item.id()}, trying to change item in SmartHomeNG that is readonly (by {caller})") + self.logger.warning(f"update_item: {item}, trying to change item in SmartHomeNG that is readonly (by {caller})") def poll_bridge(self): - """ - Polls for health state of the bridge - """ + """ Polls for health state of the bridge """ - self.logger.info("poll_bridge: Checking online and health status of bridge") - self.publish_zigbee2mqtt_topic(self.topic_level1, 'bridge', 'request', 'health_check', '', '') + self.logger.info("poll_bridge: Checking health status of bridge") + self.publish_z2m_topic('bridge', 'request', 'health_check') - for topic_level2 in self.zigbee2mqtt_plugin_devices: - if self.zigbee2mqtt_plugin_devices[topic_level2].get('online') is True and self.zigbee2mqtt_plugin_devices[topic_level2].get('online_timeout') is True: - if self.zigbee2mqtt_plugin_devices[topic_level2]['online_timeout'] < datetime.now(): - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = False - self._set_item_value(topic_level2, 'item_online', False, 'poll_device') - self.logger.info(f"poll_device: {topic_level2} is not online any more - online_timeout={self.zigbee2mqtt_plugin_devices[topic_level2]['online_timeout']}, now={datetime.now()}") + def add_z2m_subscription(self, device: str, topic_3: str, topic_4: str, topic_5: str, payload_type: str, bool_values=None, item=None, callback=None): + """ build the topic in zigbee2mqtt style and add the subscription to mqtt """ - def add_zigbee2mqtt_subscription(self, topic_level1: str, topic_level2: str, topic_level3: str, topic_level4: str, topic_level5: str, payload_type: str, bool_values: list = None, item=None, callback=None): - """ - build the topic in zigbee2mqtt style and add the subscription to mqtt - - :param topic_level1: basetopic of topic to subscribe to - :param topic_level2: unique part of topic to subscribe to - :param topic_level3: level3 of topic to subscribe to - :param topic_level4: level4 of topic to subscribe to - :param topic_level5: level5 of topic to subscribe to - :param payload_type: payload type of the topic (for this subscription to the topic) - :param bool_values: bool values (for this subscription to the topic) - :param item: item that should receive the payload as value. Used by the standard handler (if no callback function is specified) - :param callback: a plugin can provide an own callback function, if special handling of the payload is needed - :return: None - """ - - tpc = self._build_topic_str(topic_level1, topic_level2, topic_level3, topic_level4, topic_level5) + tpc = self._build_topic_str(device, topic_3, topic_4, topic_5) self.add_subscription(tpc, payload_type, bool_values=bool_values, callback=callback) - def publish_zigbee2mqtt_topic(self, topic_level1: str, topic_level2: str, topic_level3: str, topic_level4: str, topic_level5: str, payload, item=None, qos: int = None, retain: bool = False, bool_values: list = None): - """ - build the topic in zigbee2mqtt style and publish to mqtt - - :param topic_level1: basetopic of topic to publish - :param topic_level2: unique part of topic to publish; ZigbeeDevice - :param topic_level3: level3 of topic to publish - :param topic_level4: level4 of topic to publish - :param topic_level5: level5 of topic to publish - :param payload: payload to publish - :param item: item (if relevant) - :param qos: qos for this message (optional) - :param retain: retain flag for this message (optional) - :param bool_values: bool values (for publishing this topic, optional) - :return: None - """ + def publish_z2m_topic(self, device: str, topic_3: str = '', topic_4: str = '', topic_5: str = '', payload='', item=None, qos: int = 0, retain: bool = False, bool_values=None): + """ build the topic in zigbee2mqtt style and publish to mqtt """ - tpc = self._build_topic_str(topic_level1, topic_level2, topic_level3, topic_level4, topic_level5) - # self.logger.debug(f"Publish to topic <{tpc}> with payload <{payload}>") + tpc = self._build_topic_str(device, topic_3, topic_4, topic_5) self.publish_topic(tpc, payload, item, qos, retain, bool_values) - def on_mqtt_announce(self, topic: str, payload, qos=None, retain=None): + def on_mqtt_msg(self, topic: str, payload, qos=None, retain=None): """ Callback function to handle received messages :param topic: mqtt topic :param payload: mqtt message payload - :param qos: qos for this message (optional) - :param retain: retain flag for this message (optional) + :param qos: qos for this message (unused) + :param retain: retain flag for this message (unused) """ - wrk = topic.split('/') - topic_level1 = wrk[0] - topic_level2 = wrk[1] - topic_level3 = '' - topic_level4 = '' - topic_level5 = '' - if len(wrk) > 2: - topic_level3 = wrk[2] - if len(wrk) > 3: - topic_level4 = wrk[3] - if len(wrk) > 4: - topic_level5 = wrk[4] - - if self.debug_log: - self.logger.debug(f"on_mqtt_announce: topic_level1={topic_level1}, topic_level2={topic_level2}, topic_level3={topic_level3}, topic_level4={topic_level4}, topic_level5={topic_level5}, payload={payload}") - - # Handle data from bridge - if topic_level2 == 'bridge': - if topic_level3 == 'state': - # Payloads are 'online' and 'offline'; equal to LWT - if self.debug_log: - self.logger.debug(f"LWT: detail: {topic_level3} datetime: {datetime.now()} payload: {payload}") - if topic_level2 not in self.zigbee2mqtt_plugin_devices: - self.zigbee2mqtt_plugin_devices[topic_level2] = {} - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = bool(payload) - - elif topic_level3 == 'response': - if topic_level4 == 'health_check': - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=response, topic_level4=health_check, topic_level5=, payload={'data': {'healthy': True}, 'status': 'ok'} - if type(payload) is dict: - self.zigbee2mqtt_plugin_devices[topic_level2]['health_status'] = payload - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = bool(payload['data']['healthy']) - else: - if self.debug_log: - self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") - - elif topic_level4 == 'permit_join': - # {"data":{"value":true},"status":"ok"} - if type(payload) is dict: - self.zigbee2mqtt_plugin_devices[topic_level2]['permit_join'] = payload - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = True - else: - if self.debug_log: - self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") - - elif topic_level4 == 'networkmap': - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=None, topic_level4=networkmap, topic_level5=None, payload={'data': {'routes': False, 'type': 'raw', 'value': {'links': [{'depth': 1, 'linkquality': 5, 'lqi': 5, 'relationship': 1, 'routes': [], 'source': {'ieeeAddr': '0x588e81fffe28dec5', 'networkAddress': 39405}, 'sourceIeeeAddr': '0x588e81fffe28dec5', 'sourceNwkAddr': 39405, 'target': {'ieeeAddr': '0x00124b001cd4bbf0', 'networkAddress': 0}, 'targetIeeeAddr': '0x00124b001cd4bbf0'}, {'depth': 1, 'linkquality': 155, 'lqi': 155, 'relationship': 1, 'routes': [], 'source': {'ieeeAddr': '0x00124b00231e45b8', 'networkAddress': 18841}, 'sourceIeeeAddr': '0x00124b00231e45b8', 'sourceNwkAddr': 18841, 'target': {'ieeeAddr': '0x00124b001cd4bbf0', 'networkAddress': 0}, 'targetIeeeAddr': '0x00124b001cd4bbf0'}, {'depth': 1, 'linkquality': 1, 'lqi': 1, 'relationship': 1, 'routes': [], 'source': {'ieeeAddr': '0x00158d00067a0c2d', 'networkAddress': 60244}, 'sourceIeeeAddr': '0x00158d00067a0c2d', 'sourceNwkAddr': 60244, 'target': {'ieeeAddr': '0x00124b001cd4bbf0', 'networkAddress': 0}, 'targetIeeeAddr': '0x00124b001cd4bbf0'}], 'nodes': [{'definition': None, 'failed': [], 'friendlyName': 'Coordinator', 'ieeeAddr': '0x00124b001cd4bbf0', 'lastSeen': None, 'networkAddress': 0, 'type': 'Coordinator'}, {'definition': {'description': 'TRADFRI open/close remote', 'model': 'E1766', 'supports': 'battery, action, linkquality', 'vendor': 'IKEA'}, 'friendlyName': 'TRADFRI E1766_01', 'ieeeAddr': '0x588e81fffe28dec5', 'lastSeen': 1618408062253, 'manufacturerName': 'IKEA of Sweden', 'modelID': 'TRADFRI open/close remote', 'networkAddress': 39405, 'type': 'EndDevice'}, {'definition': {'description': 'Temperature and humidity sensor', 'model': 'SNZB-02', 'supports': 'battery, temperature, humidity, voltage, linkquality', 'vendor': 'SONOFF'}, 'friendlyName': 'SNZB02_01', 'ieeeAddr': '0x00124b00231e45b8', 'lastSeen': 1618407530272, 'manufacturerName': 'eWeLink', 'modelID': 'TH01', 'networkAddress': 18841, 'type': 'EndDevice'}, {'definition': {'description': 'Aqara vibration sensor', 'model': 'DJT11LM', 'supports': 'battery, action, strength, sensitivity, voltage, linkquality', 'vendor': 'Xiaomi'}, 'friendlyName': 'DJT11LM_01', 'ieeeAddr': '0x00158d00067a0c2d', 'lastSeen': 1618383303863, 'manufacturerName': 'LUMI', 'modelID': 'lumi.vibration.aq1', 'networkAddress': 60244, 'type': 'EndDevice'}]}}, 'status': 'ok', 'transaction': 'q15of-1'} - if type(payload) is dict: - self.zigbee2mqtt_plugin_devices[topic_level2]['networkmap'] = payload - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = True - else: - if self.debug_log: - self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") - - elif topic_level3 == 'config': - if topic_level4 == '': - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=config, topic_level4=, topic_level5=, payload={'commit': 'abd8a09', 'coordinator': {'meta': {'maintrel': 3, 'majorrel': 2, 'minorrel': 6, 'product': 0, 'revision': 20201127, 'transportrev': 2}, 'type': 'zStack12'}, 'log_level': 'info', 'network': {'channel': 11, 'extendedPanID': '0xdddddddddddddddd', 'panID': 6754}, 'permit_join': False, 'version': '1.18.2'} - if type(payload) is dict: - self.zigbee2mqtt_plugin_devices[topic_level2]['config'] = payload - else: - if self.debug_log: - self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") - - elif topic_level4 == 'devices': - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=config, topic_level4=devices, topic_level5=, payload=[{'dateCode': '20201127', 'friendly_name': 'Coordinator', 'ieeeAddr': '0x00124b001cd4bbf0', 'lastSeen': 1618861562211, 'networkAddress': 0, 'softwareBuildID': 'zStack12', 'type': 'Coordinator'}, {'dateCode': '20190311', 'description': 'TRADFRI open/close remote', 'friendly_name': 'TRADFRI E1766_01', 'hardwareVersion': 1, 'ieeeAddr': '0x588e81fffe28dec5', 'lastSeen': 1618511300581, 'manufacturerID': 4476, 'manufacturerName': 'IKEA of Sweden', 'model': 'E1766', 'modelID': 'TRADFRI open/close remote', 'networkAddress': 39405, 'powerSource': 'Battery', 'softwareBuildID': '2.2.010', 'type': 'EndDevice', 'vendor': 'IKEA'}, {'dateCode': '20201026', 'description': 'Temperature and humidity sensor', 'friendly_name': 'SNZB02_01', 'hardwareVersion': 1, 'ieeeAddr': '0x00124b00231e45b8', 'lastSeen': 1618861025534, 'manufacturerID': 0, 'manufacturerName': 'eWeLink', 'model': 'SNZB-02', 'modelID': 'TH01', 'networkAddress': 18841, 'powerSource': 'Battery', 'type': 'EndDevice', 'vendor': 'SONOFF'}, {'description': 'Aqara vibration sensor', 'friendly_name': 'DJT11LM_01', 'ieeeAddr': '0x00158d00067a0c2d', 'lastSeen': 1618383303863, 'manufacturerID': 4151, 'manufacturerName': 'LUMI', 'model': 'DJT11LM', 'modelID': 'lumi.vibration.aq1', 'networkAddress': 60244, 'powerSource': 'Battery', 'type': 'EndDevice', 'vendor': 'Xiaomi'}] - if type(payload) is list: - self._get_zigbee_meta_data(payload) - - elif topic_level3 == 'log': - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={"message":[{"dateCode":"20201127","friendly_name":"Coordinator","ieeeAddr":"0x00124b001cd4bbf0","lastSeen":1617961599543,"networkAddress":0,"softwareBuildID":"zStack12","type":"Coordinator"},{"dateCode":"20190311","description":"TRADFRI open/close remote","friendly_name":"TRADFRI E1766_01","hardwareVersion":1,"ieeeAddr":"0x588e81fffe28dec5","lastSeen":1617873345111,"manufacturerID":4476,"manufacturerName":"IKEA of Sweden","model":"E1766","modelID":"TRADFRI open/close remote","networkAddress":39405,"powerSource":"Battery","softwareBuildID":"2.2.010","type":"EndDevice","vendor":"IKEA"},{"dateCode":"20201026","description":"Temperature and humidity sensor","friendly_name":"SNZB02_01","hardwareVersion":1,"ieeeAddr":"0x00124b00231e45b8","lastSeen":1617961176234,"manufacturerID":0,"manufacturerName":"eWeLink","model":"SNZB-02","modelID":"TH01","networkAddress":18841,"powerSource":"Battery","type":"EndDevice","vendor":"SONOFF"}],"type":"devices"}' - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': {'friendly_name': '0x00158d00067a0c2d'}, 'type': 'device_connected'} - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': 'Publish \'set\' \'sensitivity\' to \'DJT11LM_01\' failed: \'Error: Write 0x00158d00067a0c2d/1 genBasic({"65293":{"value":21,"type":32}}, {"timeout":35000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":4447,"transactionSequenceNumber":null,"writeUndiv":false}) failed (Data request failed with error: \'MAC transaction expired\' (240))\'', 'meta': {'friendly_name': 'DJT11LM_01'}, 'type': 'zigbee_publish_error'} - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': 'announce', 'meta': {'friendly_name': 'DJT11LM_01'}, 'type': 'device_announced'} - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': {'cluster': 'genOnOff', 'from': 'TRADFRI E1766_01', 'to': 'default_bind_group'}, 'type': 'device_bind_failed'} - if isinstance(payload, dict) and 'message' in payload and 'type' in payload: - message = payload['message'] - message_type = payload['type'] - if message_type == 'devices' and isinstance(message, list): - self._get_zigbee_meta_data(message) - - elif topic_level3 == 'info': - # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=info, topic_level4=, topic_level5=, payload={'commit': 'abd8a09', 'config': {'advanced': {'adapter_concurrent': None, 'adapter_delay': None, 'availability_blacklist': [], 'availability_blocklist': [], 'availability_passlist': [], 'availability_timeout': 0, 'availability_whitelist': [], 'cache_state': True, 'cache_state_persistent': True, 'cache_state_send_on_startup': True, 'channel': 11, 'elapsed': False, 'ext_pan_id': [221, 221, 221, 221, 221, 221, 221, 221], 'homeassistant_discovery_topic': 'homeassistant', 'homeassistant_legacy_triggers': True, 'homeassistant_status_topic': 'hass/status', 'last_seen': 'disable', 'legacy_api': True, 'log_directory': '/opt/zigbee2mqtt/data/log/%TIMESTAMP%', 'log_file': 'log.txt', 'log_level': 'info', 'log_output': ['console', 'file'], 'log_rotation': True, 'log_syslog': {}, 'pan_id': 6754, 'report': False, 'soft_reset_timeout': 0, 'timestamp_format': 'YYYY-MM-DD HH:mm:ss'}, 'ban': [], 'blocklist': [], 'device_options': {}, 'devices': {'0x00124b00231e45b8': {'friendly_name': 'SNZB02_01'}, '0x00158d00067a0c2d': {'friendly_name': 'DJT11LM_01'}, '0x588e81fffe28dec5': {'friendly_name': 'TRADFRI E1766_01'}}, 'experimental': {'output': 'json'}, 'external_converters': [], 'frontend': {'host': '0.0.0.0', 'port': 8082}, 'groups': {}, 'homeassistant': False, 'map_options': {'graphviz': {'colors': {'fill': {'coordinator': '#e04e5d', 'enddevice': '#fff8ce', 'router': '#4ea3e0'}, 'font': {'coordinator': '#ffffff', 'enddevice': '#000000', 'router': '#ffffff'}, 'line': {'active': '#009900', 'inactive': '#994444'}}}}, 'mqtt': {'base_topic': 'zigbee2mqtt', 'force_disable_retain': False, 'include_device_information': True, 'keepalive': 60, 'reject_unauthorized': True, 'server': 'mqtt://localhost:1883', 'version': 4}, 'ota': {'disable_automatic_update_check': True, 'update_check_interval': 1440}, 'passlist': [], 'permit_join': False, 'serial': {'disable_led': False, 'port': '/dev/ttyACM0'}, 'whitelist': []}, 'config_schema': {'definitions': {'device': {'properties': {'debounce': {'description': 'Debounces messages of this device', 'title': 'Debounce', 'type': 'number'}, 'debounce_ignore': {'description': 'Protects unique payload values of specified payload properties from overriding within debounce time', 'examples': ['action'], 'items': {'type': 'string'}, 'title': 'Ignore debounce', 'type': 'array'}, 'filtered_attributes': {'description': 'Allows to prevent certain attributes from being published', 'examples': ['temperature', 'battery', 'action'], 'items': {'type': 'string'}, 'title': 'Filtered attributes', 'type': 'array'}, 'friendly_name': {'description': 'Used in the MQTT topic of a device. By default this is the device ID', 'readOnly': True, 'title': 'Friendly name', 'type': 'string'}, 'optimistic': {'description': 'Publish optimistic state after set (default true)', 'title': 'Optimistic', 'type': 'boolean'}, 'qos': {'descritption': 'QoS level for MQTT messages of this device', 'title': 'QoS', 'type': 'number'}, 'retain': {'description': 'Retain MQTT messages of this device', 'title': 'Retain', 'type': 'boolean'}, 'retention': {'description': 'Sets the MQTT Message Expiry in seconds, Make sure to set mqtt.version to 5', 'title': 'Retention', 'type': 'number'}}, 'required': ['friendly_name'], 'type': 'object'}, 'group': {'properties': {'devices': {'items': {'type': 'string'}, 'type': 'array'}, 'filtered_attributes': {'items': {'type': 'string'}, 'type': 'array'}, 'friendly_name': {'type': 'string'}, 'optimistic': {'type': 'boolean'}, 'qos': {'type': 'number'}, 'retain': {'type': 'boolean'}}, 'required': ['friendly_name'], 'type': 'object'}}, 'properties': {'advanced': {'properties': {'adapter_concurrent': {'description': 'Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)', 'requiresRestart': True, 'title': 'Adapter concurrency', 'type': ['number', 'null']}, 'adapter_delay': {'description': 'Adapter delay', 'requiresRestart': True, 'title': 'Adapter delay', 'type': ['number', 'null']}, 'availability_blacklist': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Availability blacklist (deprecated, use availability_blocklist)', 'type': 'array'}, 'availability_blocklist': {'description': 'Prevent devices from being checked for availability', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Availability Blocklist', 'type': 'array'}, 'availability_passlist': {'description': 'Only enable availability check for certain devices', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Availability passlist', 'type': 'array'}, 'availability_timeout': {'default': 0, 'description': 'Availability timeout in seconds when enabled, devices will be checked if they are still online. Only AC powered routers are checked for availability', 'minimum': 0, 'requiresRestart': True, 'title': 'Availability Timeout', 'type': 'number'}, 'availability_whitelist': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Availability whitelist (deprecated, use passlist)', 'type': 'array'}, 'baudrate': {'description': 'Baudrate for serial port, default: 115200 for Z-Stack, 38400 for Deconz', 'examples': [38400, 115200], 'requiresRestart': True, 'title': 'Baudrate', 'type': 'number'}, 'cache_state': {'default': True, 'description': 'MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant', 'title': 'Cache state', 'type': 'boolean'}, 'cache_state_persistent': {'default': True, 'description': 'Persist cached state, only used when cache_state: true', 'title': 'Persist cache state', 'type': 'boolean'}, 'cache_state_send_on_startup': {'default': True, 'description': 'Send cached state on startup, only used when cache_state: true', 'title': 'Send cached state on startup', 'type': 'boolean'}, 'channel': {'default': 11, 'description': 'Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)', 'examples': [11, 15, 20, 25], 'maximum': 26, 'minimum': 11, 'requiresRestart': True, 'title': 'ZigBee channel', 'type': 'number'}, 'elapsed': {'default': False, 'description': 'Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg', 'title': 'Elapsed', 'type': 'boolean'}, 'ext_pan_id': {'description': 'Zigbee extended pan ID, changing requires repairing all devices!', 'items': {'type': 'number'}, 'requiresRestart': True, 'title': 'Ext Pan ID', 'type': 'array'}, 'homeassistant_discovery_topic': {'description': 'Home Assistant discovery topic', 'examples': ['homeassistant'], 'requiresRestart': True, 'title': 'Homeassistant discovery topic', 'type': 'string'}, 'homeassistant_legacy_triggers': {'default': True, 'description': "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discoverd", 'title': 'Home Assistant legacy triggers', 'type': 'boolean'}, 'homeassistant_status_topic': {'description': 'Home Assistant status topic', 'examples': ['homeassistant/status'], 'requiresRestart': True, 'title': 'Home Assistant status topic', 'type': 'string'}, 'ikea_ota_use_test_url': {'default': False, 'description': 'Use IKEA TRADFRI OTA test server, see OTA updates documentation', 'requiresRestart': True, 'title': 'IKEA TRADFRI OTA use test url', 'type': 'boolean'}, 'last_seen': {'default': 'disable', 'description': 'Add a last_seen attribute to MQTT messages, contains date/time of last Zigbee message', 'enum': ['disable', 'ISO_8601', 'ISO_8601_local', 'epoch'], 'title': 'Last seen', 'type': 'string'}, 'legacy_api': {'default': True, 'description': 'Disables the legacy api (false = disable)', 'requiresRestart': True, 'title': 'Legacy API', 'type': 'boolean'}, 'log_directory': {'description': 'Location of log directory', 'examples': ['data/log/%TIMESTAMP%'], 'requiresRestart': True, 'title': 'Log directory', 'type': 'string'}, 'log_file': {'default': 'log.txt', 'description': 'Log file name, can also contain timestamp', 'examples': ['zigbee2mqtt_%TIMESTAMP%.log'], 'requiresRestart': True, 'title': 'Log file', 'type': 'string'}, 'log_level': {'default': 'info', 'description': 'Logging level', 'enum': ['info', 'warn', 'error', 'debug'], 'title': 'Log level', 'type': 'string'}, 'log_output': {'description': 'Output location of the log, leave empty to supress logging', 'items': {'enum': ['console', 'file', 'syslog'], 'type': 'string'}, 'requiresRestart': True, 'title': 'Log output', 'type': 'array'}, 'log_rotation': {'default': True, 'description': 'Log rotation', 'requiresRestart': True, 'title': 'Log rotation', 'type': 'boolean'}, 'log_syslog': {'properties': {'app_name': {'default': 'Zigbee2MQTT', 'description': 'The name of the application (Default: Zigbee2MQTT).', 'title': 'Localhost', 'type': 'string'}, 'eol': {'default': '/n', 'description': 'The end of line character to be added to the end of the message (Default: Message without modifications).', 'title': 'eol', 'type': 'string'}, 'host': {'default': 'localhost', 'description': 'The host running syslogd, defaults to localhost.', 'title': 'Host', 'type': 'string'}, 'localhost': {'default': 'localhost', 'description': 'Host to indicate that log messages are coming from (Default: localhost).', 'title': 'Localhost', 'type': 'string'}, 'path': {'default': '/dev/log', 'description': 'The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).', 'examples': ['/dev/log', '/var/run/syslog'], 'title': 'Path', 'type': 'string'}, 'pid': {'default': 'process.pid', 'description': 'PID of the process that log messages are coming from (Default process.pid).', 'title': 'PID', 'type': 'string'}, 'port': {'default': 123, 'description': "The port on the host that syslog is running on, defaults to syslogd's default port.", 'title': 'Port', 'type': 'number'}, 'protocol': {'default': 'tcp4', 'description': 'The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).', 'examples': ['tcp4', 'udp4', 'tls4', 'unix', 'unix-connect'], 'title': 'Protocol', 'type': 'string'}, 'type': {'default': '5424', 'description': 'The type of the syslog protocol to use (Default: BSD, also valid: 5424).', 'title': 'Type', 'type': 'string'}}, 'title': 'syslog', 'type': 'object'}, 'network_key': {'description': 'Network encryption key, changing requires repairing all devices!', 'oneOf': [{'title': 'Network key(string)', 'type': 'string'}, {'items': {'type': 'number'}, 'title': 'Network key(array)', 'type': 'array'}], 'requiresRestart': True, 'title': 'Network key'}, 'pan_id': {'description': 'ZigBee pan ID, changing requires repairing all devices!', 'oneOf': [{'title': 'Pan ID (string)', 'type': 'string'}, {'title': 'Pan ID (number)', 'type': 'number'}], 'requiresRestart': True, 'title': 'Pan ID'}, 'report': {'description': 'Enables report feature (deprecated)', 'readOnly': True, 'requiresRestart': True, 'title': 'Reporting', 'type': 'boolean'}, 'rtscts': {'description': 'RTS / CTS Hardware Flow Control for serial port', 'requiresRestart': True, 'title': 'RTS / CTS', 'type': 'boolean'}, 'soft_reset_timeout': {'description': 'Soft reset ZNP after timeout', 'minimum': 0, 'readOnly': True, 'requiresRestart': True, 'title': 'Soft reset timeout (deprecated)', 'type': 'number'}, 'timestamp_format': {'description': 'Log timestamp format', 'examples': ['YYYY-MM-DD HH:mm:ss'], 'requiresRestart': True, 'title': 'Timestamp format', 'type': 'string'}}, 'title': 'Advanced', 'type': 'object'}, 'ban': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Ban (deprecated, use blocklist)', 'type': 'array'}, 'blocklist': {'description': 'Block devices from the network (by ieeeAddr)', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Blocklist', 'type': 'array'}, 'device_options': {'type': 'object'}, 'devices': {'patternProperties': {'^.*$': {'$ref': '#/definitions/device'}}, 'propertyNames': {'pattern': '^0x[\\d\\w]{16}$'}, 'type': 'object'}, 'experimental': {'properties': {'output': {'description': 'Examples when \'state\' of a device is published json: topic: \'zigbee2mqtt/my_bulb\' payload \'{"state": "ON"}\' attribute: topic \'zigbee2mqtt/my_bulb/state\' payload \'ON\' attribute_and_json: both json and attribute (see above)', 'enum': ['attribute_and_json', 'attribute', 'json'], 'title': 'MQTT output type', 'type': 'string'}, 'transmit_power': {'description': 'Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)', 'requiresRestart': True, 'title': 'Transmit power', 'type': ['number', 'null']}}, 'title': 'Experimental', 'type': 'object'}, 'external_converters': {'description': 'You can define external converters to e.g. add support for a DiY device', 'examples': ['DIYRuZ_FreePad.js'], 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'External converters', 'type': 'array'}, 'frontend': {'properties': {'auth_token': {'description': 'Enables authentication, disabled by default', 'requiresRestart': True, 'title': 'Auth token', 'type': ['string', 'null']}, 'host': {'default': ' 0.0.0.0', 'description': 'Frontend binding host', 'requiresRestart': True, 'title': 'Bind host', 'type': 'string'}, 'port': {'default': 8080, 'description': 'Frontend binding port', 'requiresRestart': True, 'title': 'Port', 'type': 'number'}}, 'title': 'Frontend', 'type': 'object'}, 'groups': {'patternProperties': {'^.*$': {'$ref': '#/definitions/group'}}, 'propertyNames': {'pattern': '^[\\w].*$'}, 'type': 'object'}, 'homeassistant': {'default': False, 'description': 'Home Assistant integration (MQTT discovery)', 'title': 'Home Assistant integration', 'type': 'boolean'}, 'map_options': {'properties': {'graphviz': {'properties': {'colors': {'properties': {'fill': {'properties': {'coordinator': {'type': 'string'}, 'enddevice': {'type': 'string'}, 'router': {'type': 'string'}}, 'type': 'object'}, 'font': {'properties': {'coordinator': {'type': 'string'}, 'enddevice': {'type': 'string'}, 'router': {'type': 'string'}}, 'type': 'object'}, 'line': {'properties': {'active': {'type': 'string'}, 'inactive': {'type': 'string'}}, 'type': 'object'}}, 'type': 'object'}}, 'type': 'object'}}, 'title': 'Networkmap', 'type': 'object'}, 'mqtt': {'properties': {'base_topic': {'description': 'MQTT base topic for Zigbee2MQTT MQTT messages', 'examples': ['zigbee2mqtt'], 'requiresRestart': True, 'title': 'Base topic', 'type': 'string'}, 'ca': {'description': 'Absolute path to SSL/TLS certificate of CA used to sign server and client certificates', 'examples': ['/etc/ssl/mqtt-ca.crt'], 'requiresRestart': True, 'title': 'Certificate authority', 'type': 'string'}, 'cert': {'description': 'Absolute path to SSL/TLS certificate for client-authentication', 'examples': ['/etc/ssl/mqtt-client.crt'], 'requiresRestart': True, 'title': 'SSL/TLS certificate', 'type': 'string'}, 'client_id': {'description': 'MQTT client ID', 'examples': ['MY_CLIENT_ID'], 'requiresRestart': True, 'title': 'Client ID', 'type': 'string'}, 'force_disable_retain': {'default': False, 'description': "Disable retain for all send messages. ONLY enable if you MQTT broker doesn't support retained message (e.g. AWS IoT core, Azure IoT Hub, Google Cloud IoT core, IBM Watson IoT Platform). Enabling will break the Home Assistant integration", 'requiresRestart': True, 'title': 'Force disable retain', 'type': 'boolean'}, 'include_device_information': {'default': False, 'description': 'Include device information to mqtt messages', 'title': 'Include device information', 'type': 'boolean'}, 'keepalive': {'default': 60, 'description': 'MQTT keepalive in second', 'requiresRestart': True, 'title': 'Keepalive', 'type': 'number'}, 'key': {'description': 'Absolute path to SSL/TLS key for client-authentication', 'examples': ['/etc/ssl/mqtt-client.key'], 'requiresRestart': True, 'title': 'SSL/TLS key', 'type': 'string'}, 'password': {'description': 'MQTT server authentication password', 'examples': ['ILOVEPELMENI'], 'requiresRestart': True, 'title': 'Password', 'type': 'string'}, 'reject_unauthorized': {'default': True, 'description': 'Disable self-signed SSL certificate', 'requiresRestart': True, 'title': 'Reject unauthorized', 'type': 'boolean'}, 'server': {'description': 'MQTT server URL (use mqtts:// for SSL/TLS connection)', 'examples': ['mqtt://localhost:1883'], 'requiresRestart': True, 'title': 'MQTT server', 'type': 'string'}, 'user': {'description': 'MQTT server authentication user', 'examples': ['johnnysilverhand'], 'requiresRestart': True, 'title': 'User', 'type': 'string'}, 'version': {'default': 4, 'description': 'MQTT protocol version', 'examples': [4, 5], 'requiresRestart': True, 'title': 'Version', 'type': ['number', 'null']}}, 'required': ['base_topic', 'server'], 'title': 'MQTT', 'type': 'object'}, 'ota': {'properties': {'disable_automatic_update_check': {'default': False, 'description': 'Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.', 'title': 'Disable automatic update check', 'type': 'boolean'}, 'update_check_interval': {'default': 1440, 'description': 'Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.', 'title': 'Update check interval', 'type': 'number'}}, 'title': 'OTA updates', 'type': 'object'}, 'passlist': {'description': 'Allow only certain devices to join the network (by ieeeAddr). Note that all devices not on the passlist will be removed from the network!', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Passlist', 'type': 'array'}, 'permit_join': {'default': False, 'description': 'Allow new devices to join (re-applied at restart)', 'title': 'Permit join', 'type': 'boolean'}, 'serial': {'properties': {'adapter': {'description': 'Adapter type, not needed unless you are experiencing problems', 'enum': ['deconz', 'zstack', 'zigate', 'ezsp'], 'requiresRestart': True, 'title': 'Adapter', 'type': ['string', 'null']}, 'disable_led': {'default': False, 'description': 'Disable LED of the adapter if supported', 'requiresRestart': True, 'title': 'Disable led', 'type': 'boolean'}, 'port': {'description': 'Location of the adapter. To autodetect the port, set null', 'examples': ['/dev/ttyACM0'], 'requiresRestart': True, 'title': 'Port', 'type': ['string', 'null']}}, 'title': 'Serial', 'type': 'object'}, 'whitelist': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Whitelist (deprecated, use passlist)', 'type': 'array'}}, 'required': ['mqtt'], 'type': 'object'}, 'coordinator': {'meta': {'maintrel': 3, 'majorrel': 2, 'minorrel': 6, 'product': 0, 'revision': 20201127, 'transportrev': 2}, 'type': 'zStack12'}, 'log_level': 'info', 'network': {'channel': 11, 'extended_pan_id': '0xdddddddddddddddd', 'pan_id': 6754}, 'permit_join': False, 'restart_required': False, 'version': '1.18.2'} - if topic_level4 == '': - if type(payload) is dict: - self.zigbee2mqtt_plugin_devices[topic_level2]['info'] = payload - self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = True - else: - if self.debug_log: - self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") - - elif topic_level3 == 'event': - # {"type":"device_joined","data":{"friendly_name":"0x90fd9ffffe6494fc","ieee_address":"0x90fd9ffffe6494fc"}} - # {"type":"device_announce","data":{"friendly_name":"0x90fd9ffffe6494fc","ieee_address":"0x90fd9ffffe6494fc"}} - # {"type":"device_interview","data":{"friendly_name":"0x90fd9ffffe6494fc","status":"started","ieee_address":"0x90fd9ffffe6494fc"}} - # {"type":"device_interview","data":{"friendly_name":"0x90fd9ffffe6494fc","status":"successful","ieee_address":"0x90fd9ffffe6494fc","supported":true,"definition":{"model":"LED1624G9","vendor":"IKEA","description":"TRADFRI LED bulb E14/E26/E27 600 lumen, dimmable, color, opal white"}}} - # {"type":"device_interview","data":{"friendly_name":"0x90fd9ffffe6494fc","status":"failed","ieee_address":"0x90fd9ffffe6494fc"}} - # {"type":"device_leave","data":{"ieee_address":"0x90fd9ffffe6494fc"}} - if topic_level4 == '': - # event_type = payload.get('type') - if self.debug_log: - self.logger.debug(f"event info message not implemented yet.") - - elif topic_level3 == 'devices': - if self.debug_log: - self.logger.debug(f"zigbee2mqtt/bridge/devices not implemented yet. Raw msg was: {payload}.") - - # if isinstance(payload, list): - # for entry in payload: - # - # friendly_name = entry['friendly_name'] - # exposes = entry['definition']['exposes'] + z2m_base, device, topic_3, topic_4, topic_5, *_ = (topic + '////').split('/') + self.logger.debug(f"received mqtt msg: z2m_base={z2m_base}, device={device}, topic_3={topic_3}, topic_4={topic_4}, topic_5={topic_5}, payload={payload}") - else: - if self.debug_log: - self.logger.debug(f"Function type message not implemented yet.") - - # Handle Data from connected devices - elif (topic_level3 + topic_level4 + topic_level5) == '': - # topic_level1=zigbee2mqtt, topic_level2=SNZB02_01, topic_level3=, topic_level4=, topic_level5=, payload '{"battery":100,"device":{"applicationVersion":5,"dateCode":"20201026","friendlyName":"SNZB02_01","hardwareVersion":1,"ieeeAddr":"0x00124b00231e45b8","manufacturerID":0,"manufacturerName":"eWeLink","model":"SNZB-02","networkAddress":18841,"powerSource":"Battery","type":"EndDevice","zclVersion":1},"humidity":45.12,"linkquality":157,"temperature":16.26,"voltage":3200}' - # topic_level1=zigbee2mqtt, topic_level2=TRADFRI E1766_01, topic_level3=, topic_level4=, topic_level5=, payload={'battery': 74, 'device': {'applicationVersion': 33, 'dateCode': '20190311', 'friendlyName': 'TRADFRI E1766_01', 'hardwareVersion': 1, 'ieeeAddr': '0x588e81fffe28dec5', 'manufacturerID': 4476, 'manufacturerName': 'IKEA of Sweden', 'model': 'E1766', 'networkAddress': 39405, 'powerSource': 'Battery', 'softwareBuildID': '2.2.010', 'stackVersion': 98, 'type': 'EndDevice', 'zclVersion': 3}, 'linkquality': 141} - # topic_level1=zigbee2mqtt, topic_level2=LEDVANCE_E27_TW_01, topic_level3=, topic_level4=, topic_level5=, payload={'brightness': 254, 'color': {'x': 0.4599, 'y': 0.4106}, 'color_mode': 'color_temp', 'color_temp': 370, 'color_temp_startup': 65535, 'last_seen': 1632943562477, 'linkquality': 39, 'state': 'ON', 'update': {'state': 'idle'}, 'update_available': False} - # topic_level1=zigbee2mqtt, topic_level2=0xf0d1b800001574df, topic_level3=, topic_level4=, topic_level5=, payload={'brightness': 166, 'color': {'hue': 296, 'saturation': 69}, 'color_mode': 'hs', 'color_temp': 405, 'last_seen': 1638183778409, 'linkquality': 159, 'state': 'ON', 'update': {'state': 'idle'}, 'update_available': False} - - if type(payload) is dict: - # Wenn Geräte zur Laufzeit des Plugins hinzugefügt werden, werden diese im dict ergänzt - if not self.zigbee2mqtt_devices.get(topic_level2): - self.zigbee2mqtt_devices[topic_level2] = {} - self.logger.info(f"New device discovered: {topic_level2}") - - # Korrekturen in der Payload - - # Umbenennen des Key 'friendlyName' in 'friendly_name', damit er identisch zu denen aus Log Topic und Config Topic ist - if 'device' in payload: - meta = payload['device'] - if 'friendlyName' in meta: - meta['friendly_name'] = meta.pop('friendlyName') - del payload['device'] - - if not self.zigbee2mqtt_devices[topic_level2].get('meta'): - self.zigbee2mqtt_devices[topic_level2]['meta'] = {} - self.zigbee2mqtt_devices[topic_level2]['meta'].update(meta) - - # Korrektur des Lastseen - if 'last_seen' in payload: - last_seen = payload['last_seen'] - if isinstance(last_seen, int): - payload.update({'last_seen': datetime.fromtimestamp(last_seen / 1000)}) - elif isinstance(last_seen, str): - try: - payload.update({'last_seen': datetime.strptime(last_seen, "%Y-%m-%dT%H:%M:%S.%fZ").replace(microsecond=0)}) - except Exception as e: - if self.debug_log: - self.logger.debug(f"Error {e} occurred during decoding of last_seen using format '%Y-%m-%dT%H:%M:%S.%fZ'.") - try: - payload.update({'last_seen': datetime.strptime(last_seen, "%Y-%m-%dT%H:%M:%SZ")}) - except Exception as e: - if self.debug_log: - self.logger.debug(f"Error {e} occurred during decoding of last_seen using format '%Y-%m-%dT%H:%M:%SZ'.") - - # Korrektur der Brightness von 0-254 auf 0-100% - if 'brightness' in payload: - try: - payload.update({'brightness': int(round(payload['brightness'] * 100 / 255, 0))}) - except Exception as e: - if self.debug_log: - self.logger.debug(f"Error {e} occurred during decoding of brightness.") + if z2m_base != self.z2m_base: + self.logger.error(f'received mqtt msg with wrong base topic {topic}. Please report') + return + + # check handlers + if hasattr(self, HANDLE_IN_PREFIX + HANDLE_DEV + device): + if getattr(self, HANDLE_IN_PREFIX + HANDLE_DEV + device)(device, topic_3, topic_4, topic_5, payload, qos, retain): + return + + if not isinstance(payload, dict): + return - # Korrektur der Farbtemperatur von "mired scale" (Reziproke Megakelvin) auf Kelvin - if 'color_temp' in payload: + # Wenn Geräte zur Laufzeit des Plugins hinzugefügt werden, werden diese im dict ergänzt + if device not in self._devices: + self._devices[device] = {} + self.logger.info(f"New device discovered: {device}") + + # Korrekturen in der Payload + + # Umbenennen des Key 'friendlyName' in 'friendly_name', damit er identisch zu denen aus Log Topic und Config Topic ist + if 'device' in payload: + meta = payload['device'] + if 'friendlyName' in meta: + meta['friendly_name'] = meta.pop('friendlyName') + del payload['device'] + + if 'meta' not in self._devices[device]: + self._devices[device]['meta'] = {} + self._devices[device]['meta'].update(meta) + + # Korrektur des Lastseen + if 'last_seen' in payload: + last_seen = payload['last_seen'] + if isinstance(last_seen, int): + payload.update({'last_seen': datetime.fromtimestamp(last_seen / 1000)}) + elif isinstance(last_seen, str): + try: + payload.update({'last_seen': datetime.strptime(last_seen, "%Y-%m-%dT%H:%M:%S.%fZ").replace(microsecond=0)}) + except Exception: try: - payload.update({'color_temp': int(round(10000 / int(payload['color_temp']), 0)) * 100}) + payload.update({'last_seen': datetime.strptime(last_seen, "%Y-%m-%dT%H:%M:%SZ")}) except Exception as e: - if self.debug_log: - self.logger.debug(f"Error {e} occurred during decoding of color_temp.") - - # Verarbeitung von Farbdaten - if 'color_mode' in payload and 'color' in payload: - color_mode = payload['color_mode'] - color = payload.pop('color') - - if color_mode == 'hs': - payload['hue'] = color['hue'] - payload['saturation'] = color['saturation'] - - if color_mode == 'xy': - payload['color_x'] = color['x'] - payload['color_y'] = color['y'] - - if not self.zigbee2mqtt_devices[topic_level2].get('data'): - self.zigbee2mqtt_devices[topic_level2]['data'] = {} - self.zigbee2mqtt_devices[topic_level2]['data'].update(payload) - - # Setzen des Itemwertes - if topic_level2 in list(self.zigbee2mqtt_plugin_devices.keys()): - if self.debug_log: - self.logger.debug(f"Item to be checked for update and to be updated") - for element in payload: - itemtype = f"item_{element}" - value = payload[element] - item = self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items'].get(itemtype, None) - src = self.get_shortname() + ':' + topic_level2 - if self.debug_log: - self.logger.debug(f"element: {element}, itemtype: {itemtype}, value: {value}, item: {item}") - - if item is not None: - item(value, src) - self.logger.info(f"{topic_level2}: Item '{item.id()}' set to value {value}") - else: - self.logger.info(f"{topic_level2}: No item for itemtype '{itemtype}' defined to set to {value}") - - # self.zigbee2mqtt_plugin_devices[topic_level2]['online_timeout'] = datetime.now()+timedelta(seconds=self._cycle+5) - - @staticmethod - def _build_topic_str(topic_level1: str, topic_level2: str, topic_level3: str, topic_level4: str, topic_level5: str) -> str: - """ - Build the mqtt topic as string - - :param topic_level1: base topic of topic to publish - :param topic_level2: unique part of topic to publish - :param topic_level3: level3 of topic to publish - :param topic_level4: level4 of topic to publish - :param topic_level5: level5 of topic to publish - """ + self.logger.debug(f"Error {e} occurred during decoding of last_seen using format '%Y-%m-%dT%H:%M:%SZ'.") + + if 'data' not in self._devices[device]: + self._devices[device]['data'] = {} + self._devices[device]['data'].update(payload) + + # Setzen des Itemwertes + for attr in payload: + if attr in self._devices[device]: + item = self._devices[device][attr].get('item') + src = self.get_shortname() + ':' + device + + # check handlers + if hasattr(self, HANDLE_IN_PREFIX + HANDLE_ATTR + attr): + if getattr(self, HANDLE_IN_PREFIX + HANDLE_ATTR + attr)(device, attr, payload, item): + return + + value = payload[attr] + self._devices[device][attr]['value'] = value + self.logger.debug(f"attribute: {attr}, value: {value}, item: {item}") + + if item is not None: + item(value, src) + self.logger.info(f"{device}: Item '{item}' set to value {value}") + else: + self.logger.info(f"{device}: No item for attribute '{attr}' defined to set to {value}") - tpc = f"{topic_level1}/{topic_level2}" - if topic_level3 != '': - tpc = f"{tpc}/{topic_level3}" - if topic_level4 != '': - tpc = f"{tpc}/{topic_level4}" - if topic_level5 != '': - tpc = f"{tpc}/{topic_level5}" - return tpc + def _build_topic_str(self, device: str, topic_3: str, topic_4: str, topic_5: str) -> str: + """ Build the mqtt topic as string """ + return "/".join(filter(None, (self.z2m_base, device, topic_3, topic_4, topic_5))) - def _get_zigbee_meta_data(self, device_data: list): + def _get_device_data(self, device_data: list, is_group=False): """ Extract the Zigbee Meta-Data for a certain device out of the device_data :param device_data: Payload of the bridge config message + :param is_group: indicates wether device is a real device or a group """ - + # device_data is list of dicts for element in device_data: if type(element) is dict: device = element.get('friendly_name') if device: if 'lastSeen' in element: element.update({'lastSeen': datetime.fromtimestamp(element['lastSeen'] / 1000)}) - if not self.zigbee2mqtt_devices.get(device): - self.zigbee2mqtt_devices[device] = {} - if not self.zigbee2mqtt_devices[device].get('meta'): - self.zigbee2mqtt_devices[device]['meta'] = {} - self.zigbee2mqtt_devices[device]['meta'].update(element) - else: - if self.debug_log: - self.logger.debug(f"(Received payload {device_data} is not of type dict") - @staticmethod - def _bool2str(bool_value: bool, typus: int) -> str: - """ - Turns bool value to string + # create device entry if needed + if device not in self._devices: + self._devices[device] = {'isgroup': is_group} - :param bool_value: bool value - :param typus: type of string the bool_value will be transferred to - :return: string containing bool expression - """ + # easier to read + _device = self._devices[device] - if type(bool_value) is bool: - if typus == 1: - result = 'ON' if bool_value is True else 'OFF' - elif typus == 2: - result = 'an' if bool_value is True else 'aus' - elif typus == 3: - result = 'ja' if bool_value is True else 'nein' - else: - result = 'typus noch nicht definiert' - else: - result = 'Wert ist nicht vom Type bool' - return result + # scenes in devices + try: + for endpoint in element['endpoints']: + if element['endpoints'][endpoint].get('scenes'): + _device['scenes'] = { + name: id for id, name in (x.values() for x in element['endpoints'][endpoint]['scenes']) + } + except KeyError: + pass + + # scenes in groups + if element.get('scenes'): + _device['scenes'] = { + name: id for id, name in (scene.values() for scene in element['scenes']) + } + + # put list of scene names in scenelist item + # key "scenelist" only present if attr/device requested in item tree + if _device.get('scenelist'): + try: + scenelist = list(_device['scenes'].keys()) + _device['scenelist']['item'](scenelist) + except (KeyError, ValueError, TypeError): + pass - def _get_current_status_of_all_devices_linked_to_items(self): - """ - Try to get current status of all devices linked to items; Works only if value es exposed - """ + # TODO: possibly remove after further parsing + # just copy meta + if 'meta' not in _device: + _device['meta'] = {} + _device['meta'].update(element) - for device in self.zigbee2mqtt_plugin_devices: - attribut = (list(self.zigbee2mqtt_plugin_devices[device]['connected_items'].keys())[0])[5:] - payload = '{"' + str(attribut) + '" : ""}' - self.publish_zigbee2mqtt_topic(self.topic_level1, str(device), 'get', '', '', payload) + # TODO: parse meta and extract valid values for attrs - def _handle_hex_in_topic_level2(self, topic_level2: str, item) -> str: - """ - check if zigbee device short name has been used without parentheses; if so this will be normally parsed to a number and therefore mismatch with defintion - """ + self.logger.info(f'Imported {"group" if is_group else "device"} {device}') + else: + self.logger.debug(f"(Received payload {device_data} is not of type dict") - try: - topic_level2 = int(topic_level2) - self.logger.warning(f"Probably for item {item.id()} the IEEE Adress has been used for item attribute 'zigbee2mqtt_topic'. Trying to make that work but it will cause exceptions. To prevent this, the short name need to be defined as string by using parentheses") - topic_level2 = str(hex(topic_level2)) - except Exception: - pass - return topic_level2 + def _read_all_data(self): + """ Try to get current status of all devices linked to items """ - def _get_zigbee2mqtt_topic_from_item(self, item) -> str: - """ - Get zigbee2mqtt_topic for given item search from given item in parent direction - """ + for device in self._devices: + for attr in self._devices[device]: + a = self._devices[device][attr] + if a.get('read', False) and a.get('item') is not None: + self.publish_z2m_topic(device, attr, 'get') - zigbee2mqtt_topic = None + def _get_z2m_topic_from_item(self, item) -> str: + """ Get z2m_topic for given item search from given item in parent direction """ + topic = '' lookup_item = item - for i in range(3): - zigbee2mqtt_topic = self.get_iattr_value(lookup_item.conf, 'zigbee2mqtt_topic') - if zigbee2mqtt_topic is not None: + for _ in range(3): + topic = self.get_iattr_value(lookup_item.conf, Z2M_TOPIC) + if topic: break else: lookup_item = lookup_item.return_parent() - if zigbee2mqtt_topic is None: - self.logger.error('zigbee2mqtt_topic is not defined or instance not given') - else: - zigbee2mqtt_topic = self._handle_hex_in_topic_level2(zigbee2mqtt_topic, item) + return topic + +# +# special handlers for devices / attributes +# +# handlers_in: activated if values come in from mqtt +# +# def handle_in_dev_(self, device: str, topic_3: str = "", topic_4: str = "", topic_5: str = "", payload={}, qos=None, retain=None) +# def handle_in_attr_(self, device: str, attr: str, payload={}, item=None) +# +# return True: stop further processing +# return False/None: continue processing (possibly with changed payload) +# + + def _handle_in_dev_bridge(self, device: str, topic_3: str = "", topic_4: str = "", topic_5: str = "", payload={}, qos=None, retain=None): + """ handle device topics for "bridge" """ - return zigbee2mqtt_topic + # catch AssertionError + try: + # easier to read + _bridge = self._devices[device] + + if topic_3 == 'state': + self.logger.debug(f"state: detail: {topic_3} datetime: {datetime.now()} payload: {payload}") + _bridge['online'] = bool(payload) + + elif topic_3 in ('config', 'info'): + assert isinstance(payload, dict), 'dict' + _bridge[topic_3] = payload + _bridge['online'] = True + + elif topic_3 == 'response' and topic_4 in ('health_check', 'permit_join', 'networkmap'): + assert isinstance(payload, dict), 'dict' + _bridge[topic_4] = payload + _bridge['online'] = True + + if topic_4 == 'health_check': + _bridge['online'] = bool(payload['data']['healthy']) + + elif topic_3 == 'devices' or topic_3 == 'groups': + assert isinstance(payload, list), 'list' + self._get_device_data(payload, topic_4 == 'groups') + + for entry in payload: + friendly_name = entry.get('friendly_name') + if friendly_name in self._devices: + try: + self._devices[friendly_name]['exposes'] = entry['definition']['exposes'] + except (KeyError, TypeError): + pass + + elif topic_3 == 'log': + assert isinstance(payload, dict), 'dict' + if 'message' in payload and payload.get('type') == 'devices': + self._get_device_data(payload['message']) + + else: + self.logger.debug(f"Function type message bridge/{topic_3}/{topic_4} not implemented yet.") + except AssertionError as e: + self.logger.debug(f'Response format not of type {e}, ignoring data') + + def _handle_in_attr_color(self, device: str, attr: str, payload={}, item=None): + """ automatically sync rgb items """ + if item is not None: + col = payload['color'] + if 'x' in col and 'y' in col and 'brightness' in payload: + r, g, b = self._color_xy_to_rgb(color=col, brightness=payload['brightness'] / 254) + try: + items_default = True + item_r = item.r + item_g = item.g + item_b = item.b + except AttributeError: + items_default = False + + try: + items_custom = True + # try to get user-specified items to override default + item_r = self._devices[device]['color_r']['item'] + item_g = self._devices[device]['color_g']['item'] + item_b = self._devices[device]['color_b']['item'] + except (AttributeError, KeyError): + items_custom = False + + if not items_default and not items_custom: + return + + try: + item_r(r, self.get_shortname()) + item_g(g, self.get_shortname()) + item_b(b, self.get_shortname()) + except Exception as e: + self.logger.warning(f'Trying to set rgb color values for color item {item}, but appropriate subitems ({item_r}, {item_g}, {item_b}) missing: {e}') + + try: + target = self._devices[device]['color_rgb']['item'] + except (AttributeError, KeyError): + return + target(f'{r:x}{g:x}{b:x}', self.get_shortname()) + + def _handle_in_attr_brightness(self, device: str, attr: str, payload={}, item=None): + """ automatically set brightness percent """ + if item is not None: + target = None + try: + target = item.percent + except AttributeError: + pass + + try: + target = self._devices[device]['brightness_percent']['item'] + except (AttributeError, KeyError): + pass + + if target is not None: + target(payload['brightness'] / 2.54, self.get_shortname()) + + def _handle_in_attr_color_temp(self, device: str, attr: str, payload={}, item=None): + """ automatically set color temp in kelvin """ + if item is not None: + target = None + try: + target = item.kelvin + except AttributeError: + pass + + try: + target = self._devices[device]['color_temp_kelvin']['item'] + except (AttributeError, KeyError): + pass + + if target is not None: + target(int(1000000 / payload['color_temp']), self.get_shortname()) + +# +# handlers out: activated when values are sent out from shng +# +# def _handle_out_(self, item, value, topic_3, topic_4, topic_5, device, attr): +# return value, topic_3, topic_4, topic_5, abort +# + + def _handle_out_dev_bridge(self, item, value, topic_3, topic_4, topic_5, device, attr): + # statically defined cmds for interaction with z2m-gateway + # independent from connected devices + bridge_cmds = { + 'permit_join': {'setval': 'VAL', 't5': ''}, + 'health_check': {'setval': None, 't5': ''}, + 'restart': {'setval': None, 't5': ''}, + 'networkmap': {'setval': 'raw', 't5': 'remove'}, + 'device_remove': {'setval': 'STR', 't5': ''}, + 'device_configure': {'setval': 'STR', 't5': ''}, + 'device_options': {'setval': 'STR', 't5': ''}, + 'device_rename': {'setval': 'STR', 't5': ''} + } + + if attr in bridge_cmds: + topic_3 = 'request' + topic_4 = attr + topic_5 = bridge_cmds[attr]['t5'] + payload = '' + if attr.startswith('device_'): + topic_4, topic_5 = attr.split('_') + if bridge_cmds[attr]['setval'] == 'VAL': + payload = value + if bridge_cmds[attr]['setval'] == 'STR': + payload = str(value) + elif bridge_cmds[attr]['setval'] == 'PATH': + payload = item.property.path + + value = payload + + return value, topic_3, topic_4, topic_5, False + + def _handle_out_attr_color_r(self, item, value, topic_3, topic_4, topic_5, device, attr): + try: + self._color_sync_from_rgb(self._devices[device]['state']['item']) + except Exception as e: + self.logger.debug(f'problem calling color sync: {e}') + return value, topic_3, topic_4, topic_5, True + + def _handle_out_attr_color_g(self, item, value, topic_3, topic_4, topic_5, device, attr): + try: + self._color_sync_from_rgb(self._devices[device]['state']['item']) + except Exception as e: + self.logger.debug(f'problem calling color sync: {e}') + return value, topic_3, topic_4, topic_5, True + + def _handle_out_attr_color_b(self, item, value, topic_3, topic_4, topic_5, device, attr): + try: + self._color_sync_from_rgb(self._devices[device]['state']['item']) + except Exception as e: + self.logger.debug(f'problem calling color sync: {e}') + return value, topic_3, topic_4, topic_5, True + + def _handle_out_attr_brightness_percent(self, item, value, topic_3, topic_4, topic_5, device, attr): + brightness = value * 2.54 + try: + self._devices[device]['brightness']['item'](brightness) + except (KeyError, AttributeError): + pass + return value, topic_3, topic_4, topic_5, True + + def _handle_out_attr_color_temp_kelvin(self, item, value, topic_3, topic_4, topic_5, device, attr): + kelvin = int(1000000 / value) + try: + self._devices[device]['color_temp']['item'](kelvin) + except (KeyError, AttributeError): + pass + return value, topic_3, topic_4, topic_5, True + + def _handle_out_attr_color_rgb(self, item, value, topic_3, topic_4, topic_5, device, attr): + if item is not None: + try: + col = {} + rgb = item() + col['r'] = int(rgb[0:2], 16) + col['g'] = int(rgb[2:4], 16) + col['b'] = int(rgb[4:6], 16) + + for color in ('r', 'g', 'b'): + target = None + try: + target = getattr(item.return_parent(), color) + except AttributeError: + pass + + try: + target = self._devices[device]['color_' + color]['item'] + except (AttributeError, KeyError): + pass + + if target is not None: + target(col[color], self.get_shortname()) + + self._color_sync_from_rgb(self._devices[device]['state']['item']) + + except Exception as e: + self.logger.debug(f'problem calling color sync: {e}') + + return value, topic_3, topic_4, topic_5, True + +# +# Attention - color conversions xy/rgb: +# due to - probably - rounding differences, this ist not a true +# 1:1 conversion. Use at your own discretion... +# + + def _color_xy_to_rgb(self, color={}, x=None, y=None, brightness=1): + if color: + x = color.get('x') + y = color.get('y') + c = Converter() + return c.xy_to_rgb(x, y, brightness) + + def _color_rgb_to_xy(self, r, g, b): + c = Converter() + return c.rgb_to_xyb(r, g, b) + + def _color_sync_to_rgb(self, item, source=''): + """ sync xy color to rgb, needs struct items """ + self._item_color_xy_to_rgb(item.color, item.brightness, item.color.r, item.color.g, item.color.b, source) + + def _color_sync_from_rgb(self, item, source='', rgb=''): + """ sync rgb color to xy, needs struct items """ + self._item_color_rgb_to_xy(item.color.r, item.color.g, item.color.b, item.color, item.brightness, source) + + def _item_color_xy_to_rgb(self, item_xy, item_brightness, item_r, item_g, item_b, source=''): + """ convert xy and brightness item data to rgb and assign """ + try: + x = item_xy()['x'] + y = item_xy()['y'] + except (ValueError, TypeError, KeyError): + self.logger.warning(f"Item {item_xy} doesn't contain a valid {{'x': x, 'y': y}} color definition: {item_xy()}") + return + + try: + bright = item_brightness() + if bright < 0 or bright > 254: + raise TypeError + bright /= 254 + except (ValueError, TypeError): + self.logger.warning(f"Item {item_brightness} doesn't contain a valid brightness value: {item_brightness()}") + return + + r, g, b = self._color_xy_to_rgb(x=x, y=y, brightness=bright) + + try: + item_r(r, source) + item_g(g, source) + item_b(b, source) + except (ValueError, TypeError): + self.logger.warning(f"Error on assigning rgb values {r},{g},{b} to items {item_r}, {item_g}, {item_b}") + + def _item_color_rgb_to_xy(self, item_r, item_g, item_b, item_xy, item_brightness, source=''): + """ convert r, g, b items data to xy and brightness and assign """ + try: + r = item_r() + g = item_g() + b = item_b() + except (ValueError, TypeError): + self.logger.warning(f"Error on getting rgb values from items {item_r}, {item_g}, {item_b}") + return + + x, y, bright = self._color_rgb_to_xy(r, g, b) + + try: + item_xy({'x': x, 'y': y}, source) + item_brightness(bright * 254, source) + except (ValueError, TypeError): + self.logger.warning(f"Error on assigning values {x},{y}, {bright} to items {item_xy} and {item_brightness}") + return diff --git a/zigbee2mqtt/_pv_1_1_2/__init__.py b/zigbee2mqtt/_pv_1_1_2/__init__.py new file mode 100755 index 000000000..eeb800dc8 --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/__init__.py @@ -0,0 +1,680 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2021- Michael Wenzel wenzel_michael@web.de +######################################################################### +# This file is part of SmartHomeNG. +# +# This plugin connect Zigbee2MQTT to SmartHomeNG. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +from datetime import datetime +import logging + +from lib.model.mqttplugin import * +from lib.utils import Utils +from .webif import WebInterface + + +class Zigbee2Mqtt(MqttPlugin): + """ + Main class of the Plugin. Does all plugin specific stuff and provides the update functions for the items + """ + + PLUGIN_VERSION = '1.1.2' + + def __init__(self, sh): + """ + Initializes the plugin. + """ + + # Call init code of parent class (MqttPlugin) + super().__init__() + if not self._init_complete: + return + + self.logger.info('Init Zigbee2Mqtt Plugin') + + # Enable / Disable debug log generation depending on log level + if self.logger.isEnabledFor(logging.DEBUG): + self.debug_log = True + else: + self.debug_log = False + + # get the parameters for the plugin (as defined in metadata plugin.yaml): + self.topic_level1 = self.get_parameter_value('base_topic').lower() + self.cycle = self.get_parameter_value('poll_period') + self.read_at_init = self.get_parameter_value('read_at_init') + self.webif_pagelength = self.get_parameter_value('webif_pagelength') + + # Initialization code goes here + self.zigbee2mqtt_devices = {} # to hold device information for web interface; contains data of all found devices + self.zigbee2mqtt_plugin_devices = {} # to hold device information for web interface; contains data of all devices addressed in items + self.zigbee2mqtt_items = [] # to hold item information for web interface; contains list of all items + + # Add subscription to get bridge announces + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'devices', '', '', 'list', callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'state', '', '', 'bool', bool_values=['offline', 'online'], callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'info', '', '', 'dict', callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'log', '', '', 'dict', callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'extensions', '', '', 'list', callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'config', '', '', 'dict', callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'groups', '', '', 'list', callback=self.on_mqtt_announce) + self.add_zigbee2mqtt_subscription(self.topic_level1, 'bridge', 'response', '', '', 'dict', callback=self.on_mqtt_announce) + + # Add subscription to get device announces + self.add_zigbee2mqtt_subscription(self.topic_level1, '+', '', '', '', 'dict', callback=self.on_mqtt_announce) + + self.local_ip = '' + self.alive = None + + # if plugin should start even without web interface + self.init_webinterface(WebInterface) + return + + # ToDo: Verarbeiten der bridge_devices bridge/log/device + + def run(self): + """ + Run method for the plugin + """ + + self.logger.debug("Run method called") + + # get local ip + self.local_ip = Utils.get_local_ipv4_address() + self.logger.info(f"local ip adress is {self.local_ip}") + + # start subscription to all topics + self.start_subscriptions() + + self.scheduler_add('poll_bridge', self.poll_bridge, cycle=self.cycle) + self.publish_zigbee2mqtt_topic(self.topic_level1, 'bridge', 'config', 'devices', 'get', '') + + if self.read_at_init: + self.publish_zigbee2mqtt_topic(self.topic_level1, 'bridge', 'request', 'restart', '', '') + + self.alive = True + + try: + self._get_current_status_of_all_devices_linked_to_items() + except Exception: + pass + + def stop(self): + """ + Stop method for the plugin + """ + + self.alive = False + self.logger.debug("Stop method called") + self.scheduler_remove('poll_bridge') + + # stop subscription to all topics + self.stop_subscriptions() + return + + def parse_item(self, item): + """ + Default plugin parse_item method. Is called when the plugin is initialized. + The plugin can, corresponding to its attribute keywords, decide what to do with + the item in future, like adding it to an internal array for future reference + :param item: The item to process. + :return: If the plugin needs to be informed of an items change you should return a call back function + like the function update_item down below. An example when this is needed is the knx plugin + where parse_item returns the update_item function when the attribute knx_send is found. + This means that when the items value is about to be updated, the call back function is called + with the item, caller, source and dest as arguments and in case of the knx plugin the value + can be sent to the knx with a knx write function within the knx plugin. + """ + + if self.has_iattr(item.conf, 'zigbee2mqtt_attr'): + if self.debug_log: + self.logger.debug(f"parsing item: {item.id()}") + + zigbee2mqtt_attr = self.get_iattr_value(item.conf, 'zigbee2mqtt_attr').lower() + topic_level2 = self._get_zigbee2mqtt_topic_from_item(item) + + if not topic_level2: + return + + if not self.zigbee2mqtt_plugin_devices.get(topic_level2): + self.zigbee2mqtt_plugin_devices[topic_level2] = {} + self.zigbee2mqtt_plugin_devices[topic_level2]['connected_to_item'] = False + self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items'] = {} + + self.zigbee2mqtt_plugin_devices[topic_level2]['connected_to_item'] = True + self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items']['item_' + zigbee2mqtt_attr] = item + if zigbee2mqtt_attr == 'online': + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = False + # append to list used for web interface + if item not in self.zigbee2mqtt_items: + self.zigbee2mqtt_items.append(item) + + return self.update_item + + def update_item(self, item, caller=None, source=None, dest=None): + """ + Item has been updated + + :param item: item to be updated towards the plugin + :param caller: if given it represents the callers name + :param source: if given it represents the source + :param dest: if given it represents the dest + """ + + if self.debug_log: + self.logger.debug(f"update_item: {item.id()} called by {caller} and source {source}") + + if self.alive and self.get_shortname() not in caller: + # code to execute if the plugin is not stopped AND only, if the item has not been changed for this plugin + + # get zigbee2mqtt attributes of caller item + topic_level2 = self.get_iattr_value(item.conf, 'zigbee2mqtt_topic') + topic_level2 = self._handle_hex_in_topic_level2(topic_level2, item) + + zigbee2mqtt_attr = self.get_iattr_value(item.conf, 'zigbee2mqtt_attr') + + if zigbee2mqtt_attr in ['bridge_permit_join', 'bridge_health_check', 'bridge_restart', 'bridge_networkmap_raw', 'device_remove', + 'device_ota_update_check', 'device_ota_update_update', 'device_configure', 'device_options', 'device_rename', + 'device_bind', 'device_unbind', 'device_configure_reporting', 'state', 'color_temp', 'brightness', 'hue', 'saturation']: + + self.logger.info(f"update_item: {item.id()}, item has been changed in SmartHomeNG outside of this plugin in {caller} with value {item()}") + payload = None + bool_values = None + topic_level3 = topic_level4 = topic_level5 = '' + + if zigbee2mqtt_attr == 'bridge_permit_join': + topic_level3 = 'request' + topic_level4 = 'permit_join' + payload = item() + bool_values = ['false', 'true'] + elif zigbee2mqtt_attr == 'bridge_health_check': + topic_level3 = 'request' + topic_level4 = 'health_check' + payload = '' + elif zigbee2mqtt_attr == 'bridge_restart': + topic_level3 = 'request' + topic_level4 = 'restart' + payload = '' + elif zigbee2mqtt_attr == 'bridge_networkmap_raw': + topic_level3 = 'request' + topic_level4 = 'networkmap' + payload = 'raw' + elif zigbee2mqtt_attr == 'device_remove': + topic_level3 = 'request' + topic_level4 = 'device' + topic_level5 = 'remove' + payload = str(item()) + # elif zigbee2mqtt_attr == 'device_ota_update_check': + # topic_level3 = 'request' + # topic_level4 = 'device' + # payload = 'raw' + # bool_values = None + # elif zigbee2mqtt_attr == 'device_ota_update_update': + # topic_level3 = 'request' + # topic_level4 = 'device' + # payload = 'raw' + # bool_values = None + elif zigbee2mqtt_attr == 'device_configure': + topic_level3 = 'request' + topic_level4 = 'device' + topic_level5 = 'configure' + payload = str(item()) + elif zigbee2mqtt_attr == 'device_options': + topic_level3 = 'request' + topic_level4 = 'device' + topic_level5 = 'options' + payload = str(item()) + elif zigbee2mqtt_attr == 'device_rename': + topic_level3 = 'request' + topic_level4 = 'device' + topic_level5 = 'rename' + payload = str(item()) + elif zigbee2mqtt_attr == 'state': + topic_level3 = 'set' + payload = '{' + f'"state" : "{self._bool2str(item(), 1)}"' + '}' + elif zigbee2mqtt_attr == 'brightness': + topic_level3 = 'set' + value = int(round(item() * 255 / 100, 0)) # Umrechnung von 0-100% in 0-254 + if value < 0 or value > 255: + self.logger.warning(f'commanded value for brightness not within allowed range; set to next valid value') + value = 0 if value < 0 else 255 + payload = '{' + f'"brightness" : "{value}"' + '}' + elif zigbee2mqtt_attr == 'color_temp': + topic_level3 = 'set' + value = int(round(1000000 / item(), 0)) + # mired scale + if value < 150 or value > 500: + self.logger.warning(f' commanded value for brightness not within allowed range; set to next valid value') + value = 150 if value < 150 else 500 + payload = '{' + f'"color_temp" : "{value}"' + '}' + elif zigbee2mqtt_attr == 'hue': + topic_level3 = 'set' + hue = item() + saturation_item = self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items']['item_saturation'] + saturation = saturation_item() + if hue < 0 or hue > 359: + self.logger.warning(f'commanded value for hue not within allowed range; set to next valid value') + hue = 0 if hue < 0 else 359 + payload = '{"color":{' + f'"hue":{hue}, "saturation":{saturation}' + '}}' + elif zigbee2mqtt_attr == 'saturation': + topic_level3 = 'set' + saturation = item() + hue_item = self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items']['item_hue'] + hue = hue_item() + if saturation < 0 or saturation > 100: + self.logger.warning(f'commanded value for hue not within allowed range; set to next valid value') + saturation = 0 if saturation < 0 else 100 + payload = '{"color":{' + f'"hue":{hue}, "saturation":{saturation}' + '}}' + else: + self.logger.warning(f"update_item: {item.id()}, attribut {zigbee2mqtt_attr} not implemented yet (by {caller})") + + if payload is not None: + self.publish_zigbee2mqtt_topic(self.topic_level1, topic_level2, topic_level3, topic_level4, topic_level5, payload, item, bool_values=bool_values) + else: + self.logger.warning(f"update_item: {item.id()}, no value/payload defined (by {caller})") + else: + self.logger.warning(f"update_item: {item.id()}, trying to change item in SmartHomeNG that is readonly (by {caller})") + + def poll_bridge(self): + """ + Polls for health state of the bridge + """ + + self.logger.info("poll_bridge: Checking online and health status of bridge") + self.publish_zigbee2mqtt_topic(self.topic_level1, 'bridge', 'request', 'health_check', '', '') + + for topic_level2 in self.zigbee2mqtt_plugin_devices: + if self.zigbee2mqtt_plugin_devices[topic_level2].get('online') is True and self.zigbee2mqtt_plugin_devices[topic_level2].get('online_timeout') is True: + if self.zigbee2mqtt_plugin_devices[topic_level2]['online_timeout'] < datetime.now(): + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = False + self._set_item_value(topic_level2, 'item_online', False, 'poll_device') + self.logger.info(f"poll_device: {topic_level2} is not online any more - online_timeout={self.zigbee2mqtt_plugin_devices[topic_level2]['online_timeout']}, now={datetime.now()}") + + def add_zigbee2mqtt_subscription(self, topic_level1: str, topic_level2: str, topic_level3: str, topic_level4: str, topic_level5: str, payload_type: str, bool_values: list = None, item=None, callback=None): + """ + build the topic in zigbee2mqtt style and add the subscription to mqtt + + :param topic_level1: basetopic of topic to subscribe to + :param topic_level2: unique part of topic to subscribe to + :param topic_level3: level3 of topic to subscribe to + :param topic_level4: level4 of topic to subscribe to + :param topic_level5: level5 of topic to subscribe to + :param payload_type: payload type of the topic (for this subscription to the topic) + :param bool_values: bool values (for this subscription to the topic) + :param item: item that should receive the payload as value. Used by the standard handler (if no callback function is specified) + :param callback: a plugin can provide an own callback function, if special handling of the payload is needed + :return: None + """ + + tpc = self._build_topic_str(topic_level1, topic_level2, topic_level3, topic_level4, topic_level5) + self.add_subscription(tpc, payload_type, bool_values=bool_values, callback=callback) + + def publish_zigbee2mqtt_topic(self, topic_level1: str, topic_level2: str, topic_level3: str, topic_level4: str, topic_level5: str, payload, item=None, qos: int = None, retain: bool = False, bool_values: list = None): + """ + build the topic in zigbee2mqtt style and publish to mqtt + + :param topic_level1: basetopic of topic to publish + :param topic_level2: unique part of topic to publish; ZigbeeDevice + :param topic_level3: level3 of topic to publish + :param topic_level4: level4 of topic to publish + :param topic_level5: level5 of topic to publish + :param payload: payload to publish + :param item: item (if relevant) + :param qos: qos for this message (optional) + :param retain: retain flag for this message (optional) + :param bool_values: bool values (for publishing this topic, optional) + :return: None + """ + + tpc = self._build_topic_str(topic_level1, topic_level2, topic_level3, topic_level4, topic_level5) + # self.logger.debug(f"Publish to topic <{tpc}> with payload <{payload}>") + self.publish_topic(tpc, payload, item, qos, retain, bool_values) + + def on_mqtt_announce(self, topic: str, payload, qos=None, retain=None): + """ + Callback function to handle received messages + + :param topic: mqtt topic + :param payload: mqtt message payload + :param qos: qos for this message (optional) + :param retain: retain flag for this message (optional) + """ + + wrk = topic.split('/') + topic_level1 = wrk[0] + topic_level2 = wrk[1] + topic_level3 = '' + topic_level4 = '' + topic_level5 = '' + if len(wrk) > 2: + topic_level3 = wrk[2] + if len(wrk) > 3: + topic_level4 = wrk[3] + if len(wrk) > 4: + topic_level5 = wrk[4] + + if self.debug_log: + self.logger.debug(f"on_mqtt_announce: topic_level1={topic_level1}, topic_level2={topic_level2}, topic_level3={topic_level3}, topic_level4={topic_level4}, topic_level5={topic_level5}, payload={payload}") + + # Handle data from bridge + if topic_level2 == 'bridge': + if topic_level3 == 'state': + # Payloads are 'online' and 'offline'; equal to LWT + if self.debug_log: + self.logger.debug(f"LWT: detail: {topic_level3} datetime: {datetime.now()} payload: {payload}") + if topic_level2 not in self.zigbee2mqtt_plugin_devices: + self.zigbee2mqtt_plugin_devices[topic_level2] = {} + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = bool(payload) + + elif topic_level3 == 'response': + if topic_level4 == 'health_check': + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=response, topic_level4=health_check, topic_level5=, payload={'data': {'healthy': True}, 'status': 'ok'} + if type(payload) is dict: + self.zigbee2mqtt_plugin_devices[topic_level2]['health_status'] = payload + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = bool(payload['data']['healthy']) + else: + if self.debug_log: + self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") + + elif topic_level4 == 'permit_join': + # {"data":{"value":true},"status":"ok"} + if type(payload) is dict: + self.zigbee2mqtt_plugin_devices[topic_level2]['permit_join'] = payload + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = True + else: + if self.debug_log: + self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") + + elif topic_level4 == 'networkmap': + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=None, topic_level4=networkmap, topic_level5=None, payload={'data': {'routes': False, 'type': 'raw', 'value': {'links': [{'depth': 1, 'linkquality': 5, 'lqi': 5, 'relationship': 1, 'routes': [], 'source': {'ieeeAddr': '0x588e81fffe28dec5', 'networkAddress': 39405}, 'sourceIeeeAddr': '0x588e81fffe28dec5', 'sourceNwkAddr': 39405, 'target': {'ieeeAddr': '0x00124b001cd4bbf0', 'networkAddress': 0}, 'targetIeeeAddr': '0x00124b001cd4bbf0'}, {'depth': 1, 'linkquality': 155, 'lqi': 155, 'relationship': 1, 'routes': [], 'source': {'ieeeAddr': '0x00124b00231e45b8', 'networkAddress': 18841}, 'sourceIeeeAddr': '0x00124b00231e45b8', 'sourceNwkAddr': 18841, 'target': {'ieeeAddr': '0x00124b001cd4bbf0', 'networkAddress': 0}, 'targetIeeeAddr': '0x00124b001cd4bbf0'}, {'depth': 1, 'linkquality': 1, 'lqi': 1, 'relationship': 1, 'routes': [], 'source': {'ieeeAddr': '0x00158d00067a0c2d', 'networkAddress': 60244}, 'sourceIeeeAddr': '0x00158d00067a0c2d', 'sourceNwkAddr': 60244, 'target': {'ieeeAddr': '0x00124b001cd4bbf0', 'networkAddress': 0}, 'targetIeeeAddr': '0x00124b001cd4bbf0'}], 'nodes': [{'definition': None, 'failed': [], 'friendlyName': 'Coordinator', 'ieeeAddr': '0x00124b001cd4bbf0', 'lastSeen': None, 'networkAddress': 0, 'type': 'Coordinator'}, {'definition': {'description': 'TRADFRI open/close remote', 'model': 'E1766', 'supports': 'battery, action, linkquality', 'vendor': 'IKEA'}, 'friendlyName': 'TRADFRI E1766_01', 'ieeeAddr': '0x588e81fffe28dec5', 'lastSeen': 1618408062253, 'manufacturerName': 'IKEA of Sweden', 'modelID': 'TRADFRI open/close remote', 'networkAddress': 39405, 'type': 'EndDevice'}, {'definition': {'description': 'Temperature and humidity sensor', 'model': 'SNZB-02', 'supports': 'battery, temperature, humidity, voltage, linkquality', 'vendor': 'SONOFF'}, 'friendlyName': 'SNZB02_01', 'ieeeAddr': '0x00124b00231e45b8', 'lastSeen': 1618407530272, 'manufacturerName': 'eWeLink', 'modelID': 'TH01', 'networkAddress': 18841, 'type': 'EndDevice'}, {'definition': {'description': 'Aqara vibration sensor', 'model': 'DJT11LM', 'supports': 'battery, action, strength, sensitivity, voltage, linkquality', 'vendor': 'Xiaomi'}, 'friendlyName': 'DJT11LM_01', 'ieeeAddr': '0x00158d00067a0c2d', 'lastSeen': 1618383303863, 'manufacturerName': 'LUMI', 'modelID': 'lumi.vibration.aq1', 'networkAddress': 60244, 'type': 'EndDevice'}]}}, 'status': 'ok', 'transaction': 'q15of-1'} + if type(payload) is dict: + self.zigbee2mqtt_plugin_devices[topic_level2]['networkmap'] = payload + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = True + else: + if self.debug_log: + self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") + + elif topic_level3 == 'config': + if topic_level4 == '': + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=config, topic_level4=, topic_level5=, payload={'commit': 'abd8a09', 'coordinator': {'meta': {'maintrel': 3, 'majorrel': 2, 'minorrel': 6, 'product': 0, 'revision': 20201127, 'transportrev': 2}, 'type': 'zStack12'}, 'log_level': 'info', 'network': {'channel': 11, 'extendedPanID': '0xdddddddddddddddd', 'panID': 6754}, 'permit_join': False, 'version': '1.18.2'} + if type(payload) is dict: + self.zigbee2mqtt_plugin_devices[topic_level2]['config'] = payload + else: + if self.debug_log: + self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") + + elif topic_level4 == 'devices': + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=config, topic_level4=devices, topic_level5=, payload=[{'dateCode': '20201127', 'friendly_name': 'Coordinator', 'ieeeAddr': '0x00124b001cd4bbf0', 'lastSeen': 1618861562211, 'networkAddress': 0, 'softwareBuildID': 'zStack12', 'type': 'Coordinator'}, {'dateCode': '20190311', 'description': 'TRADFRI open/close remote', 'friendly_name': 'TRADFRI E1766_01', 'hardwareVersion': 1, 'ieeeAddr': '0x588e81fffe28dec5', 'lastSeen': 1618511300581, 'manufacturerID': 4476, 'manufacturerName': 'IKEA of Sweden', 'model': 'E1766', 'modelID': 'TRADFRI open/close remote', 'networkAddress': 39405, 'powerSource': 'Battery', 'softwareBuildID': '2.2.010', 'type': 'EndDevice', 'vendor': 'IKEA'}, {'dateCode': '20201026', 'description': 'Temperature and humidity sensor', 'friendly_name': 'SNZB02_01', 'hardwareVersion': 1, 'ieeeAddr': '0x00124b00231e45b8', 'lastSeen': 1618861025534, 'manufacturerID': 0, 'manufacturerName': 'eWeLink', 'model': 'SNZB-02', 'modelID': 'TH01', 'networkAddress': 18841, 'powerSource': 'Battery', 'type': 'EndDevice', 'vendor': 'SONOFF'}, {'description': 'Aqara vibration sensor', 'friendly_name': 'DJT11LM_01', 'ieeeAddr': '0x00158d00067a0c2d', 'lastSeen': 1618383303863, 'manufacturerID': 4151, 'manufacturerName': 'LUMI', 'model': 'DJT11LM', 'modelID': 'lumi.vibration.aq1', 'networkAddress': 60244, 'powerSource': 'Battery', 'type': 'EndDevice', 'vendor': 'Xiaomi'}] + if type(payload) is list: + self._get_zigbee_meta_data(payload) + + elif topic_level3 == 'log': + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={"message":[{"dateCode":"20201127","friendly_name":"Coordinator","ieeeAddr":"0x00124b001cd4bbf0","lastSeen":1617961599543,"networkAddress":0,"softwareBuildID":"zStack12","type":"Coordinator"},{"dateCode":"20190311","description":"TRADFRI open/close remote","friendly_name":"TRADFRI E1766_01","hardwareVersion":1,"ieeeAddr":"0x588e81fffe28dec5","lastSeen":1617873345111,"manufacturerID":4476,"manufacturerName":"IKEA of Sweden","model":"E1766","modelID":"TRADFRI open/close remote","networkAddress":39405,"powerSource":"Battery","softwareBuildID":"2.2.010","type":"EndDevice","vendor":"IKEA"},{"dateCode":"20201026","description":"Temperature and humidity sensor","friendly_name":"SNZB02_01","hardwareVersion":1,"ieeeAddr":"0x00124b00231e45b8","lastSeen":1617961176234,"manufacturerID":0,"manufacturerName":"eWeLink","model":"SNZB-02","modelID":"TH01","networkAddress":18841,"powerSource":"Battery","type":"EndDevice","vendor":"SONOFF"}],"type":"devices"}' + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': {'friendly_name': '0x00158d00067a0c2d'}, 'type': 'device_connected'} + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': 'Publish \'set\' \'sensitivity\' to \'DJT11LM_01\' failed: \'Error: Write 0x00158d00067a0c2d/1 genBasic({"65293":{"value":21,"type":32}}, {"timeout":35000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":4447,"transactionSequenceNumber":null,"writeUndiv":false}) failed (Data request failed with error: \'MAC transaction expired\' (240))\'', 'meta': {'friendly_name': 'DJT11LM_01'}, 'type': 'zigbee_publish_error'} + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': 'announce', 'meta': {'friendly_name': 'DJT11LM_01'}, 'type': 'device_announced'} + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=log, topic_level4=, topic_level5=, payload={'message': {'cluster': 'genOnOff', 'from': 'TRADFRI E1766_01', 'to': 'default_bind_group'}, 'type': 'device_bind_failed'} + if isinstance(payload, dict) and 'message' in payload and 'type' in payload: + message = payload['message'] + message_type = payload['type'] + if message_type == 'devices' and isinstance(message, list): + self._get_zigbee_meta_data(message) + + elif topic_level3 == 'info': + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=info, topic_level4=, topic_level5=, payload={'commit': 'abd8a09', 'config': {'advanced': {'adapter_concurrent': None, 'adapter_delay': None, 'availability_blacklist': [], 'availability_blocklist': [], 'availability_passlist': [], 'availability_timeout': 0, 'availability_whitelist': [], 'cache_state': True, 'cache_state_persistent': True, 'cache_state_send_on_startup': True, 'channel': 11, 'elapsed': False, 'ext_pan_id': [221, 221, 221, 221, 221, 221, 221, 221], 'homeassistant_discovery_topic': 'homeassistant', 'homeassistant_legacy_triggers': True, 'homeassistant_status_topic': 'hass/status', 'last_seen': 'disable', 'legacy_api': True, 'log_directory': '/opt/zigbee2mqtt/data/log/%TIMESTAMP%', 'log_file': 'log.txt', 'log_level': 'info', 'log_output': ['console', 'file'], 'log_rotation': True, 'log_syslog': {}, 'pan_id': 6754, 'report': False, 'soft_reset_timeout': 0, 'timestamp_format': 'YYYY-MM-DD HH:mm:ss'}, 'ban': [], 'blocklist': [], 'device_options': {}, 'devices': {'0x00124b00231e45b8': {'friendly_name': 'SNZB02_01'}, '0x00158d00067a0c2d': {'friendly_name': 'DJT11LM_01'}, '0x588e81fffe28dec5': {'friendly_name': 'TRADFRI E1766_01'}}, 'experimental': {'output': 'json'}, 'external_converters': [], 'frontend': {'host': '0.0.0.0', 'port': 8082}, 'groups': {}, 'homeassistant': False, 'map_options': {'graphviz': {'colors': {'fill': {'coordinator': '#e04e5d', 'enddevice': '#fff8ce', 'router': '#4ea3e0'}, 'font': {'coordinator': '#ffffff', 'enddevice': '#000000', 'router': '#ffffff'}, 'line': {'active': '#009900', 'inactive': '#994444'}}}}, 'mqtt': {'base_topic': 'zigbee2mqtt', 'force_disable_retain': False, 'include_device_information': True, 'keepalive': 60, 'reject_unauthorized': True, 'server': 'mqtt://localhost:1883', 'version': 4}, 'ota': {'disable_automatic_update_check': True, 'update_check_interval': 1440}, 'passlist': [], 'permit_join': False, 'serial': {'disable_led': False, 'port': '/dev/ttyACM0'}, 'whitelist': []}, 'config_schema': {'definitions': {'device': {'properties': {'debounce': {'description': 'Debounces messages of this device', 'title': 'Debounce', 'type': 'number'}, 'debounce_ignore': {'description': 'Protects unique payload values of specified payload properties from overriding within debounce time', 'examples': ['action'], 'items': {'type': 'string'}, 'title': 'Ignore debounce', 'type': 'array'}, 'filtered_attributes': {'description': 'Allows to prevent certain attributes from being published', 'examples': ['temperature', 'battery', 'action'], 'items': {'type': 'string'}, 'title': 'Filtered attributes', 'type': 'array'}, 'friendly_name': {'description': 'Used in the MQTT topic of a device. By default this is the device ID', 'readOnly': True, 'title': 'Friendly name', 'type': 'string'}, 'optimistic': {'description': 'Publish optimistic state after set (default true)', 'title': 'Optimistic', 'type': 'boolean'}, 'qos': {'descritption': 'QoS level for MQTT messages of this device', 'title': 'QoS', 'type': 'number'}, 'retain': {'description': 'Retain MQTT messages of this device', 'title': 'Retain', 'type': 'boolean'}, 'retention': {'description': 'Sets the MQTT Message Expiry in seconds, Make sure to set mqtt.version to 5', 'title': 'Retention', 'type': 'number'}}, 'required': ['friendly_name'], 'type': 'object'}, 'group': {'properties': {'devices': {'items': {'type': 'string'}, 'type': 'array'}, 'filtered_attributes': {'items': {'type': 'string'}, 'type': 'array'}, 'friendly_name': {'type': 'string'}, 'optimistic': {'type': 'boolean'}, 'qos': {'type': 'number'}, 'retain': {'type': 'boolean'}}, 'required': ['friendly_name'], 'type': 'object'}}, 'properties': {'advanced': {'properties': {'adapter_concurrent': {'description': 'Adapter concurrency (e.g. 2 for CC2531 or 16 for CC26X2R1) (default: null, uses recommended value)', 'requiresRestart': True, 'title': 'Adapter concurrency', 'type': ['number', 'null']}, 'adapter_delay': {'description': 'Adapter delay', 'requiresRestart': True, 'title': 'Adapter delay', 'type': ['number', 'null']}, 'availability_blacklist': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Availability blacklist (deprecated, use availability_blocklist)', 'type': 'array'}, 'availability_blocklist': {'description': 'Prevent devices from being checked for availability', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Availability Blocklist', 'type': 'array'}, 'availability_passlist': {'description': 'Only enable availability check for certain devices', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Availability passlist', 'type': 'array'}, 'availability_timeout': {'default': 0, 'description': 'Availability timeout in seconds when enabled, devices will be checked if they are still online. Only AC powered routers are checked for availability', 'minimum': 0, 'requiresRestart': True, 'title': 'Availability Timeout', 'type': 'number'}, 'availability_whitelist': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Availability whitelist (deprecated, use passlist)', 'type': 'array'}, 'baudrate': {'description': 'Baudrate for serial port, default: 115200 for Z-Stack, 38400 for Deconz', 'examples': [38400, 115200], 'requiresRestart': True, 'title': 'Baudrate', 'type': 'number'}, 'cache_state': {'default': True, 'description': 'MQTT message payload will contain all attributes, not only changed ones. Has to be true when integrating via Home Assistant', 'title': 'Cache state', 'type': 'boolean'}, 'cache_state_persistent': {'default': True, 'description': 'Persist cached state, only used when cache_state: true', 'title': 'Persist cache state', 'type': 'boolean'}, 'cache_state_send_on_startup': {'default': True, 'description': 'Send cached state on startup, only used when cache_state: true', 'title': 'Send cached state on startup', 'type': 'boolean'}, 'channel': {'default': 11, 'description': 'Zigbee channel, changing requires repairing all devices! (Note: use a ZLL channel: 11, 15, 20, or 25 to avoid Problems)', 'examples': [11, 15, 20, 25], 'maximum': 26, 'minimum': 11, 'requiresRestart': True, 'title': 'ZigBee channel', 'type': 'number'}, 'elapsed': {'default': False, 'description': 'Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg', 'title': 'Elapsed', 'type': 'boolean'}, 'ext_pan_id': {'description': 'Zigbee extended pan ID, changing requires repairing all devices!', 'items': {'type': 'number'}, 'requiresRestart': True, 'title': 'Ext Pan ID', 'type': 'array'}, 'homeassistant_discovery_topic': {'description': 'Home Assistant discovery topic', 'examples': ['homeassistant'], 'requiresRestart': True, 'title': 'Homeassistant discovery topic', 'type': 'string'}, 'homeassistant_legacy_triggers': {'default': True, 'description': "Home Assistant legacy triggers, when enabled Zigbee2mqt will send an empty 'action' or 'click' after one has been send. A 'sensor_action' and 'sensor_click' will be discoverd", 'title': 'Home Assistant legacy triggers', 'type': 'boolean'}, 'homeassistant_status_topic': {'description': 'Home Assistant status topic', 'examples': ['homeassistant/status'], 'requiresRestart': True, 'title': 'Home Assistant status topic', 'type': 'string'}, 'ikea_ota_use_test_url': {'default': False, 'description': 'Use IKEA TRADFRI OTA test server, see OTA updates documentation', 'requiresRestart': True, 'title': 'IKEA TRADFRI OTA use test url', 'type': 'boolean'}, 'last_seen': {'default': 'disable', 'description': 'Add a last_seen attribute to MQTT messages, contains date/time of last Zigbee message', 'enum': ['disable', 'ISO_8601', 'ISO_8601_local', 'epoch'], 'title': 'Last seen', 'type': 'string'}, 'legacy_api': {'default': True, 'description': 'Disables the legacy api (false = disable)', 'requiresRestart': True, 'title': 'Legacy API', 'type': 'boolean'}, 'log_directory': {'description': 'Location of log directory', 'examples': ['data/log/%TIMESTAMP%'], 'requiresRestart': True, 'title': 'Log directory', 'type': 'string'}, 'log_file': {'default': 'log.txt', 'description': 'Log file name, can also contain timestamp', 'examples': ['zigbee2mqtt_%TIMESTAMP%.log'], 'requiresRestart': True, 'title': 'Log file', 'type': 'string'}, 'log_level': {'default': 'info', 'description': 'Logging level', 'enum': ['info', 'warn', 'error', 'debug'], 'title': 'Log level', 'type': 'string'}, 'log_output': {'description': 'Output location of the log, leave empty to supress logging', 'items': {'enum': ['console', 'file', 'syslog'], 'type': 'string'}, 'requiresRestart': True, 'title': 'Log output', 'type': 'array'}, 'log_rotation': {'default': True, 'description': 'Log rotation', 'requiresRestart': True, 'title': 'Log rotation', 'type': 'boolean'}, 'log_syslog': {'properties': {'app_name': {'default': 'Zigbee2MQTT', 'description': 'The name of the application (Default: Zigbee2MQTT).', 'title': 'Localhost', 'type': 'string'}, 'eol': {'default': '/n', 'description': 'The end of line character to be added to the end of the message (Default: Message without modifications).', 'title': 'eol', 'type': 'string'}, 'host': {'default': 'localhost', 'description': 'The host running syslogd, defaults to localhost.', 'title': 'Host', 'type': 'string'}, 'localhost': {'default': 'localhost', 'description': 'Host to indicate that log messages are coming from (Default: localhost).', 'title': 'Localhost', 'type': 'string'}, 'path': {'default': '/dev/log', 'description': 'The path to the syslog dgram socket (i.e. /dev/log or /var/run/syslog for OS X).', 'examples': ['/dev/log', '/var/run/syslog'], 'title': 'Path', 'type': 'string'}, 'pid': {'default': 'process.pid', 'description': 'PID of the process that log messages are coming from (Default process.pid).', 'title': 'PID', 'type': 'string'}, 'port': {'default': 123, 'description': "The port on the host that syslog is running on, defaults to syslogd's default port.", 'title': 'Port', 'type': 'number'}, 'protocol': {'default': 'tcp4', 'description': 'The network protocol to log over (e.g. tcp4, udp4, tls4, unix, unix-connect, etc).', 'examples': ['tcp4', 'udp4', 'tls4', 'unix', 'unix-connect'], 'title': 'Protocol', 'type': 'string'}, 'type': {'default': '5424', 'description': 'The type of the syslog protocol to use (Default: BSD, also valid: 5424).', 'title': 'Type', 'type': 'string'}}, 'title': 'syslog', 'type': 'object'}, 'network_key': {'description': 'Network encryption key, changing requires repairing all devices!', 'oneOf': [{'title': 'Network key(string)', 'type': 'string'}, {'items': {'type': 'number'}, 'title': 'Network key(array)', 'type': 'array'}], 'requiresRestart': True, 'title': 'Network key'}, 'pan_id': {'description': 'ZigBee pan ID, changing requires repairing all devices!', 'oneOf': [{'title': 'Pan ID (string)', 'type': 'string'}, {'title': 'Pan ID (number)', 'type': 'number'}], 'requiresRestart': True, 'title': 'Pan ID'}, 'report': {'description': 'Enables report feature (deprecated)', 'readOnly': True, 'requiresRestart': True, 'title': 'Reporting', 'type': 'boolean'}, 'rtscts': {'description': 'RTS / CTS Hardware Flow Control for serial port', 'requiresRestart': True, 'title': 'RTS / CTS', 'type': 'boolean'}, 'soft_reset_timeout': {'description': 'Soft reset ZNP after timeout', 'minimum': 0, 'readOnly': True, 'requiresRestart': True, 'title': 'Soft reset timeout (deprecated)', 'type': 'number'}, 'timestamp_format': {'description': 'Log timestamp format', 'examples': ['YYYY-MM-DD HH:mm:ss'], 'requiresRestart': True, 'title': 'Timestamp format', 'type': 'string'}}, 'title': 'Advanced', 'type': 'object'}, 'ban': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Ban (deprecated, use blocklist)', 'type': 'array'}, 'blocklist': {'description': 'Block devices from the network (by ieeeAddr)', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Blocklist', 'type': 'array'}, 'device_options': {'type': 'object'}, 'devices': {'patternProperties': {'^.*$': {'$ref': '#/definitions/device'}}, 'propertyNames': {'pattern': '^0x[\\d\\w]{16}$'}, 'type': 'object'}, 'experimental': {'properties': {'output': {'description': 'Examples when \'state\' of a device is published json: topic: \'zigbee2mqtt/my_bulb\' payload \'{"state": "ON"}\' attribute: topic \'zigbee2mqtt/my_bulb/state\' payload \'ON\' attribute_and_json: both json and attribute (see above)', 'enum': ['attribute_and_json', 'attribute', 'json'], 'title': 'MQTT output type', 'type': 'string'}, 'transmit_power': {'description': 'Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)', 'requiresRestart': True, 'title': 'Transmit power', 'type': ['number', 'null']}}, 'title': 'Experimental', 'type': 'object'}, 'external_converters': {'description': 'You can define external converters to e.g. add support for a DiY device', 'examples': ['DIYRuZ_FreePad.js'], 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'External converters', 'type': 'array'}, 'frontend': {'properties': {'auth_token': {'description': 'Enables authentication, disabled by default', 'requiresRestart': True, 'title': 'Auth token', 'type': ['string', 'null']}, 'host': {'default': ' 0.0.0.0', 'description': 'Frontend binding host', 'requiresRestart': True, 'title': 'Bind host', 'type': 'string'}, 'port': {'default': 8080, 'description': 'Frontend binding port', 'requiresRestart': True, 'title': 'Port', 'type': 'number'}}, 'title': 'Frontend', 'type': 'object'}, 'groups': {'patternProperties': {'^.*$': {'$ref': '#/definitions/group'}}, 'propertyNames': {'pattern': '^[\\w].*$'}, 'type': 'object'}, 'homeassistant': {'default': False, 'description': 'Home Assistant integration (MQTT discovery)', 'title': 'Home Assistant integration', 'type': 'boolean'}, 'map_options': {'properties': {'graphviz': {'properties': {'colors': {'properties': {'fill': {'properties': {'coordinator': {'type': 'string'}, 'enddevice': {'type': 'string'}, 'router': {'type': 'string'}}, 'type': 'object'}, 'font': {'properties': {'coordinator': {'type': 'string'}, 'enddevice': {'type': 'string'}, 'router': {'type': 'string'}}, 'type': 'object'}, 'line': {'properties': {'active': {'type': 'string'}, 'inactive': {'type': 'string'}}, 'type': 'object'}}, 'type': 'object'}}, 'type': 'object'}}, 'title': 'Networkmap', 'type': 'object'}, 'mqtt': {'properties': {'base_topic': {'description': 'MQTT base topic for Zigbee2MQTT MQTT messages', 'examples': ['zigbee2mqtt'], 'requiresRestart': True, 'title': 'Base topic', 'type': 'string'}, 'ca': {'description': 'Absolute path to SSL/TLS certificate of CA used to sign server and client certificates', 'examples': ['/etc/ssl/mqtt-ca.crt'], 'requiresRestart': True, 'title': 'Certificate authority', 'type': 'string'}, 'cert': {'description': 'Absolute path to SSL/TLS certificate for client-authentication', 'examples': ['/etc/ssl/mqtt-client.crt'], 'requiresRestart': True, 'title': 'SSL/TLS certificate', 'type': 'string'}, 'client_id': {'description': 'MQTT client ID', 'examples': ['MY_CLIENT_ID'], 'requiresRestart': True, 'title': 'Client ID', 'type': 'string'}, 'force_disable_retain': {'default': False, 'description': "Disable retain for all send messages. ONLY enable if you MQTT broker doesn't support retained message (e.g. AWS IoT core, Azure IoT Hub, Google Cloud IoT core, IBM Watson IoT Platform). Enabling will break the Home Assistant integration", 'requiresRestart': True, 'title': 'Force disable retain', 'type': 'boolean'}, 'include_device_information': {'default': False, 'description': 'Include device information to mqtt messages', 'title': 'Include device information', 'type': 'boolean'}, 'keepalive': {'default': 60, 'description': 'MQTT keepalive in second', 'requiresRestart': True, 'title': 'Keepalive', 'type': 'number'}, 'key': {'description': 'Absolute path to SSL/TLS key for client-authentication', 'examples': ['/etc/ssl/mqtt-client.key'], 'requiresRestart': True, 'title': 'SSL/TLS key', 'type': 'string'}, 'password': {'description': 'MQTT server authentication password', 'examples': ['ILOVEPELMENI'], 'requiresRestart': True, 'title': 'Password', 'type': 'string'}, 'reject_unauthorized': {'default': True, 'description': 'Disable self-signed SSL certificate', 'requiresRestart': True, 'title': 'Reject unauthorized', 'type': 'boolean'}, 'server': {'description': 'MQTT server URL (use mqtts:// for SSL/TLS connection)', 'examples': ['mqtt://localhost:1883'], 'requiresRestart': True, 'title': 'MQTT server', 'type': 'string'}, 'user': {'description': 'MQTT server authentication user', 'examples': ['johnnysilverhand'], 'requiresRestart': True, 'title': 'User', 'type': 'string'}, 'version': {'default': 4, 'description': 'MQTT protocol version', 'examples': [4, 5], 'requiresRestart': True, 'title': 'Version', 'type': ['number', 'null']}}, 'required': ['base_topic', 'server'], 'title': 'MQTT', 'type': 'object'}, 'ota': {'properties': {'disable_automatic_update_check': {'default': False, 'description': 'Zigbee devices may request a firmware update, and do so frequently, causing Zigbee2MQTT to reach out to third party servers. If you disable these device initiated checks, you can still initiate a firmware update check manually.', 'title': 'Disable automatic update check', 'type': 'boolean'}, 'update_check_interval': {'default': 1440, 'description': 'Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.', 'title': 'Update check interval', 'type': 'number'}}, 'title': 'OTA updates', 'type': 'object'}, 'passlist': {'description': 'Allow only certain devices to join the network (by ieeeAddr). Note that all devices not on the passlist will be removed from the network!', 'items': {'type': 'string'}, 'requiresRestart': True, 'title': 'Passlist', 'type': 'array'}, 'permit_join': {'default': False, 'description': 'Allow new devices to join (re-applied at restart)', 'title': 'Permit join', 'type': 'boolean'}, 'serial': {'properties': {'adapter': {'description': 'Adapter type, not needed unless you are experiencing problems', 'enum': ['deconz', 'zstack', 'zigate', 'ezsp'], 'requiresRestart': True, 'title': 'Adapter', 'type': ['string', 'null']}, 'disable_led': {'default': False, 'description': 'Disable LED of the adapter if supported', 'requiresRestart': True, 'title': 'Disable led', 'type': 'boolean'}, 'port': {'description': 'Location of the adapter. To autodetect the port, set null', 'examples': ['/dev/ttyACM0'], 'requiresRestart': True, 'title': 'Port', 'type': ['string', 'null']}}, 'title': 'Serial', 'type': 'object'}, 'whitelist': {'items': {'type': 'string'}, 'readOnly': True, 'requiresRestart': True, 'title': 'Whitelist (deprecated, use passlist)', 'type': 'array'}}, 'required': ['mqtt'], 'type': 'object'}, 'coordinator': {'meta': {'maintrel': 3, 'majorrel': 2, 'minorrel': 6, 'product': 0, 'revision': 20201127, 'transportrev': 2}, 'type': 'zStack12'}, 'log_level': 'info', 'network': {'channel': 11, 'extended_pan_id': '0xdddddddddddddddd', 'pan_id': 6754}, 'permit_join': False, 'restart_required': False, 'version': '1.18.2'} + if topic_level4 == '': + if type(payload) is dict: + self.zigbee2mqtt_plugin_devices[topic_level2]['info'] = payload + self.zigbee2mqtt_plugin_devices[topic_level2]['online'] = True + else: + if self.debug_log: + self.logger.debug(f"(Received payload {payload} on topic {topic} is not of type dict") + + elif topic_level3 == 'event': + # {"type":"device_joined","data":{"friendly_name":"0x90fd9ffffe6494fc","ieee_address":"0x90fd9ffffe6494fc"}} + # {"type":"device_announce","data":{"friendly_name":"0x90fd9ffffe6494fc","ieee_address":"0x90fd9ffffe6494fc"}} + # {"type":"device_interview","data":{"friendly_name":"0x90fd9ffffe6494fc","status":"started","ieee_address":"0x90fd9ffffe6494fc"}} + # {"type":"device_interview","data":{"friendly_name":"0x90fd9ffffe6494fc","status":"successful","ieee_address":"0x90fd9ffffe6494fc","supported":true,"definition":{"model":"LED1624G9","vendor":"IKEA","description":"TRADFRI LED bulb E14/E26/E27 600 lumen, dimmable, color, opal white"}}} + # {"type":"device_interview","data":{"friendly_name":"0x90fd9ffffe6494fc","status":"failed","ieee_address":"0x90fd9ffffe6494fc"}} + # {"type":"device_leave","data":{"ieee_address":"0x90fd9ffffe6494fc"}} + if topic_level4 == '': + # event_type = payload.get('type') + if self.debug_log: + self.logger.debug(f"event info message not implemented yet.") + + elif topic_level3 == 'devices': + if self.debug_log: + self.logger.debug(f"zigbee2mqtt/bridge/devices not implemented yet. Raw msg was: {payload}.") + + # if isinstance(payload, list): + # for entry in payload: + # + # friendly_name = entry['friendly_name'] + # exposes = entry['definition']['exposes'] + + else: + if self.debug_log: + self.logger.debug(f"Function type message not implemented yet.") + + # Handle Data from connected devices + elif (topic_level3 + topic_level4 + topic_level5) == '': + # topic_level1=zigbee2mqtt, topic_level2=SNZB02_01, topic_level3=, topic_level4=, topic_level5=, payload '{"battery":100,"device":{"applicationVersion":5,"dateCode":"20201026","friendlyName":"SNZB02_01","hardwareVersion":1,"ieeeAddr":"0x00124b00231e45b8","manufacturerID":0,"manufacturerName":"eWeLink","model":"SNZB-02","networkAddress":18841,"powerSource":"Battery","type":"EndDevice","zclVersion":1},"humidity":45.12,"linkquality":157,"temperature":16.26,"voltage":3200}' + # topic_level1=zigbee2mqtt, topic_level2=TRADFRI E1766_01, topic_level3=, topic_level4=, topic_level5=, payload={'battery': 74, 'device': {'applicationVersion': 33, 'dateCode': '20190311', 'friendlyName': 'TRADFRI E1766_01', 'hardwareVersion': 1, 'ieeeAddr': '0x588e81fffe28dec5', 'manufacturerID': 4476, 'manufacturerName': 'IKEA of Sweden', 'model': 'E1766', 'networkAddress': 39405, 'powerSource': 'Battery', 'softwareBuildID': '2.2.010', 'stackVersion': 98, 'type': 'EndDevice', 'zclVersion': 3}, 'linkquality': 141} + # topic_level1=zigbee2mqtt, topic_level2=LEDVANCE_E27_TW_01, topic_level3=, topic_level4=, topic_level5=, payload={'brightness': 254, 'color': {'x': 0.4599, 'y': 0.4106}, 'color_mode': 'color_temp', 'color_temp': 370, 'color_temp_startup': 65535, 'last_seen': 1632943562477, 'linkquality': 39, 'state': 'ON', 'update': {'state': 'idle'}, 'update_available': False} + # topic_level1=zigbee2mqtt, topic_level2=0xf0d1b800001574df, topic_level3=, topic_level4=, topic_level5=, payload={'brightness': 166, 'color': {'hue': 296, 'saturation': 69}, 'color_mode': 'hs', 'color_temp': 405, 'last_seen': 1638183778409, 'linkquality': 159, 'state': 'ON', 'update': {'state': 'idle'}, 'update_available': False} + + if type(payload) is dict: + # Wenn Geräte zur Laufzeit des Plugins hinzugefügt werden, werden diese im dict ergänzt + if not self.zigbee2mqtt_devices.get(topic_level2): + self.zigbee2mqtt_devices[topic_level2] = {} + self.logger.info(f"New device discovered: {topic_level2}") + + # Korrekturen in der Payload + + # Umbenennen des Key 'friendlyName' in 'friendly_name', damit er identisch zu denen aus Log Topic und Config Topic ist + if 'device' in payload: + meta = payload['device'] + if 'friendlyName' in meta: + meta['friendly_name'] = meta.pop('friendlyName') + del payload['device'] + + if not self.zigbee2mqtt_devices[topic_level2].get('meta'): + self.zigbee2mqtt_devices[topic_level2]['meta'] = {} + self.zigbee2mqtt_devices[topic_level2]['meta'].update(meta) + + # Korrektur des Lastseen + if 'last_seen' in payload: + last_seen = payload['last_seen'] + if isinstance(last_seen, int): + payload.update({'last_seen': datetime.fromtimestamp(last_seen / 1000)}) + elif isinstance(last_seen, str): + try: + payload.update({'last_seen': datetime.strptime(last_seen, "%Y-%m-%dT%H:%M:%S.%fZ").replace(microsecond=0)}) + except Exception as e: + if self.debug_log: + self.logger.debug(f"Error {e} occurred during decoding of last_seen using format '%Y-%m-%dT%H:%M:%S.%fZ'.") + try: + payload.update({'last_seen': datetime.strptime(last_seen, "%Y-%m-%dT%H:%M:%SZ")}) + except Exception as e: + if self.debug_log: + self.logger.debug(f"Error {e} occurred during decoding of last_seen using format '%Y-%m-%dT%H:%M:%SZ'.") + + # Korrektur der Brightness von 0-254 auf 0-100% + if 'brightness' in payload: + try: + payload.update({'brightness': int(round(payload['brightness'] * 100 / 255, 0))}) + except Exception as e: + if self.debug_log: + self.logger.debug(f"Error {e} occurred during decoding of brightness.") + + # Korrektur der Farbtemperatur von "mired scale" (Reziproke Megakelvin) auf Kelvin + if 'color_temp' in payload: + try: + payload.update({'color_temp': int(round(10000 / int(payload['color_temp']), 0)) * 100}) + except Exception as e: + if self.debug_log: + self.logger.debug(f"Error {e} occurred during decoding of color_temp.") + + # Verarbeitung von Farbdaten + if 'color_mode' in payload and 'color' in payload: + color_mode = payload['color_mode'] + color = payload.pop('color') + + if color_mode == 'hs': + payload['hue'] = color['hue'] + payload['saturation'] = color['saturation'] + + if color_mode == 'xy': + payload['color_x'] = color['x'] + payload['color_y'] = color['y'] + + if not self.zigbee2mqtt_devices[topic_level2].get('data'): + self.zigbee2mqtt_devices[topic_level2]['data'] = {} + self.zigbee2mqtt_devices[topic_level2]['data'].update(payload) + + # Setzen des Itemwertes + if topic_level2 in list(self.zigbee2mqtt_plugin_devices.keys()): + if self.debug_log: + self.logger.debug(f"Item to be checked for update and to be updated") + for element in payload: + itemtype = f"item_{element}" + value = payload[element] + item = self.zigbee2mqtt_plugin_devices[topic_level2]['connected_items'].get(itemtype, None) + src = self.get_shortname() + ':' + topic_level2 + if self.debug_log: + self.logger.debug(f"element: {element}, itemtype: {itemtype}, value: {value}, item: {item}") + + if item is not None: + item(value, src) + self.logger.info(f"{topic_level2}: Item '{item.id()}' set to value {value}") + else: + self.logger.info(f"{topic_level2}: No item for itemtype '{itemtype}' defined to set to {value}") + + # self.zigbee2mqtt_plugin_devices[topic_level2]['online_timeout'] = datetime.now()+timedelta(seconds=self._cycle+5) + + @staticmethod + def _build_topic_str(topic_level1: str, topic_level2: str, topic_level3: str, topic_level4: str, topic_level5: str) -> str: + """ + Build the mqtt topic as string + + :param topic_level1: base topic of topic to publish + :param topic_level2: unique part of topic to publish + :param topic_level3: level3 of topic to publish + :param topic_level4: level4 of topic to publish + :param topic_level5: level5 of topic to publish + """ + + tpc = f"{topic_level1}/{topic_level2}" + if topic_level3 != '': + tpc = f"{tpc}/{topic_level3}" + if topic_level4 != '': + tpc = f"{tpc}/{topic_level4}" + if topic_level5 != '': + tpc = f"{tpc}/{topic_level5}" + return tpc + + def _get_zigbee_meta_data(self, device_data: list): + """ + Extract the Zigbee Meta-Data for a certain device out of the device_data + + :param device_data: Payload of the bridge config message + """ + + for element in device_data: + if type(element) is dict: + device = element.get('friendly_name') + if device: + if 'lastSeen' in element: + element.update({'lastSeen': datetime.fromtimestamp(element['lastSeen'] / 1000)}) + if not self.zigbee2mqtt_devices.get(device): + self.zigbee2mqtt_devices[device] = {} + if not self.zigbee2mqtt_devices[device].get('meta'): + self.zigbee2mqtt_devices[device]['meta'] = {} + self.zigbee2mqtt_devices[device]['meta'].update(element) + else: + if self.debug_log: + self.logger.debug(f"(Received payload {device_data} is not of type dict") + + @staticmethod + def _bool2str(bool_value: bool, typus: int) -> str: + """ + Turns bool value to string + + :param bool_value: bool value + :param typus: type of string the bool_value will be transferred to + :return: string containing bool expression + """ + + if type(bool_value) is bool: + if typus == 1: + result = 'ON' if bool_value is True else 'OFF' + elif typus == 2: + result = 'an' if bool_value is True else 'aus' + elif typus == 3: + result = 'ja' if bool_value is True else 'nein' + else: + result = 'typus noch nicht definiert' + else: + result = 'Wert ist nicht vom Type bool' + return result + + def _get_current_status_of_all_devices_linked_to_items(self): + """ + Try to get current status of all devices linked to items; Works only if value es exposed + """ + + for device in self.zigbee2mqtt_plugin_devices: + attribut = (list(self.zigbee2mqtt_plugin_devices[device]['connected_items'].keys())[0])[5:] + payload = '{"' + str(attribut) + '" : ""}' + self.publish_zigbee2mqtt_topic(self.topic_level1, str(device), 'get', '', '', payload) + + def _handle_hex_in_topic_level2(self, topic_level2: str, item) -> str: + """ + check if zigbee device short name has been used without parentheses; if so this will be normally parsed to a number and therefore mismatch with defintion + """ + + try: + topic_level2 = int(topic_level2) + self.logger.warning(f"Probably for item {item.id()} the IEEE Adress has been used for item attribute 'zigbee2mqtt_topic'. Trying to make that work but it will cause exceptions. To prevent this, the short name need to be defined as string by using parentheses") + topic_level2 = str(hex(topic_level2)) + except Exception: + pass + return topic_level2 + + def _get_zigbee2mqtt_topic_from_item(self, item) -> str: + """ + Get zigbee2mqtt_topic for given item search from given item in parent direction + """ + + zigbee2mqtt_topic = None + + lookup_item = item + for i in range(3): + zigbee2mqtt_topic = self.get_iattr_value(lookup_item.conf, 'zigbee2mqtt_topic') + if zigbee2mqtt_topic is not None: + break + else: + lookup_item = lookup_item.return_parent() + + if zigbee2mqtt_topic is None: + self.logger.error('zigbee2mqtt_topic is not defined or instance not given') + else: + zigbee2mqtt_topic = self._handle_hex_in_topic_level2(zigbee2mqtt_topic, item) + + return zigbee2mqtt_topic diff --git a/zigbee2mqtt/_pv_1_1_2/locale.yaml b/zigbee2mqtt/_pv_1_1_2/locale.yaml new file mode 100755 index 000000000..62bc7ab58 --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/locale.yaml @@ -0,0 +1,23 @@ +# translations for the web interface +plugin_translations: + # Translations for the plugin specially for the web interface + 'Relais': {'de': '=', 'en': 'Relay'} + 'Mac Adresse': {'de': '=', 'en': 'Mac Address'} + 'IP Adresse': {'de': '=', 'en': 'IP Address'} + 'Firmware Version': {'de': '=', 'en': '='} + 'neue Firmware': {'de': '=', 'en': 'new Firmware'} + 'konfiguriert': {'de': '=', 'en': 'configured'} + 'Message Durchsatz': {'de': '=', 'en': 'Message throughput'} + 'letzte Minute': {'de': '=', 'en': 'last minute'} + 'letzte 5 Min.': {'de': '=', 'en': 'last 5 min'} + 'letzte 15 Min.': {'de': '=', 'en': 'last 15 min'} + 'Energie': {'de': '=', 'en': 'Energy'} + + # Alternative format for translations of longer texts: + 'Durchschnittlich Messages je Minute empfangen': + de: '=' + en: 'Messages per minute received on average' + 'Durchschnittlich Messages je Minute gesendet': + de: '=' + en: 'Messages per minute sent on average' + diff --git a/zigbee2mqtt/_pv_1_1_2/plugin.yaml b/zigbee2mqtt/_pv_1_1_2/plugin.yaml new file mode 100755 index 000000000..7df947ef6 --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/plugin.yaml @@ -0,0 +1,120 @@ +# Metadata for the plugin +plugin: + # Global plugin attributes + type: gateway # plugin type (gateway, interface, protocol, system, web) + description: + de: 'Plugin zur Steuerung von Geräten, die mit einem Zigbee Gateway mit der Zigbee2MQTT Firmware versehen sind. Die Kommunikation erfolgt über das MQTT Module von SmartHomeNG.' + en: 'Plugin to control devices which are linked to Zigbee Gateway equipped with Zigbee2MQTT firmware. Communication is handled through the MQTT module of SmartHomeNG.' + maintainer: Michael Wenzel + tester: Michael Wenzel # Who tests this plugin? + state: develop # change to ready when done with development + keywords: iot + documentation: '' + support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1856775-support-thread-f%C3%BCr-das-zigbee2mqtt-plugin + + version: 1.1.2 # Plugin version + sh_minversion: 1.8.2 # minimum shNG version to use this plugin +# sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) + py_minversion: 3.8 # minimum Python version to use for this plugin + multi_instance: True # plugin supports multi instance + restartable: unknown + classname: Zigbee2Mqtt # class containing the plugin + +parameters: + # Definition of parameters to be configured in etc/plugin.yaml (enter 'parameters: NONE', if section should be empty) + base_topic: + type: str + default: 'zigbee2mqtt' + description: + de: TopicLevel_1 um mit dem ZigBee2MQTT Gateway zu kommunizieren (%topic%) + en: TopicLevel_1 to be used to communicate with the ZigBee2MQTT Gateway (%topic%) + + poll_period: + type: int + default: 300 + valid_min: 10 + valid_max: 3600 + description: + de: Zeitabstand in Sekunden in dem das Gateway Infos liefer soll + en: Timeperiod in seconds in which the Gateway shall send information + + read_at_init: + type: bool + default: True + description: + de: Einlesen aller Werte beim Start + en: Read all values at init + + +item_attributes: + # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) + zigbee2mqtt_topic: + type: str + mandatory: true + description: + de: TopicLevel_2 um mit dem ZigBee2MQTT Gateway zu kommunizieren; entspricht dem Friendly_Name oder Short_Name des Zigbee-Gerätes + en: TopicLevel_2 to be used to communicate with the ZigBee2MQTT Gateway + + zigbee2mqtt_attr: + type: str + mandatory: true + description: + de: "Zu lesendes/schreibendes Attribut des ZigBee2MQTT Devices. Achtung: Nicht jedes Attribut ist auf allen Device-Typen vorhanden." + en: "Attribute of ZigBee2MQTT device that shall be read/written. Note: Not every attribute is available on all device types" + valid_list_ci: + - online + - bridge_permit_join + - bridge_health_check + - bridge_restart + - bridge_networkmap_raw + - device_remove + # - device_ota_update_check + # - device_ota_update_update + - device_configure + - device_options + - device_rename + # - device_bind + # - device_unbind + - device_configure_reporting + - temperature + - humidity + - battery + - battery_low + - linkquality + - action + - vibration + - action_group + - voltage + - angle + - angle_x + - angle_x_absolute + - angle_y + - angle_y_absolute + - angle_z + - strength + - last_seen + - tamper + - sensitivity + - contact + - brightness # Helligkeit in % [0-100] + - color_temp # Farbtemperatur in Kelvin + - state + - hue + - saturation + - color_x + - color_y + - color_mode + + valid_list_description: + de: + - "" + - "Online Status des Zigbee Devices -> bool, r/o" + +item_structs: NONE + # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + +plugin_functions: NONE + # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) + +logic_parameters: NONE + # Definition of logic parameters defined by this plugin (enter 'logic_parameters: NONE', if section should be empty) diff --git a/zigbee2mqtt/_pv_1_1_2/user_doc.rst b/zigbee2mqtt/_pv_1_1_2/user_doc.rst new file mode 100755 index 000000000..43c5e44ea --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/user_doc.rst @@ -0,0 +1,134 @@ +=========== +zigbee2mqtt +=========== + +Das Plugin dienst zur Steuerung von Zigbee Devices via Zigbee2MQTT über MQTT. Notwendige Voraussetzung ist eine +funktionsfähige und laufende Installation von Zigbee2Mqtt. Die Installation, Konfiguration und der Betrieb ist hier +beschrieben: https://www.zigbee2mqtt.io/ +Dort findet man ebenfalls die unterstützten Zigbee Geräte. + +.. attention:: + + Das Plugin kommuniziert über MQTT und benötigt das mqtt Modul, welches die Kommunikation mit dem MQTT Broker + durchführt. Dieses Modul muß geladen und konfiguriert sein, damit das Plugin funktioniert. + +Getestet ist das Plugin mit folgenden Zigbee-Geräten: + +- SONOFF SNZB-02 +- IKEA TRADFRI E1766 +- Aqara DJT11LM +- TuYa TS0210 +- Aqara Opple 3fach Taster + + +Konfiguration +============= + +Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/zigbee2mqtt` beschrieben. + +Nachfolgend noch einige Zusatzinformationen. + +Konfiguration des Plugins +------------------------- + +Die Konfigruation des Plugins erfolgt über das Admin-Interface. Dafür stehen die folgenden Einstellungen zur Verfügung: + +- `base_topic`: MQTT TopicLevel_1, um mit dem ZigBee2MQTT Gateway zu kommunizieren (%topic%) +- `poll_period`: Zeitabstand in Sekunden in dem das Gateway Infos liefer soll + + +Konfiguration von Items +----------------------- + +Für die Nutzung eines Zigbee Devices müssen in dem entsprechenden Item die zwei Attribute ``zigbee2mqtt_topic`` und +``zigbee2mqtt_attr`` konfiguriert werden, wie im folgenden Beispiel gezeigt: + +.. code-block:: yaml + + sensor: + temp: + type: num + zigbee2mqtt_topic: SNZB02_01 + zigbee2mqtt_attr: temperature + hum: + type: num + zigbee2mqtt_topic: SNZB02_01 + zigbee2mqtt_attr: humidity + + +Dabei entspricht das Attribute ``zigbee2mqtt_topic`` dem Zigbee ``Friendly Name`` des Device bzw. dem MQTT Topic Level_2, um mit dem ZigBee2MQTT Gateway zu kommunizieren. + +Das Attribut ``zigbee2mqtt_attr`` entspricht dem jeweiligen Tag aus der Payload, der verwendet werden soll. Welche Tags beim jeweiligen Device verfügbar sind, kann man im WebIF des Pluigns sehen. + +Die folgenden Tags des Attributes ``zigbee2mqtt_attr`` sind definiert und werden vom Plugin unterstützt: + +- online +- bridge_permit_join +- bridge_health_check +- bridge_restart +- bridge_networkmap_raw +- device_remove +- device_configure +- device_options +- device_rename +- device_configure_reporting +- temperature +- humidity +- battery +- battery_low +- linkquality +- action +- vibration +- action_group +- voltage +- angle +- angle_x +- angle_x_absolute +- angle_y +- angle_y_absolute +- angle_z +- strength +- last_seen +- tamper +- sensitivity +- contact + + +Web Interface des Plugins +========================= + +Zigbee2Mqtt Items +----------------- + +Das Webinterface zeigt die Items an, für die ein Zigbee2Mqtt Device konfiguriert ist. + +.. image:: user_doc/assets/webif_tab1.jpg + :class: screenshot + + +Zigbee2Mqtt Devices +------------------- + +Das Webinterface zeigt Informationen zu den konfigurierten Zigbee2Mqtt Devices an, sowie etwa hinzugekommen Devices die +in SmartHomeNG noch nicht konfiguriert (mit einem Item vebunden) sind. + +.. image:: user_doc/assets/webif_tab2.jpg + :class: screenshot + + +Zigbee2Mqtt Bridge Info +----------------------- + +Das Webinterface zeigt detaillierte Informationen der Zigbee2Mqtt Bridge zu jedem verbundenen Device an. + +.. image:: user_doc/assets/webif_tab3.jpg + :class: screenshot + + +Broker Information +------------------ + +Das Webinterface zeigt Informationen zum genutzten MQTT Broker an. + +.. image:: user_doc/assets/webif_tab6.jpg + :class: screenshot diff --git a/zigbee2mqtt/_pv_1_1_2/webif/__init__.py b/zigbee2mqtt/_pv_1_1_2/webif/__init__.py new file mode 100755 index 000000000..03f4f45f8 --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/webif/__init__.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# vim: set encoding=utf-8 tabstop=4 softtabstop=4 shiftwidth=4 expandtab +######################################################################### +# Copyright 2022- Michael Wenzel zel_michael@web.de +######################################################################### +# This file is part of SmartHomeNG. +# https://www.smarthomeNG.de +# https://knx-user-forum.de/forum/supportforen/smarthome-py +# +# Sample plugin for new plugins to run with SmartHomeNG version 1.5 and +# upwards. +# +# SmartHomeNG is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# SmartHomeNG is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with SmartHomeNG. If not, see . +# +######################################################################### + +import json +import cherrypy +from lib.item import Items +from lib.model.smartplugin import SmartPluginWebIf +from jinja2 import Environment, FileSystemLoader + + +class WebInterface(SmartPluginWebIf): + + def __init__(self, webif_dir, plugin): + """ + Initialization of instance of class WebInterface + + :param webif_dir: directory where the webinterface of the plugin resides + :param plugin: instance of the plugin + :type webif_dir: str + :type plugin: object + """ + + self.logger = plugin.logger + self.webif_dir = webif_dir + self.plugin = plugin + self.items = Items.get_instance() + self.tplenv = self.init_template_environment() + self.logger.debug(f"Init WebIF of {self.plugin.get_shortname()}") + + @cherrypy.expose + def index(self, reload=None, action=None): + """ + Build index.html for cherrypy + Render the template and return the html file to be delivered to the browser + + :return: contents of the template after beeing rendered + """ + + self.plugin.get_broker_info() + + tmpl = self.tplenv.get_template('index.html') + + try: + pagelength = self.plugin.webif_pagelength + except Exception: + pagelength = 100 + + return tmpl.render(plugin_shortname=self.plugin.get_shortname(), + plugin_version=self.plugin.get_version(), + plugin_info=self.plugin.get_info(), + items=sorted(self.plugin.zigbee2mqtt_items, key=lambda k: str.lower(k['_path'])), + item_count=len(self.plugin.zigbee2mqtt_items), + p=self.plugin, + webif_pagelength=pagelength, + ) + + @cherrypy.expose + def get_data_html(self, dataSet=None): + """ + Return data to update the webpage + + For the standard update mechanism of the web interface, the dataSet to return the data for is None + + :param dataSet: Dataset for which the data should be returned (standard: None) + :return: dict with the data needed to update the web page. + """ + if dataSet is None: + # get the new data + self.plugin.get_broker_info() + data = dict() + data['broker_info'] = self.plugin._broker + data['broker_uptime'] = self.plugin.broker_uptime() + + data['item_values'] = {} + for item in self.plugin.zigbee2mqtt_items: + data['item_values'][item.id()] = {} + data['item_values'][item.id()]['value'] = item.property.value + data['item_values'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') + data['item_values'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') + + data['device_values'] = {} + for device in self.plugin.zigbee2mqtt_devices: + data['device_values'][device] = {} + if 'data' in self.plugin.zigbee2mqtt_devices[device]: + data['device_values'][device]['lqi'] = str(self.plugin.zigbee2mqtt_devices[device]['data'].get('linkquality', '-')) + data['device_values'][device]['data'] = ", ".join(list(self.plugin.zigbee2mqtt_devices[device]['data'].keys())) + else: + data['device_values'][device]['lqi'] = '-' + data['device_values'][device]['data'] = '-' + if 'meta' in self.plugin.zigbee2mqtt_devices[device]: + last_seen = self.plugin.zigbee2mqtt_devices[device]['meta'].get('lastSeen', None) + if last_seen: + data['device_values'][device]['last_seen'] = last_seen.strftime('%d.%m.%Y %H:%M:%S') + else: + data['device_values'][device]['last_seen'] = '-' + else: + data['device_values'][device]['last_seen'] = '-' + + # return it as json the web page + try: + return json.dumps(data, default=str) + except Exception as e: + self.logger.error("get_data_html exception: {}".format(e)) + return {} diff --git a/zigbee2mqtt/_pv_1_1_2/webif/static/img/plugin_logo.png b/zigbee2mqtt/_pv_1_1_2/webif/static/img/plugin_logo.png new file mode 100755 index 000000000..69cfcd9c0 Binary files /dev/null and b/zigbee2mqtt/_pv_1_1_2/webif/static/img/plugin_logo.png differ diff --git a/zigbee2mqtt/_pv_1_1_2/webif/static/img/readme.txt b/zigbee2mqtt/_pv_1_1_2/webif/static/img/readme.txt new file mode 100755 index 000000000..1a7c55eef --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/webif/static/img/readme.txt @@ -0,0 +1,6 @@ +This directory is for storing images that are used by the web interface. + +If you want to have your own logo on the top of the web interface, store it here and name it plugin_logo.. + +Extension can be png, svg or jpg + diff --git a/zigbee2mqtt/_pv_1_1_2/webif/templates/index.html b/zigbee2mqtt/_pv_1_1_2/webif/templates/index.html new file mode 100755 index 000000000..f819f7e1f --- /dev/null +++ b/zigbee2mqtt/_pv_1_1_2/webif/templates/index.html @@ -0,0 +1,374 @@ +{% extends "base_plugin.html" %} +{% set logo_frame = false %} +{% set update_interval = 5000 %} + +{% block pluginstyles %} + +{% endblock pluginstyles %} + +{% block pluginscripts %} + + +{% endblock pluginscripts %} + + +{% block headtable %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{_('Broker Host')}}{{ p.broker_config.host }}{{_('Broker Port')}}{{ p.broker_config.port }}
{{_('Benutzer')}}{{ p.broker_config.user }}{{_('Passwort')}} + {% if p.broker_config.password %} + {% for letter in p.broker_config.password %}*{% endfor %} + {% endif %} +
{{_('QoS')}}{{ p.broker_config.qos }}{{ webif_pagelength }}{{_('Zigbee2Mqtt')}}{{ 'GUI' }}
+{% endblock headtable %} + + +{% block buttons %} + +{% endblock %} + +{% set tabcount = 4 %} + +{% if not items %} + {% set start_tab = 2 %} +{% endif %} + + +{% set tab1title = "" ~ plugin_shortname ~ " Items (" ~ item_count ~ ")" %} +{% block bodytab1 %} +
+ + + + + + + + + + + + + {% for item in items %} + {% set item_id = item.id() %} + + + + + + + + + {% endfor %} + +
{{ _('Item') }}{{ _('Typ') }}{{ _('Wert') }}{{ _('Topic') }}{{ _('Letztes Update') }}{{ _('Letzter Change') }}
{{ item_id }}{{ item.property.type }}{{ item()}}{{ p.get_iattr_value(item.conf, 'zigbee2mqtt_topic') }}{{ item.last_update().strftime('%d.%m.%Y %H:%M:%S') }}{{ item.last_change().strftime('%d.%m.%Y %H:%M:%S') }}
+
+{% endblock %} + + +{% set tab2title = "" ~ plugin_shortname ~ " Devices" %} +{% block bodytab2 %} +
+ + + + + + + + + + + + + + + + + {% for device in p.zigbee2mqtt_devices %} + {% if p.zigbee2mqtt_devices[device]['meta'] %} + + + + + + + + + + + + + {% endif %} + {% endfor %} + +
{{ _('#') }}{{ _('Friendy Name') }}{{ _('IEEE Adresse') }}{{ _('Vendor') }}{{ _('Modell') }}{{ _('Description') }}{{ _('ModellID') }}{{ _('Last seen') }}{{ _('LQI') }}{{ _('bereitgestellte Daten') }}
{{ loop.index }}{{ device }}{{ p.zigbee2mqtt_devices[device]['meta']['ieeeAddr'] }}{{ p.zigbee2mqtt_devices[device]['meta']['vendor'] }}{{ p.zigbee2mqtt_devices[device]['meta']['model'] }}{{ p.zigbee2mqtt_devices[device]['meta']['description'] }}{{ p.zigbee2mqtt_devices[device]['meta']['modelID'] }}{{ _('Init...') }}{{ _('Init...') }}{{ _('Init...') }}
+
+{% endblock %} + + +{% set tab3title = "" ~ " Broker Information" %} +{% block bodytab3 %} + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if p.broker_monitoring %} + + + + + + + + + + + + {% endif %} +
{{ 'Broker Version' }}{{ p._broker.version }}{{ connection_result }}
{{ 'Active Clients' }}{{ p._broker.active_clients }}
{{ 'Subscriptions' }}{{ p._broker.subscriptions }}
{{ 'Messages stored' }}{{ p._broker.stored_messages }}
{{ 'Retained Messages' }}{{ p._broker.retained_messages }}
 
{{ _('Laufzeit') }}{{ p.broker_uptime() }}
 
+ +{% if p.broker_monitoring %} + + + + + + + + + + + + + + + + + + + + + + +
{{ _('Message Durchsatz') }}{{ _('letzte Minute') }}{{ _('letzte 5 Min.') }}{{ _('letzte 15 Min.') }}
{{ _('Durchschnittlich Messages je Minute empfangen') }}{{ p._broker.msg_rcv_1min }}     {{ p._broker.msg_rcv_5min }}     {{ p._broker.msg_rcv_15min }}
{{ _('Durchschnittlich Messages je Minute gesendet') }}{{ p._broker.msg_snt_1min }}     {{ p._broker.msg_snt_5min }}     {{ p._broker.msg_snt_15min }}
+{% endif %} +{% endblock %} + + +{% set tab4title = "" ~ plugin_shortname ~ " " ~ _('Maintenance') ~ "" %} +{% block bodytab4 %} +
+ + + + + + + + + + {% for device in p.zigbee2mqtt_devices %} + + + + + + {% endfor %} + +
{{ _('Zigbee Device') }}{{ _('Meta') }}{{ _('Data') }}
{{ device }}{{ p.zigbee2mqtt_devices[device]['meta'] }}{{ p.zigbee2mqtt_devices[device]['data'] }}
+
+
+ + + + + + + + + {% for device in p.zigbee2mqtt_plugin_devices %} + + + + + {% endfor %} + +
{{ _('Plugin Device') }}{{ _('Meta Data') }}
{{ device }}{{ p.zigbee2mqtt_plugin_devices[device]}}
+
+{% endblock %} diff --git a/zigbee2mqtt/plugin.yaml b/zigbee2mqtt/plugin.yaml index 7df947ef6..4c10a12e8 100755 --- a/zigbee2mqtt/plugin.yaml +++ b/zigbee2mqtt/plugin.yaml @@ -5,19 +5,19 @@ plugin: description: de: 'Plugin zur Steuerung von Geräten, die mit einem Zigbee Gateway mit der Zigbee2MQTT Firmware versehen sind. Die Kommunikation erfolgt über das MQTT Module von SmartHomeNG.' en: 'Plugin to control devices which are linked to Zigbee Gateway equipped with Zigbee2MQTT firmware. Communication is handled through the MQTT module of SmartHomeNG.' - maintainer: Michael Wenzel - tester: Michael Wenzel # Who tests this plugin? + maintainer: Sebastian Helms + tester: Sebastian Helms # Who tests this plugin? state: develop # change to ready when done with development keywords: iot documentation: '' support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1856775-support-thread-f%C3%BCr-das-zigbee2mqtt-plugin - version: 1.1.2 # Plugin version - sh_minversion: 1.8.2 # minimum shNG version to use this plugin + version: 2.0.0 # Plugin version + sh_minversion: 1.9.4 # minimum shNG version to use this plugin # sh_maxversion: # maximum shNG version to use this plugin (leave empty if latest) py_minversion: 3.8 # minimum Python version to use for this plugin multi_instance: True # plugin supports multi instance - restartable: unknown + restartable: yes classname: Zigbee2Mqtt # class containing the plugin parameters: @@ -26,17 +26,17 @@ parameters: type: str default: 'zigbee2mqtt' description: - de: TopicLevel_1 um mit dem ZigBee2MQTT Gateway zu kommunizieren (%topic%) - en: TopicLevel_1 to be used to communicate with the ZigBee2MQTT Gateway (%topic%) + de: Topic, um mit dem ZigBee2MQTT-Gateway zu kommunizieren (%topic%) + en: Base topic for communicating with the ZigBee2MQTT gateway (%topic%) poll_period: type: int - default: 300 + default: 900 valid_min: 10 valid_max: 3600 description: - de: Zeitabstand in Sekunden in dem das Gateway Infos liefer soll - en: Timeperiod in seconds in which the Gateway shall send information + de: Zeitabstand in Sekunden, in dem der Status des Gateway abgefragt wird + en: Interval in seconds to poll the gateway status read_at_init: type: bool @@ -45,74 +45,289 @@ parameters: de: Einlesen aller Werte beim Start en: Read all values at init + suspend_item: + type: str + default: '' + description: + de: Pfad zum Suspend-Item + en: Path to suspend item + + bool_values: + type: list + default: ['OFF', 'ON'] + description: + de: Plugin-weite Ersetzungwerte für bool-Werte + en: Plugin-wide substitution values for bool items + + z2m_gui: + type: str + default: '' + description: + de: Host:Port des zigbee2mqtt-Web-GUI + en: host:port of the zigbee2mqtt-web-GUI item_attributes: # Definition of item attributes defined by this plugin (enter 'item_attributes: NONE', if section should be empty) - zigbee2mqtt_topic: + z2m_topic: type: str - mandatory: true description: - de: TopicLevel_2 um mit dem ZigBee2MQTT Gateway zu kommunizieren; entspricht dem Friendly_Name oder Short_Name des Zigbee-Gerätes - en: TopicLevel_2 to be used to communicate with the ZigBee2MQTT Gateway + de: Name des anzusprechenden Gerätes, entweder die Seriennummer ('0xdeadbeef') oder der friendly_name + en: Name of the device to be addressed, either the serial number ('0xdeadbeef') or the friendly_name - zigbee2mqtt_attr: + z2m_attr: type: str - mandatory: true description: - de: "Zu lesendes/schreibendes Attribut des ZigBee2MQTT Devices. Achtung: Nicht jedes Attribut ist auf allen Device-Typen vorhanden." - en: "Attribute of ZigBee2MQTT device that shall be read/written. Note: Not every attribute is available on all device types" - valid_list_ci: - - online - - bridge_permit_join - - bridge_health_check - - bridge_restart - - bridge_networkmap_raw - - device_remove - # - device_ota_update_check - # - device_ota_update_update - - device_configure - - device_options - - device_rename - # - device_bind - # - device_unbind - - device_configure_reporting - - temperature - - humidity - - battery - - battery_low - - linkquality - - action - - vibration - - action_group - - voltage - - angle - - angle_x - - angle_x_absolute - - angle_y - - angle_y_absolute - - angle_z - - strength - - last_seen - - tamper - - sensitivity - - contact - - brightness # Helligkeit in % [0-100] - - color_temp # Farbtemperatur in Kelvin - - state - - hue - - saturation - - color_x - - color_y - - color_mode - - valid_list_description: - de: - - "" - - "Online Status des Zigbee Devices -> bool, r/o" - -item_structs: NONE - # Definition of item-structure templates for this plugin (enter 'item_structs: NONE', if section should be empty) + de: "Zu lesendes/schreibendes Attribut des ZigBee2MQTT Devices" + en: "Attribute of ZigBee2MQTT device that shall be read/written" + + z2m_readonly: + type: bool + description: + de: "Attribut wird nur gelesen" + en: "Attribute can only be read" + + z2m_writeonly: + type: bool + description: + de: "Attribut wird nur geschrieben. Wenn z2m_readonly auch gesetzt ist, hat readonly Vorrang" + en: "Attribute can only be written. Will be overridden by z2m_readonly" + + z2m_bool_values: + type: list + description: + de: Ersetzungwerte für bool-Werte + en: substitution values for bool items + + +item_structs: + dimmer: + action: + type: str + z2m_topic: ..:. + z2m_attr: action + enforce_updates: true + + battery: + type: num + z2m_topic: ..:. + z2m_attr: battery + + duration: + type: num + z2m_topic: ..:. + z2m_attr: action_duration + + linkquality: + type: num + z2m_topic: ..:. + z2m_attr: linkquality + + on: + type: bool + eval: True if sh...action() == 'on_press' else (False if sh...action() in ('on_press_release', 'on_hold_release') else None) + eval_trigger: ..action + + hold: + type: bool + eval: True if sh....action() == 'on_hold' else (False if sh....action() == 'on_hold_release' else None) + eval_trigger: ...action + + off: + type: bool + eval: True if sh...action() == 'off_press' else (False if sh...action() in ('off_press_release', 'off_hold_release') else None) + eval_trigger: ..action + + hold: + type: bool + eval: True if sh....action() == 'off_hold' else (False if sh....action() == 'off_hold_release' else None) + eval_trigger: ...action + + up: + type: bool + eval: True if sh...action() == 'up_press' else (False if sh...action() in ('up_press_release', 'up_hold_release') else None) + eval_trigger: ..action + + hold: + type: bool + eval: True if sh....action() == 'up_hold' else (False if sh....action() == 'up_hold_release' else None) + eval_trigger: ...action + + down: + type: bool + eval: True if sh...action() == 'down_press' else (False if sh...action() in ('down_press_release', 'down_hold_release') else None) + eval_trigger: ..action + + hold: + type: bool + eval: True if sh....action() == 'down_hold' else (False if sh....action() == 'down_hold_release' else None) + eval_trigger: ...action + + light_white_ambient_group: + type: bool + z2m_attr: state + z2m_bool_values: ['OFF', 'ON'] + + brightness: + type: num + z2m_topic: ..:. + z2m_attr: brightness + + percent: + type: num + z2m_topic: ..:. + z2m_attr: brightness_percent + + color_temp: + type: num + remark: 153..454, coolest, cool, neutral, warm, warmest + z2m_topic: ..:. + z2m_attr: color_temp + + kelvin: + type: num + z2m_topic: ..:. + z2m_attr: color_temp_kelvin + + color_mode: + type: str + z2m_topic: ..:. + z2m_attr: color_mode + + scene: + type: str + enforce_updates: true + z2m_topic: ..:. + z2m_attr: scene_recall + z2m_readonly: false + z2m_writeonly: true + + scenes: + type: list + z2m_topic: ..:. + z2m_attr: scenelist + z2m_readonly: true + z2m_writeonly: false + + color_startup: + type: num + remark: 153..454, coolest, cool, neutral, warm, warmest, previous + z2m_topic: ..:. + z2m_attr: color_temp_startup + + light_white_ambient: + struct: priv_z2m.light_white_ambient_group + + linkquality: + type: num + z2m_topic: ..:. + z2m_attr: linkquality + + update: + type: dict + z2m_topic: ..:. + z2m_attr: update + + installed_version: + type: num + eval: sh...()['installed_version'] + eval_trigger: .. + + latest_version: + type: num + eval: sh...()['latest_version'] + eval_trigger: .. + + state: + type: str + eval: sh...()['state'] + eval_trigger: .. + + light_rgb_group: + struct: priv_z2m.light_white_ambient_group + + color: + type: dict + z2m_topic: ..:. + z2m_attr: color + + r: + type: num + z2m_topic: ..:. + z2m_attr: color_r + + g: + type: num + z2m_topic: ..:. + z2m_attr: color_g + + b: + type: num + z2m_topic: ..:. + z2m_attr: color_b + + rgb: + type: str + z2m_topic: ..:. + z2m_attr: color_rgb + + color_startup: + type: dict + z2m_topic: ..:. + z2m_attr: color_temp_startup + + light_rgb: + struct: priv_z2m.light_rgb_group + + linkquality: + type: num + z2m_topic: ..:. + z2m_attr: linkquality + + update: + type: dict + z2m_topic: ..:. + z2m_attr: update + + installed_version: + type: num + eval: sh...()['installed_version'] + eval_trigger: .. + + latest_version: + type: num + eval: sh...()['latest_version'] + eval_trigger: .. + + state: + type: str + eval: sh...()['state'] + eval_trigger: .. + + bridge: + info: + type: dict + mqtt_topic_in: z2m/bridge + + state: + type: bool + mqtt_topic_in: z2m/bridge/state + + logging: + type: dict + mqtt_topic_in: z2m/bridge/logging + + devices: + type: list + mqtt_topic_in: z2m/bridge/devices + + groups: + type: list + mqtt_topic_in: z2m/bridge/groups + events: + type: dict + mqtt_topic_in: z2m/bridge/event + plugin_functions: NONE # Definition of plugin functions defined by this plugin (enter 'plugin_functions: NONE', if section should be empty) diff --git a/zigbee2mqtt/rgbxy.py b/zigbee2mqtt/rgbxy.py new file mode 100755 index 000000000..9022168f1 --- /dev/null +++ b/zigbee2mqtt/rgbxy.py @@ -0,0 +1,282 @@ +# -*- coding: utf-8 -*- +""" +Library for RGB / CIE1931 "x, y" coversion. +Based on Philips implementation guidance: +http://www.developers.meethue.com/documentation/color-conversions-rgb-xy +Copyright (c) 2016 Benjamin Knight / MIT License. + +modifications by SH for zigbee2mqtt plugin in smarthomeNG +""" +import math +import random +from collections import namedtuple + +__version__ = '0.5.1a' + +# Represents a CIE 1931 XY coordinate pair. +XYPoint = namedtuple('XYPoint', ['x', 'y']) + +# LivingColors Iris, Bloom, Aura, LightStrips +GamutA = ( + XYPoint(0.704, 0.296), + XYPoint(0.2151, 0.7106), + XYPoint(0.138, 0.08), +) + +# Hue A19 bulbs +GamutB = ( + XYPoint(0.675, 0.322), + XYPoint(0.4091, 0.518), + XYPoint(0.167, 0.04), +) + +# Hue BR30, A19 (Gen 3), Hue Go, LightStrips plus +GamutC = ( + XYPoint(0.692, 0.308), + XYPoint(0.17, 0.7), + XYPoint(0.153, 0.048), +) + + +def get_light_gamut(modelId): + """Gets the correct color gamut for the provided model id. + Docs: https://developers.meethue.com/develop/hue-api/supported-devices/ + """ + if modelId in ('LST001', 'LLC005', 'LLC006', 'LLC007', 'LLC010', 'LLC011', 'LLC012', 'LLC013', 'LLC014'): + return GamutA + elif modelId in ('LCT001', 'LCT007', 'LCT002', 'LCT003', 'LLM001'): + return GamutB + elif modelId in ('LCT010', 'LCT011', 'LCT012', 'LCT014', 'LCT015', 'LCT016', 'LLC020', 'LST002'): + return GamutC + else: + raise ValueError + return None + + +class ColorHelper: + + def __init__(self, gamut=GamutB): + self.Red = gamut[0] + self.Lime = gamut[1] + self.Blue = gamut[2] + + def hex_to_red(self, hex): + """Parses a valid hex color string and returns the Red RGB integer value.""" + return int(hex[0:2], 16) + + def hex_to_green(self, hex): + """Parses a valid hex color string and returns the Green RGB integer value.""" + return int(hex[2:4], 16) + + def hex_to_blue(self, hex): + """Parses a valid hex color string and returns the Blue RGB integer value.""" + return int(hex[4:6], 16) + + def hex_to_rgb(self, h): + """Converts a valid hex color string to an RGB array.""" + rgb = (self.hex_to_red(h), self.hex_to_green(h), self.hex_to_blue(h)) + return rgb + + def rgb_to_hex(self, r, g, b): + """Converts RGB to hex.""" + return '%02x%02x%02x' % (r, g, b) + + def random_rgb_value(self): + """Return a random Integer in the range of 0 to 255, representing an RGB color value.""" + return random.randrange(0, 256) + + def cross_product(self, p1, p2): + """Returns the cross product of two XYPoints.""" + return (p1.x * p2.y - p1.y * p2.x) + + def check_point_in_lamps_reach(self, p): + """Check if the provided XYPoint can be recreated by a Hue lamp.""" + v1 = XYPoint(self.Lime.x - self.Red.x, self.Lime.y - self.Red.y) + v2 = XYPoint(self.Blue.x - self.Red.x, self.Blue.y - self.Red.y) + + q = XYPoint(p.x - self.Red.x, p.y - self.Red.y) + s = self.cross_product(q, v2) / self.cross_product(v1, v2) + t = self.cross_product(v1, q) / self.cross_product(v1, v2) + + return (s >= 0.0) and (t >= 0.0) and (s + t <= 1.0) + + def get_closest_point_to_line(self, A, B, P): + """Find the closest point on a line. This point will be reproducible by a Hue lamp.""" + AP = XYPoint(P.x - A.x, P.y - A.y) + AB = XYPoint(B.x - A.x, B.y - A.y) + ab2 = AB.x * AB.x + AB.y * AB.y + ap_ab = AP.x * AB.x + AP.y * AB.y + t = ap_ab / ab2 + + if t < 0.0: + t = 0.0 + elif t > 1.0: + t = 1.0 + + return XYPoint(A.x + AB.x * t, A.y + AB.y * t) + + def get_closest_point_to_point(self, xy_point): + # Color is unreproducible, find the closest point on each line in the CIE 1931 'triangle'. + pAB = self.get_closest_point_to_line(self.Red, self.Lime, xy_point) + pAC = self.get_closest_point_to_line(self.Blue, self.Red, xy_point) + pBC = self.get_closest_point_to_line(self.Lime, self.Blue, xy_point) + + # Get the distances per point and see which point is closer to our Point. + dAB = self.get_distance_between_two_points(xy_point, pAB) + dAC = self.get_distance_between_two_points(xy_point, pAC) + dBC = self.get_distance_between_two_points(xy_point, pBC) + + lowest = dAB + closest_point = pAB + + if (dAC < lowest): + lowest = dAC + closest_point = pAC + + if (dBC < lowest): + lowest = dBC + closest_point = pBC + + # Change the xy value to a value which is within the reach of the lamp. + cx = closest_point.x + cy = closest_point.y + + return XYPoint(cx, cy) + + def get_distance_between_two_points(self, one, two): + """Returns the distance between two XYPoints.""" + dx = one.x - two.x + dy = one.y - two.y + return math.sqrt(dx * dx + dy * dy) + + def get_xy_point_from_rgb(self, red_i, green_i, blue_i): + """Returns an XYPoint object containing the closest available CIE 1931 x, y coordinates + based on the RGB input values.""" + + xy_point, _ = self.get_xy_point_from_rgb(red_i, green_i, blue_i) + return xy_point + + def get_xy_bri_from_rgb(self, red_i, green_i, blue_i): + """Returns an XYPoint object containing the closest available CIE 1931 x, y coordinates + based on the RGB input values.""" + + red = red_i / 255.0 + green = green_i / 255.0 + blue = blue_i / 255.0 + + r = ((red + 0.055) / (1.0 + 0.055))**2.4 if (red > 0.04045) else (red / 12.92) + g = ((green + 0.055) / (1.0 + 0.055))**2.4 if (green > 0.04045) else (green / 12.92) + b = ((blue + 0.055) / (1.0 + 0.055))**2.4 if (blue > 0.04045) else (blue / 12.92) + + X = r * 0.664511 + g * 0.154324 + b * 0.162028 + Y = r * 0.283881 + g * 0.668433 + b * 0.047685 + Z = r * 0.000088 + g * 0.072310 + b * 0.986039 + + try: + cx = X / (X + Y + Z) + cy = Y / (X + Y + Z) + except ZeroDivisionError: + cx = 0.167 + cy = 0.04 + Y = 0 + + # Check if the given XY value is within the colourreach of our lamps. + xy_point = XYPoint(cx, cy) + in_reach = self.check_point_in_lamps_reach(xy_point) + + if not in_reach: + xy_point = self.get_closest_point_to_point(xy_point) + + return xy_point, Y + + def get_rgb_from_xy_and_brightness(self, x, y, bri=1): + """Inverse of `get_xy_point_from_rgb`. Returns (r, g, b) for given x, y values. + Implementation of the instructions found on the Philips Hue iOS SDK docs: http://goo.gl/kWKXKl + """ + # The xy to color conversion is almost the same, but in reverse order. + # Check if the xy value is within the color gamut of the lamp. + # If not continue with step 2, otherwise step 3. + # We do this to calculate the most accurate color the given light can actually do. + xy_point = XYPoint(x, y) + + if not self.check_point_in_lamps_reach(xy_point): + # Calculate the closest point on the color gamut triangle + # and use that as xy value See step 6 of color to xy. + xy_point = self.get_closest_point_to_point(xy_point) + + # Calculate XYZ values Convert using the following formulas: + Y = bri + X = (Y / xy_point.y) * xy_point.x + Z = (Y / xy_point.y) * (1 - xy_point.x - xy_point.y) + + # Convert to RGB using Wide RGB D65 conversion + r = X * 1.656492 - Y * 0.354851 - Z * 0.255038 + g = -X * 0.707196 + Y * 1.655397 + Z * 0.036152 + b = X * 0.051713 - Y * 0.121364 + Z * 1.011530 + + # Apply reverse gamma correction + r, g, b = map( + lambda x: (12.92 * x) if (x <= 0.0031308) else ((1.0 + 0.055) * pow(x, (1.0 / 2.4)) - 0.055), + [r, g, b] + ) + + # Bring all negative components to zero + r, g, b = map(lambda x: max(0, x), [r, g, b]) + + # If one component is greater than 1, weight components by that value. + max_component = max(r, g, b) + if max_component > 1: + r, g, b = map(lambda x: x / max_component, [r, g, b]) + + r, g, b = map(lambda x: int(x * 255), [r, g, b]) + + # Convert the RGB values to your color object The rgb values from the above formulas are between 0.0 and 1.0. + return (r, g, b) + + +class Converter: + + def __init__(self, gamut=GamutB): + self.color = ColorHelper(gamut) + + def hex_to_xy(self, h): + """Converts hexadecimal colors represented as a String to approximate CIE + 1931 x and y coordinates. + """ + rgb = self.color.hex_to_rgb(h) + return self.rgb_to_xy(rgb[0], rgb[1], rgb[2]) + + def rgb_to_xy(self, red, green, blue): + """Converts red, green and blue integer values to approximate CIE 1931 + x and y coordinates. + """ + point = self.color.get_xy_point_from_rgb(red, green, blue) + return (point.x, point.y) + + def rgb_to_xyb(self, red, green, blue): + """Converts red, green and blue integer values to approximate CIE 1931 + x and y coordinates. + """ + point, bri = self.color.get_xy_bri_from_rgb(red, green, blue) + return (point.x, point.y, bri) + + def xy_to_hex(self, x, y, bri=1): + """Converts CIE 1931 x and y coordinates and brightness value from 0 to 1 + to a CSS hex color.""" + r, g, b = self.color.get_rgb_from_xy_and_brightness(x, y, bri) + return self.color.rgb_to_hex(r, g, b) + + def xy_to_rgb(self, x, y, bri=1): + """Converts CIE 1931 x and y coordinates and brightness value from 0 to 1 + to a CSS hex color.""" + r, g, b = self.color.get_rgb_from_xy_and_brightness(x, y, bri) + return (r, g, b) + + def get_random_xy_color(self): + """Returns the approximate CIE 1931 x,y coordinates represented by the + supplied hexColor parameter, or of a random color if the parameter + is not passed.""" + r = self.color.random_rgb_value() + g = self.color.random_rgb_value() + b = self.color.random_rgb_value() + return self.rgb_to_xy(r, g, b) \ No newline at end of file diff --git a/zigbee2mqtt/user_doc.rst b/zigbee2mqtt/user_doc.rst index 43c5e44ea..e5bff39c6 100755 --- a/zigbee2mqtt/user_doc.rst +++ b/zigbee2mqtt/user_doc.rst @@ -2,133 +2,109 @@ zigbee2mqtt =========== -Das Plugin dienst zur Steuerung von Zigbee Devices via Zigbee2MQTT über MQTT. Notwendige Voraussetzung ist eine -funktionsfähige und laufende Installation von Zigbee2Mqtt. Die Installation, Konfiguration und der Betrieb ist hier -beschrieben: https://www.zigbee2mqtt.io/ -Dort findet man ebenfalls die unterstützten Zigbee Geräte. +Das Plugin dienst zur Steuerung von Zigbee Devices via Zigbee2MQTT über MQTT. +Notwendige Voraussetzung ist eine funktionsfähige und laufende Installation von +Zigbee2Mqtt. Dessen Installation, Konfiguration und der Betrieb ist hier +beschrieben: https://www.zigbee2mqtt.io/ Dort findet man ebenfalls die +unterstützten Zigbee Geräte. .. attention:: - Das Plugin kommuniziert über MQTT und benötigt das mqtt Modul, welches die Kommunikation mit dem MQTT Broker - durchführt. Dieses Modul muß geladen und konfiguriert sein, damit das Plugin funktioniert. + Das Plugin kommuniziert über MQTT und benötigt das mqtt-Modul, welches die + Kommunikation mit dem MQTT Broker durchführt. Dieses Modul muss geladen und + konfiguriert sein, damit das Plugin funktioniert. Getestet ist das Plugin mit folgenden Zigbee-Geräten: -- SONOFF SNZB-02 -- IKEA TRADFRI E1766 -- Aqara DJT11LM -- TuYa TS0210 -- Aqara Opple 3fach Taster +- Philips Hue white ambiance E27 800lm with Bluetooth +- Philips Hue white ambiance E26/E27 +- IKEA Tradfri LED1924G9 +- IKEA Tradfri LED1949C5 +- Philips Hue dimmer switch +Grundsätzlich kann jedes Gerät angebunden werden; für eine sinnvolle +Verarbeitung von Werten sollte ein entsprechendes struct erstellt werden, +ggfs. kann noch erweiterte Funktionalität mit zusätzlichem Code bereitgestellt +werden. + +.. hint:: + + Im Rahmen der Umstellung des Plugins auf Version 2 wurden die Attribute + umbenannt, d.h. von "zigbee2mqtt_foo" in "z2m_foo" geändert. + Das macht die Konfigurationsdateien übersichtlicher und einfacher zu + schreiben. Bestehende Dateien müssen entsprechend angepasst werden. Konfiguration ============= -Die Informationen zur Konfiguration des Plugins sind unter :doc:`/plugins_doc/config/zigbee2mqtt` beschrieben. +Die Informationen zur Konfiguration des Plugins sind +unter :doc:`/plugins_doc/config/zigbee2mqtt` beschrieben. Nachfolgend noch einige Zusatzinformationen. -Konfiguration des Plugins -------------------------- +Konfiguration von Items +----------------------- -Die Konfigruation des Plugins erfolgt über das Admin-Interface. Dafür stehen die folgenden Einstellungen zur Verfügung: +Für die Nutzung eines Zigbee Devices können - sofern vorhanden - die +mitgelieferten structs verwendet werden: -- `base_topic`: MQTT TopicLevel_1, um mit dem ZigBee2MQTT Gateway zu kommunizieren (%topic%) -- `poll_period`: Zeitabstand in Sekunden in dem das Gateway Infos liefer soll +.. code-block:: yaml + lampe1: + struct: zigbee2mqtt.light_white_ambient + z2m_topic: friendlyname1 + + lampe2: + struct: zigbee2mqtt.light_rgb + z2m_topic: friendlyname2 -Konfiguration von Items ------------------------ -Für die Nutzung eines Zigbee Devices müssen in dem entsprechenden Item die zwei Attribute ``zigbee2mqtt_topic`` und -``zigbee2mqtt_attr`` konfiguriert werden, wie im folgenden Beispiel gezeigt: +Sofern für das entsprechende Gerät kein struct vorhanden ist, können einzelne +Datenpunkte des Geräts auch direkt angesprochen werden: .. code-block:: yaml sensor: temp: type: num - zigbee2mqtt_topic: SNZB02_01 - zigbee2mqtt_attr: temperature + z2m_topic: SNZB02_01 + z2m_attr: temperature hum: type: num - zigbee2mqtt_topic: SNZB02_01 - zigbee2mqtt_attr: humidity + z2m_topic: SNZB02_01 + z2m_attr: humidity + +Dabei entspricht das Attribute ``z2m_topic`` dem Zigbee ``Friendly Name`` des +Device bzw. dem MQTT Topic Level_2, um mit dem ZigBee2MQTT Gateway zu +kommunizieren. -Dabei entspricht das Attribute ``zigbee2mqtt_topic`` dem Zigbee ``Friendly Name`` des Device bzw. dem MQTT Topic Level_2, um mit dem ZigBee2MQTT Gateway zu kommunizieren. +Das Attribut ``z2m_attr`` entspricht dem jeweiligen Tag aus der Payload, der +verwendet werden soll. Welche Tags beim jeweiligen Device verfügbar sind, kann +man im WebIF des Plugins sehen. -Das Attribut ``zigbee2mqtt_attr`` entspricht dem jeweiligen Tag aus der Payload, der verwendet werden soll. Welche Tags beim jeweiligen Device verfügbar sind, kann man im WebIF des Pluigns sehen. +Die Informationen des Zigbee2MQTT-Gateways werden unter dem z2m_topic +(Gerätenamen) ``bridge`` bereitgestellt. -Die folgenden Tags des Attributes ``zigbee2mqtt_attr`` sind definiert und werden vom Plugin unterstützt: +Die folgenden Tags des Attributes ``z2m_attr`` sind definiert und werden vom +Plugin unterstützt: - online -- bridge_permit_join -- bridge_health_check -- bridge_restart -- bridge_networkmap_raw -- device_remove -- device_configure -- device_options -- device_rename -- device_configure_reporting -- temperature -- humidity +- permit_join (bridge) +- health_check (bridge) +- restart (bridge) +- networkmap_raw (bridge) +- device_remove (bridge) +- device_configure (bridge) +- device_options (bridge) +- device_rename (bridge) +- device_configure_reporting (bridge) - battery -- battery_low - linkquality - action -- vibration -- action_group -- voltage -- angle -- angle_x -- angle_x_absolute -- angle_y -- angle_y_absolute -- angle_z -- strength - last_seen -- tamper -- sensitivity -- contact - - -Web Interface des Plugins -========================= - -Zigbee2Mqtt Items ------------------ - -Das Webinterface zeigt die Items an, für die ein Zigbee2Mqtt Device konfiguriert ist. - -.. image:: user_doc/assets/webif_tab1.jpg - :class: screenshot - - -Zigbee2Mqtt Devices -------------------- - -Das Webinterface zeigt Informationen zu den konfigurierten Zigbee2Mqtt Devices an, sowie etwa hinzugekommen Devices die -in SmartHomeNG noch nicht konfiguriert (mit einem Item vebunden) sind. - -.. image:: user_doc/assets/webif_tab2.jpg - :class: screenshot - - -Zigbee2Mqtt Bridge Info ------------------------ - -Das Webinterface zeigt detaillierte Informationen der Zigbee2Mqtt Bridge zu jedem verbundenen Device an. - -.. image:: user_doc/assets/webif_tab3.jpg - :class: screenshot - -Broker Information ------------------- +Weitere Tags werden abhängig vom Gerät unterstützt. In den meisten Fällen können +auch unbekannte Tags bei direkter Konfiguration verwendet werden. -Das Webinterface zeigt Informationen zum genutzten MQTT Broker an. -.. image:: user_doc/assets/webif_tab6.jpg - :class: screenshot diff --git a/zigbee2mqtt/user_doc/assets/webif_tab1.jpg b/zigbee2mqtt/user_doc/assets/webif_tab1.jpg deleted file mode 100755 index a0b826a16..000000000 Binary files a/zigbee2mqtt/user_doc/assets/webif_tab1.jpg and /dev/null differ diff --git a/zigbee2mqtt/user_doc/assets/webif_tab2.jpg b/zigbee2mqtt/user_doc/assets/webif_tab2.jpg deleted file mode 100755 index 89e748cd8..000000000 Binary files a/zigbee2mqtt/user_doc/assets/webif_tab2.jpg and /dev/null differ diff --git a/zigbee2mqtt/user_doc/assets/webif_tab3.jpg b/zigbee2mqtt/user_doc/assets/webif_tab3.jpg deleted file mode 100755 index fdcd11cef..000000000 Binary files a/zigbee2mqtt/user_doc/assets/webif_tab3.jpg and /dev/null differ diff --git a/zigbee2mqtt/user_doc/assets/webif_tab4.jpg b/zigbee2mqtt/user_doc/assets/webif_tab4.jpg deleted file mode 100755 index 46976bf5f..000000000 Binary files a/zigbee2mqtt/user_doc/assets/webif_tab4.jpg and /dev/null differ diff --git a/zigbee2mqtt/user_doc/assets/webif_tab5.jpg b/zigbee2mqtt/user_doc/assets/webif_tab5.jpg deleted file mode 100755 index 61d8af207..000000000 Binary files a/zigbee2mqtt/user_doc/assets/webif_tab5.jpg and /dev/null differ diff --git a/zigbee2mqtt/user_doc/assets/webif_tab6.jpg b/zigbee2mqtt/user_doc/assets/webif_tab6.jpg deleted file mode 100755 index b2b405b3e..000000000 Binary files a/zigbee2mqtt/user_doc/assets/webif_tab6.jpg and /dev/null differ diff --git a/zigbee2mqtt/webif/__init__.py b/zigbee2mqtt/webif/__init__.py index 03f4f45f8..47a0e4122 100755 --- a/zigbee2mqtt/webif/__init__.py +++ b/zigbee2mqtt/webif/__init__.py @@ -29,7 +29,6 @@ import cherrypy from lib.item import Items from lib.model.smartplugin import SmartPluginWebIf -from jinja2 import Environment, FileSystemLoader class WebInterface(SmartPluginWebIf): @@ -72,8 +71,8 @@ def index(self, reload=None, action=None): return tmpl.render(plugin_shortname=self.plugin.get_shortname(), plugin_version=self.plugin.get_version(), plugin_info=self.plugin.get_info(), - items=sorted(self.plugin.zigbee2mqtt_items, key=lambda k: str.lower(k['_path'])), - item_count=len(self.plugin.zigbee2mqtt_items), + items=sorted([i for i in self.plugin.get_item_list()], key=lambda x: x.path().lower()), + item_count=len(self.plugin.get_item_list()), p=self.plugin, webif_pagelength=pagelength, ) @@ -96,23 +95,24 @@ def get_data_html(self, dataSet=None): data['broker_uptime'] = self.plugin.broker_uptime() data['item_values'] = {} - for item in self.plugin.zigbee2mqtt_items: + for item in self.plugin.get_item_list(): data['item_values'][item.id()] = {} data['item_values'][item.id()]['value'] = item.property.value data['item_values'][item.id()]['last_update'] = item.property.last_update.strftime('%d.%m.%Y %H:%M:%S') data['item_values'][item.id()]['last_change'] = item.property.last_change.strftime('%d.%m.%Y %H:%M:%S') data['device_values'] = {} - for device in self.plugin.zigbee2mqtt_devices: - data['device_values'][device] = {} - if 'data' in self.plugin.zigbee2mqtt_devices[device]: - data['device_values'][device]['lqi'] = str(self.plugin.zigbee2mqtt_devices[device]['data'].get('linkquality', '-')) - data['device_values'][device]['data'] = ", ".join(list(self.plugin.zigbee2mqtt_devices[device]['data'].keys())) + for device in self.plugin._devices: + if 'data' in self.plugin._devices[device]: + data['device_values'][device] = { + 'lqi': str(self.plugin._devices[device]['data'].get('linkquality', '-')), + 'data': ", ".join(list(self.plugin._devices[device]['data'].keys())) + } else: - data['device_values'][device]['lqi'] = '-' - data['device_values'][device]['data'] = '-' - if 'meta' in self.plugin.zigbee2mqtt_devices[device]: - last_seen = self.plugin.zigbee2mqtt_devices[device]['meta'].get('lastSeen', None) + data['device_values'][device] = {'lqi': '-', 'data': '-'} + + if 'meta' in self.plugin._devices[device]: + last_seen = self.plugin._devices[device]['meta'].get('lastSeen', None) if last_seen: data['device_values'][device]['last_seen'] = last_seen.strftime('%d.%m.%Y %H:%M:%S') else: diff --git a/zigbee2mqtt/webif/templates/index.html b/zigbee2mqtt/webif/templates/index.html index f819f7e1f..2eb3966ea 100755 --- a/zigbee2mqtt/webif/templates/index.html +++ b/zigbee2mqtt/webif/templates/index.html @@ -168,7 +168,7 @@ {{ p.broker_config.qos }} {{ webif_pagelength }} {{_('Zigbee2Mqtt')}} - {{ 'GUI' }} + {{ 'GUI' }} @@ -205,7 +205,7 @@ {% for item in items %} {% set item_id = item.id() %} - {{ item_id }} + {{ item.property.path }} {{ item.property.type }} {{ item()}} {{ p.get_iattr_value(item.conf, 'zigbee2mqtt_topic') }} @@ -238,16 +238,16 @@ - {% for device in p.zigbee2mqtt_devices %} - {% if p.zigbee2mqtt_devices[device]['meta'] %} + {% for device in p._devices %} + {% if p._devices[device]['meta'] %} {{ loop.index }} {{ device }} - {{ p.zigbee2mqtt_devices[device]['meta']['ieeeAddr'] }} - {{ p.zigbee2mqtt_devices[device]['meta']['vendor'] }} - {{ p.zigbee2mqtt_devices[device]['meta']['model'] }} - {{ p.zigbee2mqtt_devices[device]['meta']['description'] }} - {{ p.zigbee2mqtt_devices[device]['meta']['modelID'] }} + {{ p._devices[device]['meta']['ieee_address'] }} + {{ p._devices[device]['meta']['manufacturer'] }} + {{ p._devices[device]['meta']['model'] }} + {{ p._devices[device]['meta']['description'] }} + {{ p._devices[device]['meta']['model_id'] }} {{ _('Init...') }} {{ _('Init...') }} {{ _('Init...') }}