diff --git a/zigbee2mqtt/__init__.py b/zigbee2mqtt/__init__.py index 6006a2cf9..78d353ad8 100755 --- a/zigbee2mqtt/__init__.py +++ b/zigbee2mqtt/__init__.py @@ -89,7 +89,7 @@ def __init__(self, sh, **kwargs): # Add subscription to get bridge announces bridge_subs = [ ['devices', 'list', None], - ['state', 'bool', ['offline', 'online']], + ['state', 'dict', None], ['info', 'dict', None], ['log', 'dict', None], ['extensions', 'list', None], @@ -195,7 +195,7 @@ def parse_item(self, item): 'write': write, } if item.type() == 'bool': - data['bval'] = bval + data['bool_values'] = bval self._devices[device][attr].update(data) @@ -270,7 +270,9 @@ def update_item(self, item, caller='', source=None, dest=None): topic_3 = 'set' topic_4 = topic_5 = '' payload = None - bool_values = _attr.get('bool_values', self.bool_values) + bool_values = _attr.get('bool_values') + if bool_values is None: + bool_values = self.bool_values scenes = _device.get('scenes') value = item() @@ -288,27 +290,27 @@ def update_item(self, item, caller='', source=None, dest=None): # 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) + attr, 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) + 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_z2m_topic(device, topic_3, topic_4, topic_5, payload, item, bool_values=bool_values) + if value is not None: + payload = json.dumps({ + attr: value + }) else: - self.logger.warning(f"update_item: {item}, no payload defined (by {caller})") + payload = None + + 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}, trying to change item in SmartHomeNG that is readonly (by {caller})") @@ -347,10 +349,14 @@ def on_mqtt_msg(self, topic: str, payload, qos=None, retain=None): self.logger.error(f'received mqtt msg with wrong base topic {topic}. Please report') return - # check handlers + # check / call 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 + result = getattr(self, HANDLE_IN_PREFIX + HANDLE_DEV + device)(device, topic_3, topic_4, topic_5, payload, qos, retain) + if not isinstance(result, dict): + if result: + return + else: + payload = result if not isinstance(payload, dict): return @@ -400,7 +406,7 @@ def on_mqtt_msg(self, topic: str, payload, qos=None, retain=None): # 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 + continue value = payload[attr] self._devices[device][attr]['value'] = value @@ -508,6 +514,7 @@ def _get_z2m_topic_from_item(self, item) -> str: # # return True: stop further processing # return False/None: continue processing (possibly with changed payload) +# return : continue with returned dict as new payload # def _handle_in_dev_bridge(self, device: str, topic_3: str = "", topic_4: str = "", topic_5: str = "", payload={}, qos=None, retain=None): @@ -519,23 +526,29 @@ def _handle_in_dev_bridge(self, device: str, topic_3: str = "", topic_4: str = " _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) + return {'online': bool(['offline', 'online'].index(payload.get(topic_3)))} elif topic_3 in ('config', 'info'): assert isinstance(payload, dict), 'dict' _bridge[topic_3] = payload - _bridge['online'] = True + payload['online'] = True + + if payload.get('restart_required', None) is True: + self.publish_z2m_topic('bridge', 'request', 'restart') elif topic_3 == 'response' and topic_4 in ('health_check', 'permit_join', 'networkmap'): + # permit_join: {"data":{"value":true},"status":"ok"} + # 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'} assert isinstance(payload, dict), 'dict' _bridge[topic_4] = payload - _bridge['online'] = True + payload['online'] = True if topic_4 == 'health_check': - _bridge['online'] = bool(payload['data']['healthy']) + # topic_level1=zigbee2mqtt, topic_level2=bridge, topic_level3=response, topic_level4=health_check, topic_level5=, payload={'data': {'healthy': True}, 'status': 'ok'} + payload['online'] = bool(payload['data']['healthy']) elif topic_3 == 'devices' or topic_3 == 'groups': + # 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'}] assert isinstance(payload, list), 'list' self._get_device_data(payload, topic_4 == 'groups') @@ -547,7 +560,14 @@ def _handle_in_dev_bridge(self, device: str, topic_3: str = "", topic_4: str = " except (KeyError, TypeError): pass + return {topic_3: payload} + elif topic_3 == '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'} assert isinstance(payload, dict), 'dict' if 'message' in payload and payload.get('type') == 'devices': self._get_device_data(payload['message']) @@ -641,8 +661,8 @@ def _handle_out_dev_bridge(self, item, value, topic_3, topic_4, topic_5, device, # statically defined cmds for interaction with z2m-gateway # independent from connected devices bridge_cmds = { - 'permit_join': {'setval': 'VAL', 't5': ''}, - 'health_check': {'setval': None, 't5': ''}, + 'permit_join': {'setval': 'VAL', 'attr': 'value', 't5': ''}, + 'health_check': {'t5': ''}, 'restart': {'setval': None, 't5': ''}, 'networkmap': {'setval': 'raw', 't5': 'remove'}, 'device_remove': {'setval': 'STR', 't5': ''}, @@ -658,37 +678,42 @@ def _handle_out_dev_bridge(self, item, value, topic_3, topic_4, topic_5, device, payload = '' if attr.startswith('device_'): topic_4, topic_5 = attr.split('_') - if bridge_cmds[attr]['setval'] == 'VAL': + sv = bridge_cmds[attr].get('setval', '') + if sv == 'VAL': payload = value - if bridge_cmds[attr]['setval'] == 'STR': + if sv == 'STR': payload = str(value) - elif bridge_cmds[attr]['setval'] == 'PATH': + elif sv == 'PATH': payload = item.property.path + elif sv is None: + payload = None + if 'attr' in bridge_cmds[attr]: + attr = bridge_cmds[attr]['attr'] value = payload - return value, topic_3, topic_4, topic_5, False + return attr, 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 + return attr, 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 + return attr, 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 + return attr, 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 @@ -696,7 +721,7 @@ def _handle_out_attr_brightness_percent(self, item, value, topic_3, topic_4, top self._devices[device]['brightness']['item'](brightness) except (KeyError, AttributeError): pass - return value, topic_3, topic_4, topic_5, True + return attr, 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) @@ -704,7 +729,7 @@ def _handle_out_attr_color_temp_kelvin(self, item, value, topic_3, topic_4, topi self._devices[device]['color_temp']['item'](kelvin) except (KeyError, AttributeError): pass - return value, topic_3, topic_4, topic_5, True + return attr, 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: @@ -735,7 +760,7 @@ def _handle_out_attr_color_rgb(self, item, value, topic_3, topic_4, topic_5, dev except Exception as e: self.logger.debug(f'problem calling color sync: {e}') - return value, topic_3, topic_4, topic_5, True + return attr, value, topic_3, topic_4, topic_5, True # # Attention - color conversions xy/rgb: diff --git a/zigbee2mqtt/plugin.yaml b/zigbee2mqtt/plugin.yaml index 4c10a12e8..525a57dce 100755 --- a/zigbee2mqtt/plugin.yaml +++ b/zigbee2mqtt/plugin.yaml @@ -13,11 +13,11 @@ plugin: support: https://knx-user-forum.de/forum/supportforen/smarthome-py/1856775-support-thread-f%C3%BCr-das-zigbee2mqtt-plugin version: 2.0.0 # Plugin version - sh_minversion: 1.9.4 # minimum shNG version to use this plugin + sh_minversion: 1.9.5.6 # 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: yes + restartable: True classname: Zigbee2Mqtt # class containing the plugin parameters: @@ -117,6 +117,11 @@ item_structs: z2m_topic: ..:. z2m_attr: action_duration + last_seen: + type: foo + z2m_topic: ..:. + z2m_attr: last_seen + linkquality: type: num z2m_topic: ..:. @@ -214,6 +219,11 @@ item_structs: z2m_topic: ..:. z2m_attr: color_temp_startup + last_seen: + type: foo + z2m_topic: ..:. + z2m_attr: last_seen + light_white_ambient: struct: priv_z2m.light_white_ambient_group @@ -304,29 +314,67 @@ item_structs: eval_trigger: .. bridge: - info: - type: dict - mqtt_topic_in: z2m/bridge + z2m_topic: bridge - state: + online: type: bool - mqtt_topic_in: z2m/bridge/state + z2m_topic: ..:. + z2m_attr: online + z2m_readonly: true + + config: + type: dict + z2m_topic: ..:. + z2m_attr: config + z2m_readonly: true + + coordinator: + type: dict + z2m_topic: ..:. + z2m_attr: coordinator + z2m_readonly: true + + config_schema: + type: dict + z2m_topic: ..:. + z2m_attr: config_schema + z2m_readonly: true - logging: + network: type: dict - mqtt_topic_in: z2m/bridge/logging + z2m_topic: ..:. + z2m_attr: network + z2m_readonly: true + + permit_join: + type: bool + z2m_topic: ..:. + z2m_attr: permit_join + z2m_bool_values: [false, true] + + restart: + type: bool + z2m_topic: ..:. + z2m_attr: restart + z2m_bool_values: [false, true] + z2m_writeonly: true + autotimer: 1=False + + version: + type: str + z2m_topic: ..:. + z2m_attr: version devices: type: list - mqtt_topic_in: z2m/bridge/devices + z2m_topic: ..:. + z2m_attr: devices groups: type: list - mqtt_topic_in: z2m/bridge/groups + z2m_topic: ..:. + z2m_attr: 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)