From ef284071f0c9edfe68e542b10a3da1f1dce4069e Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Fri, 7 Jun 2024 10:06:52 +0200 Subject: [PATCH] feat: add `ZWaveNode.createDump()` method (#6906) --- packages/cc/src/lib/CommandClass.ts | 9 +- packages/core/src/security/SecurityClass.ts | 10 ++ packages/core/src/values/Cache.ts | 7 +- packages/zwave-js/src/lib/node/Dump.ts | 79 +++++++++ packages/zwave-js/src/lib/node/Endpoint.ts | 42 +++++ packages/zwave-js/src/lib/node/Node.ts | 167 +++++++++++++++++++- packages/zwave-js/src/lib/node/utils.ts | 19 ++- 7 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 packages/zwave-js/src/lib/node/Dump.ts diff --git a/packages/cc/src/lib/CommandClass.ts b/packages/cc/src/lib/CommandClass.ts index ac0229a6274b..8e9d6f62a167 100644 --- a/packages/cc/src/lib/CommandClass.ts +++ b/packages/cc/src/lib/CommandClass.ts @@ -804,7 +804,10 @@ export class CommandClass implements ICommandClass { } /** Returns a list of all value names that are defined for this CommandClass */ - public getDefinedValueIDs(applHost: ZWaveApplicationHost): ValueID[] { + public getDefinedValueIDs( + applHost: ZWaveApplicationHost, + includeInternal: boolean = false, + ): ValueID[] { // In order to compare value ids, we need them to be strings const ret = new Map(); @@ -844,7 +847,7 @@ export class CommandClass implements ICommandClass { } // Skip internal values - if (value.options.internal) continue; + if (value.options.internal && !includeInternal) continue; // And determine if this value should be automatically "created" if (!this.shouldAutoCreateValue(applHost, value)) continue; @@ -864,7 +867,7 @@ export class CommandClass implements ICommandClass { // ... which don't have a CC value definition // ... or one that does not mark the value ID as internal const ccValue = ccValues.find((value) => value.is(valueId)); - if (!ccValue || !ccValue.options.internal) { + if (!ccValue || !ccValue.options.internal || includeInternal) { addValueId(valueId.property, valueId.propertyKey); } } diff --git a/packages/core/src/security/SecurityClass.ts b/packages/core/src/security/SecurityClass.ts index 6e8f9e4ddc04..ed06e521a877 100644 --- a/packages/core/src/security/SecurityClass.ts +++ b/packages/core/src/security/SecurityClass.ts @@ -32,6 +32,16 @@ export function securityClassIsS2( ); } +/** Tests if the given security class is valid for use with Z-Wave LR */ +export function securityClassIsLongRange( + secClass: SecurityClass | undefined, +): secClass is S2SecurityClass { + return ( + secClass === SecurityClass.S2_AccessControl + || secClass === SecurityClass.S2_Authenticated + ); +} + /** An array of security classes, ordered from high (index 0) to low (index > 0) */ export const securityClassOrder = [ SecurityClass.S2_AccessControl, diff --git a/packages/core/src/values/Cache.ts b/packages/core/src/values/Cache.ts index 2a10fc63a87b..3cea1cd36ed0 100644 --- a/packages/core/src/values/Cache.ts +++ b/packages/core/src/values/Cache.ts @@ -6,7 +6,12 @@ import type { ValueMetadata } from "./Metadata"; import type { ValueID } from "./_Types"; // export type SerializableValue = number | string | boolean | Map | JSONObject; -type SerializedValue = number | string | boolean | JSONObject | undefined; +export type SerializedValue = + | number + | string + | boolean + | JSONObject + | undefined; export interface CacheValue extends Pick diff --git a/packages/zwave-js/src/lib/node/Dump.ts b/packages/zwave-js/src/lib/node/Dump.ts new file mode 100644 index 000000000000..af837bf58cac --- /dev/null +++ b/packages/zwave-js/src/lib/node/Dump.ts @@ -0,0 +1,79 @@ +import type { + CommandClassInfo, + DataRate, + FLiRS, + SerializedValue, + ValueMetadata, +} from "@zwave-js/core/safe"; +import { type JSONObject } from "@zwave-js/shared"; + +export interface DeviceClassDump { + key: number; + label: string; +} + +export interface DeviceClassesDump { + basic: DeviceClassDump; + generic: DeviceClassDump; + specific: DeviceClassDump; +} + +export interface CommandClassDump extends CommandClassInfo { + values: ValueDump[]; +} + +export interface ValueDump { + property: string | number; + propertyKey?: string | number; + metadata?: ValueMetadata; + value?: SerializedValue; + timestamp?: string; + internal?: boolean; +} + +export interface EndpointDump { + index: number; + deviceClass: DeviceClassesDump | "unknown"; + maySupportBasicCC: boolean; + commandClasses: Record; +} + +export interface NodeDump { + id: number; + manufacturer?: string; + label?: string; + description?: string; + fingerprint: { + // Hex representation: + manufacturerId: string; + productType: string; + productId: string; + firmwareVersion: string; + hardwareVersion?: number; + }; + interviewStage: string; + ready: boolean; + + dsk?: string; + securityClasses: Record; + + isListening: boolean | "unknown"; + isFrequentListening: FLiRS | "unknown"; + isRouting: boolean | "unknown"; + supportsBeaming: boolean | "unknown"; + supportsSecurity: boolean | "unknown"; + protocol: string; + supportedProtocols?: string[]; + protocolVersion: string; + sdkVersion: string; + supportedDataRates: DataRate[] | "unknown"; + + deviceClass: DeviceClassesDump | "unknown"; + maySupportBasicCC: boolean; + commandClasses: Record; + + endpoints?: Record; + + configFileName?: string; + compatFlags?: JSONObject; +} diff --git a/packages/zwave-js/src/lib/node/Endpoint.ts b/packages/zwave-js/src/lib/node/Endpoint.ts index 158d8aecb9d0..e6f738531418 100644 --- a/packages/zwave-js/src/lib/node/Endpoint.ts +++ b/packages/zwave-js/src/lib/node/Endpoint.ts @@ -26,6 +26,7 @@ import { isDeepStrictEqual } from "node:util"; import type { Driver } from "../driver/Driver"; import { cacheKeys } from "../driver/NetworkCache"; import type { DeviceClass } from "./DeviceClass"; +import type { EndpointDump } from "./Dump"; import type { ZWaveNode } from "./Node"; /** @@ -487,4 +488,45 @@ export class Endpoint implements IZWaveEndpoint { ZWavePlusCCValues.userIcon.endpoint(this.index), ); } + + /** + * @internal + * Returns a dump of this endpoint's information for debugging purposes + */ + public createEndpointDump(): EndpointDump { + const ret: EndpointDump = { + index: this.index, + deviceClass: "unknown", + commandClasses: {}, + maySupportBasicCC: this.maySupportBasicCC(), + }; + + if (this.deviceClass) { + ret.deviceClass = { + basic: { + key: this.deviceClass.basic.key, + label: this.deviceClass.basic.label, + }, + generic: { + key: this.deviceClass.generic.key, + label: this.deviceClass.generic.label, + }, + specific: { + key: this.deviceClass.specific.key, + label: this.deviceClass.specific.label, + }, + }; + } + + for (const [ccId, info] of this._implementedCommandClasses) { + ret.commandClasses[getCCName(ccId)] = { ...info, values: [] }; + } + + for (const [prop, value] of Object.entries(ret)) { + // @ts-expect-error + if (value === undefined) delete ret[prop]; + } + + return ret; + } } diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 6fd230ab9d47..ba908d153a96 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -150,10 +150,11 @@ import { SetValueStatus, supervisionResultToSetValueResult, } from "@zwave-js/cc/safe"; -import type { - DeviceConfig, - Notification, - NotificationValueDefinition, +import { + type DeviceConfig, + type Notification, + type NotificationValueDefinition, + embeddedDevicesDir, } from "@zwave-js/config"; import { CRC16_CCITT, @@ -171,7 +172,7 @@ import { NOT_KNOWN, NodeType, type NodeUpdatePayload, - type ProtocolVersion, + ProtocolVersion, Protocols, type RSSI, RssiError, @@ -195,6 +196,7 @@ import { actuatorCCs, allCCs, applicationCCs, + dskToString, encapsulationCCs, getCCName, getDSTInfo, @@ -206,9 +208,11 @@ import { isZWaveError, nonApplicationCCs, normalizeValueID, + securityClassIsLongRange, securityClassIsS2, securityClassOrder, sensorCCs, + serializeCacheValue, supervisedCommandFailed, supervisedCommandSucceeded, timespan, @@ -241,6 +245,7 @@ import { padStart } from "alcalzone-shared/strings"; import { isArray, isObject } from "alcalzone-shared/typeguards"; import { randomBytes } from "node:crypto"; import { EventEmitter } from "node:events"; +import path from "node:path"; import { isDeepStrictEqual } from "node:util"; import { RemoveNodeReason } from "../controller/Inclusion"; import { determineNIF } from "../controller/NodeInformationFrame"; @@ -263,6 +268,7 @@ import { RequestNodeInfoResponse, } from "../serialapi/network-mgmt/RequestNodeInfoMessages"; import { DeviceClass } from "./DeviceClass"; +import { type NodeDump, type ValueDump } from "./Dump"; import { Endpoint } from "./Endpoint"; import { formatLifelineHealthCheckSummary, @@ -847,6 +853,10 @@ export class ZWaveNode extends Endpoint return ret; } + public get hardwareVersion(): MaybeNotKnown { + return this.getValue(VersionCCValues.hardwareVersion.id); + } + public get sdkVersion(): MaybeNotKnown { return this.getValue(VersionCCValues.sdkVersion.id); } @@ -7358,4 +7368,151 @@ ${formatRouteHealthCheckSummary(this.id, otherNode.id, summary)}`, } return true; } + + /** Returns a dump of this node's information for debugging purposes */ + public createDump(): NodeDump { + const { index, ...endpointDump } = this.createEndpointDump(); + const ret: NodeDump = { + id: this.id, + manufacturer: this.deviceConfig?.manufacturer, + label: this.label, + description: this.deviceConfig?.description, + fingerprint: { + manufacturerId: this.manufacturerId != undefined + ? formatId(this.manufacturerId) + : "unknown", + productType: this.productType != undefined + ? formatId(this.productType) + : "unknown", + productId: this.productId != undefined + ? formatId(this.productId) + : "unknown", + firmwareVersion: this.firmwareVersion ?? "unknown", + }, + interviewStage: getEnumMemberName( + InterviewStage, + this.interviewStage, + ), + ready: this.ready, + + dsk: this.dsk ? dskToString(this.dsk) : undefined, + securityClasses: {}, + + isListening: this.isListening ?? "unknown", + isFrequentListening: this.isFrequentListening ?? "unknown", + isRouting: this.isRouting ?? "unknown", + supportsBeaming: this.supportsBeaming ?? "unknown", + supportsSecurity: this.supportsSecurity ?? "unknown", + protocol: getEnumMemberName(Protocols, this.protocol), + supportedProtocols: this.driver.controller.getProvisioningEntry( + this.id, + )?.supportedProtocols?.map((p) => getEnumMemberName(Protocols, p)), + protocolVersion: this.protocolVersion != undefined + ? getEnumMemberName(ProtocolVersion, this.protocolVersion) + : "unknown", + sdkVersion: this.sdkVersion ?? "unknown", + supportedDataRates: this.supportedDataRates + ? [...this.supportedDataRates] + : "unknown", + + ...endpointDump, + }; + + if (this.hardwareVersion != undefined) { + ret.fingerprint.hardwareVersion = this.hardwareVersion; + } + + for (const secClass of securityClassOrder) { + if ( + this.protocol === Protocols.ZWaveLongRange + && !securityClassIsLongRange(secClass) + ) { + continue; + } + ret.securityClasses[getEnumMemberName(SecurityClass, secClass)] = + this.hasSecurityClass(secClass) ?? "unknown"; + } + + const allValueIds = nodeUtils.getDefinedValueIDsInternal( + this.driver, + this, + true, + ); + + const collectValues = ( + endpointIndex: number, + getCollection: (ccId: CommandClasses) => ValueDump[] | undefined, + ) => { + for (const valueId of allValueIds) { + if ((valueId.endpoint ?? 0) !== endpointIndex) continue; + + const value = this._valueDB.getValue(valueId); + const metadata = this._valueDB.getMetadata(valueId); + const timestamp = this._valueDB.getTimestamp(valueId); + const timestampAsDate = timestamp + ? new Date(timestamp).toISOString() + : undefined; + + const ccInstance = CommandClass.createInstanceUnchecked( + this.driver, + this, + valueId.commandClass, + ); + const isInternalValue = ccInstance?.isInternalValue(valueId); + + const valueDump: ValueDump = { + ...pick(valueId, [ + "property", + "propertyKey", + ]), + metadata, + value: serializeCacheValue(value), + timestamp: timestampAsDate, + }; + if (isInternalValue) valueDump.internal = true; + + for (const [prop, value] of Object.entries(valueDump)) { + // @ts-expect-error + if (value === undefined) delete valueDump[prop]; + } + + getCollection(valueId.commandClass)?.push(valueDump); + } + }; + collectValues(0, (ccId) => ret.commandClasses[getCCName(ccId)]?.values); + + for (const endpoint of this.getAllEndpoints()) { + ret.endpoints ??= {}; + const endpointDump = endpoint.createEndpointDump(); + collectValues( + index, + (ccId) => endpointDump.commandClasses[getCCName(ccId)]?.values, + ); + ret.endpoints[endpoint.index] = endpointDump; + } + + if (this.deviceConfig) { + const relativePath = path.relative( + embeddedDevicesDir, + this.deviceConfig.filename, + ); + if (relativePath.startsWith("..")) { + // The path is outside our embedded config dir, take the full path + ret.configFileName = this.deviceConfig.filename; + } else { + ret.configFileName = relativePath; + } + + if (this.deviceConfig.compat) { + // TODO: Check if everything comes through this way. + ret.compatFlags = this.deviceConfig.compat; + } + } + for (const [prop, value] of Object.entries(ret)) { + // @ts-expect-error + if (value === undefined) delete ret[prop]; + } + + return ret; + } } diff --git a/packages/zwave-js/src/lib/node/utils.ts b/packages/zwave-js/src/lib/node/utils.ts index 5dbb3c0665cb..fc3c3eeaa6cf 100644 --- a/packages/zwave-js/src/lib/node/utils.ts +++ b/packages/zwave-js/src/lib/node/utils.ts @@ -298,6 +298,18 @@ export function filterRootApplicationCCValueIDs( export function getDefinedValueIDs( applHost: ZWaveApplicationHost, node: IZWaveNode, +): TranslatedValueID[] { + return getDefinedValueIDsInternal(applHost, node, false); +} + +/** + * @internal + * Returns a list of all value names that are defined on all endpoints of this node + */ +export function getDefinedValueIDsInternal( + applHost: ZWaveApplicationHost, + node: IZWaveNode, + includeInternal: boolean = false, ): TranslatedValueID[] { let ret: ValueID[] = []; const allowControlled: CommandClasses[] = [ @@ -316,7 +328,12 @@ export function getDefinedValueIDs( cc, ); if (ccInstance) { - ret.push(...ccInstance.getDefinedValueIDs(applHost)); + ret.push( + ...ccInstance.getDefinedValueIDs( + applHost, + includeInternal, + ), + ); } } }