Skip to content

Commit 8302596

Browse files
committed
feat: add read_reporting_config API endpoint
1 parent 965f2a9 commit 8302596

File tree

4 files changed

+235
-0
lines changed

4 files changed

+235
-0
lines changed

lib/extension/bridge.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default class Bridge extends Extension {
3232
private requestLookup: {[key: string]: (message: KeyValue | string) => Promise<Zigbee2MQTTResponse<Zigbee2MQTTResponseEndpoints>>} = {
3333
"device/options": this.deviceOptions,
3434
"device/configure_reporting": this.deviceConfigureReporting,
35+
"device/read_reporting_config": this.deviceReadReportingConfig,
3536
"device/remove": this.deviceRemove,
3637
"device/interview": this.deviceInterview,
3738
"device/generate_external_definition": this.deviceGenerateExternalDefinition,
@@ -518,6 +519,46 @@ export default class Bridge extends Extension {
518519
});
519520
}
520521

522+
@bind async deviceReadReportingConfig(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/read_reporting_config">> {
523+
if (
524+
typeof message !== "object" ||
525+
message.id === undefined ||
526+
message.endpoint === undefined ||
527+
message.cluster === undefined ||
528+
message.configs === undefined
529+
) {
530+
throw new Error("Invalid payload");
531+
}
532+
533+
const device = this.getEntity("device", message.id);
534+
const endpoint = device.endpoint(message.endpoint);
535+
536+
if (!endpoint) {
537+
throw new Error(`Device '${device.ID}' does not have endpoint '${message.endpoint}'`);
538+
}
539+
540+
await endpoint.readReportingConfig(
541+
message.cluster,
542+
message.configs,
543+
message.manufacturerCode ? {manufacturerCode: message.manufacturerCode} : {},
544+
);
545+
546+
await this.publishDevices();
547+
548+
const responseData: Zigbee2MQTTAPI["bridge/response/device/read_reporting_config"] = {
549+
id: message.id,
550+
endpoint: message.endpoint,
551+
cluster: message.cluster,
552+
configs: message.configs,
553+
};
554+
555+
if (message.manufacturerCode) {
556+
responseData.manufacturerCode = message.manufacturerCode;
557+
}
558+
559+
return utils.getResponse(message, responseData);
560+
}
561+
521562
@bind async deviceInterview(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/interview">> {
522563
if (typeof message !== "object" || message.id === undefined) {
523564
throw new Error("Invalid payload");

lib/types/api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,22 @@ export interface Zigbee2MQTTAPI {
722722
reportable_change: number;
723723
};
724724

725+
"bridge/request/device/read_reporting_config": {
726+
id: string;
727+
endpoint: string | number;
728+
cluster: string | number;
729+
configs: {direction?: number; attribute: string | number | {ID: number; type: number}}[];
730+
manufacturerCode?: number;
731+
};
732+
733+
"bridge/response/device/read_reporting_config": {
734+
id: string;
735+
endpoint: string | number;
736+
cluster: string | number;
737+
configs: {direction?: number; attribute: string | number | {ID: number; type: number}}[];
738+
manufacturerCode?: number;
739+
};
740+
725741
"bridge/request/group/remove": {
726742
id: string;
727743
force?: boolean;
@@ -913,6 +929,7 @@ export type Zigbee2MQTTRequestEndpoints =
913929
| "bridge/request/device/options"
914930
| "bridge/request/device/rename"
915931
| "bridge/request/device/configure_reporting"
932+
| "bridge/request/device/read_reporting_config"
916933
| "bridge/request/group/remove"
917934
| "bridge/request/group/add"
918935
| "bridge/request/group/rename"
@@ -959,6 +976,7 @@ export type Zigbee2MQTTResponseEndpoints =
959976
| "bridge/response/device/options"
960977
| "bridge/response/device/rename"
961978
| "bridge/response/device/configure_reporting"
979+
| "bridge/response/device/read_reporting_config"
962980
| "bridge/response/group/remove"
963981
| "bridge/response/group/add"
964982
| "bridge/response/group/rename"

test/extensions/bridge.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3841,6 +3841,179 @@ describe("Extension: Bridge", () => {
38413841
);
38423842
});
38433843

3844+
it("Should allow to read reporting config with endpoint as number", async () => {
3845+
const device = devices.bulb;
3846+
const endpoint = device.getEndpoint(1)!;
3847+
endpoint.bind.mockClear();
3848+
endpoint.readReportingConfig.mockClear();
3849+
mockMQTTPublishAsync.mockClear();
3850+
mockMQTTEvents.message(
3851+
"zigbee2mqtt/bridge/request/device/read_reporting_config",
3852+
stringify({
3853+
id: "0x000b57fffec6a5b2",
3854+
endpoint: 1,
3855+
cluster: "genLevelCtrl",
3856+
configs: [{attribute: "currentLevel"}],
3857+
}),
3858+
);
3859+
await flushPromises();
3860+
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
3861+
expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {});
3862+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
3863+
"zigbee2mqtt/bridge/response/device/read_reporting_config",
3864+
stringify({
3865+
data: {
3866+
id: "0x000b57fffec6a5b2",
3867+
endpoint: 1,
3868+
cluster: "genLevelCtrl",
3869+
configs: [{attribute: "currentLevel"}],
3870+
},
3871+
status: "ok",
3872+
}),
3873+
{},
3874+
);
3875+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
3876+
});
3877+
3878+
it("Should allow to read reporting config with endpoint as string", async () => {
3879+
const device = devices.bulb;
3880+
const endpoint = device.getEndpoint(1)!;
3881+
endpoint.bind.mockClear();
3882+
endpoint.readReportingConfig.mockClear();
3883+
mockMQTTPublishAsync.mockClear();
3884+
mockMQTTEvents.message(
3885+
"zigbee2mqtt/bridge/request/device/read_reporting_config",
3886+
stringify({
3887+
id: "0x000b57fffec6a5b2",
3888+
endpoint: "1",
3889+
cluster: "genLevelCtrl",
3890+
configs: [{attribute: "currentLevel"}],
3891+
}),
3892+
);
3893+
await flushPromises();
3894+
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
3895+
expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {});
3896+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
3897+
"zigbee2mqtt/bridge/response/device/read_reporting_config",
3898+
stringify({
3899+
data: {
3900+
id: "0x000b57fffec6a5b2",
3901+
endpoint: "1",
3902+
cluster: "genLevelCtrl",
3903+
configs: [{attribute: "currentLevel"}],
3904+
},
3905+
status: "ok",
3906+
}),
3907+
{},
3908+
);
3909+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
3910+
});
3911+
3912+
it("Should allow to read reporting config with manufacturer code", async () => {
3913+
const device = devices.bulb;
3914+
const endpoint = device.getEndpoint(1)!;
3915+
endpoint.bind.mockClear();
3916+
endpoint.readReportingConfig.mockClear();
3917+
mockMQTTPublishAsync.mockClear();
3918+
mockMQTTEvents.message(
3919+
"zigbee2mqtt/bridge/request/device/read_reporting_config",
3920+
stringify({
3921+
id: "0x000b57fffec6a5b2",
3922+
endpoint: 1,
3923+
cluster: "genLevelCtrl",
3924+
configs: [{attribute: "currentLevel"}],
3925+
manufacturerCode: 0x1234,
3926+
}),
3927+
);
3928+
await flushPromises();
3929+
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1);
3930+
expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {manufacturerCode: 0x1234});
3931+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
3932+
"zigbee2mqtt/bridge/response/device/read_reporting_config",
3933+
stringify({
3934+
data: {
3935+
id: "0x000b57fffec6a5b2",
3936+
endpoint: 1,
3937+
cluster: "genLevelCtrl",
3938+
configs: [{attribute: "currentLevel"}],
3939+
manufacturerCode: 0x1234,
3940+
},
3941+
status: "ok",
3942+
}),
3943+
{},
3944+
);
3945+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
3946+
});
3947+
3948+
it("Should throw error when read reporting config is called with malformed payload", async () => {
3949+
const device = devices.bulb;
3950+
const endpoint = device.getEndpoint(1)!;
3951+
endpoint.readReportingConfig.mockClear();
3952+
mockMQTTPublishAsync.mockClear();
3953+
mockMQTTEvents.message(
3954+
"zigbee2mqtt/bridge/request/device/read_reporting_config",
3955+
stringify({
3956+
id: "bulb",
3957+
// endpoint: '1',
3958+
cluster: "genLevelCtrl",
3959+
configs: [{attribute: "currentLevel"}],
3960+
}),
3961+
);
3962+
await flushPromises();
3963+
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
3964+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
3965+
"zigbee2mqtt/bridge/response/device/read_reporting_config",
3966+
stringify({data: {}, status: "error", error: "Invalid payload"}),
3967+
{},
3968+
);
3969+
});
3970+
3971+
it("Should throw error when read reporting config is called for non-existing device", async () => {
3972+
const device = devices.bulb;
3973+
const endpoint = device.getEndpoint(1)!;
3974+
endpoint.readReportingConfig.mockClear();
3975+
mockMQTTPublishAsync.mockClear();
3976+
mockMQTTEvents.message(
3977+
"zigbee2mqtt/bridge/request/device/read_reporting_config",
3978+
stringify({
3979+
id: "non_existing_device",
3980+
endpoint: "1",
3981+
cluster: "genLevelCtrl",
3982+
configs: [{attribute: "currentLevel"}],
3983+
}),
3984+
);
3985+
await flushPromises();
3986+
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
3987+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
3988+
"zigbee2mqtt/bridge/response/device/read_reporting_config",
3989+
stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}),
3990+
{},
3991+
);
3992+
});
3993+
3994+
it("Should throw error when read reporting config is called for non-existing endpoint", async () => {
3995+
const device = devices.bulb;
3996+
const endpoint = device.getEndpoint(1)!;
3997+
endpoint.readReportingConfig.mockClear();
3998+
mockMQTTPublishAsync.mockClear();
3999+
mockMQTTEvents.message(
4000+
"zigbee2mqtt/bridge/request/device/read_reporting_config",
4001+
stringify({
4002+
id: "0x000b57fffec6a5b2",
4003+
endpoint: "non_existing_endpoint",
4004+
cluster: "genLevelCtrl",
4005+
configs: [{attribute: "currentLevel"}],
4006+
}),
4007+
);
4008+
await flushPromises();
4009+
expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0);
4010+
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
4011+
"zigbee2mqtt/bridge/response/device/read_reporting_config",
4012+
stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}),
4013+
{},
4014+
);
4015+
});
4016+
38444017
it("Should allow to create a backup", async () => {
38454018
fs.mkdirSync(path.join(data.mockDir, "ext_converters"));
38464019
fs.writeFileSync(path.join(data.mockDir, "ext_converters", "afile.js"), "test123");

test/mocks/zigbeeHerdsman.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export class Endpoint {
104104
unbind: Mock;
105105
save: Mock;
106106
configureReporting: Mock;
107+
readReportingConfig: Mock;
107108
meta: Record<string, unknown>;
108109
binds: ZHBind[];
109110
profileID: number | undefined;
@@ -138,6 +139,7 @@ export class Endpoint {
138139
this.unbind = vi.fn();
139140
this.save = vi.fn();
140141
this.configureReporting = vi.fn();
142+
this.readReportingConfig = vi.fn();
141143
this.meta = meta;
142144
this.binds = binds;
143145
this.profileID = profileID;
@@ -218,6 +220,7 @@ export class Endpoint {
218220
this.unbind.mockClear();
219221
this.save.mockClear();
220222
this.configureReporting.mockClear();
223+
this.readReportingConfig.mockClear();
221224
this.addToGroup.mockClear();
222225
this.removeFromGroup.mockClear();
223226
this.getClusterAttributeValue.mockClear();

0 commit comments

Comments
 (0)