UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

1,655 lines (1,525 loc) 143 kB
import { ECDHProfiles, inclusionTimeouts, KEXFailType, KEXSchemes, ManufacturerSpecificCCValues, Security2CCKEXFail, Security2CCKEXSet, Security2CCNetworkKeyGet, Security2CCNetworkKeyVerify, Security2CCPublicKeyReport, Security2CCTransferEnd, utils as ccUtils, VersionCCValues, type AssociationAddress, type AssociationGroup, } from "@zwave-js/cc"; import { authHomeIdFromDSK, CommandClasses, computePRK, decodeX25519KeyDER, deriveTempKeys, dskFromString, dskToString, encodeX25519KeyDERSPKI, Firmware, indexDBsByNode, isRecoverableZWaveError, isTransmissionError, isZWaveError, NodeType, NODE_ID_BROADCAST, nwiHomeIdFromDSK, ProtocolType, RFRegion, RSSI, SecurityClass, securityClassIsS2, securityClassOrder, ValueDB, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; import { migrateNVM } from "@zwave-js/nvmedit"; import type { Message, SuccessIndicator } from "@zwave-js/serial"; import { FunctionType } from "@zwave-js/serial"; import { createThrowingMap, flatMap, getEnumMemberName, getErrorMessage, Mixin, num2hex, padVersion, pick, ReadonlyObjectKeyMap, ReadonlyThrowingMap, ThrowingMap, TypedEventEmitter, } from "@zwave-js/shared"; import { distinct } from "alcalzone-shared/arrays"; import { wait } from "alcalzone-shared/async"; import { createDeferredPromise, DeferredPromise, } from "alcalzone-shared/deferred-promise"; import { isObject } from "alcalzone-shared/typeguards"; import crypto from "crypto"; import semver from "semver"; import util from "util"; import type { Driver } from "../driver/Driver"; import { cacheKeys, cacheKeyUtils } from "../driver/NetworkCache"; import type { StatisticsEventCallbacks } from "../driver/Statistics"; import { DeviceClass } from "../node/DeviceClass"; import { ZWaveNode } from "../node/Node"; import { VirtualNode } from "../node/VirtualNode"; import { InterviewStage, LifelineRoutes, NodeStatus } from "../node/_Types"; import { GetControllerCapabilitiesRequest, GetControllerCapabilitiesResponse, } from "../serialapi/capability/GetControllerCapabilitiesMessages"; import { GetControllerVersionRequest, GetControllerVersionResponse, } from "../serialapi/capability/GetControllerVersionMessages"; import { GetProtocolVersionRequest, GetProtocolVersionResponse, } from "../serialapi/capability/GetProtocolVersionMessages"; import { GetSerialApiCapabilitiesRequest, GetSerialApiCapabilitiesResponse, } from "../serialapi/capability/GetSerialApiCapabilitiesMessages"; import { GetSerialApiInitDataRequest, GetSerialApiInitDataResponse, } from "../serialapi/capability/GetSerialApiInitDataMessages"; import { HardResetRequest } from "../serialapi/capability/HardResetRequest"; import { SerialAPISetupCommand, SerialAPISetup_CommandUnsupportedResponse, SerialAPISetup_GetLRMaximumPayloadSizeRequest, SerialAPISetup_GetLRMaximumPayloadSizeResponse, SerialAPISetup_GetMaximumPayloadSizeRequest, SerialAPISetup_GetMaximumPayloadSizeResponse, SerialAPISetup_GetPowerlevelRequest, SerialAPISetup_GetPowerlevelResponse, SerialAPISetup_GetRFRegionRequest, SerialAPISetup_GetRFRegionResponse, SerialAPISetup_GetSupportedCommandsRequest, SerialAPISetup_GetSupportedCommandsResponse, SerialAPISetup_SetNodeIDTypeRequest, SerialAPISetup_SetNodeIDTypeResponse, SerialAPISetup_SetPowerlevelRequest, SerialAPISetup_SetPowerlevelResponse, SerialAPISetup_SetRFRegionRequest, SerialAPISetup_SetRFRegionResponse, SerialAPISetup_SetTXStatusReportRequest, SerialAPISetup_SetTXStatusReportResponse, } from "../serialapi/capability/SerialAPISetupMessages"; import { SetApplicationNodeInformationRequest } from "../serialapi/capability/SetApplicationNodeInformationRequest"; import { GetControllerIdRequest, GetControllerIdResponse, } from "../serialapi/memory/GetControllerIdMessages"; import { GetBackgroundRSSIRequest, GetBackgroundRSSIResponse, } from "../serialapi/misc/GetBackgroundRSSIMessages"; import { SetRFReceiveModeRequest, SetRFReceiveModeResponse, } from "../serialapi/misc/SetRFReceiveModeMessages"; import { SetSerialApiTimeoutsRequest, SetSerialApiTimeoutsResponse, } from "../serialapi/misc/SetSerialApiTimeoutsMessages"; import { AddNodeDSKToNetworkRequest, AddNodeStatus, AddNodeToNetworkRequest, AddNodeToNetworkRequestStatusReport, AddNodeType, computeNeighborDiscoveryTimeout, EnableSmartStartListenRequest, } from "../serialapi/network-mgmt/AddNodeToNetworkRequest"; import { AssignReturnRouteRequest } from "../serialapi/network-mgmt/AssignReturnRouteMessages"; import { AssignSUCReturnRouteRequest } from "../serialapi/network-mgmt/AssignSUCReturnRouteMessages"; import { DeleteReturnRouteRequest } from "../serialapi/network-mgmt/DeleteReturnRouteMessages"; import { DeleteSUCReturnRouteRequest } from "../serialapi/network-mgmt/DeleteSUCReturnRouteMessages"; import { GetRoutingInfoRequest, GetRoutingInfoResponse, } from "../serialapi/network-mgmt/GetRoutingInfoMessages"; import { GetSUCNodeIdRequest, GetSUCNodeIdResponse, } from "../serialapi/network-mgmt/GetSUCNodeIdMessages"; import { IsFailedNodeRequest, IsFailedNodeResponse, } from "../serialapi/network-mgmt/IsFailedNodeMessages"; import { RemoveFailedNodeRequest, RemoveFailedNodeResponse, RemoveFailedNodeStartFlags, RemoveFailedNodeStatus, type RemoveFailedNodeRequestStatusReport, } from "../serialapi/network-mgmt/RemoveFailedNodeMessages"; import { RemoveNodeFromNetworkRequest, RemoveNodeFromNetworkRequestStatusReport, RemoveNodeStatus, RemoveNodeType, } from "../serialapi/network-mgmt/RemoveNodeFromNetworkRequest"; import { ReplaceFailedNodeRequest, ReplaceFailedNodeRequestStatusReport, ReplaceFailedNodeResponse, ReplaceFailedNodeStartFlags, ReplaceFailedNodeStatus, } from "../serialapi/network-mgmt/ReplaceFailedNodeRequest"; import { NodeNeighborUpdateStatus, RequestNodeNeighborUpdateReport, RequestNodeNeighborUpdateRequest, } from "../serialapi/network-mgmt/RequestNodeNeighborUpdateMessages"; import { SetSUCNodeIdRequest } from "../serialapi/network-mgmt/SetSUCNodeIDMessages"; import { ExtNVMReadLongBufferRequest, ExtNVMReadLongBufferResponse, } from "../serialapi/nvm/ExtNVMReadLongBufferMessages"; import { ExtNVMReadLongByteRequest, ExtNVMReadLongByteResponse, } from "../serialapi/nvm/ExtNVMReadLongByteMessages"; import { ExtNVMWriteLongBufferRequest, ExtNVMWriteLongBufferResponse, } from "../serialapi/nvm/ExtNVMWriteLongBufferMessages"; import { ExtNVMWriteLongByteRequest, ExtNVMWriteLongByteResponse, } from "../serialapi/nvm/ExtNVMWriteLongByteMessages"; import { GetNVMIdRequest, GetNVMIdResponse, NVMId, nvmSizeToBufferSize, } from "../serialapi/nvm/GetNVMIdMessages"; import { NVMOperationsCloseRequest, NVMOperationsOpenRequest, NVMOperationsReadRequest, NVMOperationsResponse, NVMOperationStatus, NVMOperationsWriteRequest, } from "../serialapi/nvm/NVMOperationsMessages"; import { NodeIDType, ZWaveApiVersion, ZWaveLibraryTypes, } from "../serialapi/_Types"; import { ControllerStatistics, ControllerStatisticsHost, } from "./ControllerStatistics"; import { minFeatureVersions, ZWaveFeature } from "./Features"; import { downloadFirmwareUpdate, getAvailableFirmwareUpdates, } from "./FirmwareUpdateService"; import { ExclusionOptions, ExclusionStrategy, FoundNode, InclusionOptions, InclusionOptionsInternal, InclusionResult, InclusionState, InclusionStrategy, InclusionUserCallbacks, PlannedProvisioningEntry, ProvisioningEntryStatus, ReplaceNodeOptions, SmartStartProvisioningEntry, } from "./Inclusion"; import { determineNIF } from "./NodeInformationFrame"; import { assertProvisioningEntry } from "./utils"; import type { UnknownZWaveChipType } from "./ZWaveChipTypes"; import { protocolVersionToSDKVersion } from "./ZWaveSDKVersions"; import type { FirmwareUpdateFileInfo, FirmwareUpdateInfo, GetFirmwareUpdatesOptions, HealNodeStatus, SDKVersion, } from "./_Types"; // Strongly type the event emitter events interface ControllerEventCallbacks extends StatisticsEventCallbacks<ControllerStatistics> { "inclusion failed": () => void; "exclusion failed": () => void; "inclusion started": (secure: boolean, strategy: InclusionStrategy) => void; "exclusion started": () => void; "inclusion stopped": () => void; "exclusion stopped": () => void; "node found": (node: FoundNode) => void; "node added": (node: ZWaveNode, result: InclusionResult) => void; "node removed": (node: ZWaveNode, replaced: boolean) => void; "heal network progress": ( progress: ReadonlyMap<number, HealNodeStatus>, ) => void; "heal network done": (result: ReadonlyMap<number, HealNodeStatus>) => void; } export type ControllerEvents = Extract<keyof ControllerEventCallbacks, string>; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ZWaveController extends ControllerStatisticsHost {} @Mixin([ControllerStatisticsHost]) export class ZWaveController extends TypedEventEmitter<ControllerEventCallbacks> { /** @internal */ public constructor(private readonly driver: Driver) { super(); this._nodes = createThrowingMap((nodeId) => { throw new ZWaveError( `Node ${nodeId} was not found!`, ZWaveErrorCodes.Controller_NodeNotFound, ); }); // register message handlers driver.registerRequestHandler( FunctionType.AddNodeToNetwork, this.handleAddNodeStatusReport.bind(this), ); driver.registerRequestHandler( FunctionType.RemoveNodeFromNetwork, this.handleRemoveNodeStatusReport.bind(this), ); driver.registerRequestHandler( FunctionType.ReplaceFailedNode, this.handleReplaceNodeStatusReport.bind(this), ); } private _type: ZWaveLibraryTypes | undefined; public get type(): ZWaveLibraryTypes | undefined { return this._type; } private _sdkVersion: string | undefined; public get sdkVersion(): string | undefined { return this._sdkVersion; } private _zwaveApiVersion: ZWaveApiVersion | undefined; public get zwaveApiVersion(): ZWaveApiVersion | undefined { return this._zwaveApiVersion; } private _zwaveChipType: string | UnknownZWaveChipType | undefined; public get zwaveChipType(): string | UnknownZWaveChipType | undefined { return this._zwaveChipType; } private _homeId: number | undefined; /** A 32bit number identifying the current network */ public get homeId(): number | undefined { return this._homeId; } private _ownNodeId: number | undefined; /** The ID of the controller in the current network */ public get ownNodeId(): number | undefined { return this._ownNodeId; } private _isPrimary: boolean | undefined; public get isPrimary(): boolean | undefined { return this._isPrimary; } private _isUsingHomeIdFromOtherNetwork: boolean | undefined; public get isUsingHomeIdFromOtherNetwork(): boolean | undefined { return this._isUsingHomeIdFromOtherNetwork; } private _isSISPresent: boolean | undefined; public get isSISPresent(): boolean | undefined { return this._isSISPresent; } private _wasRealPrimary: boolean | undefined; public get wasRealPrimary(): boolean | undefined { return this._wasRealPrimary; } private _isSIS: boolean | undefined; public get isSIS(): boolean | undefined { return this._isSIS; } private _isSUC: boolean | undefined; public get isSUC(): boolean | undefined { return this._isSUC; } private _nodeType: NodeType | undefined; public get nodeType(): NodeType | undefined { return this._nodeType; } /** Checks if the SDK version is greater than the given one */ public sdkVersionGt(version: SDKVersion): boolean | undefined { if (this._sdkVersion === undefined) { return undefined; } const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion); return semver.gt(padVersion(sdkVersion), padVersion(version)); } /** Checks if the SDK version is greater than or equal to the given one */ public sdkVersionGte(version: SDKVersion): boolean | undefined { if (this._sdkVersion === undefined) { return undefined; } const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion); return semver.gte(padVersion(sdkVersion), padVersion(version)); } /** Checks if the SDK version is lower than the given one */ public sdkVersionLt(version: SDKVersion): boolean | undefined { if (this._sdkVersion === undefined) { return undefined; } const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion); return semver.lt(padVersion(sdkVersion), padVersion(version)); } /** Checks if the SDK version is lower than or equal to the given one */ public sdkVersionLte(version: SDKVersion): boolean | undefined { if (this._sdkVersion === undefined) { return undefined; } const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion); return semver.lte(padVersion(sdkVersion), padVersion(version)); } private _manufacturerId: number | undefined; public get manufacturerId(): number | undefined { return this._manufacturerId; } private _productType: number | undefined; public get productType(): number | undefined { return this._productType; } private _productId: number | undefined; public get productId(): number | undefined { return this._productId; } private _firmwareVersion: string | undefined; public get firmwareVersion(): string | undefined { return this._firmwareVersion; } private _supportedFunctionTypes: FunctionType[] | undefined; public get supportedFunctionTypes(): readonly FunctionType[] | undefined { return this._supportedFunctionTypes; } /** Checks if a given Z-Wave function type is supported by this controller */ public isFunctionSupported(functionType: FunctionType): boolean { if (this._supportedFunctionTypes == null) { throw new ZWaveError( "Cannot check yet if a function is supported by the controller. The interview process has not been completed.", ZWaveErrorCodes.Driver_NotReady, ); } return this._supportedFunctionTypes.indexOf(functionType) > -1; } private _supportedSerialAPISetupCommands: | SerialAPISetupCommand[] | undefined; public get supportedSerialAPISetupCommands(): | readonly SerialAPISetupCommand[] | undefined { return this._supportedSerialAPISetupCommands; } /** Checks if a given Serial API setup command is supported by this controller */ public isSerialAPISetupCommandSupported( command: SerialAPISetupCommand, ): boolean { if (!this._supportedSerialAPISetupCommands) { throw new ZWaveError( "Cannot check yet if a Serial API setup command is supported by the controller. The interview process has not been completed.", ZWaveErrorCodes.Driver_NotReady, ); } return this._supportedSerialAPISetupCommands.indexOf(command) > -1; } /** * Tests if the controller supports a certain feature. * Returns `undefined` if this information isn't known yet. */ public supportsFeature(feature: ZWaveFeature): boolean | undefined { switch (feature) { case ZWaveFeature.SmartStart: return this.sdkVersionGte(minFeatureVersions[feature]); } } /** Throws if the controller does not support a certain feature */ private assertFeature(feature: ZWaveFeature): void { if (!this.supportsFeature(feature)) { throw new ZWaveError( `The controller does not support the ${getEnumMemberName( ZWaveFeature, feature, )} feature`, ZWaveErrorCodes.Controller_NotSupported, ); } } private _sucNodeId: number | undefined; public get sucNodeId(): number | undefined { return this._sucNodeId; } private _supportsTimers: boolean | undefined; public get supportsTimers(): boolean | undefined { return this._supportsTimers; } /** Whether the controller is known to support soft reset */ public get supportsSoftReset(): boolean | undefined { return this.driver.cacheGet(cacheKeys.controller.supportsSoftReset); } /** @internal */ public set supportsSoftReset(value: boolean | undefined) { this.driver.cacheSet(cacheKeys.controller.supportsSoftReset, value); } private _nodes: ThrowingMap<number, ZWaveNode>; /** A dictionary of the nodes connected to this controller */ public get nodes(): ReadonlyThrowingMap<number, ZWaveNode> { return this._nodes; } /** Returns the node with the given DSK */ public getNodeByDSK(dsk: Buffer | string): ZWaveNode | undefined { if (typeof dsk === "string") dsk = dskFromString(dsk); for (const node of this._nodes.values()) { if (node.dsk?.equals(dsk)) return node; } } /** Returns the controller node's value DB */ public get valueDB(): ValueDB { return this._nodes.get(this._ownNodeId!)!.valueDB; } private _healNetworkActive: boolean = false; /** Returns whether the network or a node is currently being healed. */ public get isHealNetworkActive(): boolean { return this._healNetworkActive; } /** Returns a reference to the (virtual) broadcast node, which allows sending commands to all nodes */ public getBroadcastNode(): VirtualNode { return new VirtualNode( NODE_ID_BROADCAST, this.driver, this.nodes.values(), ); } /** Creates a virtual node that can be used to send multicast commands to several nodes */ public getMulticastGroup(nodeIDs: number[]): VirtualNode { const nodes = nodeIDs.map((id) => this._nodes.getOrThrow(id)); return new VirtualNode(undefined, this.driver, nodes); } /** @internal */ public get provisioningList(): readonly SmartStartProvisioningEntry[] { return ( this.driver.cacheGet(cacheKeys.controller.provisioningList) ?? [] ); } private set provisioningList( value: readonly SmartStartProvisioningEntry[], ) { this.driver.cacheSet(cacheKeys.controller.provisioningList, value); } /** Adds the given entry (DSK and security classes) to the controller's SmartStart provisioning list or replaces an existing entry */ public provisionSmartStartNode(entry: PlannedProvisioningEntry): void { // Make sure the controller supports SmartStart this.assertFeature(ZWaveFeature.SmartStart); // And that the entry contains valid data assertProvisioningEntry(entry); const provisioningList = [...this.provisioningList]; const index = provisioningList.findIndex((e) => e.dsk === entry.dsk); if (index === -1) { provisioningList.push(entry); } else { provisioningList[index] = entry; } this.provisioningList = provisioningList; this.autoProvisionSmartStart(); } /** * Removes the given DSK or node ID from the controller's SmartStart provisioning list. * * **Note:** If this entry corresponds to an included node, it will **NOT** be excluded */ public unprovisionSmartStartNode(dskOrNodeId: string | number): void { const provisioningList = [...this.provisioningList]; const entry = this.getProvisioningEntryInternal(dskOrNodeId); if (!entry) return; const index = provisioningList.indexOf(entry); if (index >= 0) { provisioningList.splice(index, 1); this.autoProvisionSmartStart(); this.provisioningList = provisioningList; } } private getProvisioningEntryInternal( dskOrNodeId: string | number, ): SmartStartProvisioningEntry | undefined { if (typeof dskOrNodeId === "string") { return this.provisioningList.find((e) => e.dsk === dskOrNodeId); } else { // The provisioning list may or may not contain the node ID for an entry, even if the node is already included. let ret = this.provisioningList.find( (e) => "nodeId" in e && e.nodeId === dskOrNodeId, ); if (!ret) { // Try to get the DSK from the node instance const dsk = this.nodes.get(dskOrNodeId)?.dsk; if (dsk) { ret = this.provisioningList.find( (e) => e.dsk === dskToString(dsk), ); } } return ret; } } /** * Returns the entry for the given DSK or node ID from the controller's SmartStart provisioning list. */ public getProvisioningEntry( dskOrNodeId: string | number, ): Readonly<SmartStartProvisioningEntry> | undefined { const entry = this.getProvisioningEntryInternal(dskOrNodeId); // Try to look up the node ID for this entry if (entry) { const ret: SmartStartProvisioningEntry = { ...entry, }; const node = typeof dskOrNodeId === "string" ? this.getNodeByDSK(dskOrNodeId) : this.nodes.get(dskOrNodeId); if (node) ret.nodeId = node.id; return ret; } } /** * Returns all entries from the controller's SmartStart provisioning list. */ public getProvisioningEntries(): SmartStartProvisioningEntry[] { // Determine which DSKs belong to which node IDs const dskNodeMap = new Map<string, number>(); for (const node of this.nodes.values()) { if (node.dsk) dskNodeMap.set(dskToString(node.dsk), node.id); } // Make copies so no one can modify the internal list (except for user info) return this.provisioningList.map((e) => { const { dsk, securityClasses, nodeId, ...rest } = e; return { dsk, securityClasses: [...securityClasses], ...(dskNodeMap.has(dsk) ? { nodeId: dskNodeMap.get(dsk)! } : {}), ...rest, }; }); } /** Returns whether the SmartStart provisioning list contains active entries that have not been included yet */ public hasPlannedProvisioningEntries(): boolean { return this.provisioningList.some( (e) => (e.status == undefined || e.status === ProvisioningEntryStatus.Active) && !this.getNodeByDSK(e.dsk), ); } /** * @internal * Automatically starts smart start inclusion if there are nodes pending inclusion. */ public autoProvisionSmartStart(): void { // Make sure the controller supports SmartStart if (!this.supportsFeature(ZWaveFeature.SmartStart)) return; if (this.hasPlannedProvisioningEntries()) { // SmartStart should be enabled // eslint-disable-next-line @typescript-eslint/no-empty-function void this.enableSmartStart().catch(() => {}); } else { // SmartStart should be disabled // eslint-disable-next-line @typescript-eslint/no-empty-function void this.disableSmartStart().catch(() => {}); } } /** * @internal * Queries the controller IDs and its Serial API capabilities */ public async identify(): Promise<void> { // get the home and node id of the controller this.driver.controllerLog.print(`querying controller IDs...`); const ids = await this.driver.sendMessage<GetControllerIdResponse>( new GetControllerIdRequest(this.driver), { supportCheck: false }, ); this._homeId = ids.homeId; this._ownNodeId = ids.ownNodeId; this.driver.controllerLog.print( `received controller IDs: home ID: ${num2hex(this._homeId)} own node ID: ${this._ownNodeId}`, ); // Figure out what the serial API can do this.driver.controllerLog.print(`querying API capabilities...`); const apiCaps = await this.driver.sendMessage<GetSerialApiCapabilitiesResponse>( new GetSerialApiCapabilitiesRequest(this.driver), { supportCheck: false, }, ); this._firmwareVersion = apiCaps.firmwareVersion; this._manufacturerId = apiCaps.manufacturerId; this._productType = apiCaps.productType; this._productId = apiCaps.productId; this._supportedFunctionTypes = apiCaps.supportedFunctionTypes; this.driver.controllerLog.print( `received API capabilities: firmware version: ${this._firmwareVersion} manufacturer ID: ${num2hex(this._manufacturerId)} product type: ${num2hex(this._productType)} product ID: ${num2hex(this._productId)} supported functions: ${this._supportedFunctionTypes .map((fn) => `\n · ${FunctionType[fn]} (${num2hex(fn)})`) .join("")}`, ); } /** * @internal * Interviews the controller for the necessary information. * @param restoreFromCache Asynchronous callback for the driver to restore the network from cache after nodes are created */ public async interview( restoreFromCache: () => Promise<void>, ): Promise<void> { // get basic controller version info this.driver.controllerLog.print(`querying version info...`); const version = await this.driver.sendMessage<GetControllerVersionResponse>( new GetControllerVersionRequest(this.driver), { supportCheck: false, }, ); this._sdkVersion = version.libraryVersion; this._type = version.controllerType; this.driver.controllerLog.print( `received version info: controller type: ${getEnumMemberName(ZWaveLibraryTypes, this._type)} library version: ${this._sdkVersion}`, ); // If supported, get more fine-grained version info if (this.isFunctionSupported(FunctionType.GetProtocolVersion)) { this.driver.controllerLog.print( `querying protocol version info...`, ); const protocol = await this.driver.sendMessage<GetProtocolVersionResponse>( new GetProtocolVersionRequest(this.driver), ); // Overwrite the SDK version with the more fine grained protocol version. We can assume this to be // valid for 7.x firmwares, where SDK and protocol version are the same. this._sdkVersion = protocol.protocolVersion; let message = `received protocol version info: protocol type: ${getEnumMemberName( ProtocolType, protocol.protocolType, )} protocol version: ${protocol.protocolVersion}`; if (protocol.applicationFrameworkBuildNumber) { message += ` appl. framework build no.: ${protocol.applicationFrameworkBuildNumber}`; } if (protocol.gitCommitHash) { message += ` git commit hash: ${protocol.gitCommitHash}`; } this.driver.controllerLog.print(message); } this.driver.controllerLog.print( `supported Z-Wave features: ${Object.keys(ZWaveFeature) .filter((k) => /^\d+$/.test(k)) .map((k) => parseInt(k) as ZWaveFeature) .filter((feat) => this.supportsFeature(feat)) .map((feat) => `\n · ${getEnumMemberName(ZWaveFeature, feat)}`) .join("")}`, ); // find out what the controller can do this.driver.controllerLog.print(`querying controller capabilities...`); const ctrlCaps = await this.driver.sendMessage<GetControllerCapabilitiesResponse>( new GetControllerCapabilitiesRequest(this.driver), { supportCheck: false, }, ); this._isPrimary = !ctrlCaps.isSecondary; this._isUsingHomeIdFromOtherNetwork = ctrlCaps.isUsingHomeIdFromOtherNetwork; this._isSISPresent = ctrlCaps.isSISPresent; this._wasRealPrimary = ctrlCaps.wasRealPrimary; this._isSUC = ctrlCaps.isStaticUpdateController; this.driver.controllerLog.print( `received controller capabilities: controller role: ${this._isPrimary ? "primary" : "secondary"} is the SUC: ${this._isSUC} started this network: ${!this._isUsingHomeIdFromOtherNetwork} SIS is present: ${this._isSISPresent} was real primary: ${this._wasRealPrimary}`, ); // Figure out which sub commands of SerialAPISetup are supported if (this.isFunctionSupported(FunctionType.SerialAPISetup)) { this.driver.controllerLog.print( `querying serial API setup capabilities...`, ); const setupCaps = await this.driver.sendMessage<SerialAPISetup_GetSupportedCommandsResponse>( new SerialAPISetup_GetSupportedCommandsRequest(this.driver), ); this._supportedSerialAPISetupCommands = setupCaps.supportedCommands; this.driver.controllerLog.print( `supported serial API setup commands:${this._supportedSerialAPISetupCommands .map( (cmd) => `\n· ${getEnumMemberName( SerialAPISetupCommand, cmd, )}`, ) .join("")}`, ); } else { this._supportedSerialAPISetupCommands = []; } // Enable TX status report if supported if ( this.isSerialAPISetupCommandSupported( SerialAPISetupCommand.SetTxStatusReport, ) ) { this.driver.controllerLog.print(`Enabling TX status report...`); const resp = await this.driver.sendMessage<SerialAPISetup_SetTXStatusReportResponse>( new SerialAPISetup_SetTXStatusReportRequest(this.driver, { enabled: true, }), ); this.driver.controllerLog.print( `Enabling TX status report ${ resp.success ? "successful" : "failed" }...`, ); } // find the SUC this.driver.controllerLog.print(`finding SUC...`); const suc = await this.driver.sendMessage<GetSUCNodeIdResponse>( new GetSUCNodeIdRequest(this.driver), { supportCheck: false }, ); this._sucNodeId = suc.sucNodeId; if (this._sucNodeId === 0) { this.driver.controllerLog.print(`No SUC present in the network`); } else if (this._sucNodeId === this._ownNodeId) { this.driver.controllerLog.print(`This is the SUC`); } else { this.driver.controllerLog.print( `SUC has node ID ${this.sucNodeId}`, ); } // There needs to be a SUC/SIS in the network. If not, we promote ourselves to one if the following conditions are met: // We are the primary controller, but we are not SUC, there is no SUC and there is no SIS if ( this._isPrimary && this._sucNodeId === 0 && !this._isSUC && !this._isSISPresent ) { this.driver.controllerLog.print( `There is no SUC/SIS in the network - promoting ourselves...`, ); try { const result = await this.configureSUC( this._ownNodeId!, true, true, ); if (result) { this._sucNodeId = this._ownNodeId; } this.driver.controllerLog.print( `Promotion to SUC/SIS ${result ? "succeeded" : "failed"}.`, result ? undefined : "warn", ); } catch (e) { this.driver.controllerLog.print( `Error while promoting to SUC/SIS: ${getErrorMessage(e)}`, "error", ); } } // if it's a bridge controller, request the virtual nodes if ( this.type === ZWaveLibraryTypes["Bridge Controller"] && this.isFunctionSupported(FunctionType.FUNC_ID_ZW_GET_VIRTUAL_NODES) ) { // TODO: send FUNC_ID_ZW_GET_VIRTUAL_NODES message } // Request additional information about the controller/Z-Wave chip this.driver.controllerLog.print( `querying additional controller information...`, ); const initData = await this.driver.sendMessage<GetSerialApiInitDataResponse>( new GetSerialApiInitDataRequest(this.driver), ); // and remember the new info this._zwaveApiVersion = initData.zwaveApiVersion; this._zwaveChipType = initData.zwaveChipType; this._isPrimary = initData.isPrimary; this._isSIS = initData.isSIS; this._nodeType = initData.nodeType; this._supportsTimers = initData.supportsTimers; // ignore the initVersion, no clue what to do with it this.driver.controllerLog.print( `received additional controller information: Z-Wave API version: ${this._zwaveApiVersion.version} (${ this._zwaveApiVersion.kind })${ this._zwaveChipType ? ` Z-Wave chip type: ${ typeof this._zwaveChipType === "string" ? this._zwaveChipType : `unknown (type: ${num2hex( this._zwaveChipType.type, )}, version: ${num2hex(this._zwaveChipType.version)})` }` : "" } node type ${getEnumMemberName(NodeType, this._nodeType)} controller role: ${this._isPrimary ? "primary" : "secondary"} controller is the SIS: ${this._isSIS} controller supports timers: ${this._supportsTimers} nodes in the network: ${initData.nodeIds.join(", ")}`, ); // Index the value DB for optimal performance const valueDBIndexes = indexDBsByNode([ this.driver.valueDB!, this.driver.metadataDB!, ]); // create an empty entry in the nodes map so we can initialize them afterwards for (const nodeId of initData.nodeIds) { this._nodes.set( nodeId, new ZWaveNode( nodeId, this.driver, undefined, undefined, undefined, // Use the previously created index to avoid doing extra work when creating the value DB this.createValueDBForNode( nodeId, valueDBIndexes.get(nodeId), ), ), ); } // Now try to deserialize all nodes from the cache await restoreFromCache(); // Set manufacturer information for the controller node const controllerValueDB = this.valueDB; controllerValueDB.setMetadata( ManufacturerSpecificCCValues.manufacturerId.id, ManufacturerSpecificCCValues.manufacturerId.meta, ); controllerValueDB.setMetadata( ManufacturerSpecificCCValues.productType.id, ManufacturerSpecificCCValues.productType.meta, ); controllerValueDB.setMetadata( ManufacturerSpecificCCValues.productId.id, ManufacturerSpecificCCValues.productId.meta, ); controllerValueDB.setValue( ManufacturerSpecificCCValues.manufacturerId.id, this._manufacturerId, ); controllerValueDB.setValue( ManufacturerSpecificCCValues.productType.id, this._productType, ); controllerValueDB.setValue( ManufacturerSpecificCCValues.productId.id, this._productId, ); // Set firmware version information for the controller node controllerValueDB.setMetadata( VersionCCValues.firmwareVersions.id, VersionCCValues.firmwareVersions.meta, ); controllerValueDB.setValue(VersionCCValues.firmwareVersions.id, [ this._firmwareVersion, ]); controllerValueDB.setMetadata( VersionCCValues.sdkVersion.id, VersionCCValues.sdkVersion.meta, ); controllerValueDB.setValue( VersionCCValues.sdkVersion.id, this._sdkVersion, ); if ( this.type !== ZWaveLibraryTypes["Bridge Controller"] && this.isFunctionSupported(FunctionType.SetSerialApiTimeouts) ) { const { ack, byte } = this.driver.options.timeouts; this.driver.controllerLog.print( `setting serial API timeouts: ack = ${ack} ms, byte = ${byte} ms`, ); const resp = await this.driver.sendMessage<SetSerialApiTimeoutsResponse>( new SetSerialApiTimeoutsRequest(this.driver, { ackTimeout: ack, byteTimeout: byte, }), ); this.driver.controllerLog.print( `serial API timeouts overwritten. The old values were: ack = ${resp.oldAckTimeout} ms, byte = ${resp.oldByteTimeout} ms`, ); } this.driver.controllerLog.print("Interview completed"); } private createValueDBForNode(nodeId: number, ownKeys?: Set<string>) { return new ValueDB( nodeId, this.driver.valueDB!, this.driver.metadataDB!, ownKeys, ); } /** * Sets the NIF of the controller to the Gateway device type and to include the CCs supported by Z-Wave JS. * Warning: This only works when followed up by a hard-reset, so don't call this directly * @internal */ public async setControllerNIF(): Promise<void> { this.driver.controllerLog.print("Updating the controller NIF..."); await this.driver.sendMessage( new SetApplicationNodeInformationRequest(this.driver, { isListening: true, ...determineNIF(), }), ); } /** * Performs a hard reset on the controller. This wipes out all configuration! * Warning: The driver needs to re-interview the controller, so don't call this directly * @internal */ public async hardReset(): Promise<void> { // begin the reset process try { this.driver.controllerLog.print("performing hard reset..."); await this.driver.sendMessage(new HardResetRequest(this.driver), { supportCheck: false, }); this.driver.controllerLog.print(`hard reset succeeded`); // Clean up this._nodes.forEach((node) => node.removeAllListeners()); this._nodes.clear(); } catch (e) { this.driver.controllerLog.print( `hard reset failed: ${getErrorMessage(e)}`, "error", ); throw e; } } private _inclusionState: InclusionState = InclusionState.Idle; public get inclusionState(): InclusionState { return this._inclusionState; } /** @internal */ public setInclusionState(state: InclusionState): void { if (this._inclusionState === state) return; this._inclusionState = state; if ( state === InclusionState.Idle && this._smartStartEnabled && this.supportsFeature(ZWaveFeature.SmartStart) ) { // If Smart Start was enabled before the inclusion/exclusion, // enable it again and ignore errors // eslint-disable-next-line @typescript-eslint/no-empty-function this.enableSmartStart().catch(() => {}); } } private _smartStartEnabled: boolean = false; private _includeController: boolean = false; private _exclusionOptions: ExclusionOptions | undefined; private _inclusionOptions: InclusionOptionsInternal | undefined; private _nodePendingInclusion: ZWaveNode | undefined; private _nodePendingExclusion: ZWaveNode | undefined; private _nodePendingReplace: ZWaveNode | undefined; private _replaceFailedPromise: DeferredPromise<boolean> | undefined; /** * Starts the inclusion process of new nodes. * Resolves to true when the process was started, and false if the inclusion was already active. * * @param options Defines the inclusion strategy to use. */ public async beginInclusion( options: InclusionOptions = { strategy: InclusionStrategy.Insecure, }, ): Promise<boolean> { if ( this._inclusionState === InclusionState.Including || this._inclusionState === InclusionState.Excluding || this._inclusionState === InclusionState.Busy ) { return false; } // Protect against invalid inclusion options if ( !(options.strategy in InclusionStrategy) || // @ts-expect-error We're checking for user errors options.strategy === InclusionStrategy.SmartStart ) { throw new ZWaveError( `Invalid inclusion strategy: ${options.strategy}`, ZWaveErrorCodes.Argument_Invalid, ); } // Leave SmartStart listening mode so we can switch to exclusion mode await this.pauseSmartStart(); this.setInclusionState(InclusionState.Including); this._inclusionOptions = options; try { this.driver.controllerLog.print( `Starting inclusion process with strategy ${getEnumMemberName( InclusionStrategy, options.strategy, )}...`, ); // kick off the inclusion process await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Any, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print( `The controller is now ready to add nodes`, ); this.emit( "inclusion started", // TODO: Remove first parameter in next major version options.strategy !== InclusionStrategy.Insecure, options.strategy, ); } catch (e) { this.setInclusionState(InclusionState.Idle); if ( isZWaveError(e) && e.code === ZWaveErrorCodes.Controller_CallbackNOK ) { this.driver.controllerLog.print( `Starting the inclusion failed`, "error", ); throw new ZWaveError( "The inclusion could not be started.", ZWaveErrorCodes.Controller_InclusionFailed, ); } throw e; } return true; } /** @internal */ public async beginInclusionSmartStart( provisioningEntry: PlannedProvisioningEntry, ): Promise<boolean> { if ( this._inclusionState === InclusionState.Including || this._inclusionState === InclusionState.Excluding || this._inclusionState === InclusionState.Busy ) { return false; } // Disable listening mode so we can switch to inclusion mode await this.stopInclusion(); this.setInclusionState(InclusionState.Including); this._inclusionOptions = { strategy: InclusionStrategy.SmartStart, provisioning: provisioningEntry, }; try { this.driver.controllerLog.print( `Including SmartStart node with DSK ${provisioningEntry.dsk}`, ); // kick off the inclusion process const dskBuffer = dskFromString(provisioningEntry.dsk); await this.driver.sendMessage( new AddNodeDSKToNetworkRequest(this.driver, { nwiHomeId: nwiHomeIdFromDSK(dskBuffer), authHomeId: authHomeIdFromDSK(dskBuffer), highPower: true, networkWide: true, }), ); this.emit( "inclusion started", // TODO: Remove first parameter in next major version true, InclusionStrategy.SmartStart, ); } catch (e) { this.setInclusionState(InclusionState.Idle); // Error handling for this happens at the call site throw e; } return true; } /** * Is used internally to stop an active inclusion process without waiting for a confirmation * @internal */ public async stopInclusionNoCallback(): Promise<void> { await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print(`The inclusion process was stopped`); this.emit("inclusion stopped"); } /** * Finishes an inclusion process. This must only be called after the ProtocolDone status is received. * Returns the ID of the newly added node. */ private async finishInclusion(): Promise<number> { this.driver.controllerLog.print(`finishing inclusion process...`); const response = await this.driver.sendMessage<AddNodeToNetworkRequestStatusReport>( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Stop, highPower: true, networkWide: true, }), ); if (response.status === AddNodeStatus.Done) { return response.statusContext!.nodeId; } this.driver.controllerLog.print( `Finishing the inclusion failed`, "error", ); throw new ZWaveError( "Finishing the inclusion failed", ZWaveErrorCodes.Controller_InclusionFailed, ); } /** * Stops an active inclusion process. Resolves to true when the controller leaves inclusion mode, * and false if the inclusion was not active. */ public async stopInclusion(): Promise<boolean> { if (this._inclusionState !== InclusionState.Including) { return false; } this.driver.controllerLog.print(`stopping inclusion process...`); try { // stop the inclusion process await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Stop, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print( `The inclusion process was stopped`, ); this.emit("inclusion stopped"); this.setInclusionState(InclusionState.Idle); return true; } catch (e) { if ( isZWaveError(e) && e.code === ZWaveErrorCodes.Controller_CallbackNOK ) { this.driver.controllerLog.print( `Stopping the inclusion failed`, "error", ); throw new ZWaveError( "The inclusion could not be stopped.", ZWaveErrorCodes.Controller_InclusionFailed, ); } throw e; } } /** * Puts the controller into listening mode for Smart Start inclusion. * Whenever a node on the provisioning list announces itself, it will automatically be added. * * Resolves to `true` when the listening mode is started or was active, and `false` if it is scheduled for later activation. */ private async enableSmartStart(): Promise<boolean> { if (!this.supportsFeature(ZWaveFeature.SmartStart)) { this.driver.controllerLog.print( `Smart Start is not supported by this controller, NOT enabling listening mode...`, "warn", ); } this._smartStartEnabled = true; if (this._inclusionState === InclusionState.Idle) { this.setInclusionState(InclusionState.SmartStart); this.driver.controllerLog.print( `Enabling Smart Start listening mode...`, ); try { await this.driver.sendMessage( new EnableSmartStartListenRequest(this.driver, {}), ); this.driver.controllerLog.print( `Smart Start listening mode enabled`, ); return true; } catch (e) { this.setInclusionState(InclusionState.Idle); this.driver.controllerLog.print( `Smart Start listening mode could not be enabled: ${getErrorMessage( e, )}`, "error", ); throw e; } } else if (this._inclusionState === InclusionState.SmartStart) { return true; } else { this.driver.controllerLog.print( `Smart Start listening mode scheduled for later activation...`, ); return false; } } /** * Disables the listening mode for Smart Start inclusion. * * Resolves to `true` when the listening mode is stopped, and `false` if was not active. */ private async disableSmartStart(): Promise<boolean> { if (!this.supportsFeature(ZWaveFeature.SmartStart)) return true; this._smartStartEnabled = false; if (this._inclusionState === InclusionState.SmartStart) { this.setInclusionState(InclusionState.Idle); this.driver.controllerLog.print( `disabling Smart Start listening mode...`, ); try { await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print( `Smart Start listening mode disabled`, ); return true; } catch (e) { this.setInclusionState(InclusionState.SmartStart); this.driver.controllerLog.print( `Smart Start listening mode could not be disabled: ${getErrorMessage( e, )}`, "error", ); throw e; } } else if (this._inclusionState === InclusionState.Idle) { return true; } else { this.driver.controllerLog.print( `Smart Start listening mode disabled`, ); return true; } } private async pauseSmartStart(): Promise<boolean> { if (!this.supportsFeature(ZWaveFeature.SmartStart)) return true; if (this._inclusionState === InclusionState.SmartStart) { this.driver.controllerLog.print( `Leaving Smart Start listening mode...`, ); try { await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print( `Left Smart Start listening mode`, ); return true; } catch (e) { this.driver.controllerLog.print( `Smart Start listening mode could not be left: ${getErrorMessage( e, )}`, "error", ); throw e; } } else { return true; } } /** * Starts the exclusion process of new nodes. * Resolves to true when the process was started, and false if an inclusion or exclusion process was already active. * * @param options Influences the exclusion process and what happens with the Smart Start provisioning list. */ public async beginExclusion(options?: ExclusionOptions): Promise<boolean>; /** * Starts the exclusion process of new nodes. * Resolves to true when the process was started, and false if an inclusion or exclusion process was already active. * * @param unprovision Whether the removed node should also be removed from the Smart Start provisioning list. * A value of `"inactive"` will keep the provisioning entry, but disable it. * * @deprecated Use the overload with {@link ExclusionOptions} instead. */ public async beginExclusion( unprovision: boolean | "inactive", ): Promise<boolean>; public async beginExclusion( options: ExclusionOptions | boolean | "inactive" = { strategy: ExclusionStrategy.DisableProvisioningEntry, }, ): Promise<boolean> { if ( this._inclusionState === InclusionState.Including || this._inclusionState === InclusionState.Excluding || this._inclusionState === InclusionState.Busy ) { return false; } if (typeof options === "boolean") { options = { strategy: options ? ExclusionStrategy.Unprovision : ExclusionStrategy.ExcludeOnly, }; } else if (options === "inactive") { options = { strategy: ExclusionStrategy.DisableProvisioningEntry, }; } // Leave SmartStart listening mode so we can switch to exclusion mode await this.pauseSmartStart(); this.setInclusionState(InclusionState.Excluding); this.driver.controllerLog.print(`starting exclusion process...`); try { // kick off the inclusion process await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { removeNodeType: RemoveNodeType.Any, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print( `The controller is now ready to remove nodes`, ); this._exclusionOptions = options; this.emit("exclusion started"); return true; } catch (e) { this.setInclusionState(InclusionState.Idle); if ( isZWaveError(e) && e.code === ZWaveErrorCodes.Controller_CallbackNOK ) { this.driver.controllerLog.print( `Starting the exclusion failed`, "error", ); throw new ZWaveError( "The exclusion could not be started.", ZWaveErrorCodes.Controller_ExclusionFailed, ); } throw e; } } /** * Is used internally to stop an active exclusion process without waiting for confirmation * @internal */ public async stopExclusionNoCallback(): Promise<void> { await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { callbackId: 0, // disable callbacks removeNodeType: RemoveNodeType.Stop, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print(`the exclusion process was stopped`); this.emit("exclusion stopped"); } /** * Stops an active exclusion process. Resolves to true when the controller leaves exclusion mode, * and false if the inclusion was not active. */ public async stopExclusion(): Promise<boolean> { if (this._inclusionState !== InclusionState.Excluding) { return false; } this.driver.controllerLog.print(`stopping exclusion process...`); try { // kick off the inclusion process await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { removeNodeType: RemoveNodeType.Stop, highPower: true, networkWide: true, }), ); this.driver.controllerLog.print( `the exclusion process was stopped`, ); this.emit("exclusion stopped"); this.setInclusionState(InclusionState.Idle); return true; } catch (e) { if ( isZWaveError(e) && e.code === ZWaveErrorCodes.Controller_CallbackNOK ) { this.driver.controllerLog.print( `Stopping the exclusion failed`, "error", ); throw new ZWaveError( "The exclusion could not be stopped.", ZWaveErrorCodes.Controller_ExclusionFailed,