Skip to content

Commit

Permalink
fix: Expose custom clusters to MQTT (#22583)
Browse files Browse the repository at this point in the history
* (feat) Expose Custom Clusters in MQTT

- Introducing bridge/definitions
- Updating test case for custom clusters
- Updating new path after ZCL revamp with 0.47.0

This change is needed for nurikk/zigbee2mqtt-frontend#2001

* Fixing test case

* Update lib/model/device.ts

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>

* Update test/bridge.test.js

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>

* Removing the publishDefinition call from some events. updating tests

---------

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
  • Loading branch information
LaurentChardin and Koenkk committed May 21, 2024
1 parent 75608c7 commit d484405
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 12 deletions.
35 changes: 33 additions & 2 deletions lib/extension/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import data from '../util/data';
import JSZip from 'jszip';
import fs from 'fs';
import * as zhc from 'zigbee-herdsman-converters';
import {CustomClusters, ClusterDefinition, ClusterName} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';
import {Clusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/cluster';
import winston from 'winston';

const requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
Expand Down Expand Up @@ -104,7 +106,9 @@ export default class Bridge extends Extension {

this.eventBus.onEntityRenamed(this, () => this.publishInfo());
this.eventBus.onGroupMembersChanged(this, () => this.publishGroups());
this.eventBus.onDevicesChanged(this, () => this.publishDevices() && this.publishInfo());
this.eventBus.onDevicesChanged(this, () => this.publishDevices() &&
this.publishInfo() &&
this.publishDefinitions());
this.eventBus.onPermitJoinChanged(this, () => !this.zigbee.isStopping() && this.publishInfo());
this.eventBus.onScenesChanged(this, () => {
this.publishDevices();
Expand All @@ -121,6 +125,7 @@ export default class Bridge extends Extension {
});
this.eventBus.onDeviceLeave(this, (data) => {
this.publishDevices();
this.publishDefinitions();
publishEvent('device_leave', {ieee_address: data.ieeeAddr, friendly_name: data.name});
});
this.eventBus.onDeviceNetworkAddressChanged(this, () => this.publishDevices());
Expand All @@ -142,6 +147,7 @@ export default class Bridge extends Extension {
await this.publishInfo();
await this.publishDevices();
await this.publishGroups();
await this.publishDefinitions();

this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
}
Expand Down Expand Up @@ -614,6 +620,8 @@ export default class Bridge extends Extension {
if (entity instanceof Device) {
this.publishGroups();
this.publishDevices();
// Refresh Cluster definition
this.publishDefinitions();
return utils.getResponse(message, {id: ID, block, force}, null);
} else {
this.publishGroups();
Expand Down Expand Up @@ -742,6 +750,27 @@ export default class Bridge extends Extension {
'bridge/groups', stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}

async publishDefinitions(): Promise<void> {
interface ClusterDefinitionPayload {
clusters: Readonly<Record<ClusterName, Readonly<ClusterDefinition>>>,
custom_clusters: {[key: string] : CustomClusters}
}

const data: ClusterDefinitionPayload = {
clusters: Clusters,
custom_clusters: {},
};

for (const device of this.zigbee.devices()) {
if (Object.keys(device.customClusters).length !== 0) {
data.custom_clusters[device.ieeeAddr] = device.customClusters;
}
}

await this.mqtt.publish('bridge/definitions', stringify(data),
{retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
}

getDefinitionPayload(device: Device): DefinitionPayload {
if (!device.definition) return null;
// @ts-expect-error icon is valid for external definitions
Expand All @@ -752,7 +781,7 @@ export default class Bridge extends Extension {
icon = icon.replace('${model}', utils.sanitizeImageParameter(device.definition.model));
}

return {
const payload: DefinitionPayload = {
model: device.definition.model,
vendor: device.definition.vendor,
description: device.definition.description,
Expand All @@ -761,5 +790,7 @@ export default class Bridge extends Extension {
options: device.definition.options,
icon,
};

return payload;
}
}
4 changes: 4 additions & 0 deletions lib/model/device.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable brace-style */
import * as settings from '../util/settings';
import * as zhc from 'zigbee-herdsman-converters';
import {CustomClusters} from 'zigbee-herdsman/dist/zspec/zcl/definition/tstype';

export default class Device {
public zh: zh.Device;
Expand All @@ -16,6 +17,9 @@ export default class Device {
get isSupported(): boolean {
return this.zh.type === 'Coordinator' || (this.definition && !this.definition.generated);
}
get customClusters(): CustomClusters {
return this.zh.customClusters;
}

constructor(device: zh.Device) {
this.zh = device;
Expand Down
104 changes: 99 additions & 5 deletions test/bridge.test.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion test/controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -667,7 +667,7 @@ describe('Controller', () => {
MQTT.events['connect']();
await flushPromises();
jest.runOnlyPendingTimers();
expect(MQTT.publish).toHaveBeenCalledTimes(13);
expect(MQTT.publish).toHaveBeenCalledTimes(14);
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/info', expect.any(String), { retain: true, qos: 0 }, expect.any(Function));
});

Expand Down
23 changes: 19 additions & 4 deletions test/stub/zigbeeHerdsman.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ const clusters = {
'msCO2': 1037
}

const custom_clusters = {
custom_1: {
ID: 64672,
manufacturerCode: 4617,
attributes: {
attribute_0: {ID: 0, type: 49},
},
commands: {
command_0: { ID: 0, response: 0, parameters: [{name: 'reset', type: 40}], },
},
commandsResponse: {},
},
}

class Endpoint {
constructor(ID, inputClusters, outputClusters, deviceIeeeAddress, binds=[], clusterValues={}, configuredReportings=[], profileID=null, deviceID=null, meta={}) {
this.deviceIeeeAddress = deviceIeeeAddress;
Expand Down Expand Up @@ -102,7 +116,7 @@ class Endpoint {
}

class Device {
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName, dateCode= null, softwareBuildID=null) {
constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName, dateCode= null, softwareBuildID=null, customClusters = {}) {
this.type = type;
this.ieeeAddr = ieeeAddr;
this.dateCode = dateCode;
Expand All @@ -118,7 +132,7 @@ class Device {
this.ping = jest.fn();
this.removeFromNetwork = jest.fn();
this.removeFromDatabase = jest.fn();
this.customClusters = {};
this.customClusters = customClusters;
this.addCustomCluster = jest.fn();
this.save = jest.fn();
this.manufacturerName = manufacturerName;
Expand Down Expand Up @@ -211,7 +225,8 @@ const devices = {
'bj_scene_switch': new Device('EndDevice', '0xd85def11a1002caa', 50117, 4398, [new Endpoint(10, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa', [{target: bulb_color_2.endpoints[0], cluster: {ID: 8, name: 'genLevelCtrl'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 6, name: 'genOnOff'}}, {target: bulb_color_2.endpoints[0], cluster: {ID: 768, name: 'lightingColorCtrl'}},]), new Endpoint(11, [0,4096], [3,4,5,6,8,25,768,4096], '0xd85def11a1002caa')], true, 'Battery', 'RB01', false, 'Busch-Jaeger', '20161222', '1.2.0'),
'GW003-AS-IN-TE-FC': new Device('Router', '0x0017548104a44669', 6545,4699, [new Endpoint(1, [3], [0,3,513,514], '0x0017548104a44669')], true, "Mains (single phase)", 'Adapter Zigbee FUJITSU'),
'BMCT-SLZ': new Device('Router', '0x18fc26000000cafe', 6546,4617, [new Endpoint(1, [0,3,4,5,258,1794,2820,2821,64672], [10,25], '0x18fc26000000cafe')], true, "Mains (single phase)", 'RBSH-MMS-ZB-EU'),

'BMCT_SLZ': new Device('Router', '0x0026decafe000473', 6546,4617, [new Endpoint(1, [0,3,4,5,258,1794,2820,2821,64672], [10,25], '0x0026decafe000473')], true, "Mains (single phase)", 'RBSH-MMS-ZB-EU', false, null, null, null, custom_clusters),
'bulb_custom_cluster': new Device('Router', '0x000b57fffec6a5c2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5c2')], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm", false, null, null, null, custom_clusters),
}

const mock = {
Expand Down Expand Up @@ -268,5 +283,5 @@ jest.mock('zigbee-herdsman', () => ({
}));

module.exports = {
events, ...mock, constructor: mockConstructor, devices, groups, returnDevices
events, ...mock, constructor: mockConstructor, devices, groups, returnDevices, custom_clusters
};

0 comments on commit d484405

Please sign in to comment.