Skip to content

Commit f26ade4

Browse files
NerivecKoenkk
andcommitted
feat: Add new bind/reporting/map features (#29750)
Co-authored-by: Koen Kanters <[email protected]>
1 parent 3e2aefb commit f26ade4

File tree

10 files changed

+637
-135
lines changed

10 files changed

+637
-135
lines changed

lib/extension/bind.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ interface ParsedMQTTMessage {
184184
}
185185

186186
export default class Bind extends Extension {
187-
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind)`);
187+
#topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/request/device/(bind|unbind|binds/clear)`);
188188
private pollDebouncers: {[s: string]: () => void} = {};
189189

190190
// biome-ignore lint/suspicious/useAwait: API
@@ -276,6 +276,36 @@ export default class Bind extends Extension {
276276
}
277277

278278
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
279+
if (data.topic.endsWith("binds/clear")) {
280+
const message = JSON.parse(data.message) as Zigbee2MQTTAPI["bridge/request/device/binds/clear"];
281+
282+
if (typeof message !== "object" || typeof message.target !== "string") {
283+
await this.publishResponse("binds/clear", message, {}, "Invalid payload");
284+
return;
285+
}
286+
287+
const target = this.zigbee.resolveEntity(message.target);
288+
289+
if (!(target instanceof Device)) {
290+
await this.publishResponse("binds/clear", message, {}, "Invalid target");
291+
return;
292+
}
293+
294+
// this list is raw (not resolved) to allow clearing any specific target (not only currently known)
295+
const eui64List = message.ieeeList ?? ["0xffffffffffffffff"];
296+
297+
await target.zh.clearAllBindings(eui64List);
298+
299+
const responseData: Zigbee2MQTTAPI["bridge/response/device/binds/clear"] = {
300+
target: message.target,
301+
ieeeList: eui64List,
302+
};
303+
304+
await this.publishResponse("binds/clear", message, responseData);
305+
this.eventBus.emitDevicesChanged();
306+
return;
307+
}
308+
279309
const [raw, parsed, error] = this.parseMQTTMessage(data);
280310

281311
if (!raw || !parsed) {
@@ -389,7 +419,7 @@ export default class Bind extends Extension {
389419
}
390420

391421
private async publishResponse<T extends Zigbee2MQTTResponseEndpoints>(
392-
type: ParsedMQTTMessage["type"],
422+
type: ParsedMQTTMessage["type"] | "binds/clear",
393423
request: KeyValue,
394424
data: Zigbee2MQTTAPI[T],
395425
error?: string,

lib/extension/bridge.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ export default class Bridge extends Extension {
3131
private logTransport!: winston.transport;
3232
private requestLookup: {[key: string]: (message: KeyValue | string) => Promise<Zigbee2MQTTResponse<Zigbee2MQTTResponseEndpoints>>} = {
3333
"device/options": this.deviceOptions,
34-
"device/configure_reporting": this.deviceConfigureReporting,
34+
/** @deprecated 3.0 */
35+
"device/configure_reporting": this.deviceReportingConfigure,
36+
"device/reporting/configure": this.deviceReportingConfigure,
37+
"device/reporting/read": this.deviceReportingRead,
3538
"device/remove": this.deviceRemove,
3639
"device/interview": this.deviceInterview,
3740
"device/generate_external_definition": this.deviceGenerateExternalDefinition,
@@ -466,7 +469,7 @@ export default class Bridge extends Extension {
466469
return utils.getResponse(message, {from: oldOptions, to: newOptions, id: ID, restart_required: this.restartRequired});
467470
}
468471

469-
@bind async deviceConfigureReporting(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/configure_reporting">> {
472+
@bind async deviceReportingConfigure(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/configure">> {
470473
if (
471474
typeof message !== "object" ||
472475
message.id === undefined ||
@@ -518,6 +521,46 @@ export default class Bridge extends Extension {
518521
});
519522
}
520523

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

lib/extension/networkMap.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import bind from "bind-decorator";
22
import stringify from "json-stable-stringify-without-jsonify";
3+
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
4+
import type {LQITableEntry, RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
35
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
4-
56
import logger from "../util/logger";
67
import * as settings from "../util/settings";
78
import utils from "../util/utils";
@@ -185,8 +186,8 @@ export default class NetworkMap extends Extension {
185186

186187
async networkScan(includeRoutes: boolean): Promise<Zigbee2MQTTNetworkMap> {
187188
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
188-
const lqis = new Map<Device, zh.LQI>();
189-
const routingTables = new Map<Device, zh.RoutingTable>();
189+
const lqis = new Map<Device, LQITableEntry[]>();
190+
const routingTables = new Map<Device, RoutingTableEntry[]>();
190191
const failed = new Map<Device, string[]>();
191192
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
192193
try {
@@ -210,7 +211,7 @@ export default class NetworkMap extends Extension {
210211
await utils.sleep(1); // sleep 1 second between each scan to reduce stress on network.
211212

212213
try {
213-
const result = await requestWithRetry<zh.LQI>(async () => await device.zh.lqi());
214+
const result = await requestWithRetry<LQITableEntry[]>(async () => await device.zh.lqi());
214215
lqis.set(device, result);
215216
logger.debug(`LQI succeeded for '${device.name}'`);
216217
} catch (error) {
@@ -222,7 +223,7 @@ export default class NetworkMap extends Extension {
222223

223224
if (includeRoutes) {
224225
try {
225-
const result = await requestWithRetry<zh.RoutingTable>(async () => await device.zh.routingTable());
226+
const result = await requestWithRetry<RoutingTableEntry[]>(async () => await device.zh.routingTable());
226227
routingTables.set(device, result);
227228
logger.debug(`Routing table succeeded for '${device.name}'`);
228229
} catch (error) {
@@ -274,42 +275,47 @@ export default class NetworkMap extends Extension {
274275
}
275276

276277
// Add links
277-
for (const [device, lqi] of lqis) {
278-
for (const neighbor of lqi.neighbors) {
278+
for (const [device, table] of lqis) {
279+
for (const neighbor of table) {
279280
if (neighbor.relationship > 3) {
280281
// Relationship is not active, skip it
281282
continue;
282283
}
283284

285+
let neighborEui64 = neighbor.eui64;
286+
284287
// Some Xiaomi devices return 0x00 as the neighbor ieeeAddr (obviously not correct).
285288
// Determine the correct ieeeAddr based on the networkAddress.
286-
if (neighbor.ieeeAddr === "0x0000000000000000") {
287-
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.networkAddress);
289+
if (neighborEui64 === "0x0000000000000000") {
290+
const neighborDevice = this.zigbee.deviceByNetworkAddress(neighbor.nwkAddress);
288291

289292
if (neighborDevice) {
290-
neighbor.ieeeAddr = neighborDevice.ieeeAddr;
293+
neighborEui64 = neighborDevice.ieeeAddr as Eui64;
291294
}
292295
}
293296

294297
const link: Zigbee2MQTTNetworkMap["links"][number] = {
295-
source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress},
298+
source: {ieeeAddr: neighborEui64, networkAddress: neighbor.nwkAddress},
296299
target: {ieeeAddr: device.ieeeAddr, networkAddress: device.zh.networkAddress},
297-
linkquality: neighbor.linkquality,
300+
deviceType: neighbor.deviceType,
301+
rxOnWhenIdle: neighbor.rxOnWhenIdle,
302+
relationship: neighbor.relationship,
303+
permitJoining: neighbor.permitJoining,
298304
depth: neighbor.depth,
305+
lqi: neighbor.lqi,
299306
routes: [],
300-
// DEPRECATED:
301-
sourceIeeeAddr: neighbor.ieeeAddr,
307+
// below are @deprecated
308+
sourceIeeeAddr: neighborEui64,
302309
targetIeeeAddr: device.ieeeAddr,
303-
sourceNwkAddr: neighbor.networkAddress,
304-
lqi: neighbor.linkquality,
305-
relationship: neighbor.relationship,
310+
sourceNwkAddr: neighbor.nwkAddress,
311+
linkquality: neighbor.lqi,
306312
};
307313

308314
const routingTable = routingTables.get(device);
309315

310316
if (routingTable) {
311-
for (const entry of routingTable.table) {
312-
if (entry.status === "ACTIVE" && entry.nextHop === neighbor.networkAddress) {
317+
for (const entry of routingTable) {
318+
if (entry.nextHopAddress === neighbor.nwkAddress) {
313319
link.routes.push(entry);
314320
}
315321
}

lib/types/api.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import type * as zigbeeHerdsman from "zigbee-herdsman/dist";
2+
import type {Eui64} from "zigbee-herdsman/dist/zspec/tstypes";
23
import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
4+
import type {RoutingTableEntry} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
35
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
46
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
57

@@ -278,18 +280,21 @@ export interface Zigbee2MQTTNetworkMap {
278280
links: {
279281
source: {ieeeAddr: string; networkAddress: number};
280282
target: {ieeeAddr: string; networkAddress: number};
281-
linkquality: number;
283+
deviceType: number;
284+
rxOnWhenIdle: number;
285+
relationship: number;
286+
permitJoining: number;
282287
depth: number;
283-
routes: {
284-
destinationAddress: number;
285-
status: string;
286-
nextHop: number;
287-
}[];
288+
lqi: number;
289+
routes: RoutingTableEntry[];
290+
/** @deprecated 3.0 */
291+
linkquality: number;
292+
/** @deprecated 3.0 */
288293
sourceIeeeAddr: string;
294+
/** @deprecated 3.0 */
289295
targetIeeeAddr: string;
296+
/** @deprecated 3.0 */
290297
sourceNwkAddr: number;
291-
lqi: number;
292-
relationship: number;
293298
}[];
294299
}
295300

@@ -558,6 +563,16 @@ export interface Zigbee2MQTTAPI {
558563
failed: string[];
559564
};
560565

566+
"bridge/request/device/binds/clear": {
567+
target: string;
568+
ieeeList?: Eui64[];
569+
};
570+
571+
"bridge/response/device/binds/clear": {
572+
target: string;
573+
ieeeList?: Eui64[];
574+
};
575+
561576
"bridge/request/device/configure":
562577
| {
563578
id: string | number;
@@ -686,7 +701,7 @@ export interface Zigbee2MQTTAPI {
686701
homeassistant_rename: boolean;
687702
};
688703

689-
"bridge/request/device/configure_reporting": {
704+
"bridge/request/device/reporting/configure": {
690705
id: string;
691706
endpoint: string | number;
692707
cluster: string | number;
@@ -697,7 +712,7 @@ export interface Zigbee2MQTTAPI {
697712
option: Record<string, unknown>;
698713
};
699714

700-
"bridge/response/device/configure_reporting": {
715+
"bridge/response/device/reporting/configure": {
701716
id: string;
702717
endpoint: string | number;
703718
cluster: string | number;
@@ -707,6 +722,22 @@ export interface Zigbee2MQTTAPI {
707722
reportable_change: number;
708723
};
709724

725+
"bridge/request/device/reporting/read": {
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/reporting/read": {
734+
id: string;
735+
endpoint: string | number;
736+
cluster: string | number;
737+
configs: zigbeeHerdsman.Zcl.ClustersTypes.TFoundation["readReportConfigRsp"];
738+
manufacturerCode?: number;
739+
};
740+
710741
"bridge/request/group/remove": {
711742
id: string;
712743
force?: boolean;
@@ -883,6 +914,7 @@ export type Zigbee2MQTTRequestEndpoints =
883914
| "bridge/request/options"
884915
| "bridge/request/device/bind"
885916
| "bridge/request/device/unbind"
917+
| "bridge/request/device/binds/clear"
886918
| "bridge/request/device/configure"
887919
| "bridge/request/device/remove"
888920
| "bridge/request/device/ota_update/check"
@@ -896,7 +928,8 @@ export type Zigbee2MQTTRequestEndpoints =
896928
| "bridge/request/device/generate_external_definition"
897929
| "bridge/request/device/options"
898930
| "bridge/request/device/rename"
899-
| "bridge/request/device/configure_reporting"
931+
| "bridge/request/device/reporting/configure"
932+
| "bridge/request/device/reporting/read"
900933
| "bridge/request/group/remove"
901934
| "bridge/request/group/add"
902935
| "bridge/request/group/rename"
@@ -931,6 +964,7 @@ export type Zigbee2MQTTResponseEndpoints =
931964
| "bridge/response/options"
932965
| "bridge/response/device/bind"
933966
| "bridge/response/device/unbind"
967+
| "bridge/response/device/binds/clear"
934968
| "bridge/response/device/configure"
935969
| "bridge/response/device/remove"
936970
| "bridge/response/device/ota_update/check"
@@ -941,7 +975,8 @@ export type Zigbee2MQTTResponseEndpoints =
941975
| "bridge/response/device/generate_external_definition"
942976
| "bridge/response/device/options"
943977
| "bridge/response/device/rename"
944-
| "bridge/response/device/configure_reporting"
978+
| "bridge/response/device/reporting/configure"
979+
| "bridge/response/device/reporting/read"
945980
| "bridge/response/group/remove"
946981
| "bridge/response/group/add"
947982
| "bridge/response/group/rename"

lib/types/types.d.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ declare global {
3636
type Endpoint = ZHModels.Endpoint;
3737
type Device = ZHModels.Device;
3838
type Group = ZHModels.Group;
39-
// biome-ignore lint/style/useNamingConvention: API
40-
type LQI = ZHAdapterTypes.Lqi;
41-
type RoutingTable = ZHAdapterTypes.RoutingTable;
4239
type CoordinatorVersion = ZHAdapterTypes.CoordinatorVersion;
4340
type NetworkParameters = ZHAdapterTypes.NetworkParameters;
4441
interface Bind {

lib/zigbee.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,23 +399,23 @@ export default class Zigbee {
399399
}
400400

401401
async touchlinkFactoryResetFirst(): Promise<boolean> {
402-
return await this.herdsman.touchlinkFactoryResetFirst();
402+
return await this.herdsman.touchlink.factoryResetFirst();
403403
}
404404

405405
async touchlinkFactoryReset(ieeeAddr: string, channel: number): Promise<boolean> {
406-
return await this.herdsman.touchlinkFactoryReset(ieeeAddr, channel);
406+
return await this.herdsman.touchlink.factoryReset(ieeeAddr, channel);
407407
}
408408

409409
async addInstallCode(installCode: string): Promise<void> {
410410
await this.herdsman.addInstallCode(installCode);
411411
}
412412

413413
async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
414-
await this.herdsman.touchlinkIdentify(ieeeAddr, channel);
414+
await this.herdsman.touchlink.identify(ieeeAddr, channel);
415415
}
416416

417417
async touchlinkScan(): Promise<{ieeeAddr: string; channel: number}[]> {
418-
return await this.herdsman.touchlinkScan();
418+
return await this.herdsman.touchlink.scan();
419419
}
420420

421421
createGroup(id: number): Group {

0 commit comments

Comments
 (0)