mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-06-24 06:01:40 +00:00
73d8ae8956
Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
156 lines
6.3 KiB
TypeScript
156 lines
6.3 KiB
TypeScript
import bind from "bind-decorator";
|
|
import stringify from "json-stable-stringify-without-jsonify";
|
|
import Device from "../model/device";
|
|
import type {Zigbee2MQTTAPI} from "../types/api";
|
|
import logger from "../util/logger";
|
|
import * as settings from "../util/settings";
|
|
import utils from "../util/utils";
|
|
import Extension from "./extension";
|
|
|
|
/**
|
|
* This extension calls the zigbee-herdsman-converters definition configure() method
|
|
*/
|
|
export default class Configure extends Extension {
|
|
private configuring = new Set<string>();
|
|
private attempts = new Map<string, number>();
|
|
private topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`;
|
|
|
|
@bind private async onReconfigure(data: eventdata.Reconfigure): Promise<void> {
|
|
// Disabling reporting unbinds some cluster which could be bound by configure, re-setup.
|
|
if (data.device.zh.meta?.configured !== undefined) {
|
|
delete data.device.zh.meta.configured;
|
|
data.device.zh.save();
|
|
}
|
|
|
|
await this.configure(data.device, "reporting_disabled");
|
|
}
|
|
|
|
@bind private async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
|
|
if (data.topic === this.topic) {
|
|
const message = utils.parseJSON(data.message, data.message) as Zigbee2MQTTAPI["bridge/request/device/configure"];
|
|
const ID = typeof message === "object" ? message.id : message;
|
|
let error: string | undefined;
|
|
|
|
if (ID === undefined) {
|
|
error = "Invalid payload";
|
|
} else {
|
|
const device = this.zigbee.resolveEntity(ID);
|
|
|
|
if (!device || !(device instanceof Device)) {
|
|
error = `Device '${ID}' does not exist`;
|
|
} else if (!device.definition?.configure) {
|
|
error = `Device '${device.name}' cannot be configured`;
|
|
} else {
|
|
try {
|
|
await this.configure(device, "mqtt_message", true, true);
|
|
} catch (e) {
|
|
error = `Failed to configure (${(e as Error).message})`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const response = utils.getResponse<"bridge/response/device/configure">(message, {id: ID}, error);
|
|
|
|
await this.mqtt.publish("bridge/response/device/configure", stringify(response));
|
|
}
|
|
}
|
|
|
|
override start(): Promise<void> {
|
|
setImmediate(async () => {
|
|
// Only configure routers on startup, end devices are likely sleeping and
|
|
// will reconfigure once they send a message
|
|
for (const device of this.zigbee.devicesIterator((d) => d.type === "Router")) {
|
|
// Sleep 10 seconds between configuring on startup to not DDoS the coordinator when many devices have to be configured.
|
|
await utils.sleep(10);
|
|
await this.configure(device, "started");
|
|
}
|
|
});
|
|
|
|
this.eventBus.onDeviceJoined(this, async (data) => {
|
|
if (data.device.zh.meta.configured !== undefined) {
|
|
delete data.device.zh.meta.configured;
|
|
data.device.zh.save();
|
|
}
|
|
|
|
await this.configure(data.device, "zigbee_event");
|
|
});
|
|
// TODO: this is triggering for any `data.status`, but should only for `successful`? (relies on `device.definition?` early-return?)
|
|
this.eventBus.onDeviceInterview(this, (data) => this.configure(data.device, "zigbee_event"));
|
|
this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, "zigbee_event"));
|
|
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
|
this.eventBus.onReconfigure(this, this.onReconfigure);
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
private async configure(
|
|
device: Device,
|
|
event: "started" | "zigbee_event" | "reporting_disabled" | "mqtt_message",
|
|
force = false,
|
|
throwError = false,
|
|
): Promise<void> {
|
|
if (!device.definition?.configure) {
|
|
return;
|
|
}
|
|
|
|
const definitionVersion = device.definition.version;
|
|
|
|
if (!force) {
|
|
if (device.options.disabled || !device.interviewed) {
|
|
return;
|
|
}
|
|
|
|
// Only configure end devices when it is active, otherwise it will likely fails as they are sleeping.
|
|
if (device.zh.type === "EndDevice" && event !== "zigbee_event") {
|
|
return;
|
|
}
|
|
|
|
const shouldReconfigure =
|
|
// Should always reconfigure when not configured before
|
|
device.zh.meta?.configured === undefined ||
|
|
// Or should reconfigure when definition.version is not '0.0.0' and differs from last `meta.configured`.
|
|
// In older Z2M versions the stored `meta.configured` was the hash of the configure function.
|
|
// Since we don't want to reconfigure all devices, we don't re-configure when the definition has the default version of '0.0.0'.
|
|
(definitionVersion !== "0.0.0" && device.zh.meta?.configured !== definitionVersion);
|
|
if (!shouldReconfigure) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (this.configuring.has(device.ieeeAddr)) {
|
|
return;
|
|
}
|
|
|
|
const attempts = this.attempts.get(device.ieeeAddr) ?? 0;
|
|
|
|
if (attempts >= 3 && !force) {
|
|
return;
|
|
}
|
|
|
|
this.configuring.add(device.ieeeAddr);
|
|
|
|
logger.info(`Configuring '${device.name}'`);
|
|
|
|
try {
|
|
await device.definition.configure(device.zh, this.zigbee.firstCoordinatorEndpoint(), device.definition);
|
|
this.attempts.delete(device.ieeeAddr);
|
|
logger.info(`Successfully configured '${device.name}' (definition v${definitionVersion})`);
|
|
device.zh.meta.configured = definitionVersion;
|
|
device.zh.save();
|
|
this.eventBus.emitDevicesChanged();
|
|
} catch (error) {
|
|
const newAttempts = attempts + 1;
|
|
this.attempts.set(device.ieeeAddr, newAttempts);
|
|
|
|
const msg = `Failed to configure '${device.name}', attempt ${newAttempts} (${(error as Error).stack})`;
|
|
logger.error(msg);
|
|
|
|
if (throwError) {
|
|
throw error;
|
|
}
|
|
} finally {
|
|
this.configuring.delete(device.ieeeAddr);
|
|
}
|
|
}
|
|
}
|