Expose scenes (#9056)

* Expose scenes

* Republish devices and groups when changed.

* Send scene name if set.
This commit is contained in:
Koen Kanters
2021-10-08 19:02:24 +08:00
committed by GitHub
parent c88f643407
commit 3b0a906a97
6 changed files with 51 additions and 7 deletions
+7
View File
@@ -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);
}
+28 -1
View File
@@ -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<void> =>
@@ -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<void> {
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};
}),
+7
View File
@@ -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();
}
}
}
+4 -3
View File
File diff suppressed because one or more lines are too long
+2
View File
@@ -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}));
+3 -3
View File
@@ -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");