From 4d3bbce687136117b53019a1b3e8aa32fe389fcc Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Sun, 15 Mar 2020 09:38:39 +0100 Subject: [PATCH] Implement lightWithPostfix Home Assistant configuration. #3003 --- lib/extension/entityPublish.js | 8 +++- lib/extension/homeassistant.js | 77 ++++++++++++++++++++++------------ test/homeassistant.test.js | 25 +++++++++++ test/stub/data.js | 5 ++- test/stub/zigbeeHerdsman.js | 1 + 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/lib/extension/entityPublish.js b/lib/extension/entityPublish.js index beca138b..a2588dbf 100644 --- a/lib/extension/entityPublish.js +++ b/lib/extension/entityPublish.js @@ -174,8 +174,12 @@ class EntityPublish extends BaseExtension { const msg = result.state; if (postfix) { - msg[`state_${postfix}`] = msg.state; - delete msg.state; + for (const key of ['state', 'brightness']) { + if (msg.hasOwnProperty(key)) { + msg[`${key}_${postfix}`] = msg[key]; + delete msg[key]; + } + } } this.publishEntityState(entity.settings.ID, msg); diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index 085d8a5a..fd4cd542 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -3,6 +3,7 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); const zigbee2mqttVersion = require('../../package.json').version; const BaseExtension = require('./baseExtension'); +const objectAssignDeep = require(`object-assign-deep`); const cfg = { // Binary sensor @@ -539,25 +540,12 @@ const switchWithPostfix = (postfix) => { }; }; -const lightWithPostfix = (postfix) => { - return { - type: 'light', - object_id: 'light', - discovery_payload: { - schema: 'template', - command_topic: true, - command_topic_prefix: postfix, - command_on_template: ` - {"state": "on" - {%- if brightness is defined -%} - , "brightness": {{ brightness }} - {%- endif -%} - }`, - command_off_template: '{"state": "off"}', - state_template: `{{ value_json.state_${postfix} }}`, - brightness_template: `{{ value_json.brightness_${postfix} }}`, - }, - }; +const lightWithPostfix = (configType, postfix) => { + const config = objectAssignDeep.noMutate(cfg[configType]); + config['object_id'] = `light_${postfix}`; + config['discovery_payload']['command_topic_prefix'] = postfix; + config['discovery_payload']['state_topic_postfix'] = postfix; + return config; }; // Map homeassitant configurations to devices. @@ -1303,7 +1291,7 @@ const mapping = { 'GL-D-003ZS': [cfg.light_brightness_colortemp_colorxy], '66492-001': [cfg.lock, cfg.sensor_battery], '798.09': [cfg.light_brightness_colortemp_colorxy], - 'U202DST600ZB': [lightWithPostfix('l1'), lightWithPostfix('l2')], + 'U202DST600ZB': [lightWithPostfix('light_brightness', 'l1'), lightWithPostfix('light_brightness', 'l2')], 'SM10ZW': [cfg.binary_sensor_contact, cfg.sensor_battery], 'ICZB-R11D': [cfg.light_brightness], 'SW2500ZB': [cfg.switch], @@ -1364,9 +1352,43 @@ class HomeAssistant extends BaseExtension { } async onPublishEntityState(data) { - const key = ['action', 'click'].find((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== ''); + /** + * In case we deal with a lightWithPostfix configuration Zigbee2mqtt publishes + * e.g. {state_l1: ON, brightness_l1: 250} to zigbee2mqtt/mydevice. + * As the Home Assistant MQTT JSON light cannot be configured to use state_l1/brightness_l1 + * as the state variables, the state topic is set to zigbee2mqtt/mydevice/l1. + * Here we retrieve all the attributes with the _l1 values and republish them on + * zigbee2mqtt/mydevice/l1. + */ + if (data.entity.mapped && mapping[data.entity.mapped.model]) { + for (const config of mapping[data.entity.mapped.model]) { + const match = /light_(.*)/.exec(config['object_id']); + if (match) { + const postfix = match[1]; + const posfixRegExp = new RegExp(`(.*)_${postfix}`); + const payload = {}; + for (const key of Object.keys(data.payload)) { + const keyMatch = posfixRegExp.exec(key); + if (keyMatch) { + payload[keyMatch[1]] = data.payload[key]; + } + } - if (key && data.entity.type === 'device') { + await this.mqtt.publish( + `${data.entity.name}/${postfix}`, JSON.stringify(payload), {}, + ); + } + } + } + + /** + * Implements the MQTT device trigger (https://www.home-assistant.io/integrations/device_trigger.mqtt/) + * The MQTT device trigger does not support JSON parsing, so it cannot listen to zigbee2mqtt/my_device + * Whenever a device publish an {action: *} we discover an MQTT device trigger sensor + * and republish it to zigbee2mqtt/my_devic/action + */ + const key = ['action', 'click'].find((k) => data.payload.hasOwnProperty(k) && data.payload[k] !== ''); + if (data.entity.mapped && key) { const device = data.entity.device; if (!this.discoveredTriggers[device.ieeeAddr]) { this.discoveredTriggers[device.ieeeAddr] = new Set(); @@ -1379,16 +1401,15 @@ class HomeAssistant extends BaseExtension { const config = cfg[`trigger_${key}`]; config.object_id = `${key}_${value}`; const topic = this.getDiscoveryTopic(config, device); - const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); const payload = { ...config.discovery_payload, subtype: value, payload: value, topic: `${settings.get().mqtt.base_topic}/${data.entity.name}/${key}`, - device: this.getDevicePayload(data.entity.settings, mappedModel), + device: this.getDevicePayload(data.entity.settings, data.entity.mapped), }; - this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic); + await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic); this.discoveredTriggers[device.ieeeAddr].add(discoveredKey); } @@ -1443,7 +1464,11 @@ class HomeAssistant extends BaseExtension { this.getConfigs(mappedModel).forEach((config) => { const topic = this.getDiscoveryTopic(config, device); const payload = {...config.discovery_payload}; - const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`; + let stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`; + if (payload.state_topic_postfix) { + stateTopic += `/${payload.state_topic_postfix}`; + delete payload.state_topic_postfix; + } if (!payload.hasOwnProperty('state_topic') || payload.state_topic) { payload.state_topic = stateTopic; diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index 314b9c89..29116ae2 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -893,4 +893,29 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(3); }); + + it('Should republish payload to postfix topic with lightWithPostfix config', async () => { + controller = new Controller(false); + await controller.start(); + await flushPromises(); + MQTT.publish.mockClear(); + + await MQTT.events.message('zigbee2mqtt/U202DST600ZB/l2/set', JSON.stringify({state: 'ON', brightness: 20})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB', JSON.stringify({state_l2:"ON", brightness_l2:20}), {"qos": 0, "retain": false}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB/l2', JSON.stringify({state:"ON", brightness:20}), {"qos": 0, "retain": false}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/U202DST600ZB/l1', JSON.stringify({}), {"qos": 0, "retain": false}, expect.any(Function)); + }); + + it('Shouldnt crash in onPublishEntityState on group publish', async () => { + controller = new Controller(false); + await controller.start(); + await flushPromises(); + logger.error.mockClear(); + MQTT.publish.mockClear(); + + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expect(logger.error).toHaveBeenCalledTimes(0); + }); }); diff --git a/test/stub/data.js b/test/stub/data.js index 9285624f..768729aa 100644 --- a/test/stub/data.js +++ b/test/stub/data.js @@ -147,7 +147,10 @@ function writeDefaultConfiguration() { }, '0x0017880104e45526': { friendly_name: 'GL-S-007ZS', - } + }, + '0x0017880104e43559': { + friendly_name: 'U202DST600ZB' + }, }, groups: { '1': { diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index cbe07aa0..39fd02c7 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -144,6 +144,7 @@ const devices = { 'SP600_NEW': new Device('Router', '0x90fd9ffffe4b64ab', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {seMetering: {"multiplier":1,"divisor":10000}})], true, "Mains (single phase)", "SP600", false, 'Salus', '20170220'), 'MKS-CM-W5': new Device('Router', '0x90fd9ffffe4b64ac', 33901, 4476, [new Endpoint(1, [0,4,3,5,10,258,13,19,6,1,1030,8,768,1027,1029,1026], [0,3,4,6,8,5], '0x90fd9ffffe4b64aa', [], {})], true, "Mains (single phase)", "qnazj70", false), 'GL-S-007ZS': new Device('Router', '0x0017880104e45526', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45526')], true, "Mains (single phase)", 'GL-S-007ZS'), + 'U202DST600ZB': new Device('Router', '0x0017880104e43559', 6540,4151, [new Endpoint(10, [0, 6], [], '0x0017880104e43559'), new Endpoint(11, [0, 6], [], '0x0017880104e43559')], true, "Mains (single phase)", 'U202DST600ZB'), } const groups = {