From 3b0a906a97d8bfefaef67b6af9569f74ac091bd1 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Fri, 8 Oct 2021 13:02:24 +0200 Subject: [PATCH] Expose scenes (#9056) * Expose scenes * Republish devices and groups when changed. * Send scene name if set. --- lib/eventBus.ts | 7 +++++++ lib/extension/bridge.ts | 29 ++++++++++++++++++++++++++++- lib/extension/publish.ts | 7 +++++++ test/bridge.test.js | 7 ++++--- test/publish.test.js | 2 ++ test/stub/zigbeeHerdsman.js | 6 +++--- 6 files changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/eventBus.ts b/lib/eventBus.ts index 12b374c3a..7315c53a2 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -124,6 +124,13 @@ export default class EventBus { this.on('devicesChanged', callback, key); } + public emitScenesChanged(): void { + this.emitter.emit('scenesChanged'); + } + public onScenesChanged(key: ListenerKey, callback: () => void): void { + this.on('scenesChanged', callback, key); + } + public emitReportingDisabled(data: eventdata.ReportingDisabled): void { this.emitter.emit('reportingDisabled', data); } diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index aaa262f0a..231687d22 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -13,6 +13,8 @@ import Group from '../model/group'; const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`); +type Scene = {id: number, name: string}; + export default class Bridge extends Extension { private zigbee2mqttVersion: {commitHash: string, version: string}; private coordinatorVersion: zh.CoordinatorVersion; @@ -62,6 +64,10 @@ export default class Bridge extends Extension { this.eventBus.onGroupMembersChanged(this, () => this.publishGroups()); this.eventBus.onDevicesChanged(this, () => this.publishDevices() && this.publishInfo()); this.eventBus.onPermitJoinChanged(this, () => !this.zigbee.isStopping() && this.publishInfo()); + this.eventBus.onScenesChanged(this, () => { + this.publishDevices(); + this.publishGroups(); + }); // Zigbee events const publishEvent = (type: string, data: KeyValue): Promise => @@ -568,18 +574,38 @@ export default class Bridge extends Extension { 'bridge/info', stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true); } + private getScenes(entity: zh.Endpoint | zh.Group): Scene[] { + const scenes: {[id: number]: Scene} = {}; + const endpoints = utils.isEndpoint(entity) ? [entity] : entity.members; + const groupID = utils.isEndpoint(entity) ? 0 : entity.groupID; + + for (const endpoint of endpoints) { + for (const [key, data] of Object.entries(endpoint.meta?.scenes || {})) { + const split = key.split('_'); + const sceneID = parseInt(split[0], 10); + const sceneGroupID = parseInt(split[1], 10); + if (sceneGroupID === groupID) { + scenes[sceneID] = {id: sceneID, name: (data as KeyValue).name || `Scene ${sceneID}`}; + } + } + } + + return Object.values(scenes); + } + async publishDevices(): Promise { interface Data { bindings: {cluster: string, target: {type: string, endpoint?: number, ieee_address?: string, id?: number}}[] configured_reportings: {cluster: string, attribute: string | number, minimum_report_interval: number, maximum_report_interval: number, reportable_change: number}[], - clusters: {input: string[], output: string[]} + clusters: {input: string[], output: string[]}, scenes: Scene[] } const devices = this.zigbee.devices().map((device) => { const endpoints: {[s: number]: Data} = {}; for (const endpoint of device.zh.endpoints) { const data: Data = { + scenes: this.getScenes(endpoint), bindings: [], configured_reportings: [], clusters: { @@ -635,6 +661,7 @@ export default class Bridge extends Extension { return { id: g.ID, friendly_name: g.ID === 901 ? 'default_bind_group' : g.name, + scenes: this.getScenes(g.zh), members: g.zh.members.map((e) => { return {ieee_address: e.getDevice().ieeeAddr, endpoint: e.ID}; }), diff --git a/lib/extension/publish.ts b/lib/extension/publish.ts index 1aa231bdd..44f047c42 100644 --- a/lib/extension/publish.ts +++ b/lib/extension/publish.ts @@ -12,6 +12,7 @@ import bind from 'bind-decorator'; const topicRegex = new RegExp(`^(.+?)(?:/(${utils.endpointNames.join('|')}))?/(get|set)(?:/(.+))?`); const propertyEndpointRegex = new RegExp(`^(.*)_(${utils.endpointNames.join('|')})$`); const stateValues = ['on', 'off', 'toggle', 'open', 'close', 'stop', 'lock', 'unlock']; +const sceneConverterKeys = ['scene_store', 'scene_add', 'scene_remove', 'scene_remove_all']; // Legacy: don't provide default converters anymore, this is required by older z2m installs not saving group members const defaultGroupConverters = [ @@ -284,5 +285,11 @@ export default class Publish extends Extension { this.publishEntityState(toPublishEntity[ID], payload); } } + + const scenesChanged = Object.values(usedConverters) + .some((cl) => cl.some((c) => c.key.some((k) => sceneConverterKeys.includes(k)))); + if (scenesChanged) { + this.eventBus.emitScenesChanged(); + } } } diff --git a/test/bridge.test.js b/test/bridge.test.js index 67e7c46e4..eadc90f52 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -7,13 +7,14 @@ const Controller = require('../lib/controller'); const flushPromises = require('./lib/flushPromises'); const stringify = require('json-stable-stringify-without-jsonify'); -const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM} = zigbeeHerdsman.devices; +const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2} = zigbeeHerdsman.devices; zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr); zigbeeHerdsman.returnDevices.push(WXKG11LM.ieeeAddr); zigbeeHerdsman.returnDevices.push(remote.ieeeAddr); zigbeeHerdsman.returnDevices.push(ZNCZ02LM.ieeeAddr); +zigbeeHerdsman.returnDevices.push(bulb_color_2.ieeeAddr); describe('Bridge', () => { let controller; @@ -74,7 +75,7 @@ describe('Bridge', () => { await resetExtension(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/devices', - stringify([{"date_code":null,"definition":null,"endpoints":{"1":{"bindings":[],"clusters":{"input":[],"output":[]},"configured_reportings":[]}},"friendly_name":"Coordinator","ieee_address":"0x00124b00120144ae","interview_completed":false,"interviewing":false,"model_id":null,"network_address":0,"power_source":null,"software_build_id":null,"supported":false,"type":"Coordinator","manufacturer":undefined},{"date_code":null,"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","exposes":[{"features":[{"access":7,"description":"On/off state of this light","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454}],"property":"color_temp","type":"numeric","unit":"mired","value_max":454,"value_min":250},{"access":7,"description":"Color temperature after cold power on of this light","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":454,"value_min":250}],"type":"light"},{"access":2,"description":"Triggers an effect on the light (e.g. make light blink for a few seconds)","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"description":"Controls the behavior when the device is powered on","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","previous","on"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","supports_ota":true,"vendor":"IKEA"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":["genScenes","genOta"]},"configured_reportings":[{"attribute":"onOff","cluster":"genOnOff","maximum_report_interval":10,"minimum_report_interval":1,"reportable_change":20}]}},"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2","interview_completed":true,"interviewing":false,"model_id":"TRADFRI bulb E27 WS opal 980lm","network_address":40369,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router","manufacturer":undefined},{"date_code":null,"definition":{"description":"Hue dimmer switch","exposes":[{"access":1,"description":"Remaining battery in %","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"description":"Triggered action (e.g. a button click)","name":"action","property":"action","type":"enum","values":["on-press","on-hold","on-hold-release","up-press","up-hold","up-hold-release","down-press","down-hold","down-hold-release","off-press","off-hold","off-hold-release"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"324131092621","supports_ota":true,"vendor":"Philips"},"endpoints":{"1":{"bindings":[{"cluster":"genLevelCtrl","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"genOnOff","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"lightingColorCtrl","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"genOnOff","target":{"id":1,"type":"group"}},{"cluster":"genLevelCtrl","target":{"id":1,"type":"group"}}],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[]},"2":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genOta","genOnOff"]},"configured_reportings":[]}},"friendly_name":"remote","ieee_address":"0x0017880104e45517","interview_completed":true,"interviewing":false,"model_id":"RWL021","network_address":6535,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice","manufacturer":undefined},{"date_code":null,"definition":null,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[]}},"friendly_name":"0x0017880104e45518","ieee_address":"0x0017880104e45518","interview_completed":true,"interviewing":false,"model_id":"notSupportedModelID","network_address":6536,"power_source":"Battery","software_build_id":null,"supported":false,"type":"EndDevice","manufacturer":"notSupportedMfg"},{"date_code":null,"definition":{"description":"Aqara wireless switch","exposes":[{"access":1,"description":"Remaining battery in %","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"description":"Voltage of the battery in millivolts","name":"voltage","property":"voltage","type":"numeric","unit":"mV"},{"access":1,"description":"Triggered action (e.g. a button click)","name":"action","property":"action","type":"enum","values":["single","double","triple","quadruple","hold","release"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"WXKG11LM","supports_ota":false,"vendor":"Xiaomi"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[{"attribute":1337,"cluster":"genOnOff","maximum_report_interval":10,"minimum_report_interval":1,"reportable_change":20}]}},"friendly_name":"button","ieee_address":"0x0017880104e45520","interview_completed":true,"interviewing":false,"model_id":"lumi.sensor_switch.aq2","network_address":6537,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice","manufacturer":undefined},{"date_code":null,"definition":{"description":"Mi power plug ZigBee","exposes":[{"features":[{"access":7,"description":"On/off state of the switch","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"}],"type":"switch"},{"access":5,"description":"Instantaneous measured power","name":"power","property":"power","type":"numeric","unit":"W"},{"access":1,"description":"Sum of consumed energy","name":"energy","property":"energy","type":"numeric","unit":"kWh"},{"access":1,"description":"Measured temperature value","name":"temperature","property":"temperature","type":"numeric","unit":"°C"},{"access":7,"description":"Enable/disable the power outage memory, this recovers the on/off mode after power failure","name":"power_outage_memory","property":"power_outage_memory","type":"binary","value_off":false,"value_on":true},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"ZNCZ02LM","supports_ota":true,"vendor":"Xiaomi"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":[]},"configured_reportings":[]}},"friendly_name":"power_plug","ieee_address":"0x0017880104e45524","interview_completed":true,"interviewing":false,"model_id":"lumi.plug","network_address":6540,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router","manufacturer":undefined}]), + stringify([{"date_code":null,"definition":null,"endpoints":{"1":{"bindings":[],"clusters":{"input":[],"output":[]},"configured_reportings":[],"scenes":[]}},"friendly_name":"Coordinator","ieee_address":"0x00124b00120144ae","interview_completed":false,"interviewing":false,"model_id":null,"network_address":0,"power_source":null,"software_build_id":null,"supported":false,"type":"Coordinator"},{"date_code":null,"definition":{"description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","exposes":[{"features":[{"access":7,"description":"On/off state of this light","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454}],"property":"color_temp","type":"numeric","unit":"mired","value_max":454,"value_min":250},{"access":7,"description":"Color temperature after cold power on of this light","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":250},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":454},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":454,"value_min":250}],"type":"light"},{"access":2,"description":"Triggers an effect on the light (e.g. make light blink for a few seconds)","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"description":"Controls the behavior when the device is powered on","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","previous","on"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","supports_ota":true,"vendor":"IKEA"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":["genScenes","genOta"]},"configured_reportings":[{"attribute":"onOff","cluster":"genOnOff","maximum_report_interval":10,"minimum_report_interval":1,"reportable_change":20}],"scenes":[]}},"friendly_name":"bulb","ieee_address":"0x000b57fffec6a5b2","interview_completed":true,"interviewing":false,"model_id":"TRADFRI bulb E27 WS opal 980lm","network_address":40369,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router"},{"date_code":"2019.09","definition":{"description":"Hue Go","exposes":[{"features":[{"access":7,"description":"On/off state of this light","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","name":"color_temp","presets":[{"description":"Coolest temperature supported","name":"coolest","value":150},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":500}],"property":"color_temp","type":"numeric","unit":"mired","value_max":500,"value_min":150},{"access":7,"description":"Color temperature after cold power on of this light","name":"color_temp_startup","presets":[{"description":"Coolest temperature supported","name":"coolest","value":150},{"description":"Cool temperature (250 mireds / 4000 Kelvin)","name":"cool","value":250},{"description":"Neutral temperature (370 mireds / 2700 Kelvin)","name":"neutral","value":370},{"description":"Warm temperature (454 mireds / 2200 Kelvin)","name":"warm","value":454},{"description":"Warmest temperature supported","name":"warmest","value":500},{"description":"Restore previous color_temp on cold power on","name":"previous","value":65535}],"property":"color_temp_startup","type":"numeric","unit":"mired","value_max":500,"value_min":150},{"description":"Color of this light in the CIE 1931 color space (x/y)","features":[{"access":7,"name":"x","property":"x","type":"numeric"},{"access":7,"name":"y","property":"y","type":"numeric"}],"name":"color_xy","property":"color","type":"composite"},{"description":"Color of this light expressed as hue/saturation","features":[{"access":7,"name":"hue","property":"hue","type":"numeric"},{"access":7,"name":"saturation","property":"saturation","type":"numeric"}],"name":"color_hs","property":"color","type":"composite"}],"type":"light"},{"access":2,"description":"Triggers an effect on the light (e.g. make light blink for a few seconds)","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"7146060PH","supports_ota":true,"vendor":"Philips"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic","genScenes","genOnOff","genLevelCtrl","lightingColorCtrl"],"output":["genScenes","genOta"]},"configured_reportings":[],"scenes":[{"id":1,"name":"Chill scene"}]}},"friendly_name":"bulb_color_2","ieee_address":"0x000b57fffec6a5b4","interview_completed":true,"interviewing":false,"manufacturer":"Philips","model_id":"LLC020","network_address":401292,"power_source":"Mains (single phase)","software_build_id":"5.127.1.26581","supported":true,"type":"Router"},{"date_code":null,"definition":{"description":"Hue dimmer switch","exposes":[{"access":1,"description":"Remaining battery in %","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"description":"Triggered action (e.g. a button click)","name":"action","property":"action","type":"enum","values":["on-press","on-hold","on-hold-release","up-press","up-hold","up-hold-release","down-press","down-hold","down-hold-release","off-press","off-hold","off-hold-release"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"324131092621","supports_ota":true,"vendor":"Philips"},"endpoints":{"1":{"bindings":[{"cluster":"genLevelCtrl","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"genOnOff","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"lightingColorCtrl","target":{"endpoint":1,"ieee_address":"0x000b57fffec6a5b3","type":"endpoint"}},{"cluster":"genOnOff","target":{"id":1,"type":"group"}},{"cluster":"genLevelCtrl","target":{"id":1,"type":"group"}}],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[],"scenes":[]},"2":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genOta","genOnOff"]},"configured_reportings":[],"scenes":[]}},"friendly_name":"remote","ieee_address":"0x0017880104e45517","interview_completed":true,"interviewing":false,"model_id":"RWL021","network_address":6535,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice"},{"date_code":null,"definition":null,"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[],"scenes":[]}},"friendly_name":"0x0017880104e45518","ieee_address":"0x0017880104e45518","interview_completed":true,"interviewing":false,"manufacturer":"notSupportedMfg","model_id":"notSupportedModelID","network_address":6536,"power_source":"Battery","software_build_id":null,"supported":false,"type":"EndDevice"},{"date_code":null,"definition":{"description":"Aqara wireless switch","exposes":[{"access":1,"description":"Remaining battery in %","name":"battery","property":"battery","type":"numeric","unit":"%","value_max":100,"value_min":0},{"access":1,"description":"Voltage of the battery in millivolts","name":"voltage","property":"voltage","type":"numeric","unit":"mV"},{"access":1,"description":"Triggered action (e.g. a button click)","name":"action","property":"action","type":"enum","values":["single","double","triple","quadruple","hold","release"]},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"WXKG11LM","supports_ota":false,"vendor":"Xiaomi"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":["genBasic","genOnOff","genLevelCtrl","genScenes"]},"configured_reportings":[{"attribute":1337,"cluster":"genOnOff","maximum_report_interval":10,"minimum_report_interval":1,"reportable_change":20}],"scenes":[]}},"friendly_name":"button","ieee_address":"0x0017880104e45520","interview_completed":true,"interviewing":false,"model_id":"lumi.sensor_switch.aq2","network_address":6537,"power_source":"Battery","software_build_id":null,"supported":true,"type":"EndDevice"},{"date_code":null,"definition":{"description":"Mi power plug ZigBee","exposes":[{"features":[{"access":7,"description":"On/off state of the switch","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"}],"type":"switch"},{"access":5,"description":"Instantaneous measured power","name":"power","property":"power","type":"numeric","unit":"W"},{"access":1,"description":"Sum of consumed energy","name":"energy","property":"energy","type":"numeric","unit":"kWh"},{"access":1,"description":"Measured temperature value","name":"temperature","property":"temperature","type":"numeric","unit":"°C"},{"access":7,"description":"Enable/disable the power outage memory, this recovers the on/off mode after power failure","name":"power_outage_memory","property":"power_outage_memory","type":"binary","value_off":false,"value_on":true},{"access":1,"description":"Link quality (signal strength)","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"ZNCZ02LM","supports_ota":true,"vendor":"Xiaomi"},"endpoints":{"1":{"bindings":[],"clusters":{"input":["genBasic"],"output":[]},"configured_reportings":[],"scenes":[]}},"friendly_name":"power_plug","ieee_address":"0x0017880104e45524","interview_completed":true,"interviewing":false,"model_id":"lumi.plug","network_address":6540,"power_source":"Mains (single phase)","software_build_id":null,"supported":true,"type":"Router"}]), { retain: true, qos: 0 }, expect.any(Function) ); @@ -111,7 +112,7 @@ describe('Bridge', () => { logger.setTransportsEnabled(true); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/groups', - stringify([{"friendly_name":"group_1","id":1,"members":[]},{"friendly_name":"group_tradfri_remote","id":15071,"members":[{"endpoint":1,"ieee_address":"0x000b57fffec6a5b7"}]},{"friendly_name":"99","id":99,"members":[]},{"friendly_name":"group_with_tradfri","id":11,"members":[]},{"friendly_name":"thermostat_group","id":12,"members":[]},{"friendly_name":"switch_group","id":14,"members":[{"endpoint":1,"ieee_address":"0x0017880104e45524"}]},{"friendly_name":"gledopto_group","id":21,"members":[]},{"friendly_name":"default_bind_group","id":901,"members":[]},{"friendly_name":"ha_discovery_group","id":9,"members":[{"endpoint":1,"ieee_address":"0x000b57fffec6a5b7"}]},{"friendly_name":"group_2","id":2,"members":[]}]), + stringify([{"friendly_name":"group_1","id":1,"members":[],"scenes":[]},{"friendly_name":"group_tradfri_remote","id":15071,"members":[{"endpoint":1,"ieee_address":"0x000b57fffec6a5b4"}],"scenes":[]},{"friendly_name":"99","id":99,"members":[],"scenes":[]},{"friendly_name":"group_with_tradfri","id":11,"members":[],"scenes":[]},{"friendly_name":"thermostat_group","id":12,"members":[],"scenes":[]},{"friendly_name":"switch_group","id":14,"members":[{"endpoint":1,"ieee_address":"0x0017880104e45524"}],"scenes":[]},{"friendly_name":"gledopto_group","id":21,"members":[],"scenes":[]},{"friendly_name":"default_bind_group","id":901,"members":[],"scenes":[]},{"friendly_name":"ha_discovery_group","id":9,"members":[{"endpoint":1,"ieee_address":"0x000b57fffec6a5b4"},{"endpoint":2,"ieee_address":"0x0017880104e45542"}],"scenes":[{"id":4,"name":"Scene 4"}]},{"friendly_name":"group_2","id":2,"members":[],"scenes":[]}]), { retain: true, qos: 0 }, expect.any(Function) ); diff --git a/test/publish.test.js b/test/publish.test.js index 504fc0a34..c276c5f55 100644 --- a/test/publish.test.js +++ b/test/publish.test.js @@ -1401,6 +1401,8 @@ describe('Publish', () => { await flushPromises(); expect(group.command).toHaveBeenCalledTimes(1); expect(group.command).toHaveBeenCalledWith('genScenes', 'store', { groupid: 15071, sceneid: 1 }, {}); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/devices', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/groups', expect.any(String), {retain: true, qos: 0}, expect.any(Function)); await MQTT.events.message('zigbee2mqtt/bulb_color_2/set', stringify({"state": "ON", "brightness": 250, "color_temp": 20})); await MQTT.events.message('zigbee2mqtt/bulb_2/set', stringify({"state": "ON", "brightness": 110})); diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js index 13c356f10..1d2b7ff17 100644 --- a/test/stub/zigbeeHerdsman.js +++ b/test/stub/zigbeeHerdsman.js @@ -31,7 +31,7 @@ const clusters = { } class Endpoint { - constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}, configuredReportings=[], profileID=null, deviceID=null) { + constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}, configuredReportings=[], profileID=null, deviceID=null, meta={}) { this.deviceIeeeAddress = deviceIeeeAddress; this.clusterValues = clusterValues; this.ID = ID; @@ -45,7 +45,7 @@ class Endpoint { this.unbind = jest.fn(); this.save = jest.fn(); this.configureReporting = jest.fn(); - this.meta = {}; + this.meta = meta; this.binds = binds; this.profileID = profileID; this.deviceID = deviceID; @@ -127,7 +127,7 @@ class Device { const returnDevices = []; const bulb_color = new Device('Router', '0x000b57fffec6a5b3', 40399, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "LLC020"); -const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4', [], {lightingColorCtrl: {colorCapabilities: 254}})], true, "Mains (single phase)", "LLC020", false, 'Philips', '2019.09', '5.127.1.26581'); +const bulb_color_2 = new Device('Router', '0x000b57fffec6a5b4', 401292, 4107, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b4', [], {lightingColorCtrl: {colorCapabilities: 254}}, [], null, null, {'scenes': {'1_0': {name: 'Chill scene', state: {state: 'ON'}}, '4_9': {state: {state: 'OFF'}}}})], true, "Mains (single phase)", "LLC020", false, 'Philips', '2019.09', '5.127.1.26581'); const bulb_2 = new Device('Router', '0x000b57fffec6a5b7', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b7', [], {lightingColorCtrl: {colorCapabilities: 17}})], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"); const TS0601_thermostat = new Device('EndDevice', '0x0017882104a44559', 6544,4151, [new Endpoint(1, [], [], '0x0017882104a44559')], true, "Mains (single phase)", 'kud7u2l'); const ZNCZ02LM = new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45524')], true, "Mains (single phase)", "lumi.plug");