From f9aa5250487c58be7d1bfc32a150a2d9bab398a4 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Tue, 21 Dec 2021 22:02:05 +0100 Subject: [PATCH] Refactor Home Assistant extension mapping, prepares for #10132 --- lib/extension/homeassistant.ts | 109 +++++++++++++++------------------ test/homeassistant.test.js | 28 ++------- 2 files changed, 56 insertions(+), 81 deletions(-) diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 87abc97f..1c846382 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -2,7 +2,6 @@ import * as settings from '../util/settings'; import logger from '../util/logger'; import utils from '../util/utils'; import stringify from 'json-stable-stringify-without-jsonify'; -import zigbeeHerdsmanConverters from 'zigbee-herdsman-converters'; import assert from 'assert'; import Extension from './extension'; import bind from 'bind-decorator'; @@ -27,6 +26,30 @@ const ACCESS_SET = 0b010; const groupSupportedTypes = ['light', 'switch', 'lock', 'cover']; const defaultStatusTopic = 'homeassistant/status'; +const legacyMapping = [ + { + models: ['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S', + 'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045', + 'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2', + 'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM', + 'E1743'], + discovery: sensorClick, + }, + { + models: ['ICTC-G-1'], + discovery: { + type: 'sensor', + mockProperties: [{property: 'brightness', value: null}], + object_id: 'brightness', + discovery_payload: { + unit_of_measurement: 'brightness', + icon: 'mdi:brightness-5', + value_template: '{{ value_json.brightness }}', + }, + }, + }, +]; + const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): string => { if (feature.endpoint) { return feature.property.slice(0, -1 + -1 * feature.endpoint.length); @@ -39,8 +62,8 @@ const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): s * This extensions handles integration with HomeAssistant */ export default class HomeAssistant extends Extension { - private discovered: {[s: string]: {topics: Set, mockProperties: Set}} = {}; - private mapping: {[s: string]: DiscoveryEntry[]} = {}; + private discovered: {[s: string]: + {topics: Set, mockProperties: Set, objectIDs: Set}} = {}; private discoveredTriggers : {[s: string]: Set}= {}; private discoveryTopic = settings.get().advanced.homeassistant_discovery_topic; private statusTopic = settings.get().advanced.homeassistant_status_topic; @@ -62,7 +85,6 @@ export default class HomeAssistant extends Extension { } this.zigbee2MQTTVersion = (await utils.getZigbee2MQTTVersion(false)).version; - this.populateMapping(); this.eventBus.onDeviceRemoved(this, this.onDeviceRemoved); this.eventBus.onMQTTMessage(this, this.onMQTTMessage); @@ -755,46 +777,6 @@ export default class HomeAssistant extends Extension { return discoveryEntries; } - private populateMapping(): void { - for (const def of zigbeeHerdsmanConverters.definitions) { - this.mapping[def.model] = []; - - if (['WXKG01LM', 'HS1EB/HS1EB-E', 'ICZB-KPD14S', 'TERNCY-SD01', 'TERNCY-PP01', 'ICZB-KPD18S', - 'E1766', 'ZWallRemote0', 'ptvo.switch', '2AJZ4KPKEY', 'ZGRC-KEY-013', 'HGZB-02S', 'HGZB-045', - 'HGZB-1S', 'AV2010/34', 'IM6001-BTP01', 'WXKG11LM', 'WXKG03LM', 'WXKG02LM_rev1', 'WXKG02LM_rev2', - 'QBKG04LM', 'QBKG03LM', 'QBKG11LM', 'QBKG21LM', 'QBKG22LM', 'WXKG12LM', 'QBKG12LM', - 'E1743'].includes(def.model)) { - // deprecated - this.mapping[def.model].push(sensorClick); - } - - if (['ICTC-G-1'].includes(def.model)) { - // deprecated - this.mapping[def.model].push({ - type: 'sensor', - mockProperties: [{property: 'brightness', value: null}], - object_id: 'brightness', - discovery_payload: { - unit_of_measurement: 'brightness', - icon: 'mdi:brightness-5', - value_template: '{{ value_json.brightness }}', - }, - }); - } - - for (const expose of def.exposes) { - this.mapping[def.model].push(...this.exposeToConfig([expose], 'device', def)); - } - } - - // Deprecated in favour of exposes - for (const definition of utils.getExternalConvertersDefinitions(settings.get())) { - if (definition.hasOwnProperty('homeassistant')) { - this.mapping[definition.model] = definition.homeassistant; - } - } - } - @bind onDeviceRemoved(data: eventdata.DeviceRemoved): void { logger.debug(`Clearing Home Assistant discovery topic for '${data.name}'`); this.discovered[data.ieeeAddr]?.topics.forEach((topic) => { @@ -818,9 +800,9 @@ export default class HomeAssistant extends Extension { * zigbee2mqtt/mydevice/l1. */ const entity = this.zigbee.resolveEntity(data.entity.name); - if (entity.isDevice() && this.mapping[entity.definition?.model]) { - for (const config of this.mapping[entity.definition.model]) { - const match = /light_(.*)/.exec(config['object_id']); + if (entity.isDevice() && this.discovered[entity.ieeeAddr]) { + for (const objectID of this.discovered[entity.ieeeAddr].objectIDs) { + const match = /light_(.*)/.exec(objectID); if (match) { const endpoint = match[1]; const endpointRegExp = new RegExp(`(.*)_${endpoint}`); @@ -893,12 +875,26 @@ export default class HomeAssistant extends Extension { private getConfigs(entity: Device | Group): DiscoveryEntry[] { const isDevice = entity.isDevice(); /* istanbul ignore next */ - if (!entity || (isDevice && !entity.definition) || - (isDevice && !this.mapping[entity.definition.model])) return []; + if (!entity || (isDevice && !entity.definition)) return []; - let configs: DiscoveryEntry[]; + let configs: DiscoveryEntry[] = []; if (isDevice) { - configs = this.mapping[entity.definition.model].slice(); + for (const expose of entity.definition.exposes) { + configs.push(...this.exposeToConfig([expose], 'device', entity.definition)); + } + + for (const mapping of legacyMapping) { + if (mapping.models.includes(entity.definition.model)) { + configs.push(mapping.discovery); + } + } + + // Deprecated in favour of exposes + /* istanbul ignore if */ + if (entity.definition.hasOwnProperty('homeassistant')) { + // @ts-ignore + configs.push(entity.definition.homeassistant); + } } else { // group const exposesByType: {[s: string]: zhc.DefinitionExpose[]} = {}; @@ -1009,13 +1005,12 @@ export default class HomeAssistant extends Extension { if (entity.isGroup()) { if (!discover || entity.zh.members.length === 0) return; - } else if (!discover || !entity.definition || !this.mapping[entity.definition.model] || - entity.zh.interviewing || + } else if (!discover || !entity.definition || entity.zh.interviewing || (entity.settings.hasOwnProperty('homeassistant') && !entity.settings.homeassistant)) { return; } - this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set()}; + this.discovered[discoverKey] = {topics: new Set(), mockProperties: new Set(), objectIDs: new Set()}; this.getConfigs(entity).forEach((config) => { const payload = {...config.discovery_payload}; const baseTopic = `${settings.get().mqtt.base_topic}/${entity.name}`; @@ -1202,6 +1197,7 @@ export default class HomeAssistant extends Extension { const topic = this.getDiscoveryTopic(config, entity); this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false); this.discovered[discoverKey].topics.add(topic); + this.discovered[discoverKey].objectIDs.add(config.object_id); config.mockProperties?.forEach((mockProperty) => this.discovered[discoverKey].mockProperties.add(mockProperty)); }); @@ -1372,11 +1368,6 @@ export default class HomeAssistant extends Extension { this.discoveredTriggers[device.ieeeAddr].add(discoveredKey); } - // Only for homeassistant.test.js - _getMapping(): {[s: string]: DiscoveryEntry[]} { - return this.mapping; - } - _clearDiscoveredTrigger(): void { this.discoveredTriggers = {}; } diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index e97500de..65b1f4db 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -49,14 +49,16 @@ describe('HomeAssistant extension', () => { it('Should not have duplicate type/object_ids in a mapping', () => { const duplicated = []; require('zigbee-herdsman-converters').devices.forEach((d) => { - const mapping = extension._getMapping()[d.model]; + const device = {definition: d, isDevice: () => true, settings: {}}; + const configs = extension.getConfigs(device); const cfg_type_object_ids = []; - mapping.forEach((c) => { - if (cfg_type_object_ids.includes(c['type'] + '/' + c['object_id'])) { + configs.forEach((c) => { + const id = c['type'] + '/' + c['object_id']; + if (cfg_type_object_ids.includes(id)) { duplicated.push(d.model); } else { - cfg_type_object_ids.push(c['type'] + '/' + c['object_id']); + cfg_type_object_ids.push(id); } }); }); @@ -1490,24 +1492,6 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish.mock.calls[3][0]).toStrictEqual('zigbee2mqtt/button/action'); }); - it('Load Home Assistant mapping from external converters', async () => { - fs.copyFileSync(path.join(__dirname, 'assets', 'mock-external-converter-multiple.js'), path.join(data.mockDir, 'mock-external-converter-multiple.js')); - settings.set(['external_converters'], ['mock-external-converter-multiple.js']); - await resetExtension(); - - const homeassistantSwitch = { - type: 'switch', - object_id: 'switch', - discovery_payload: { - payload_off: 'OFF', - payload_on: 'ON', - value_template: '{{ value_json.state }}', - command_topic: true, - }, - }; - expect(extension._getMapping()['external_converters_device_1']).toEqual([homeassistantSwitch]); - }); - it('Should clear outdated configs', async () => { // Non-existing group -> clear MQTT.publish.mockClear();