mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-06-21 04:31:44 +00:00
dd1c449796
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
import assert from "node:assert";
|
|
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
|
import type {OtaExtraMetas} from "zigbee-herdsman/dist/controller/tstype";
|
|
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
|
import * as zhc from "zigbee-herdsman-converters";
|
|
import {access, Numeric} from "zigbee-herdsman-converters";
|
|
import logger from "../util/logger";
|
|
import * as settings from "../util/settings";
|
|
|
|
const LINKQUALITY = new Numeric("linkquality", access.STATE)
|
|
.withUnit("lqi")
|
|
.withDescription("Link quality (signal strength)")
|
|
.withValueMin(0)
|
|
.withValueMax(255)
|
|
.withCategory("diagnostic");
|
|
|
|
export default class Device {
|
|
public zh: zh.Device;
|
|
public definition?: zhc.Definition;
|
|
private _definitionModelID?: string;
|
|
|
|
get ieeeAddr(): string {
|
|
return this.zh.ieeeAddr;
|
|
}
|
|
// biome-ignore lint/style/useNamingConvention: API
|
|
get ID(): string {
|
|
return this.zh.ieeeAddr;
|
|
}
|
|
get options(): DeviceOptionsWithId {
|
|
const deviceOptions = settings.getDevice(this.ieeeAddr) ?? {friendly_name: this.ieeeAddr, ID: this.ieeeAddr};
|
|
return {...settings.get().device_options, ...deviceOptions};
|
|
}
|
|
get name(): string {
|
|
return this.zh.type === "Coordinator" ? "Coordinator" : this.options?.friendly_name;
|
|
}
|
|
get isSupported(): boolean {
|
|
return this.zh.type === "Coordinator" || Boolean(this.definition && !this.definition.generated);
|
|
}
|
|
get customClusters(): CustomClusters {
|
|
return this.zh.customClusters;
|
|
}
|
|
get otaExtraMetas(): OtaExtraMetas {
|
|
return typeof this.definition?.ota === "object" ? this.definition.ota : {};
|
|
}
|
|
get interviewed(): boolean {
|
|
return this.zh.interviewState === InterviewState.Successful || this.zh.interviewState === InterviewState.Failed;
|
|
}
|
|
|
|
constructor(device: zh.Device) {
|
|
this.zh = device;
|
|
}
|
|
|
|
exposes(): zhc.Expose[] {
|
|
const exposes: zhc.Expose[] = [];
|
|
assert(this.definition, "Cannot retreive exposes before definition is resolved");
|
|
if (typeof this.definition.exposes === "function") {
|
|
const options: KeyValue = this.options;
|
|
exposes.push(...this.definition.exposes(this.zh, options));
|
|
} else {
|
|
exposes.push(...this.definition.exposes);
|
|
}
|
|
exposes.push(LINKQUALITY);
|
|
return exposes;
|
|
}
|
|
|
|
async resolveDefinition(ignoreCache = false): Promise<void> {
|
|
if (this.interviewed && (!this.definition || this._definitionModelID !== this.zh.modelID || ignoreCache)) {
|
|
this.definition = await zhc.findByDevice(this.zh, true);
|
|
this._definitionModelID = this.zh.modelID;
|
|
}
|
|
}
|
|
|
|
async reInterview(eventBus: EventBus): Promise<void> {
|
|
// logic follows that of receiving `deviceInterview` event from ZH layer
|
|
logger.info(`Interviewing '${this.name}'`);
|
|
eventBus.emitDeviceInterview({status: "started", device: this});
|
|
|
|
try {
|
|
await this.zh.interview(true);
|
|
// A re-interview can for example result in a different modelId, therefore reconsider the definition.
|
|
await this.resolveDefinition(true);
|
|
logger.info(`Successfully interviewed '${this.name}'`);
|
|
eventBus.emitDeviceInterview({status: "successful", device: this});
|
|
} catch (error) {
|
|
eventBus.emitDeviceInterview({status: "failed", device: this});
|
|
throw new Error(`Interview of '${this.name}' (${this.ieeeAddr}) failed: ${error}`);
|
|
}
|
|
}
|
|
|
|
ensureInSettings(): void {
|
|
if (this.zh.type !== "Coordinator" && !settings.getDevice(this.zh.ieeeAddr)) {
|
|
settings.addDevice(this.zh.ieeeAddr);
|
|
}
|
|
}
|
|
|
|
endpoint(key?: string | number): zh.Endpoint | undefined {
|
|
if (!key) {
|
|
key = "default";
|
|
} else if (!Number.isNaN(Number(key))) {
|
|
return this.zh.getEndpoint(Number(key));
|
|
}
|
|
|
|
if (this.definition?.endpoint) {
|
|
const ID = this.definition.endpoint(this.zh)[key];
|
|
|
|
if (ID) {
|
|
return this.zh.getEndpoint(ID);
|
|
}
|
|
}
|
|
|
|
return key === "default" ? this.zh.endpoints[0] : undefined;
|
|
}
|
|
|
|
endpointName(endpoint: zh.Endpoint): string | undefined {
|
|
let epName: string | undefined;
|
|
|
|
if (this.definition?.endpoint) {
|
|
const mapping = this.definition.endpoint(this.zh);
|
|
|
|
for (const name in mapping) {
|
|
if (mapping[name] === endpoint.ID) {
|
|
epName = name;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* v8 ignore next */
|
|
return epName === "default" ? undefined : epName;
|
|
}
|
|
|
|
getEndpointNames(): string[] {
|
|
const names: string[] = [];
|
|
|
|
if (this.definition?.endpoint) {
|
|
for (const name in this.definition.endpoint(this.zh)) {
|
|
if (name !== "default") {
|
|
names.push(name);
|
|
}
|
|
}
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
isDevice(): this is Device {
|
|
return true;
|
|
}
|
|
|
|
isGroup(): this is Group {
|
|
return false;
|
|
}
|
|
}
|