diff --git a/lib/extension/bind.ts b/lib/extension/bind.ts index b62d950c96..69e78a46eb 100755 --- a/lib/extension/bind.ts +++ b/lib/extension/bind.ts @@ -184,7 +184,7 @@ interface ParsedMQTTMessage { } export default class Bind extends Extension { - #topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`); + #topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind|binds/clear)`); private pollDebouncers: {[s: string]: () => void} = {}; // biome-ignore lint/suspicious/useAwait: API @@ -276,6 +276,36 @@ export default class Bind extends Extension { } @bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise { + if (data.topic.endsWith("binds/clear")) { + const message = JSON.parse(data.message) as Zigbee2MQTTAPI["bridge/request/device/binds/clear"]; + + if (typeof message !== "object" || typeof message.target !== "string") { + await this.publishResponse("binds/clear", message, {}, "Invalid payload"); + return; + } + + const target = this.zigbee.resolveEntity(message.target); + + if (!(target instanceof Device)) { + await this.publishResponse("binds/clear", message, {}, "Invalid target"); + return; + } + + // this list is raw (not resolved) to allow clearing any specific target (not only currently known) + const eui64List = message.ieeeList ?? ["0xffffffffffffffff"]; + + await target.zh.clearAllBindings(eui64List); + + const responseData: Zigbee2MQTTAPI["bridge/response/device/binds/clear"] = { + target: message.target, + ieeeList: eui64List, + }; + + await this.publishResponse("binds/clear", message, responseData); + this.eventBus.emitDevicesChanged(); + return; + } + const [raw, parsed, error] = this.parseMQTTMessage(data); if (!raw || !parsed) { @@ -389,7 +419,7 @@ export default class Bind extends Extension { } private async publishResponse( - type: ParsedMQTTMessage["type"], + type: ParsedMQTTMessage["type"] | "binds/clear", request: KeyValue, data: Zigbee2MQTTAPI[T], error?: string, diff --git a/lib/extension/bridge.ts b/lib/extension/bridge.ts index a72cdf50ec..09339bb245 100644 --- a/lib/extension/bridge.ts +++ b/lib/extension/bridge.ts @@ -31,7 +31,10 @@ export default class Bridge extends Extension { private logTransport!: winston.transport; private requestLookup: {[key: string]: (message: KeyValue | string) => Promise>} = { "device/options": this.deviceOptions, - "device/configure_reporting": this.deviceConfigureReporting, + /** @deprecated 3.0 */ + "device/configure_reporting": this.deviceReportingConfigure, + "device/reporting/configure": this.deviceReportingConfigure, + "device/reporting/read": this.deviceReportingRead, "device/remove": this.deviceRemove, "device/interview": this.deviceInterview, "device/generate_external_definition": this.deviceGenerateExternalDefinition, @@ -466,7 +469,7 @@ export default class Bridge extends Extension { return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired}); } - @bind async deviceConfigureReporting(message: string | KeyValue): Promise> { + @bind async deviceReportingConfigure(message: string | KeyValue): Promise> { if ( typeof message !== "object" || message.id === undefined || @@ -518,6 +521,46 @@ export default class Bridge extends Extension { }); } + @bind async deviceReportingRead(message: string | KeyValue): Promise> { + if ( + typeof message !== "object" || + message.id === undefined || + message.endpoint === undefined || + message.cluster === undefined || + message.configs === undefined + ) { + throw new Error("Invalid payload"); + } + + const device = this.getEntity("device", message.id); + const endpoint = device.endpoint(message.endpoint); + + if (!endpoint) { + throw new Error(`Device '${device.ID}' does not have endpoint '${message.endpoint}'`); + } + + const response = await endpoint.readReportingConfig( + message.cluster, + message.configs, + message.manufacturerCode ? {manufacturerCode: message.manufacturerCode} : {}, + ); + + await this.publishDevices(); + + const responseData: Zigbee2MQTTAPI["bridge/response/device/reporting/read"] = { + id: message.id, + endpoint: message.endpoint, + cluster: message.cluster, + configs: response, + }; + + if (message.manufacturerCode) { + responseData.manufacturerCode = message.manufacturerCode; + } + + return utils.getResponse(message, responseData); + } + @bind async deviceInterview(message: string | KeyValue): Promise> { if (typeof message !== "object" || message.id === undefined) { throw new Error("Invalid payload"); diff --git a/lib/extension/networkMap.ts b/lib/extension/networkMap.ts index fbee79e5f4..0a29581782 100644 --- a/lib/extension/networkMap.ts +++ b/lib/extension/networkMap.ts @@ -1,7 +1,8 @@ import bind from "bind-decorator"; import stringify from "json-stable-stringify-without-jsonify"; +import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes"; +import type {LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes"; import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api"; - import logger from "../util/logger"; import * as settings from "../util/settings"; import utils from "../util/utils"; @@ -185,8 +186,8 @@ export default class NetworkMap extends Extension { async networkScan(includeRoutes: boolean): Promise { logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`); - const lqis = new Map(); - const routingTables = new Map(); + const lqis = new Map(); + const routingTables = new Map(); const failed = new Map(); const requestWithRetry = async (request: () => Promise): Promise => { try { @@ -210,7 +211,7 @@ export default class NetworkMap extends Extension { await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network. try { - const result = await requestWithRetry(async () => await device.zh.lqi()); + const result = await requestWithRetry(async () => await device.zh.lqi()); lqis.set(device, result); logger.debug(`LQI succeeded for '${device.name}'`); } catch (error) { @@ -222,7 +223,7 @@ export default class NetworkMap extends Extension { if (includeRoutes) { try { - const result = await requestWithRetry(async () => await device.zh.routingTable()); + const result = await requestWithRetry(async () => await device.zh.routingTable()); routingTables.set(device, result); logger.debug(`Routing table succeeded for '${device.name}'`); } catch (error) { @@ -274,42 +275,47 @@ export default class NetworkMap extends Extension { } // Add links - for (const [device, lqi] of lqis) { - for (const neighbor of lqi.neighbors) { + for (const [device, table] of lqis) { + for (const neighbor of table) { if (neighbor.relationship > 3) { // Relationship is not active, skip it continue; } + let neighborEui64 = neighbor.eui64; + // Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct). // Determine the correct ieeeAddr based on the networkAddress. - if (neighbor.ieeeAddr === "0x0000000000000000") { - const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress); + if (neighborEui64 === "0x0000000000000000") { + const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.nwkAddress); if (neighborDevice) { - neighbor.ieeeAddr = neighborDevice.ieeeAddr; + neighborEui64 = neighborDevice.ieeeAddr as Eui64; } } const link: Zigbee2MQTTNetworkMap["links"][number] = { - source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress}, + source: {ieeeAddr: neighborEui64, networkAddress: neighbor.nwkAddress}, target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress}, - linkquality: neighbor.linkquality, + deviceType: neighbor.deviceType, + rxOnWhenIdle: neighbor.rxOnWhenIdle, + relationship: neighbor.relationship, + permitJoining: neighbor.permitJoining, depth: neighbor.depth, + lqi: neighbor.lqi, routes: [], - // DEPRECATED: - sourceIeeeAddr: neighbor.ieeeAddr, + // below are @deprecated + sourceIeeeAddr: neighborEui64, targetIeeeAddr: device.ieeeAddr, - sourceNwkAddr: neighbor.networkAddress, - lqi: neighbor.linkquality, - relationship: neighbor.relationship, + sourceNwkAddr: neighbor.nwkAddress, + linkquality: neighbor.lqi, }; const routingTable = routingTables.get(device); if (routingTable) { - for (const entry of routingTable.table) { - if (entry.status === "ACTIVE" && entry.nextHop === neighbor.networkAddress) { + for (const entry of routingTable) { + if (entry.nextHopAddress === neighbor.nwkAddress) { link.routes.push(entry); } } diff --git a/lib/types/api.ts b/lib/types/api.ts index 9a3790bb87..c46c956af7 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -1,5 +1,7 @@ import type * as zigbeeHerdsman from "zigbee-herdsman/dist"; +import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes"; import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype"; +import type {RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes"; import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters"; import type {Base} from "zigbee-herdsman-converters/lib/exposes"; @@ -278,18 +280,21 @@ export interface Zigbee2MQTTNetworkMap { links: { source: {ieeeAddr: string; networkAddress: number}; target: {ieeeAddr: string; networkAddress: number}; - linkquality: number; + deviceType: number; + rxOnWhenIdle: number; + relationship: number; + permitJoining: number; depth: number; - routes: { - destinationAddress: number; - status: string; - nextHop: number; - }[]; + lqi: number; + routes: RoutingTableEntry[]; + /** @deprecated 3.0 */ + linkquality: number; + /** @deprecated 3.0 */ sourceIeeeAddr: string; + /** @deprecated 3.0 */ targetIeeeAddr: string; + /** @deprecated 3.0 */ sourceNwkAddr: number; - lqi: number; - relationship: number; }[]; } @@ -558,6 +563,16 @@ export interface Zigbee2MQTTAPI { failed: string[]; }; + "bridge/request/device/binds/clear": { + target: string; + ieeeList?: Eui64[]; + }; + + "bridge/response/device/binds/clear": { + target: string; + ieeeList?: Eui64[]; + }; + "bridge/request/device/configure": | { id: string | number; @@ -686,7 +701,7 @@ export interface Zigbee2MQTTAPI { homeassistant_rename: boolean; }; - "bridge/request/device/configure_reporting": { + "bridge/request/device/reporting/configure": { id: string; endpoint: string | number; cluster: string | number; @@ -697,7 +712,7 @@ export interface Zigbee2MQTTAPI { option: Record; }; - "bridge/response/device/configure_reporting": { + "bridge/response/device/reporting/configure": { id: string; endpoint: string | number; cluster: string | number; @@ -707,6 +722,22 @@ export interface Zigbee2MQTTAPI { reportable_change: number; }; + "bridge/request/device/reporting/read": { + id: string; + endpoint: string | number; + cluster: string | number; + configs: {direction?: number; attribute: string | number | {ID: number; type: number}}[]; + manufacturerCode?: number; + }; + + "bridge/response/device/reporting/read": { + id: string; + endpoint: string | number; + cluster: string | number; + configs: zigbeeHerdsman.Zcl.ClustersTypes.TFoundation["readReportConfigRsp"]; + manufacturerCode?: number; + }; + "bridge/request/group/remove": { id: string; force?: boolean; @@ -883,6 +914,7 @@ export type Zigbee2MQTTRequestEndpoints = | "bridge/request/options" | "bridge/request/device/bind" | "bridge/request/device/unbind" + | "bridge/request/device/binds/clear" | "bridge/request/device/configure" | "bridge/request/device/remove" | "bridge/request/device/ota_update/check" @@ -896,7 +928,8 @@ export type Zigbee2MQTTRequestEndpoints = | "bridge/request/device/generate_external_definition" | "bridge/request/device/options" | "bridge/request/device/rename" - | "bridge/request/device/configure_reporting" + | "bridge/request/device/reporting/configure" + | "bridge/request/device/reporting/read" | "bridge/request/group/remove" | "bridge/request/group/add" | "bridge/request/group/rename" @@ -931,6 +964,7 @@ export type Zigbee2MQTTResponseEndpoints = | "bridge/response/options" | "bridge/response/device/bind" | "bridge/response/device/unbind" + | "bridge/response/device/binds/clear" | "bridge/response/device/configure" | "bridge/response/device/remove" | "bridge/response/device/ota_update/check" @@ -941,7 +975,8 @@ export type Zigbee2MQTTResponseEndpoints = | "bridge/response/device/generate_external_definition" | "bridge/response/device/options" | "bridge/response/device/rename" - | "bridge/response/device/configure_reporting" + | "bridge/response/device/reporting/configure" + | "bridge/response/device/reporting/read" | "bridge/response/group/remove" | "bridge/response/group/add" | "bridge/response/group/rename" diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 3ee8639058..cc09c32d3f 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -36,9 +36,6 @@ declare global { type Endpoint = ZHModels.Endpoint; type Device = ZHModels.Device; type Group = ZHModels.Group; - // biome-ignore lint/style/useNamingConvention: API - type LQI = ZHAdapterTypes.Lqi; - type RoutingTable = ZHAdapterTypes.RoutingTable; type CoordinatorVersion = ZHAdapterTypes.CoordinatorVersion; type NetworkParameters = ZHAdapterTypes.NetworkParameters; interface Bind { diff --git a/lib/zigbee.ts b/lib/zigbee.ts index ccd3cb6022..d22014664f 100644 --- a/lib/zigbee.ts +++ b/lib/zigbee.ts @@ -399,11 +399,11 @@ export default class Zigbee { } async touchlinkFactoryResetFirst(): Promise { - return await this.herdsman.touchlinkFactoryResetFirst(); + return await this.herdsman.touchlink.factoryResetFirst(); } async touchlinkFactoryReset(ieeeAddr: string, channel: number): Promise { - return await this.herdsman.touchlinkFactoryReset(ieeeAddr, channel); + return await this.herdsman.touchlink.factoryReset(ieeeAddr, channel); } async addInstallCode(installCode: string): Promise { @@ -411,11 +411,11 @@ export default class Zigbee { } async touchlinkIdentify(ieeeAddr: string, channel: number): Promise { - await this.herdsman.touchlinkIdentify(ieeeAddr, channel); + await this.herdsman.touchlink.identify(ieeeAddr, channel); } async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> { - return await this.herdsman.touchlinkScan(); + return await this.herdsman.touchlink.scan(); } createGroup(id: number): Group { diff --git a/test/extensions/bind.test.ts b/test/extensions/bind.test.ts index 49ef36eba4..8e409655ff 100644 --- a/test/extensions/bind.test.ts +++ b/test/extensions/bind.test.ts @@ -795,4 +795,84 @@ describe("Extension: Bind", () => { // Should only call Hue bulb, not e.g. tradfri expect(devices.bulb_2.getEndpoint(1)!.read).toHaveBeenCalledTimes(0); }); + + it("clears all bindings", async () => { + const device = devices.remote; + + device.mockClear(); + mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({transaction: "1234", target: "remote"})); + await flushPromises(); + + expect(device.clearAllBindings).toHaveBeenCalledTimes(1); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/binds/clear", + stringify({ + transaction: "1234", + data: { + target: "remote", + ieeeList: ["0xffffffffffffffff"], + }, + status: "ok", + }), + {}, + ); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true}); + }); + + it("clears targeted bindings", async () => { + const device = devices.remote; + const target = devices.bulb_color; + + device.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/binds/clear", + stringify({transaction: "1234", target: "remote", ieeeList: [target.ieeeAddr]}), + ); + await flushPromises(); + + expect(device.clearAllBindings).toHaveBeenCalledTimes(1); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/binds/clear", + stringify({ + transaction: "1234", + data: { + target: "remote", + ieeeList: [target.ieeeAddr], + }, + status: "ok", + }), + {}, + ); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true}); + }); + + it("throw on invalid clears bindings payload", async () => { + const device = devices.remote; + + device.mockClear(); + mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({targetz: "remote"})); + await flushPromises(); + + expect(device.clearAllBindings).toHaveBeenCalledTimes(0); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/binds/clear", + stringify({data: {}, status: "error", error: "Invalid payload"}), + {}, + ); + }); + + it("throw on invalid clears bindings target", async () => { + const device = devices.remote; + + device.mockClear(); + mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/binds/clear", stringify({target: "remotez"})); + await flushPromises(); + + expect(device.clearAllBindings).toHaveBeenCalledTimes(0); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/binds/clear", + stringify({data: {}, status: "error", error: "Invalid target"}), + {}, + ); + }); }); diff --git a/test/extensions/bridge.test.ts b/test/extensions/bridge.test.ts index 2e83a5f35f..b50c8859cd 100644 --- a/test/extensions/bridge.test.ts +++ b/test/extensions/bridge.test.ts @@ -17,6 +17,7 @@ import {Controller} from "../../lib/controller"; import Bridge from "../../lib/extension/bridge"; import * as settings from "../../lib/util/settings"; import utils, {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils"; +import {Zcl} from "zigbee-herdsman"; returnDevices.push(devices.coordinator.ieeeAddr); returnDevices.push(devices.bulb.ieeeAddr); @@ -3537,11 +3538,11 @@ describe("Extension: Bridge", () => { it("Should allow to touchlink factory reset (succeeds)", async () => { mockMQTTPublishAsync.mockClear(); - mockZHController.touchlinkFactoryResetFirst.mockClear(); - mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(true); + mockZHController.touchlink.factoryResetFirst.mockClear(); + mockZHController.touchlink.factoryResetFirst.mockReturnValueOnce(true); mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", ""); await flushPromises(); - expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlink.factoryResetFirst).toHaveBeenCalledTimes(1); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/touchlink/factory_reset", stringify({data: {}, status: "ok"}), @@ -3551,12 +3552,12 @@ describe("Extension: Bridge", () => { it("Should allow to touchlink factory reset specific device", async () => { mockMQTTPublishAsync.mockClear(); - mockZHController.touchlinkFactoryReset.mockClear(); - mockZHController.touchlinkFactoryReset.mockReturnValueOnce(true); + mockZHController.touchlink.factoryReset.mockClear(); + mockZHController.touchlink.factoryReset.mockReturnValueOnce(true); mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", stringify({ieee_address: "0x1239", channel: 12})); await flushPromises(); - expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledTimes(1); - expect(mockZHController.touchlinkFactoryReset).toHaveBeenCalledWith("0x1239", 12); + expect(mockZHController.touchlink.factoryReset).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlink.factoryReset).toHaveBeenCalledWith("0x1239", 12); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/touchlink/factory_reset", stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}), @@ -3607,11 +3608,11 @@ describe("Extension: Bridge", () => { it("Should allow to touchlink identify specific device", async () => { mockMQTTPublishAsync.mockClear(); - mockZHController.touchlinkIdentify.mockClear(); + mockZHController.touchlink.identify.mockClear(); mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/identify", stringify({ieee_address: "0x1239", channel: 12})); await flushPromises(); - expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(1); - expect(mockZHController.touchlinkIdentify).toHaveBeenCalledWith("0x1239", 12); + expect(mockZHController.touchlink.identify).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlink.identify).toHaveBeenCalledWith("0x1239", 12); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/touchlink/identify", stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}), @@ -3621,10 +3622,10 @@ describe("Extension: Bridge", () => { it("Touchlink identify fails when payload is invalid", async () => { mockMQTTPublishAsync.mockClear(); - mockZHController.touchlinkIdentify.mockClear(); + mockZHController.touchlink.identify.mockClear(); mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/identify", stringify({ieee_address: "0x1239"})); await flushPromises(); - expect(mockZHController.touchlinkIdentify).toHaveBeenCalledTimes(0); + expect(mockZHController.touchlink.identify).toHaveBeenCalledTimes(0); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/touchlink/identify", stringify({data: {}, status: "error", error: "Invalid payload"}), @@ -3634,11 +3635,11 @@ describe("Extension: Bridge", () => { it("Should allow to touchlink factory reset (fails)", async () => { mockMQTTPublishAsync.mockClear(); - mockZHController.touchlinkFactoryResetFirst.mockClear(); - mockZHController.touchlinkFactoryResetFirst.mockReturnValueOnce(false); + mockZHController.touchlink.factoryResetFirst.mockClear(); + mockZHController.touchlink.factoryResetFirst.mockReturnValueOnce(false); mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/factory_reset", ""); await flushPromises(); - expect(mockZHController.touchlinkFactoryResetFirst).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlink.factoryResetFirst).toHaveBeenCalledTimes(1); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/touchlink/factory_reset", stringify({data: {}, status: "error", error: "Failed to factory reset device through Touchlink"}), @@ -3648,14 +3649,14 @@ describe("Extension: Bridge", () => { it("Should allow to touchlink scan", async () => { mockMQTTPublishAsync.mockClear(); - mockZHController.touchlinkScan.mockClear(); - mockZHController.touchlinkScan.mockReturnValueOnce([ + mockZHController.touchlink.scan.mockClear(); + mockZHController.touchlink.scan.mockReturnValueOnce([ {ieeeAddr: "0x123", channel: 12}, {ieeeAddr: "0x124", channel: 24}, ]); mockMQTTEvents.message("zigbee2mqtt/bridge/request/touchlink/scan", ""); await flushPromises(); - expect(mockZHController.touchlinkScan).toHaveBeenCalledTimes(1); + expect(mockZHController.touchlink.scan).toHaveBeenCalledTimes(1); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( "zigbee2mqtt/bridge/response/touchlink/scan", stringify({ @@ -3678,7 +3679,7 @@ describe("Extension: Bridge", () => { endpoint.configureReporting.mockClear(); mockMQTTPublishAsync.mockClear(); mockMQTTEvents.message( - "zigbee2mqtt/bridge/request/device/configure_reporting", + "zigbee2mqtt/bridge/request/device/reporting/configure", stringify({ id: "0x000b57fffec6a5b2", endpoint: 1, @@ -3699,7 +3700,7 @@ describe("Extension: Bridge", () => { undefined, ); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( - "zigbee2mqtt/bridge/response/device/configure_reporting", + "zigbee2mqtt/bridge/response/device/reporting/configure", stringify({ data: { id: "0x000b57fffec6a5b2", @@ -3724,7 +3725,7 @@ describe("Extension: Bridge", () => { endpoint.configureReporting.mockClear(); mockMQTTPublishAsync.mockClear(); mockMQTTEvents.message( - "zigbee2mqtt/bridge/request/device/configure_reporting", + "zigbee2mqtt/bridge/request/device/reporting/configure", stringify({ id: "0x000b57fffec6a5b2", endpoint: "1", @@ -3745,7 +3746,7 @@ describe("Extension: Bridge", () => { undefined, ); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( - "zigbee2mqtt/bridge/response/device/configure_reporting", + "zigbee2mqtt/bridge/response/device/reporting/configure", stringify({ data: { id: "0x000b57fffec6a5b2", @@ -3769,7 +3770,7 @@ describe("Extension: Bridge", () => { endpoint.configureReporting.mockClear(); mockMQTTPublishAsync.mockClear(); mockMQTTEvents.message( - "zigbee2mqtt/bridge/request/device/configure_reporting", + "zigbee2mqtt/bridge/request/device/reporting/configure", stringify({ id: "bulb", // endpoint: '1', @@ -3783,7 +3784,7 @@ describe("Extension: Bridge", () => { await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( - "zigbee2mqtt/bridge/response/device/configure_reporting", + "zigbee2mqtt/bridge/response/device/reporting/configure", stringify({data: {}, status: "error", error: "Invalid payload"}), {}, ); @@ -3795,7 +3796,7 @@ describe("Extension: Bridge", () => { endpoint.configureReporting.mockClear(); mockMQTTPublishAsync.mockClear(); mockMQTTEvents.message( - "zigbee2mqtt/bridge/request/device/configure_reporting", + "zigbee2mqtt/bridge/request/device/reporting/configure", stringify({ id: "non_existing_device", endpoint: "1", @@ -3809,7 +3810,7 @@ describe("Extension: Bridge", () => { await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( - "zigbee2mqtt/bridge/response/device/configure_reporting", + "zigbee2mqtt/bridge/response/device/reporting/configure", stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}), {}, ); @@ -3821,7 +3822,7 @@ describe("Extension: Bridge", () => { endpoint.configureReporting.mockClear(); mockMQTTPublishAsync.mockClear(); mockMQTTEvents.message( - "zigbee2mqtt/bridge/request/device/configure_reporting", + "zigbee2mqtt/bridge/request/device/reporting/configure", stringify({ id: "0x000b57fffec6a5b2", endpoint: "non_existing_endpoint", @@ -3835,7 +3836,243 @@ describe("Extension: Bridge", () => { await flushPromises(); expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); expect(mockMQTTPublishAsync).toHaveBeenCalledWith( - "zigbee2mqtt/bridge/response/device/configure_reporting", + "zigbee2mqtt/bridge/response/device/reporting/configure", + stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}), + {}, + ); + }); + + it("Should allow to read reporting config with endpoint as number", async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.bind.mockClear(); + endpoint.readReportingConfig.mockClear(); + endpoint.readReportingConfig.mockResolvedValueOnce([ + { + status: Zcl.Status.SUCCESS, + direction: Zcl.Direction.CLIENT_TO_SERVER, + attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID, + dataType: Zcl.DataType.UINT8, + minRepIntval: 10, + maxRepIntval: 60, + repChange: 2, + }, + ]); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/reporting/read", + stringify({ + id: "0x000b57fffec6a5b2", + endpoint: 1, + cluster: "genLevelCtrl", + configs: [{attribute: "currentLevel"}], + }), + ); + await flushPromises(); + expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1); + expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {}); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/reporting/read", + stringify({ + data: { + id: "0x000b57fffec6a5b2", + endpoint: 1, + cluster: "genLevelCtrl", + configs: [ + { + status: Zcl.Status.SUCCESS, + direction: Zcl.Direction.CLIENT_TO_SERVER, + attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID, + dataType: Zcl.DataType.UINT8, + minRepIntval: 10, + maxRepIntval: 60, + repChange: 2, + }, + ], + }, + status: "ok", + }), + {}, + ); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true}); + }); + + it("Should allow to read reporting config with endpoint as string", async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.bind.mockClear(); + endpoint.readReportingConfig.mockClear(); + endpoint.readReportingConfig.mockResolvedValueOnce([ + { + status: Zcl.Status.SUCCESS, + direction: Zcl.Direction.CLIENT_TO_SERVER, + attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID, + dataType: Zcl.DataType.UINT8, + minRepIntval: 10, + maxRepIntval: 60, + repChange: 2, + }, + ]); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/reporting/read", + stringify({ + id: "0x000b57fffec6a5b2", + endpoint: "1", + cluster: "genLevelCtrl", + configs: [{attribute: "currentLevel"}], + }), + ); + await flushPromises(); + expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1); + expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {}); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/reporting/read", + stringify({ + data: { + id: "0x000b57fffec6a5b2", + endpoint: "1", + cluster: "genLevelCtrl", + configs: [ + { + status: Zcl.Status.SUCCESS, + direction: Zcl.Direction.CLIENT_TO_SERVER, + attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID, + dataType: Zcl.DataType.UINT8, + minRepIntval: 10, + maxRepIntval: 60, + repChange: 2, + }, + ], + }, + status: "ok", + }), + {}, + ); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true}); + }); + + it("Should allow to read reporting config with manufacturer code", async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.bind.mockClear(); + endpoint.readReportingConfig.mockClear(); + endpoint.readReportingConfig.mockResolvedValueOnce([ + { + status: Zcl.Status.SUCCESS, + direction: Zcl.Direction.CLIENT_TO_SERVER, + attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID, + dataType: Zcl.DataType.UINT8, + minRepIntval: 10, + maxRepIntval: 60, + repChange: 2, + }, + ]); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/reporting/read", + stringify({ + id: "0x000b57fffec6a5b2", + endpoint: 1, + cluster: "genLevelCtrl", + configs: [{attribute: "currentLevel"}], + manufacturerCode: 0x1234, + }), + ); + await flushPromises(); + expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(1); + expect(endpoint.readReportingConfig).toHaveBeenCalledWith("genLevelCtrl", [{attribute: "currentLevel"}], {manufacturerCode: 0x1234}); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/reporting/read", + stringify({ + data: { + id: "0x000b57fffec6a5b2", + endpoint: 1, + cluster: "genLevelCtrl", + configs: [ + { + status: Zcl.Status.SUCCESS, + direction: Zcl.Direction.CLIENT_TO_SERVER, + attrId: Zcl.Clusters.genLevelCtrl.attributes.currentLevel.ID, + dataType: Zcl.DataType.UINT8, + minRepIntval: 10, + maxRepIntval: 60, + repChange: 2, + }, + ], + manufacturerCode: 0x1234, + }, + status: "ok", + }), + {}, + ); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true}); + }); + + it("Should throw error when read reporting config is called with malformed payload", async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.readReportingConfig.mockClear(); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/reporting/read", + stringify({ + id: "bulb", + // endpoint: '1', + cluster: "genLevelCtrl", + configs: [{attribute: "currentLevel"}], + }), + ); + await flushPromises(); + expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/reporting/read", + stringify({data: {}, status: "error", error: "Invalid payload"}), + {}, + ); + }); + + it("Should throw error when read reporting config is called for non-existing device", async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.readReportingConfig.mockClear(); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/reporting/read", + stringify({ + id: "non_existing_device", + endpoint: "1", + cluster: "genLevelCtrl", + configs: [{attribute: "currentLevel"}], + }), + ); + await flushPromises(); + expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/reporting/read", + stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}), + {}, + ); + }); + + it("Should throw error when read reporting config is called for non-existing endpoint", async () => { + const device = devices.bulb; + const endpoint = device.getEndpoint(1)!; + endpoint.readReportingConfig.mockClear(); + mockMQTTPublishAsync.mockClear(); + mockMQTTEvents.message( + "zigbee2mqtt/bridge/request/device/reporting/read", + stringify({ + id: "0x000b57fffec6a5b2", + endpoint: "non_existing_endpoint", + cluster: "genLevelCtrl", + configs: [{attribute: "currentLevel"}], + }), + ); + await flushPromises(); + expect(endpoint.readReportingConfig).toHaveBeenCalledTimes(0); + expect(mockMQTTPublishAsync).toHaveBeenCalledWith( + "zigbee2mqtt/bridge/response/device/reporting/read", stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}), {}, ); diff --git a/test/extensions/networkMap.test.ts b/test/extensions/networkMap.test.ts index 96cea43441..df24a01779 100644 --- a/test/extensions/networkMap.test.ts +++ b/test/extensions/networkMap.test.ts @@ -1,5 +1,6 @@ // biome-ignore assist/source/organizeImports: import mocks first import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes"; import * as data from "../mocks/data"; import {mockLogger} from "../mocks/logger"; import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt"; @@ -25,6 +26,22 @@ returnDevices.push( const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug]; +const LQI_BASE_RSP = { + extendedPanId: [], + deviceType: 0, + rxOnWhenIdle: 0, + reserved1: 0, + permitJoining: 0, + reserved2: 0, +}; + +const ROUTING_BASE_RSP = { + memoryConstrained: 0, + manyToOne: 0, + routeRecordRequired: 0, + reserved1: 0, +}; + describe("Extension: NetworkMap", () => { let controller: Controller; @@ -39,58 +56,62 @@ describe("Extension: NetworkMap", () => { * | -> CC2530_ROUTER -> WXKG02LM_rev1 * */ - devices.coordinator.lqi.mockResolvedValueOnce({ - neighbors: [ - { - ieeeAddr: devices.bulb_color.ieeeAddr, - networkAddress: devices.bulb_color.networkAddress, - relationship: 2, - depth: 1, - linkquality: 120, - }, - {ieeeAddr: devices.bulb.ieeeAddr, networkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92}, - { - ieeeAddr: devices.external_converter_device.ieeeAddr, - networkAddress: devices.external_converter_device.networkAddress, - relationship: 2, - depth: 1, - linkquality: 92, - }, - ], - }); - devices.coordinator.routingTable.mockResolvedValueOnce({ - table: [{destinationAddress: devices.CC2530_ROUTER.networkAddress, status: "ACTIVE", nextHop: devices.bulb.networkAddress}], - }); - devices.bulb.lqi.mockResolvedValueOnce({ - neighbors: [ - { - ieeeAddr: devices.bulb_color.ieeeAddr, - networkAddress: devices.bulb_color.networkAddress, - relationship: 1, - depth: 2, - linkquality: 110, - }, - { - ieeeAddr: devices.CC2530_ROUTER.ieeeAddr, - networkAddress: devices.CC2530_ROUTER.networkAddress, - relationship: 1, - depth: 2, - linkquality: 100, - }, - ], - }); - devices.CC2530_ROUTER.lqi.mockResolvedValueOnce({ - neighbors: [ - {ieeeAddr: "0x0000000000000000", networkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, linkquality: 130}, - { - ieeeAddr: devices.bulb_color.ieeeAddr, - networkAddress: devices.bulb_color.networkAddress, - relationship: 4, - depth: 2, - linkquality: 130, - }, - ], - }); + devices.coordinator.lqi.mockResolvedValueOnce([ + { + eui64: devices.bulb_color.ieeeAddr as Eui64, + nwkAddress: devices.bulb_color.networkAddress, + relationship: 2, + depth: 1, + lqi: 120, + ...LQI_BASE_RSP, + }, + {eui64: devices.bulb.ieeeAddr as Eui64, nwkAddress: devices.bulb.networkAddress, relationship: 2, depth: 1, lqi: 92, ...LQI_BASE_RSP}, + { + eui64: devices.external_converter_device.ieeeAddr as Eui64, + nwkAddress: devices.external_converter_device.networkAddress, + relationship: 2, + depth: 1, + lqi: 92, + ...LQI_BASE_RSP, + }, + ]); + devices.coordinator.routingTable.mockResolvedValueOnce([ + { + destinationAddress: devices.CC2530_ROUTER.networkAddress, + status: "ACTIVE", + nextHopAddress: devices.bulb.networkAddress, + ...ROUTING_BASE_RSP, + }, + ]); + devices.bulb.lqi.mockResolvedValueOnce([ + { + eui64: devices.bulb_color.ieeeAddr as Eui64, + nwkAddress: devices.bulb_color.networkAddress, + relationship: 1, + depth: 2, + lqi: 110, + ...LQI_BASE_RSP, + }, + { + eui64: devices.CC2530_ROUTER.ieeeAddr as Eui64, + nwkAddress: devices.CC2530_ROUTER.networkAddress, + relationship: 1, + depth: 2, + lqi: 100, + ...LQI_BASE_RSP, + }, + ]); + devices.CC2530_ROUTER.lqi.mockResolvedValueOnce([ + {eui64: "0x0000000000000000", nwkAddress: devices.WXKG02LM_rev1.networkAddress, relationship: 1, depth: 2, lqi: 130, ...LQI_BASE_RSP}, + { + eui64: devices.bulb_color.ieeeAddr as Eui64, + nwkAddress: devices.bulb_color.networkAddress, + relationship: 4, + depth: 2, + lqi: 130, + ...LQI_BASE_RSP, + }, + ]); devices.unsupported_router.lqi.mockRejectedValueOnce("failed").mockRejectedValueOnce("failed"); devices.unsupported_router.routingTable.mockRejectedValueOnce("failed").mockRejectedValueOnce("failed"); }; @@ -143,6 +164,9 @@ describe("Extension: NetworkMap", () => { value: { links: [ { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 120, lqi: 120, @@ -155,11 +179,14 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 92, lqi: 92, relationship: 2, - routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}], + routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}], source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369}, sourceIeeeAddr: "0x000b57fffec6a5b2", sourceNwkAddr: 40369, @@ -167,6 +194,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 92, lqi: 92, @@ -179,6 +209,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 2, linkquality: 110, lqi: 110, @@ -191,6 +224,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x000b57fffec6a5b2", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 2, linkquality: 100, lqi: 100, @@ -203,6 +239,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x000b57fffec6a5b2", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 2, linkquality: 130, lqi: 130, @@ -492,6 +531,9 @@ describe("Extension: NetworkMap", () => { value: { links: [ { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 120, lqi: 120, @@ -504,11 +546,14 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 92, lqi: 92, relationship: 2, - routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}], + routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}], source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369}, sourceIeeeAddr: "0x000b57fffec6a5b2", sourceNwkAddr: 40369, @@ -516,6 +561,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 92, lqi: 92, @@ -528,6 +576,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 2, linkquality: 130, lqi: 130, @@ -649,6 +700,9 @@ describe("Extension: NetworkMap", () => { value: { links: [ { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 120, lqi: 120, @@ -661,11 +715,14 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 92, lqi: 92, relationship: 2, - routes: [{destinationAddress: 6540, nextHop: 40369, status: "ACTIVE"}], + routes: [{destinationAddress: 6540, nextHopAddress: 40369, status: "ACTIVE", ...ROUTING_BASE_RSP}], source: {ieeeAddr: "0x000b57fffec6a5b2", networkAddress: 40369}, sourceIeeeAddr: "0x000b57fffec6a5b2", sourceNwkAddr: 40369, @@ -673,6 +730,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 1, linkquality: 92, lqi: 92, @@ -685,6 +745,9 @@ describe("Extension: NetworkMap", () => { targetIeeeAddr: "0x00124b00120144ae", }, { + deviceType: 0, + rxOnWhenIdle: 0, + permitJoining: 0, depth: 2, linkquality: 130, lqi: 130, diff --git a/test/mocks/zigbeeHerdsman.ts b/test/mocks/zigbeeHerdsman.ts index 1c6750daac..5eb882e8fc 100644 --- a/test/mocks/zigbeeHerdsman.ts +++ b/test/mocks/zigbeeHerdsman.ts @@ -1,9 +1,9 @@ import assert from "node:assert"; -import type {Mock} from "vitest"; +import {type Mock, vi} from "vitest"; import type {AdapterTypes} from "zigbee-herdsman"; - import {Zcl} from "zigbee-herdsman"; import {InterviewState} from "zigbee-herdsman/dist/controller/model/device"; +import type {BindingTableEntry, LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes"; import {DEFAULT_BIND_GROUP_ID} from "../../lib/util/utils"; import type {EventHandler} from "./utils"; @@ -104,6 +104,7 @@ export class Endpoint { unbind: Mock; save: Mock; configureReporting: Mock; + readReportingConfig: Mock; meta: Record; binds: ZHBind[]; profileID: number | undefined; @@ -138,6 +139,7 @@ export class Endpoint { this.unbind = vi.fn(); this.save = vi.fn(); this.configureReporting = vi.fn(); + this.readReportingConfig = vi.fn(); this.meta = meta; this.binds = binds; this.profileID = profileID; @@ -218,6 +220,7 @@ export class Endpoint { this.unbind.mockClear(); this.save.mockClear(); this.configureReporting.mockClear(); + this.readReportingConfig.mockClear(); this.addToGroup.mockClear(); this.removeFromGroup.mockClear(); this.getClusterAttributeValue.mockClear(); @@ -247,8 +250,10 @@ export class Device { lastSeen: number | undefined; isDeleted: boolean; linkquality?: number; - lqi: Mock; - routingTable: Mock; + lqi: Mock<() => Promise>; + routingTable: Mock<() => Promise>; + bindingTable: Mock<() => Promise>; + clearAllBindings: Mock<() => Promise>; constructor( type: string, @@ -285,8 +290,10 @@ export class Device { this.manufacturerName = manufacturerName; this.lastSeen = 1000; this.isDeleted = false; - this.lqi = vi.fn(() => ({neighbors: []})); - this.routingTable = vi.fn(() => ({table: []})); + this.lqi = vi.fn(() => Promise.resolve([] as LQITableEntry[])); + this.routingTable = vi.fn(() => Promise.resolve([] as RoutingTableEntry[])); + this.bindingTable = vi.fn(() => Promise.resolve([] as BindingTableEntry[])); + this.clearAllBindings = vi.fn(() => {}); } getEndpoint(ID: number): Endpoint | undefined { @@ -302,6 +309,8 @@ export class Device { this.save.mockClear(); this.lqi.mockClear(); this.routingTable.mockClear(); + this.bindingTable.mockClear(); + this.clearAllBindings.mockClear(); this.meta = {}; for (const ep of this.endpoints) { @@ -1136,10 +1145,12 @@ export const mockController = { }, start: vi.fn((): Promise => Promise.resolve("reset")), stop: vi.fn(), - touchlinkIdentify: vi.fn(), - touchlinkScan: vi.fn(), - touchlinkFactoryReset: vi.fn(), - touchlinkFactoryResetFirst: vi.fn(), + touchlink: { + identify: vi.fn(), + scan: vi.fn(), + factoryReset: vi.fn(), + factoryResetFirst: vi.fn(), + }, addInstallCode: vi.fn(), permitJoin: vi.fn(), getPermitJoin: vi.fn((): boolean => false),