fix: Expose custom clusters to MQTT (#22583)

* (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>
This commit is contained in:
Laurent ChardinandKoen Kanters authored and GitHub committed 2024-05-21 19:54:18 +02:00
1 parent 75608c79c8
commit d484405cf0
5 files changed
+156 -12

No files matched your search

+33 -2
View File
@@ -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/(.*)`);
@@ -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();
@@ -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());
@@ -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);
}
@@ -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();
@@ -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
@@ -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,
@@ -761,5 +790,7 @@ export default class Bridge extends Extension {
options: device.definition.options,
icon,
};
return payload;
}
}
+4
View File
@@ -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;
@@ -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;
+99 -5
View File
@@ -22,7 +22,7 @@ jest.mock("jszip", () =>
})
);
const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2, WSDCGQ11LM, zigfred_plus} = zigbeeHerdsman.devices;
const {coordinator, bulb, unsupported, WXKG11LM, remote, ZNCZ02LM, bulb_color_2, WSDCGQ11LM, zigfred_plus, bulb_custom_cluster} = zigbeeHerdsman.devices;
zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr);
zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr);
zigbeeHerdsman.returnDevices.push(unsupported.ieeeAddr);
@@ -32,6 +32,7 @@ zigbeeHerdsman.returnDevices.push(ZNCZ02LM.ieeeAddr);
zigbeeHerdsman.returnDevices.push(bulb_color_2.ieeeAddr);
zigbeeHerdsman.returnDevices.push(WSDCGQ11LM.ieeeAddr);
zigbeeHerdsman.returnDevices.push(zigfred_plus.ieeeAddr);
zigbeeHerdsman.returnDevices.push(bulb_custom_cluster.ieeeAddr);
describe('Bridge', () => {
let controller;
@@ -96,7 +97,17 @@ describe('Bridge', () => {
// console.log(MQTT.publish.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/devices')[1])
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
stringify([{"date_code":null,"definition":null,"disabled":false,"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":true,"type":"Coordinator"},{"date_code":null,"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","exposes":[{"features":[{"access":7,"description":"On/off state of this light","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","label":"Brightness","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","label":"Color temp","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","label":"Color temp startup","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)","label":"Effect","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"category":"config","description":"Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.","label":"Power-on behavior","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","on","toggle","previous"]},{"access":7,"category":"config","description":"Advanced color behavior","features":[{"access":2,"description":"Controls whether color and color temperature can be set while light is off","label":"Execute if off","name":"execute_if_off","property":"execute_if_off","type":"binary","value_off":false,"value_on":true}],"label":"Color options","name":"color_options","property":"color_options","type":"composite"},{"access":2,"category":"config","description":"Initiate device identification","label":"Identify","name":"identify","property":"identify","type":"enum","values":["identify"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","options":[{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).","label":"Identify timeout","name":"identify_timeout","property":"identify_timeout","type":"numeric","value_max":30,"value_min":1},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"IKEA"},"description":"this is my bulb","disabled":false,"endpointLine truncated
stringify([{"date_code":null,"definition":null,"disabled":false,"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":true,"type":"Coordinator"},{"date_code":null,"definition":{"description":"TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm","exposes":[{"features":[{"access":7,"description":"On/off state of this light","label":"State","name":"state","property":"state","type":"binary","value_off":"OFF","value_on":"ON","value_toggle":"TOGGLE"},{"access":7,"description":"Brightness of this light","label":"Brightness","name":"brightness","property":"brightness","type":"numeric","value_max":254,"value_min":0},{"access":7,"description":"Color temperature of this light","label":"Color temp","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","label":"Color temp startup","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)","label":"Effect","name":"effect","property":"effect","type":"enum","values":["blink","breathe","okay","channel_change","finish_effect","stop_effect"]},{"access":7,"category":"config","description":"Controls the behavior when the device is powered on after power loss. If you get an `UNSUPPORTED_ATTRIBUTE` error, the device does not support it.","label":"Power-on behavior","name":"power_on_behavior","property":"power_on_behavior","type":"enum","values":["off","on","toggle","previous"]},{"access":7,"category":"config","description":"Advanced color behavior","features":[{"access":2,"description":"Controls whether color and color temperature can be set while light is off","label":"Execute if off","name":"execute_if_off","property":"execute_if_off","type":"binary","value_off":false,"value_on":true}],"label":"Color options","name":"color_options","property":"color_options","type":"composite"},{"access":2,"category":"config","description":"Initiate device identification","label":"Identify","name":"identify","property":"identify","type":"enum","values":["identify"]},{"access":1,"category":"diagnostic","description":"Link quality (signal strength)","label":"Linkquality","name":"linkquality","property":"linkquality","type":"numeric","unit":"lqi","value_max":255,"value_min":0}],"model":"LED1545G12","options":[{"access":2,"description":"Controls the transition time (in seconds) of on/off, brightness, color temperature (if applicable) and color (if applicable) changes. Defaults to `0` (no transition).","label":"Transition","name":"transition","property":"transition","type":"numeric","value_min":0},{"access":2,"description":"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).","label":"Color sync","name":"color_sync","property":"color_sync","type":"binary","value_off":false,"value_on":true},{"access":2,"description":"Sets the duration of the identification procedure in seconds (i.e., how long the device would flash).The value ranges from 1 to 30 seconds (default: 3).","label":"Identify timeout","name":"identify_timeout","property":"identify_timeout","type":"numeric","value_max":30,"value_min":1},{"access":2,"description":"State actions will also be published as 'action' when true (default false).","label":"State action","name":"state_action","property":"state_action","type":"binary","value_off":false,"value_on":true}],"supports_ota":true,"vendor":"IKEA"},"description":"this is my bulb","disabled":false,"endpointLine truncated
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish definitions on startup', async () => {
await resetExtension();
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/definitions',
expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)),
{ retain: true, qos: 0 },
expect.any(Function)
);
@@ -183,7 +194,7 @@ describe('Bridge', () => {
);
});
it('Should publish event when device joined', async () => {
it('Should publish devices when device joined', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceNetworkAddressChanged({device: zigbeeHerdsman.devices.bulb});
await flushPromises();
@@ -245,7 +256,7 @@ describe('Bridge', () => {
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.bulb, status: 'successful'});
await zigbeeHerdsman.events.deviceInterview({device: zigbeeHerdsman.devices.unsupported, status: 'successful'});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(6);
expect(MQTT.publish).toHaveBeenCalledTimes(7);
// console.log(MQTT.publish.mock.calls.filter((c) => c[0] === 'zigbee2mqtt/bridge/event'))
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
@@ -265,13 +276,19 @@ describe('Bridge', () => {
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/definitions',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should publish event and devices when device leaves', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceLeave({ieeeAddr: zigbeeHerdsman.devices.bulb.ieeeAddr});
await flushPromises();
expect(MQTT.publish).toHaveBeenCalledTimes(2);
expect(MQTT.publish).toHaveBeenCalledTimes(3);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({"type":"device_leave","data":{"ieee_address":"0x000b57fffec6a5b2","friendly_name":"bulb"}}),
@@ -284,6 +301,12 @@ describe('Bridge', () => {
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith( // Defintitions should be updated on device event
'zigbee2mqtt/bridge/definitions',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
});
it('Should allow permit join', async () => {
@@ -1440,4 +1463,75 @@ describe('Bridge', () => {
expect(payload['icon']).not.toBeUndefined()
expect(payload.icon).toBe('_------_-._Z-NC-Z02-LM_');
});
it('Should publish bridge info, devices and definitions when a device with custom_clusters joined', async () => {
MQTT.publish.mockClear();
await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb_custom_cluster});
await flushPromises();
// console.log(MQTT.publish.mock.calls);
expect(MQTT.publish).toHaveBeenCalledTimes(5);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/info',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/definitions',
expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/event',
stringify({"data":{"friendly_name":"0x000b57fffec6a5c2","ieee_address":"0x000b57fffec6a5c2"},"type":"device_joined"}),
{ retain: false, qos: 0 },
expect.any(Function)
);
});
it('Should publish bridge info, devices and definitions when a device with custom_clusters is reconfigured', async () => {
// Adding a device first
await zigbeeHerdsman.events.deviceJoined({device: zigbeeHerdsman.devices.bulb_custom_cluster});
await flushPromises();
MQTT.publish.mockClear();
// After cleaning, reconfigure it
MQTT.events.message('zigbee2mqtt/bridge/request/device/configure', zigbeeHerdsman.devices.bulb_custom_cluster.ieeeAddr);
await flushPromises();
// console.log(MQTT.publish.mock.calls);
expect(MQTT.publish).toHaveBeenCalledTimes(4);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/info',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/devices',
expect.any(String),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/definitions',
expect.stringContaining(stringify(zigbeeHerdsman.custom_clusters)),
{ retain: true, qos: 0 },
expect.any(Function)
);
expect(MQTT.publish).toHaveBeenCalledWith(
'zigbee2mqtt/bridge/response/device/configure',
expect.any(String),
{ retain: false, qos: 0 },
expect.any(Function)
);
});
});
+1 -1
View File
@@ -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));
});
+19 -4
View File
@@ -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;
@@ -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;
@@ -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;
@@ -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 = {
@@ -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
};