Skip to content
34 changes: 32 additions & 2 deletions lib/extension/bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -276,6 +276,36 @@ export default class Bind extends Extension {
}

@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
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) {
Expand Down Expand Up @@ -389,7 +419,7 @@ export default class Bind extends Extension {
}

private async publishResponse<T extends Zigbee2MQTTResponseEndpoints>(
type: ParsedMQTTMessage["type"],
type: ParsedMQTTMessage["type"] | "binds/clear",
request: KeyValue,
data: Zigbee2MQTTAPI[T],
error?: string,
Expand Down
47 changes: 45 additions & 2 deletions lib/extension/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ export default class Bridge extends Extension {
private logTransport!: winston.transport;
private requestLookup: {[key: string]: (message: KeyValue | string) => Promise<Zigbee2MQTTResponse<Zigbee2MQTTResponseEndpoints>>} = {
"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,
Expand Down Expand Up @@ -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<Zigbee2MQTTResponse<"bridge/response/device/configure_reporting">> {
@bind async deviceReportingConfigure(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/configure">> {
if (
typeof message !== "object" ||
message.id === undefined ||
Expand Down Expand Up @@ -518,6 +521,46 @@ export default class Bridge extends Extension {
});
}

@bind async deviceReportingRead(message: string | KeyValue): Promise<Zigbee2MQTTResponse<"bridge/response/device/reporting/read">> {
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<Zigbee2MQTTResponse<"bridge/response/device/interview">> {
if (typeof message !== "object" || message.id === undefined) {
throw new Error("Invalid payload");
Expand Down
44 changes: 25 additions & 19 deletions lib/extension/networkMap.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -185,8 +186,8 @@ export default class NetworkMap extends Extension {

async networkScan(includeRoutes: boolean): Promise<Zigbee2MQTTNetworkMap> {
logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`);
const lqis = new Map<Device, zh.LQI>();
const routingTables = new Map<Device, zh.RoutingTable>();
const lqis = new Map<Device, LQITableEntry[]>();
const routingTables = new Map<Device, RoutingTableEntry[]>();
const failed = new Map<Device, string[]>();
const requestWithRetry = async <T>(request: () => Promise<T>): Promise<T> => {
try {
Expand All @@ -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<zh.LQI>(async () => await device.zh.lqi());
const result = await requestWithRetry<LQITableEntry[]>(async () => await device.zh.lqi());
lqis.set(device, result);
logger.debug(`LQI succeeded for '${device.name}'`);
} catch (error) {
Expand All @@ -222,7 +223,7 @@ export default class NetworkMap extends Extension {

if (includeRoutes) {
try {
const result = await requestWithRetry<zh.RoutingTable>(async () => await device.zh.routingTable());
const result = await requestWithRetry<RoutingTableEntry[]>(async () => await device.zh.routingTable());
routingTables.set(device, result);
logger.debug(`Routing table succeeded for '${device.name}'`);
} catch (error) {
Expand Down Expand Up @@ -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);
}
}
Expand Down
59 changes: 47 additions & 12 deletions lib/types/api.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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;
}[];
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -697,7 +712,7 @@ export interface Zigbee2MQTTAPI {
option: Record<string, unknown>;
};

"bridge/response/device/configure_reporting": {
"bridge/response/device/reporting/configure": {
id: string;
endpoint: string | number;
cluster: string | number;
Expand All @@ -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;
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down
3 changes: 0 additions & 3 deletions lib/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 4 additions & 4 deletions lib/zigbee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,23 +399,23 @@ export default class Zigbee {
}

async touchlinkFactoryResetFirst(): Promise<boolean> {
return await this.herdsman.touchlinkFactoryResetFirst();
return await this.herdsman.touchlink.factoryResetFirst();
}

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

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

async touchlinkIdentify(ieeeAddr: string, channel: number): Promise<void> {
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 {
Expand Down
Loading