import bind from "bind-decorator"; import Device from "../model/device"; import type {Zigbee2MQTTAPI} from "../types/api"; import logger from "../util/logger"; import * as settings from "../util/settings"; import {stringify} from "../util/stringify"; 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(); private attempts = new Map(); private topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`; @bind private async onReconfigure(data: eventdata.Reconfigure): Promise { // 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 { 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 { 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 { 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.emitExposesAndDevicesChanged(device); } 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); } } }