import { actuatorCCs , authHomeIdFromDSK , CommandClasses , computePRK , decodeX25519KeyDER , deriveTempKeys , dskFromString , dskToString , encodeX25519KeyDERSPKI , indexDBsByNode , isRecoverableZWaveError , isTransmissionError , isValidDSK , isZWaveError , NODE_ID_BROADCAST , nwiHomeIdFromDSK , SecurityClass , securityClassIsS2 , securityClassOrder , ValueDB , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { migrateNVM } from "@zwave-js/nvmedit" ; import { flatMap , getEnumMemberName , getErrorMessage , JSONObject , Mixin , num2hex , ObjectKeyMap , padVersion , pick , ReadonlyObjectKeyMap , 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 { composeObject } from "alcalzone-shared/objects" ; import { isArray , isObject } from "alcalzone-shared/typeguards" ; import crypto from "crypto" ; import semver from "semver" ; import util from "util" ; import { Security2CCKEXFail , Security2CCKEXSet , Security2CCNetworkKeyGet , Security2CCNetworkKeyVerify , Security2CCPublicKeyReport , Security2CCTransferEnd , } from "../commandclass" ; import type { AssociationCC } from "../commandclass/AssociationCC" ; import type { AssociationGroup , AssociationGroupInfoCC , } from "../commandclass/AssociationGroupInfoCC" ; import { getManufacturerIdValueId , getManufacturerIdValueMetadata , getProductIdValueId , getProductIdValueMetadata , getProductTypeValueId , getProductTypeValueMetadata , } from "../commandclass/ManufacturerSpecificCC" ; import type { AssociationAddress , EndpointAddress , MultiChannelAssociationCC , } from "../commandclass/MultiChannelAssociationCC" ; import { ECDHProfiles , inclusionTimeouts , KEXFailType , KEXSchemes , } from "../commandclass/Security2/shared" ; import { getFirmwareVersionsMetadata , getFirmwareVersionsValueId , } from "../commandclass/VersionCC" ; import type { Driver , RequestHandler } from "../driver/Driver" ; import type { StatisticsEventCallbacks } from "../driver/Statistics" ; import { FunctionType } from "../message/Constants" ; import type { Message } from "../message/Message" ; import type { SuccessIndicator } from "../message/SuccessIndicator" ; import { DeviceClass } from "../node/DeviceClass" ; import { ZWaveNode } from "../node/Node" ; import { InterviewStage , NodeStatus } from "../node/Types" ; import { VirtualNode } from "../node/VirtualNode" ; import { GetBackgroundRSSIRequest , GetBackgroundRSSIResponse , } from "../serialapi/misc/GetBackgroundRSSIMessages" ; import { NodeIDType , RFRegion , 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/misc/SerialAPISetupMessages" ; import { SetRFReceiveModeRequest , SetRFReceiveModeResponse , } from "../serialapi/misc/SetRFReceiveModeMessages" ; 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 { AddNodeDSKToNetworkRequest , AddNodeStatus , AddNodeToNetworkRequest , AddNodeToNetworkRequestStatusReport , AddNodeType , EnableSmartStartListenRequest , } from "./AddNodeToNetworkRequest" ; import { AssignReturnRouteRequest } from "./AssignReturnRouteMessages" ; import { AssignSUCReturnRouteRequest } from "./AssignSUCReturnRouteMessages" ; import { ControllerStatistics , ControllerStatisticsHost , } from "./ControllerStatistics" ; import { DeleteReturnRouteRequest } from "./DeleteReturnRouteMessages" ; import { DeleteSUCReturnRouteRequest } from "./DeleteSUCReturnRouteMessages" ; import { minFeatureVersions , ZWaveFeature } from "./Features" ; import { GetControllerCapabilitiesRequest , GetControllerCapabilitiesResponse , } from "./GetControllerCapabilitiesMessages" ; import { GetControllerIdRequest , GetControllerIdResponse , } from "./GetControllerIdMessages" ; import { GetControllerVersionRequest , GetControllerVersionResponse , } from "./GetControllerVersionMessages" ; import { GetRoutingInfoRequest , GetRoutingInfoResponse , } from "./GetRoutingInfoMessages" ; import { GetSerialApiCapabilitiesRequest , GetSerialApiCapabilitiesResponse , } from "./GetSerialApiCapabilitiesMessages" ; import { GetSerialApiInitDataRequest , GetSerialApiInitDataResponse , } from "./GetSerialApiInitDataMessages" ; import { GetSUCNodeIdRequest , GetSUCNodeIdResponse , } from "./GetSUCNodeIdMessages" ; import { HardResetRequest } from "./HardResetRequest" ; import { IncludedProvisioningEntry , InclusionOptions , InclusionOptionsInternal , InclusionResult , InclusionState , InclusionStrategy , InclusionUserCallbacks , PlannedProvisioningEntry , ReplaceNodeOptions , SmartStartProvisioningEntry , } from "./Inclusion" ; import { IsFailedNodeRequest , IsFailedNodeResponse , } from "./IsFailedNodeMessages" ; import { RemoveFailedNodeRequest , RemoveFailedNodeRequestStatusReport , RemoveFailedNodeResponse , RemoveFailedNodeStartFlags , RemoveFailedNodeStatus , } from "./RemoveFailedNodeMessages" ; import { RemoveNodeFromNetworkRequest , RemoveNodeFromNetworkRequestStatusReport , RemoveNodeStatus , RemoveNodeType , } from "./RemoveNodeFromNetworkRequest" ; import { ReplaceFailedNodeRequest , ReplaceFailedNodeRequestStatusReport , ReplaceFailedNodeResponse , ReplaceFailedNodeStartFlags , ReplaceFailedNodeStatus , } from "./ReplaceFailedNodeRequest" ; import { NodeNeighborUpdateStatus , RequestNodeNeighborUpdateReport , RequestNodeNeighborUpdateRequest , } from "./RequestNodeNeighborUpdateMessages" ; import type { RSSI } from "./SendDataShared" ; import { SetSerialApiTimeoutsRequest , SetSerialApiTimeoutsResponse , } from "./SetSerialApiTimeoutsMessages" ; import { SetSUCNodeIdRequest } from "./SetSUCNodeIDMessages" ; import { ZWaveLibraryTypes } from "./ZWaveLibraryTypes" ; import { protocolVersionToSDKVersion } from "./ZWaveSDKVersions" ; export type HealNodeStatus = "pending" | "done" | "failed" | "skipped" ; export type SerialAPIVersion = `${ number }.${ number }` | `${ number }.${ number }.${ number }` ; export type ThrowingMap < K , V > = Map < K , V > & { getOrThrow ( key ) : V ; } ; export type ReadonlyThrowingMap < K , V > = ReadonlyMap < K , V > & { getOrThrow ( key ) : V ; } ; 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 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 > ; export interface ZWaveController extends ControllerStatisticsHost { } @ Mixin ( [ ControllerStatisticsHost ] ) export class ZWaveController extends TypedEventEmitter < ControllerEventCallbacks > { internal public constructor ( private readonly driver ) { super ( ) ; this . _nodes = new Map < number , ZWaveNode > ( ) as ThrowingMap < number , ZWaveNode > ; this . _nodes . getOrThrow = function ( this , nodeId ) { const node = this . get ( nodeId ) ; if ( ! node ) { throw new ZWaveError ( `Node ${ nodeId } was not found!` , ZWaveErrorCodes . Controller_NodeNotFound ) ; } return node ; } . bind ( this . _nodes ) ; 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 _libraryVersion : string | undefined ; public get libraryVersion ( ) : string | undefined { return this . _libraryVersion ; } private _type : ZWaveLibraryTypes | undefined ; public get type ( ) : ZWaveLibraryTypes | undefined { return this . _type ; } 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 _isSecondary : boolean | undefined ; public get isSecondary ( ) : boolean | undefined { return this . _isSecondary ; } 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 _isStaticUpdateController : boolean | undefined ; public get isStaticUpdateController ( ) : boolean | undefined { return this . _isStaticUpdateController ; } private _isSlave : boolean | undefined ; public get isSlave ( ) : boolean | undefined { return this . _isSlave ; } private _serialApiVersion : string | undefined ; public get serialApiVersion ( ) : string | undefined { return this . _serialApiVersion ; } /** Checks if the Serial API version is greater than the given one */ public serialApiGt ( version ) { if ( this . _libraryVersion === undefined ) { return undefined ; } const sdkVersion = protocolVersionToSDKVersion ( this . _libraryVersion ) ; return semver . gt ( padVersion ( sdkVersion ) , padVersion ( version ) ) ; } /** Checks if the Serial API version is greater than or equal to the given one */ public serialApiGte ( version ) { if ( this . _libraryVersion === undefined ) { return undefined ; } const sdkVersion = protocolVersionToSDKVersion ( this . _libraryVersion ) ; return semver . gte ( padVersion ( sdkVersion ) , padVersion ( version ) ) ; } /** Checks if the Serial API version is lower than the given one */ public serialApiLt ( version ) { if ( this . _libraryVersion === undefined ) { return undefined ; } const sdkVersion = protocolVersionToSDKVersion ( this . _libraryVersion ) ; return semver . lt ( padVersion ( sdkVersion ) , padVersion ( version ) ) ; } /** Checks if the Serial API version is lower than or equal to the given one */ public serialApiLte ( version ) { if ( this . _libraryVersion === undefined ) { return undefined ; } const sdkVersion = protocolVersionToSDKVersion ( this . _libraryVersion ) ; 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 _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 ) { 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 ) { 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 ) { switch ( feature ) { case ZWaveFeature . SmartStart : return this . serialApiGte ( minFeatureVersions [ feature ] ) ; } } /** Throws if the controller does not support a certain feature */ private assertFeature ( feature ) { 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 ; } private _supportsSoftReset : boolean | undefined ; /** Whether the controller is known to support soft reset */ public get supportsSoftReset ( ) : boolean | undefined { return this . _supportsSoftReset ; } internal public set supportsSoftReset ( value ) { this . _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 ) { 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 ( ) { 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 ) { const nodes = nodeIDs . map ( ( id ) => this . _nodes . getOrThrow ( id ) ) ; return new VirtualNode ( undefined , this . driver , nodes ) ; } private _provisioningList : SmartStartProvisioningEntry [ ] = [ ] ; internal public get provisioningList ( ) : readonly SmartStartProvisioningEntry [ ] { return this . _provisioningList ; } /** Adds the given entry (DSK and security classes) to the controller's SmartStart provisioning list or replaces an existing entry */ public provisionSmartStartNode ( entry ) { this . assertFeature ( ZWaveFeature . SmartStart ) ; const index = this . _provisioningList . findIndex ( ( e ) => e . dsk === entry . dsk ) ; if ( index === - 1 ) { this . _provisioningList . push ( entry ) ; const node = this . getNodeByDSK ( entry . dsk ) ; if ( node ) this . markNodeOnProvisioningList ( node ) ; } else { this . _provisioningList [ index ] = entry ; } this . autoProvisionSmartStart ( ) ; void this . driver . saveNetworkToCache ( ) ; } /**
     * 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 ) { const index = this . _provisioningList . findIndex ( ( e ) => e . dsk === dskOrNodeId || ( typeof dskOrNodeId === "number" && "nodeId" in e && e . nodeId === dskOrNodeId ) ) ; if ( index >= 0 ) { this . _provisioningList . splice ( index , 1 ) ; this . autoProvisionSmartStart ( ) ; void this . driver . saveNetworkToCache ( ) ; } } /**
     * Returns the entry for the given DSK from the controller's SmartStart provisioning list.
     */ public getProvisioningEntry ( dsk ) { return this . _provisioningList . find ( ( e ) => e . dsk === dsk ) ; } /**
     * Returns all entries from the controller's SmartStart provisioning list.
     */ public getProvisioningEntries ( ) { return this . _provisioningList . map ( ( e ) => { const { dsk , securityClasses , nodeId , ... rest } = e ; return { dsk , securityClasses : [ ... securityClasses ] , ... ( nodeId != undefined ? { nodeId } : { } ) , ... rest } ; } ) ; } /** Returns whether the SmartStart provisioning list contains entries that have not been included yet */ public hasPlannedProvisioningEntries ( ) { return this . _provisioningList . some ( ( e ) => ! ( "nodeId" in e ) ) ; } internal public autoProvisionSmartStart ( ) { if ( ! this . supportsFeature ( ZWaveFeature . SmartStart ) ) return ; if ( this . hasPlannedProvisioningEntries ( ) ) { void this . enableSmartStart ( ) . catch ( ( ) => { } ) ; } else { void this . disableSmartStart ( ) . catch ( ( ) => { } ) ; } } private markNodeOnProvisioningList ( node ) { if ( node . dsk ) { const entry = this . _provisioningList . find ( ( e ) => e . dsk === dskToString ( node . dsk ! ) ) ; if ( entry ) ( entry as IncludedProvisioningEntry ) . nodeId = node . id ; } } private unmarkNodeOnProvisioningList ( nodeId ) { const entry = this . _provisioningList . find ( ( e ) => "nodeId" in e && e . nodeId === nodeId ) ; if ( entry ) delete entry . nodeId ; } internal public async identify ( ) { 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 }` ) ; this . driver . controllerLog . print ( `querying API capabilities...` ) ; const apiCaps = await this . driver . sendMessage < GetSerialApiCapabilitiesResponse > ( new GetSerialApiCapabilitiesRequest ( this . driver ) , { supportCheck : false } ) ; this . _serialApiVersion = apiCaps . serialApiVersion ; this . _manufacturerId = apiCaps . manufacturerId ; this . _productType = apiCaps . productType ; this . _productId = apiCaps . productId ; this . _supportedFunctionTypes = apiCaps . supportedFunctionTypes ; this . driver . controllerLog . print ( `received API capabilities:
  serial API version:  ${ this . _serialApiVersion }
  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 param restoreFromCache public async interview ( restoreFromCache ) { this . driver . controllerLog . print ( `querying version info...` ) ; const version = await this . driver . sendMessage < GetControllerVersionResponse > ( new GetControllerVersionRequest ( this . driver ) , { supportCheck : false } ) ; this . _libraryVersion = version . libraryVersion ; this . _type = version . controllerType ; this . driver . controllerLog . print ( `received version info:
  controller type: ${ ZWaveLibraryTypes [ this . _type ] }
  library version: ${ this . _libraryVersion }` ) ; 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 ( "" ) }` ) ; this . driver . controllerLog . print ( `querying controller capabilities...` ) ; const ctrlCaps = await this . driver . sendMessage < GetControllerCapabilitiesResponse > ( new GetControllerCapabilitiesRequest ( this . driver ) , { supportCheck : false } ) ; this . _isSecondary = ctrlCaps . isSecondary ; this . _isUsingHomeIdFromOtherNetwork = ctrlCaps . isUsingHomeIdFromOtherNetwork ; this . _isSISPresent = ctrlCaps . isSISPresent ; this . _wasRealPrimary = ctrlCaps . wasRealPrimary ; this . _isStaticUpdateController = ctrlCaps . isStaticUpdateController ; this . driver . controllerLog . print ( `received controller capabilities:
  controller role:     ${ this . _isSecondary ? "secondary" : "primary" }
  is in other network: ${ this . _isUsingHomeIdFromOtherNetwork }
  is SIS present:      ${ this . _isSISPresent }
  was real primary:    ${ this . _wasRealPrimary }
  is a SUC:            ${ this . _isStaticUpdateController }` ) ; 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 = [ ] ; } 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" }...` ) ; } 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 }` ) ; } if ( ! this . _isSecondary && this . _sucNodeId === 0 && ! this . _isStaticUpdateController && ! 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 ( this . type === ZWaveLibraryTypes [ "Bridge Controller" ] && this . isFunctionSupported ( FunctionType . FUNC_ID_ZW_GET_VIRTUAL_NODES ) ) { } this . driver . controllerLog . print ( `querying node information...` ) ; const initData = await this . driver . sendMessage < GetSerialApiInitDataResponse > ( new GetSerialApiInitDataRequest ( this . driver ) ) ; this . _isSecondary = initData . isSecondary ; this . _isStaticUpdateController = initData . isStaticUpdateController ; this . _isSlave = initData . isSlave ; this . _supportsTimers = initData . supportsTimers ; this . driver . controllerLog . print ( `received node information:
  controller role:            ${ this . _isSecondary ? "secondary" : "primary" }
  controller is a SUC:        ${ this . _isStaticUpdateController }
  controller is a slave:      ${ this . _isSlave }
  controller supports timers: ${ this . _supportsTimers }
  nodes in the network:       ${ initData . nodeIds . join ( ", " ) }` ) ; const valueDBIndexes = indexDBsByNode ( [ this . driver . valueDB ! , this . driver . metadataDB ! , ] ) ; for ( const nodeId of initData . nodeIds ) { this . _nodes . set ( nodeId , new ZWaveNode ( nodeId , this . driver , undefined , undefined , undefined , this . createValueDBForNode ( nodeId , valueDBIndexes . get ( nodeId ) ) ) ) ; } await restoreFromCache ( ) ; const controllerValueDB = this . valueDB ; controllerValueDB . setMetadata ( getManufacturerIdValueId ( ) , getManufacturerIdValueMetadata ( ) ) ; controllerValueDB . setMetadata ( getProductTypeValueId ( ) , getProductTypeValueMetadata ( ) ) ; controllerValueDB . setMetadata ( getProductIdValueId ( ) , getProductIdValueMetadata ( ) ) ; controllerValueDB . setValue ( getManufacturerIdValueId ( ) , this . _manufacturerId ) ; controllerValueDB . setValue ( getProductTypeValueId ( ) , this . _productType ) ; controllerValueDB . setValue ( getProductIdValueId ( ) , this . _productId ) ; controllerValueDB . setMetadata ( getFirmwareVersionsValueId ( ) , getFirmwareVersionsMetadata ( ) ) ; controllerValueDB . setValue ( getFirmwareVersionsValueId ( ) , [ this . _serialApiVersion , ] ) ; 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 , ownKeys ? ) { return new ValueDB ( nodeId , this . driver . valueDB ! , this . driver . metadataDB ! , ownKeys ) ; } internal public hardReset ( ) { this . driver . controllerLog . print ( "performing hard reset..." ) ; return new Promise ( async ( resolve , reject ) => { const handler = ( _msg ) => { this . driver . controllerLog . print ( `  hard reset succeeded` ) ; this . _nodes . forEach ( ( node ) => node . removeAllListeners ( ) ) ; this . _nodes . clear ( ) ; resolve ( ) ; return true ; } ; this . driver . registerRequestHandler ( FunctionType . HardReset , handler , true ) ; try { await this . driver . sendMessage ( new HardResetRequest ( this . driver ) , { supportCheck : false } ) ; } catch ( e ) { this . driver . controllerLog . print ( `  hard reset failed: ${ getErrorMessage ( e ) }` , "error" ) ; this . driver . unregisterRequestHandler ( FunctionType . HardReset , handler ) ; reject ( e ) ; } } ) ; } private _inclusionState : InclusionState = InclusionState . Idle ; public get inclusionState ( ) : InclusionState { return this . _inclusionState ; } internal public setInclusionState ( state ) { if ( this . _inclusionState === state ) return ; this . _inclusionState = state ; if ( state === InclusionState . Idle && this . _smartStartEnabled && this . supportsFeature ( ZWaveFeature . SmartStart ) ) { this . enableSmartStart ( ) . catch ( ( ) => { } ) ; } } private _smartStartEnabled : boolean = false ; private _includeController : boolean = false ; private _unprovisionRemovedNode : boolean = false ; private _inclusionOptions : InclusionOptionsInternal | undefined ; private _nodePendingInclusion : ZWaveNode | undefined ; private _nodePendingExclusion : ZWaveNode | undefined ; private _nodePendingReplace : ZWaveNode | undefined ; private _replaceFailedPromise : DeferredPromise < boolean > | undefined ; param options public async beginInclusion ( options ) ; param includeNonSecure deprecated public async beginInclusion ( includeNonSecure ? ) ; public async beginInclusion ( options ? ) { if ( this . _inclusionState === InclusionState . Including || this . _inclusionState === InclusionState . Excluding || this . _inclusionState === InclusionState . Busy ) { return false ; } if ( options == undefined ) { options = { strategy : InclusionStrategy . Security_S0 } ; } else if ( typeof options === "boolean" ) { options = { strategy : options ? InclusionStrategy . Insecure : InclusionStrategy . Security_S0 } ; } if ( ! ( options . strategy in InclusionStrategy ) || options . strategy === InclusionStrategy . SmartStart ) { throw new ZWaveError ( `Invalid inclusion strategy: ${ options . strategy }` , ZWaveErrorCodes . Argument_Invalid ) ; } if ( this . _inclusionState === InclusionState . SmartStart ) { await this . stopInclusion ( ) ; } this . setInclusionState ( InclusionState . Including ) ; this . _inclusionOptions = options ; try { this . driver . controllerLog . print ( `Starting inclusion process with strategy ${ getEnumMemberName ( InclusionStrategy , options . strategy ) }...` ) ; 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" , 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 ) { if ( this . _inclusionState === InclusionState . Including || this . _inclusionState === InclusionState . Excluding || this . _inclusionState === InclusionState . Busy ) { return false ; } 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 }` ) ; 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" , true , InclusionStrategy . SmartStart ) ; } catch ( e ) { this . setInclusionState ( InclusionState . Idle ) ; throw e ; } return true ; } internal public async stopInclusionNoCallback ( ) { await this . driver . sendMessage ( new AddNodeToNetworkRequest ( this . driver , { callbackId : 0 , 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 ( ) { 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 ( ) { if ( this . _inclusionState !== InclusionState . Including ) { return false ; } this . driver . controllerLog . print ( `stopping inclusion process...` ) ; try { 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 ( ) { 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 ( ) { 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 . stopInclusion ( ) ; 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 ; } } param unprovision public async beginExclusion ( unprovision = false ) { if ( this . _inclusionState === InclusionState . Including || this . _inclusionState === InclusionState . Excluding || this . _inclusionState === InclusionState . Busy ) { return false ; } if ( this . _inclusionState === InclusionState . SmartStart ) { await this . stopInclusion ( ) ; } this . setInclusionState ( InclusionState . Excluding ) ; this . driver . controllerLog . print ( `starting exclusion process...` ) ; try { 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 . _unprovisionRemovedNode = unprovision ; 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 ; } } internal public async stopExclusionNoCallback ( ) { await this . driver . sendMessage ( new RemoveNodeFromNetworkRequest ( this . driver , { callbackId : 0 , 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 ( ) { if ( this . _inclusionState !== InclusionState . Excluding ) { return false ; } this . driver . controllerLog . print ( `stopping exclusion process...` ) ; try { 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 ) ; } throw e ; } } private async secureBootstrapS0 ( node , assumeSupported = false ) { if ( ! this . driver . securityManager ) { node . securityClasses . set ( SecurityClass . S0_Legacy , false ) ; return ; } try { if ( assumeSupported && ! node . supportsCC ( CommandClasses . Security ) ) { node . addCC ( CommandClasses . Security , { secure : true , isSupported : true , version : 1 } ) ; } const api = node . commandClasses . Security . withOptions ( { expire : 10000 } ) ; await api . getSecurityScheme ( ) ; await api . getNonce ( ) ; await api . setNetworkKey ( this . driver . securityManager . networkKey ) ; if ( this . _includeController ) { await api . inheritSecurityScheme ( ) ; } node . securityClasses . set ( SecurityClass . S0_Legacy , true ) ; } catch ( e ) { let errorMessage = `Security S0 bootstrapping failed, the node was not granted the S0 security class` ; if ( ! isZWaveError ( e ) ) { errorMessage += `: ${ e as any }` ; } else if ( e . code === ZWaveErrorCodes . Controller_MessageExpired ) { errorMessage += ": a secure inclusion timer has elapsed." ; } else if ( e . code !== ZWaveErrorCodes . Controller_MessageDropped && e . code !== ZWaveErrorCodes . Controller_NodeTimeout ) { errorMessage += `: ${ e . message }` ; } this . driver . controllerLog . logNode ( node . id , errorMessage , "warn" ) ; node . securityClasses . set ( SecurityClass . S0_Legacy , false ) ; node . removeCC ( CommandClasses . Security ) ; } } private _bootstrappingS2NodeId : number | undefined ; internal public get bootstrappingS2NodeId ( ) : number | undefined { return this . _bootstrappingS2NodeId ; } private cancelBootstrapS2Promise : DeferredPromise < KEXFailType > | undefined ; public cancelSecureBootstrapS2 ( reason ) { if ( this . cancelBootstrapS2Promise ) { this . cancelBootstrapS2Promise . resolve ( reason ) ; this . cancelBootstrapS2Promise = undefined ; } } private async secureBootstrapS2 ( node , assumeSupported = false ) { const unGrantSecurityClasses = ( ) => { for ( const secClass of securityClassOrder ) { node . securityClasses . set ( secClass , false ) ; } } ; if ( ! this . driver . securityManager2 ) { unGrantSecurityClasses ( ) ; return ; } if ( assumeSupported && ! node . supportsCC ( CommandClasses [ "Security 2" ] ) ) { node . addCC ( CommandClasses [ "Security 2" ] , { secure : true , isSupported : true , version : 1 } ) ; } let userCallbacks ; const inclusionOptions = this . _inclusionOptions as InclusionOptionsInternal & { strategy : InclusionStrategy . Security_S2 | InclusionStrategy . SmartStart ; } ; if ( "provisioning" in inclusionOptions ) { userCallbacks = { abort ( ) { } , grantSecurityClasses : ( requested ) => { return Promise . resolve ( { clientSideAuth : false , securityClasses : requested . securityClasses . filter ( ( r ) => inclusionOptions . provisioning . securityClasses . includes ( r ) ) } ) ; } , validateDSKAndEnterPIN : ( dsk ) => { const fullDSK = inclusionOptions . provisioning . dsk ; const pin = fullDSK . slice ( 0 , 5 ) ; if ( pin + dsk !== fullDSK ) return Promise . resolve ( false ) ; return Promise . resolve ( pin ) ; } } ; } else { userCallbacks = inclusionOptions . userCallbacks ; } const deleteTempKey = ( ) => { this . driver . securityManager2 ?. deleteNonce ( node . id ) ; this . driver . securityManager2 ?. tempKeys . delete ( node . id ) ; } ; this . _bootstrappingS2NodeId = node . id ; this . cancelBootstrapS2Promise = createDeferredPromise ( ) ; try { const api = node . commandClasses [ "Security 2" ] ; const abort = async ( failType ? ) : Promise < void > => { if ( failType != undefined ) { try { await api . abortKeyExchange ( failType ) ; } catch { } } unGrantSecurityClasses ( ) ; deleteTempKey ( ) ; this . _bootstrappingS2NodeId = undefined ; this . cancelBootstrapS2Promise = undefined ; } ; const abortUser = ( ) => { setImmediate ( ( ) => { try { userCallbacks . abort ( ) ; } catch { } } ) ; return abort ( KEXFailType . BootstrappingCanceled ) ; } ; const kexParams = await api . withOptions ( { expire : inclusionTimeouts . TA1 } ) . getKeyExchangeParameters ( ) ; if ( ! kexParams ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: did not receive the node's desired security classes.` , level : "warn" } ) ; return abort ( ) ; } if ( ! kexParams . supportedKEXSchemes . includes ( KEXSchemes . KEXScheme1 ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: No supported key exchange scheme.` , level : "warn" } ) ; return abort ( KEXFailType . NoSupportedScheme ) ; } else if ( ! kexParams . supportedECDHProfiles . includes ( ECDHProfiles . Curve25519 ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: No supported ECDH profile.` , level : "warn" } ) ; return abort ( KEXFailType . NoSupportedCurve ) ; } const supportedKeys = kexParams . requestedKeys . filter ( ( k ) => securityClassOrder . includes ( k as any ) ) ; if ( ! supportedKeys . length ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: None of the requested security classes are supported.` , level : "warn" } ) ; return abort ( KEXFailType . NoKeyMatch ) ; } const grantResult = await Promise . race ( [ wait ( inclusionTimeouts . TAI1 , true ) . then ( ( ) => false as const ) , userCallbacks . grantSecurityClasses ( { securityClasses : supportedKeys , clientSideAuth : false } ) . catch ( ( ) => false as const ) , ] ) ; if ( grantResult === false ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: User rejected the requested security classes or interaction timed out.` , level : "warn" } ) ; return abortUser ( ) ; } const grantedKeys = supportedKeys . filter ( ( k ) => grantResult . securityClasses . includes ( k ) ) ; if ( ! grantedKeys . length ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: None of the requested keys were granted by the user.` , level : "warn" } ) ; return abortUser ( ) ; } await api . grantKeys ( { grantedKeys , permitCSA : false , selectedECDHProfile : ECDHProfiles . Curve25519 , selectedKEXScheme : KEXSchemes . KEXScheme1 } ) ; const pubKeyResponse = await this . driver . waitForCommand < Security2CCPublicKeyReport | Security2CCKEXFail > ( ( cc ) => cc instanceof Security2CCPublicKeyReport || cc instanceof Security2CCKEXFail , inclusionTimeouts . TA2 ) ; if ( pubKeyResponse instanceof Security2CCKEXFail || pubKeyResponse . includingNode ) { this . driver . controllerLog . logNode ( node . id , { message : `The joining node canceled the Security S2 bootstrapping.` , direction : "inbound" , level : "warn" } ) ; return abort ( ) ; } const nodePublicKey = pubKeyResponse . publicKey ; const timerStartTAI2 = Date . now ( ) ; const keyPair = await util . promisify ( crypto . generateKeyPair ) ( "x25519" ) ; const publicKey = decodeX25519KeyDER ( keyPair . publicKey . export ( { type : "spki" , format : "der" } ) ) ; await api . sendPublicKey ( publicKey ) ; if ( grantedKeys . includes ( SecurityClass . S2_AccessControl ) || grantedKeys . includes ( SecurityClass . S2_Authenticated ) ) { const dsk = dskToString ( nodePublicKey . slice ( 0 , 16 ) ) . slice ( 5 ) ; const tai2RemainingMs = inclusionTimeouts . TAI2 - ( Date . now ( ) - timerStartTAI2 ) ; const pinResult = await Promise . race ( [ wait ( tai2RemainingMs , true ) . then ( ( ) => false as const ) , userCallbacks . validateDSKAndEnterPIN ( dsk ) . catch ( ( ) => false as const ) , ] ) ; if ( typeof pinResult !== "string" || ! /^\d{5}$/ . test ( pinResult ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: User rejected the DSK, entered an invalid PIN or the interaction timed out.` , level : "warn" } ) ; return abortUser ( ) ; } nodePublicKey . writeUInt16BE ( parseInt ( pinResult , 10 ) , 0 ) ; } const sharedSecret = crypto . diffieHellman ( { publicKey : crypto . createPublicKey ( { key : encodeX25519KeyDERSPKI ( nodePublicKey ) , format : "der" , type : "spki" } ) , privateKey : keyPair . privateKey } ) ; const tempKeys = deriveTempKeys ( computePRK ( sharedSecret , publicKey , nodePublicKey ) ) ; this . driver . securityManager2 . deleteNonce ( node . id ) ; this . driver . securityManager2 . tempKeys . set ( node . id , { keyCCM : tempKeys . tempKeyCCM , personalizationString : tempKeys . tempPersonalizationString } ) ; const tai2RemainingMs = inclusionTimeouts . TAI2 - ( Date . now ( ) - timerStartTAI2 ) ; if ( tai2RemainingMs < 1 ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: a secure inclusion timer has elapsed` , level : "warn" } ) ; return abortUser ( ) ; } const keySetEcho = await Promise . race ( [ this . driver . waitForCommand < Security2CCKEXSet | Security2CCKEXFail > ( ( cc ) => cc instanceof Security2CCKEXSet || cc instanceof Security2CCKEXFail , tai2RemainingMs ) , this . cancelBootstrapS2Promise , ] ) ; if ( typeof keySetEcho === "number" ) { return abort ( keySetEcho ) ; } if ( keySetEcho instanceof Security2CCKEXFail || ! keySetEcho . echo ) { this . driver . controllerLog . logNode ( node . id , { message : `The joining node canceled the Security S2 bootstrapping.` , direction : "inbound" , level : "warn" } ) ; return abort ( ) ; } else if ( keySetEcho . grantedKeys . length !== grantedKeys . length || ! keySetEcho . grantedKeys . every ( ( k ) => grantedKeys . includes ( k ) ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: Granted key mismatch.` , level : "warn" } ) ; return abort ( KEXFailType . WrongSecurityLevel ) ; } await api . confirmGrantedKeys ( { requestCSA : kexParams . requestCSA , requestedKeys : [ ... kexParams . requestedKeys ] , supportedECDHProfiles : [ ... kexParams . supportedECDHProfiles ] , supportedKEXSchemes : [ ... kexParams . supportedKEXSchemes ] } ) ; for ( let i = 0 ; i < grantedKeys . length ; i ++ ) { const keyRequest = await this . driver . waitForCommand < Security2CCNetworkKeyGet | Security2CCKEXFail > ( ( cc ) => cc instanceof Security2CCNetworkKeyGet || cc instanceof Security2CCKEXFail , inclusionTimeouts . TA3 ) ; if ( keyRequest instanceof Security2CCKEXFail ) { this . driver . controllerLog . logNode ( node . id , { message : `The joining node canceled the Security S2 bootstrapping.` , direction : "inbound" , level : "warn" } ) ; return abort ( ) ; } const securityClass = keyRequest . requestedKey ; if ( ! this . driver . securityManager2 . hasUsedSecurityClass ( node . id , SecurityClass . Temporary ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: Node used wrong key to communicate.` , level : "warn" } ) ; return abort ( KEXFailType . WrongSecurityLevel ) ; } else if ( ! grantedKeys . includes ( securityClass ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: Node used key it was not granted.` , level : "warn" } ) ; return abort ( KEXFailType . KeyNotGranted ) ; } await api . sendNetworkKey ( securityClass , this . driver . securityManager2 . getKeysForSecurityClass ( securityClass ) . pnk ) ; node . securityClasses . set ( securityClass , true ) ; const verify = await this . driver . waitForCommand < Security2CCNetworkKeyVerify | Security2CCKEXFail > ( ( cc ) => cc instanceof Security2CCNetworkKeyVerify || cc instanceof Security2CCKEXFail , inclusionTimeouts . TA4 ) ; if ( verify instanceof Security2CCKEXFail ) { this . driver . controllerLog . logNode ( node . id , { message : `The joining node canceled the Security S2 bootstrapping.` , direction : "inbound" , level : "warn" } ) ; return abort ( ) ; } if ( ! this . driver . securityManager2 . hasUsedSecurityClass ( node . id , securityClass ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: Node used wrong key to communicate.` , level : "warn" } ) ; return abort ( KEXFailType . NoVerify ) ; } node . securityClasses . delete ( securityClass ) ; this . driver . securityManager2 . deleteNonce ( node . id ) ; await api . confirmKeyVerification ( ) ; } const transferEnd = await this . driver . waitForCommand < Security2CCTransferEnd > ( ( cc ) => cc instanceof Security2CCTransferEnd , inclusionTimeouts . TA5 ) ; if ( ! transferEnd . keyRequestComplete ) { this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping failed: Node did not confirm completion of the key exchange` , level : "warn" } ) ; return abort ( ) ; } for ( const securityClass of securityClassOrder ) { node . securityClasses . set ( securityClass , grantedKeys . includes ( securityClass ) ) ; } node . dsk = nodePublicKey . slice ( 0 , 16 ) ; this . driver . controllerLog . logNode ( node . id , { message : `Security S2 bootstrapping successful with these security classes:${ [ ... node . securityClasses . entries ( ) , ] . filter ( ( [ , v ] ) => v ) . map ( ( [ k ] ) => `\n· ${ getEnumMemberName ( SecurityClass , k ) }` ) . join ( "" ) }` } ) ; } catch ( e ) { let errorMessage = `Security S2 bootstrapping failed, the node was not granted any S2 security class` ; if ( ! isZWaveError ( e ) ) { errorMessage += `: ${ e as any }` ; } else if ( e . code === ZWaveErrorCodes . Controller_MessageExpired ) { errorMessage += ": a secure inclusion timer has elapsed." ; } else if ( e . code !== ZWaveErrorCodes . Controller_MessageDropped && e . code !== ZWaveErrorCodes . Controller_NodeTimeout ) { errorMessage += `: ${ e . message }` ; } this . driver . controllerLog . logNode ( node . id , errorMessage , "warn" ) ; unGrantSecurityClasses ( ) ; node . removeCC ( CommandClasses [ "Security 2" ] ) ; } finally { deleteTempKey ( ) ; this . _bootstrappingS2NodeId = undefined ; this . cancelBootstrapS2Promise = undefined ; } } /** Ensures that the node knows where to reach the controller */ private async bootstrapLifelineAndWakeup ( node ) { if ( securityClassIsS2 ( node . getHighestSecurityClass ( ) ) ) { for ( const cc of [ CommandClasses [ "Wake Up" ] , CommandClasses . Association , CommandClasses [ "Multi Channel Association" ] , CommandClasses . Version , ] ) { if ( node . supportsCC ( cc ) ) { node . addCC ( cc , { secure : true } ) ; } } } if ( node . supportsCC ( CommandClasses [ "Z-Wave Plus Info" ] ) ) { if ( node . supportsCC ( CommandClasses . Association ) || node . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Configuring Z-Wave+ Lifeline association...` , direction : "none" } ) ; const ownNodeId = this . driver . controller . ownNodeId ! ; try { if ( node . supportsCC ( CommandClasses . Association ) ) { await node . commandClasses . Association . addNodeIds ( 1 , ownNodeId ) ; } else { await node . commandClasses [ "Multi Channel Association" ] . addDestinations ( { groupId : 1 , endpoints : [ { nodeId : ownNodeId , endpoint : 0 } ] } ) ; } await this . assignReturnRoute ( node . id , ownNodeId ) ; } catch ( e ) { if ( isTransmissionError ( e ) || isRecoverableZWaveError ( e ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Failed to configure Z-Wave+ Lifeline association: ${ e . message }` , direction : "none" , level : "warn" } ) ; } else { throw e ; } } } else { this . driver . controllerLog . logNode ( node . id , { message : `Cannot configure Z-Wave+ Lifeline association: Node does not support associations...` , direction : "none" , level : "warn" } ) ; } } if ( node . supportsCC ( CommandClasses [ "Wake Up" ] ) ) { try { let supportedVersion ; if ( node . supportsCC ( CommandClasses . Version ) ) { supportedVersion = await node . commandClasses . Version . getCCVersion ( CommandClasses [ "Wake Up" ] ) ; } supportedVersion ??= 1 ; if ( supportedVersion > 0 ) { node . addCC ( CommandClasses [ "Wake Up" ] , { version : supportedVersion } ) ; const instance = node . createCCInstance ( CommandClasses [ "Wake Up" ] ) ! ; await instance . interview ( ) ; } } catch ( e ) { if ( isTransmissionError ( e ) || isRecoverableZWaveError ( e ) ) { this . driver . controllerLog . logNode ( node . id , { message : `Cannot configure wakeup destination: ${ e . message }` , direction : "none" , level : "warn" } ) ; } else { throw e ; } } } } /**
     * Is called when an AddNode request is received from the controller.
     * Handles and controls the inclusion process.
     */ private async handleAddNodeStatusReport ( msg ) { this . driver . controllerLog . print ( `handling add node request (status = ${ AddNodeStatus [ msg . status ] })` ) ; if ( this . _inclusionState !== InclusionState . Including || this . _inclusionOptions == undefined ) { this . driver . controllerLog . print ( `  inclusion is NOT active, ignoring it...` ) ; return true ; } switch ( msg . status ) { case AddNodeStatus . Failed : this . driver . controllerLog . print ( `Adding the node failed` , "error" ) ; this . emit ( "inclusion failed" ) ; try { await this . stopInclusion ( ) ; } catch { } return true ; case AddNodeStatus . AddingController : this . _includeController = true ; case AddNodeStatus . AddingSlave : { this . _nodePendingInclusion = new ZWaveNode ( msg . statusContext ! . nodeId , this . driver , new DeviceClass ( this . driver . configManager , msg . statusContext ! . basic ! , msg . statusContext ! . generic ! , msg . statusContext ! . specific ! ) , msg . statusContext ! . supportedCCs , msg . statusContext ! . controlledCCs , this . createValueDBForNode ( msg . statusContext ! . nodeId , new Set ( ) ) ) ; return true ; } case AddNodeStatus . ProtocolDone : { let nodeId ; try { nodeId = await this . finishInclusion ( ) ; } catch { } try { await this . stopInclusionNoCallback ( ) ; } catch { } if ( ! nodeId || ! this . _nodePendingInclusion ) { this . setInclusionState ( InclusionState . Idle ) ; this . _nodePendingInclusion = undefined ; return true ; } else if ( nodeId === NODE_ID_BROADCAST ) { this . driver . controllerLog . print ( `Cannot add a node with the Node ID ${ NODE_ID_BROADCAST }, aborting...` , "warn" ) ; this . setInclusionState ( InclusionState . Idle ) ; this . _nodePendingInclusion = undefined ; return true ; } this . setInclusionState ( InclusionState . Busy ) ; const newNode = this . _nodePendingInclusion ; const supportedCommandClasses = [ ... newNode . implementedCommandClasses . entries ( ) , ] . filter ( ( [ , info ] ) => info . isSupported ) . map ( ( [ cc ] ) => cc ) ; const controlledCommandClasses = [ ... newNode . implementedCommandClasses . entries ( ) , ] . filter ( ( [ , info ] ) => info . isControlled ) . map ( ( [ cc ] ) => cc ) ; this . driver . controllerLog . print ( `finished adding node ${ newNode . id }:
  basic device class:    ${ newNode . deviceClass ?. basic . label }
  generic device class:  ${ newNode . deviceClass ?. generic . label }
  specific device class: ${ newNode . deviceClass ?. specific . label }
  supported CCs: ${ supportedCommandClasses . map ( ( cc ) => `\n  · ${ CommandClasses [ cc ] } (${ num2hex ( cc ) })` ) . join ( "" ) }
  controlled CCs: ${ controlledCommandClasses . map ( ( cc ) => `\n  · ${ CommandClasses [ cc ] } (${ num2hex ( cc ) })` ) . join ( "" ) }` ) ; this . _nodes . set ( newNode . id , newNode ) ; this . _nodePendingInclusion = undefined ; newNode . markAsAlive ( ) ; newNode . hasSUCReturnRoute = await this . assignSUCReturnRoute ( newNode . id ) ; const opts = this . _inclusionOptions ; let lowSecurity = false ; if ( newNode . supportsCC ( CommandClasses [ "Security 2" ] ) && ( opts . strategy === InclusionStrategy . Default || opts . strategy === InclusionStrategy . Security_S2 || opts . strategy === InclusionStrategy . SmartStart ) ) { await this . secureBootstrapS2 ( newNode ) ; const actualSecurityClass = newNode . getHighestSecurityClass ( ) ; if ( actualSecurityClass == undefined || actualSecurityClass < SecurityClass . S2_Unauthenticated ) { lowSecurity = true ; } } else if ( newNode . supportsCC ( CommandClasses . Security ) && ( opts . strategy === InclusionStrategy . Security_S0 || ( opts . strategy === InclusionStrategy . Default && ( opts . forceSecurity || ( newNode . deviceClass ?. specific ?? newNode . deviceClass ?. generic ) ?. requiresSecurity ) ) ) ) { await this . secureBootstrapS0 ( newNode ) ; const actualSecurityClass = newNode . getHighestSecurityClass ( ) ; if ( actualSecurityClass == undefined || actualSecurityClass < SecurityClass . S0_Legacy ) { lowSecurity = true ; } } this . _includeController = false ; await this . bootstrapLifelineAndWakeup ( newNode ) ; const result = { } ; if ( lowSecurity ) result . lowSecurity = true ; this . markNodeOnProvisioningList ( newNode ) ; this . emit ( "node added" , newNode , result ) ; this . setInclusionState ( InclusionState . Idle ) ; return true ; } } return false ; } /**
     * Is called when an ReplaceFailed request is received from the controller.
     * Handles and controls the replace process.
     */ private async handleReplaceNodeStatusReport ( msg ) { this . driver . controllerLog . print ( `handling replace node request (status = ${ ReplaceFailedNodeStatus [ msg . replaceStatus ] })` ) ; if ( this . _inclusionOptions == undefined ) { this . driver . controllerLog . print ( `  currently NOT replacing a node, ignoring it...` ) ; return true ; } switch ( msg . replaceStatus ) { case ReplaceFailedNodeStatus . NodeOK : this . setInclusionState ( InclusionState . Idle ) ; this . _replaceFailedPromise ?. reject ( new ZWaveError ( `The node could not be replaced because it has responded` , ZWaveErrorCodes . ReplaceFailedNode_NodeOK ) ) ; break ; case ReplaceFailedNodeStatus . FailedNodeReplaceFailed : this . setInclusionState ( InclusionState . Idle ) ; this . _replaceFailedPromise ?. reject ( new ZWaveError ( `The failed node has not been replaced` , ZWaveErrorCodes . ReplaceFailedNode_Failed ) ) ; break ; case ReplaceFailedNodeStatus . FailedNodeReplace : this . driver . controllerLog . print ( `The failed node is ready to be replaced, inclusion started...` ) ; this . emit ( "inclusion started" , this . _inclusionOptions . strategy !== InclusionStrategy . Insecure , this . _inclusionOptions . strategy ) ; this . setInclusionState ( InclusionState . Including ) ; this . _replaceFailedPromise ?. resolve ( true ) ; return true ; case ReplaceFailedNodeStatus . FailedNodeReplaceDone : this . driver . controllerLog . print ( `The failed node was replaced` ) ; this . emit ( "inclusion stopped" ) ; if ( this . _nodePendingReplace ) { this . emit ( "node removed" , this . _nodePendingReplace , true ) ; this . unmarkNodeOnProvisioningList ( this . _nodePendingReplace . id ) ; this . _nodes . delete ( this . _nodePendingReplace . id ) ; this . setInclusionState ( InclusionState . Busy ) ; const newNode = new ZWaveNode ( this . _nodePendingReplace . id , this . driver , undefined , undefined , undefined , this . createValueDBForNode ( this . _nodePendingReplace . id , new Set ( ) ) ) ; this . _nodePendingReplace = undefined ; this . _nodes . set ( newNode . id , newNode ) ; newNode . markAsAlive ( ) ; newNode . hasSUCReturnRoute = await this . assignSUCReturnRoute ( newNode . id ) ; const strategy = this . _inclusionOptions . strategy ; let lowSecurity = false ; if ( strategy === InclusionStrategy . Security_S2 ) { await this . secureBootstrapS2 ( newNode , true ) ; const actualSecurityClass = newNode . getHighestSecurityClass ( ) ; if ( actualSecurityClass == undefined || actualSecurityClass < SecurityClass . S2_Unauthenticated ) { lowSecurity = true ; } } else if ( strategy === InclusionStrategy . Security_S0 ) { await this . secureBootstrapS0 ( newNode , true ) ; const actualSecurityClass = newNode . getHighestSecurityClass ( ) ; if ( actualSecurityClass == undefined || actualSecurityClass < SecurityClass . S0_Legacy ) { lowSecurity = true ; } } await this . bootstrapLifelineAndWakeup ( newNode ) ; const result = { } ; if ( lowSecurity ) result . lowSecurity = true ; this . setInclusionState ( InclusionState . Idle ) ; this . markNodeOnProvisioningList ( newNode ) ; this . emit ( "node added" , newNode , result ) ; } return true ; } this . emit ( "inclusion failed" ) ; return false ; } /**
     * Is called when a RemoveNode request is received from the controller.
     * Handles and controls the exclusion process.
     */ private async handleRemoveNodeStatusReport ( msg ) { this . driver . controllerLog . print ( `handling remove node request (status = ${ RemoveNodeStatus [ msg . status ] })` ) ; if ( this . _inclusionState !== InclusionState . Excluding ) { this . driver . controllerLog . print ( `  exclusion is NOT active, ignoring it...` ) ; return true ; } switch ( msg . status ) { case RemoveNodeStatus . Failed : this . driver . controllerLog . print ( `Removing the node failed` , "error" ) ; this . emit ( "exclusion failed" ) ; try { await this . stopExclusion ( ) ; } catch { } return true ; case RemoveNodeStatus . RemovingSlave : case RemoveNodeStatus . RemovingController : { this . _nodePendingExclusion = this . nodes . get ( msg . statusContext ! . nodeId ) ; return true ; } case RemoveNodeStatus . Done : { try { await this . stopExclusionNoCallback ( ) ; } catch { } if ( ! this . _nodePendingExclusion ) { this . setInclusionState ( InclusionState . Idle ) ; return true ; } const nodeId = this . _nodePendingExclusion . id ; this . driver . controllerLog . print ( `Node ${ nodeId } was removed` ) ; if ( this . _unprovisionRemovedNode ) this . unprovisionSmartStartNode ( nodeId ) ; this . emit ( "node removed" , this . _nodePendingExclusion , false ) ; this . unmarkNodeOnProvisioningList ( nodeId ) ; this . _nodes . delete ( nodeId ) ; this . _nodePendingExclusion = undefined ; this . setInclusionState ( InclusionState . Idle ) ; return true ; } } return false ; } private _healNetworkProgress = new Map < number , HealNodeStatus > ( ) ; /**
     * Performs a healing process for all alive nodes in the network,
     * requesting updated neighbor lists and assigning fresh routes to
     * association targets.
     */ public beginHealingNetwork ( ) { if ( this . _healNetworkActive ) return false ; this . _healNetworkActive = true ; this . driver . controllerLog . print ( `starting network heal...` ) ; this . _healNetworkProgress . clear ( ) ; for ( const [ id , node ] of this . _nodes ) { if ( id === this . _ownNodeId ) continue ; if ( node . status === NodeStatus . Dead || ( node . status === NodeStatus . Asleep && node . interviewStage === InterviewStage . ProtocolInfo ) ) { this . driver . controllerLog . logNode ( id , `Skipping heal because the node is not responding.` ) ; this . _healNetworkProgress . set ( id , "skipped" ) ; } else { this . _healNetworkProgress . set ( id , "pending" ) ; } } void this . healNetwork ( ) . catch ( ( ) => { } ) ; this . emit ( "heal network progress" , new Map ( this . _healNetworkProgress ) ) ; return true ; } private async healNetwork ( ) { const pendingNodes = new Set ( [ ... this . _healNetworkProgress ] . filter ( ( [ , status ] ) => status === "pending" ) . map ( ( [ nodeId ] ) => nodeId ) ) ; const todoListening = [ ] ; const todoSleeping = [ ] ; const addTodo = ( nodeId ) => { if ( pendingNodes . has ( nodeId ) ) { pendingNodes . delete ( nodeId ) ; const node = this . nodes . getOrThrow ( nodeId ) ; if ( node . canSleep ) { this . driver . controllerLog . logNode ( nodeId , "added to healing queue for sleeping nodes" ) ; todoSleeping . push ( nodeId ) ; } else { this . driver . controllerLog . logNode ( nodeId , "added to healing queue for listening nodes" ) ; todoListening . push ( nodeId ) ; } } } ; try { const neighbors = await this . getNodeNeighbors ( this . _ownNodeId ! ) ; neighbors . forEach ( ( id ) => addTodo ( id ) ) ; } catch { } const doHeal = async ( nodeId ) => { const result = await this . healNodeInternal ( nodeId ) . catch ( ( ) => false ) ; if ( ! this . _healNetworkActive ) return ; this . _healNetworkProgress . set ( nodeId , result ? "done" : "failed" ) ; this . emit ( "heal network progress" , new Map ( this . _healNetworkProgress ) ) ; try { const neighbors = await this . getNodeNeighbors ( nodeId ) ; neighbors . forEach ( ( id ) => addTodo ( id ) ) ; } catch { } } ; while ( todoListening . length > 0 ) { const nodeId = todoListening . shift ( ) ! ; await doHeal ( nodeId ) ; if ( ! this . _healNetworkActive ) return ; } pendingNodes . forEach ( ( nodeId ) => addTodo ( nodeId ) ) ; while ( todoListening . length > 0 ) { const nodeId = todoListening . shift ( ) ! ; await doHeal ( nodeId ) ; if ( ! this . _healNetworkActive ) return ; } this . driver . controllerLog . print ( "Healing sleeping nodes in parallel. Wake them up to heal." ) ; const tasks = todoSleeping . map ( ( nodeId ) => doHeal ( nodeId ) ) ; await Promise . all ( tasks ) ; if ( this . _healNetworkActive ) { this . emit ( "heal network done" , new Map ( this . _healNetworkProgress ) ) ; } this . _healNetworkActive = false ; } /**
     * Stops an network healing process. Resolves false if the process was not active, true otherwise.
     */ public stopHealingNetwork ( ) { if ( ! this . _healNetworkActive ) return false ; this . _healNetworkActive = false ; this . driver . controllerLog . print ( `stopping network heal...` ) ; this . driver . rejectTransactions ( ( t ) => t . message instanceof RequestNodeNeighborUpdateRequest || t . message instanceof DeleteReturnRouteRequest || t . message instanceof AssignReturnRouteRequest ) ; return true ; } /**
     * Performs a healing process for a single alive node in the network,
     * updating the neighbor list and assigning fresh routes to
     * association targets.
     *
     * Returns `true` if the process succeeded, `false` otherwise.
     */ public async healNode ( nodeId ) { const node = this . nodes . getOrThrow ( nodeId ) ; const keepAwake = node . keepAwake ; if ( this . _healNetworkActive ) return false ; this . _healNetworkActive = true ; if ( node . status === NodeStatus . Dead || ( node . status === NodeStatus . Asleep && node . interviewStage === InterviewStage . ProtocolInfo ) ) { this . driver . controllerLog . logNode ( nodeId , `Skipping heal because the node is not responding.` ) ; return false ; } try { this . driver . controllerLog . logNode ( nodeId , `Healing node...` ) ; node . keepAwake = true ; return await this . healNodeInternal ( nodeId ) ; } finally { this . _healNetworkActive = false ; node . keepAwake = keepAwake ; } } private async healNodeInternal ( nodeId ) { const node = this . nodes . getOrThrow ( nodeId ) ; this . driver . controllerLog . logNode ( nodeId , { message : `healing node...` , direction : "none" } ) ; const maxAttempts = 5 ; for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) { if ( ! this . _healNetworkActive ) return false ; this . driver . controllerLog . logNode ( nodeId , { message : `refreshing neighbor list (attempt ${ attempt })...` , direction : "outbound" } ) ; try { const resp = await this . driver . sendMessage < RequestNodeNeighborUpdateReport > ( new RequestNodeNeighborUpdateRequest ( this . driver , { nodeId } ) ) ; if ( resp . updateStatus === NodeNeighborUpdateStatus . UpdateDone ) { this . driver . controllerLog . logNode ( nodeId , { message : "neighbor list refreshed..." , direction : "inbound" } ) ; break ; } else { this . driver . controllerLog . logNode ( nodeId , { message : "refreshing neighbor list failed..." , direction : "inbound" , level : "warn" } ) ; } } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `refreshing neighbor list failed: ${ getErrorMessage ( e ) }` , "warn" ) ; } if ( attempt === maxAttempts ) { this . driver . controllerLog . logNode ( nodeId , { message : `failed to update the neighbor list after ${ maxAttempts } attempts, healing failed` , level : "warn" , direction : "none" } ) ; return false ; } } if ( await this . deleteSUCReturnRoute ( nodeId ) ) { node . hasSUCReturnRoute = false ; } node . hasSUCReturnRoute = await this . assignSUCReturnRoute ( nodeId ) ; for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) { this . driver . controllerLog . logNode ( nodeId , { message : `deleting return routes (attempt ${ attempt })...` , direction : "outbound" } ) ; try { await this . driver . sendMessage ( new DeleteReturnRouteRequest ( this . driver , { nodeId } ) ) ; break ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `deleting return routes failed: ${ getErrorMessage ( e ) }` , "warn" ) ; } if ( attempt === maxAttempts ) { this . driver . controllerLog . logNode ( nodeId , { message : `failed to delete return routes after ${ maxAttempts } attempts, healing failed` , level : "warn" , direction : "none" } ) ; return false ; } } let associatedNodes = [ ] ; const maxReturnRoutes = 4 ; try { associatedNodes = distinct ( flatMap < number , AssociationAddress [ ] > ( [ ... ( this . getAssociations ( { nodeId } ) . values ( ) as any ) ] , ( assocs ) => assocs . map ( ( a ) => a . nodeId ) ) ) . sort ( ) ; } catch { } if ( ! associatedNodes . includes ( this . _ownNodeId ! ) ) { associatedNodes . unshift ( this . _ownNodeId ! ) ; } if ( associatedNodes . length > maxReturnRoutes ) { associatedNodes = associatedNodes . slice ( 0 , maxReturnRoutes ) ; } this . driver . controllerLog . logNode ( nodeId , { message : `assigning return routes to the following nodes:
${ associatedNodes . join ( ", " ) }` , direction : "outbound" } ) ; for ( const destinationNodeId of associatedNodes ) { for ( let attempt = 1 ; attempt <= maxAttempts ; attempt ++ ) { this . driver . controllerLog . logNode ( nodeId , { message : `assigning return route to node ${ destinationNodeId } (attempt ${ attempt })...` , direction : "outbound" } ) ; try { await this . driver . sendMessage ( new AssignReturnRouteRequest ( this . driver , { nodeId , destinationNodeId } ) ) ; break ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `assigning return route failed: ${ getErrorMessage ( e ) }` , "warn" ) ; } if ( attempt === maxAttempts ) { this . driver . controllerLog . logNode ( nodeId , { message : `failed to assign return route after ${ maxAttempts } attempts, healing failed` , level : "warn" , direction : "none" } ) ; return false ; } } } this . driver . controllerLog . logNode ( nodeId , { message : `healed successfully` , direction : "none" } ) ; return true ; } /** Configures the given Node to be SUC/SIS or not */ public async configureSUC ( nodeId , enableSUC , enableSIS ) { const result = await this . driver . sendMessage < Message & SuccessIndicator > ( new SetSUCNodeIdRequest ( this . driver , { sucNodeId : nodeId , enableSUC , enableSIS } ) ) ; return result . isOK ( ) ; } public async assignSUCReturnRoute ( nodeId ) { this . driver . controllerLog . logNode ( nodeId , { message : `Assigning SUC return route...` , direction : "outbound" } ) ; try { const result = await this . driver . sendMessage < Message & SuccessIndicator > ( new AssignSUCReturnRouteRequest ( this . driver , { nodeId } ) ) ; return result . isOK ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `Assigning SUC return route failed: ${ getErrorMessage ( e ) }` , "error" ) ; return false ; } } public async deleteSUCReturnRoute ( nodeId ) { this . driver . controllerLog . logNode ( nodeId , { message : `Deleting SUC return route...` , direction : "outbound" } ) ; try { const result = await this . driver . sendMessage < Message & SuccessIndicator > ( new DeleteSUCReturnRouteRequest ( this . driver , { nodeId } ) ) ; return result . isOK ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `Deleting SUC return route failed: ${ getErrorMessage ( e ) }` , "error" ) ; return false ; } } public async assignReturnRoute ( nodeId , destinationNodeId ) { this . driver . controllerLog . logNode ( nodeId , { message : `Assigning return route to node ${ destinationNodeId }...` , direction : "outbound" } ) ; try { const result = await this . driver . sendMessage < Message & SuccessIndicator > ( new AssignReturnRouteRequest ( this . driver , { nodeId , destinationNodeId } ) ) ; return result . isOK ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `Assigning return route failed: ${ getErrorMessage ( e ) }` , "error" ) ; return false ; } } public async deleteReturnRoute ( nodeId ) { this . driver . controllerLog . logNode ( nodeId , { message : `Deleting all return routes...` , direction : "outbound" } ) ; try { const result = await this . driver . sendMessage < Message & SuccessIndicator > ( new DeleteReturnRouteRequest ( this . driver , { nodeId } ) ) ; return result . isOK ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `Deleting return routes failed: ${ getErrorMessage ( e ) }` , "error" ) ; return false ; } } /**
     * Returns a dictionary of all association groups of this node or endpoint and their information.
     * If no endpoint is given, the associations of the root device (endpoint 0) are returned.
     * This only works AFTER the interview process
     */ public getAssociationGroups ( source ) ; deprecated public getAssociationGroups ( nodeId ) ; public getAssociationGroups ( source ) { const nodeId = typeof source === "number" ? source : source . nodeId ; const endpointIndex = typeof source === "number" ? 0 : source . endpoint ?? 0 ; const node = this . nodes . getOrThrow ( nodeId ) ; const endpoint = node . getEndpointOrThrow ( endpointIndex ) ; let assocInstance ; let mcInstance ; if ( endpoint . supportsCC ( CommandClasses . Association ) ) { assocInstance = endpoint . createCCInstanceUnsafe < AssociationCC > ( CommandClasses . Association ) ! ; } else { throw new ZWaveError ( `Node ${ nodeId }${ endpointIndex > 0 ? `, endpoint ${ endpointIndex }` : "" } does not support associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } if ( endpoint . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { mcInstance = endpoint . createCCInstanceUnsafe < MultiChannelAssociationCC > ( CommandClasses [ "Multi Channel Association" ] ) ; } const assocGroupCount = assocInstance . getGroupCountCached ( ) ?? 0 ; const mcGroupCount = mcInstance ?. getGroupCountCached ( ) ?? 0 ; const groupCount = Math . max ( assocGroupCount , mcGroupCount ) ; const ret = new Map < number , AssociationGroup > ( ) ; if ( endpoint . supportsCC ( CommandClasses [ "Association Group Information" ] ) ) { const agiInstance = endpoint . createCCInstance < AssociationGroupInfoCC > ( CommandClasses [ "Association Group Information" ] ) ! ; for ( let group = 1 ; group <= groupCount ; group ++ ) { const assocConfig = node . deviceConfig ?. getAssociationConfigForEndpoint ( endpointIndex , group ) ; const multiChannel = ! ! mcInstance && group <= mcGroupCount ; ret . set ( group , { maxNodes : ( multiChannel ? mcInstance ! : assocInstance ) . getMaxNodesCached ( group ) || 1 , isLifeline : group === 1 , label : assocConfig ?. label ?? agiInstance . getGroupNameCached ( group ) ?? `Unnamed group ${ group }` , multiChannel , profile : agiInstance . getGroupProfileCached ( group ) , issuedCommands : agiInstance . getIssuedCommandsCached ( group ) } ) ; } } else { for ( let group = 1 ; group <= groupCount ; group ++ ) { const assocConfig = node . deviceConfig ?. getAssociationConfigForEndpoint ( endpointIndex , group ) ; const multiChannel = ! ! mcInstance && group <= mcGroupCount ; ret . set ( group , { maxNodes : ( multiChannel ? mcInstance ! : assocInstance ) . getMaxNodesCached ( group ) || assocConfig ?. maxNodes || 1 , isLifeline : assocConfig ?. isLifeline ?? group === 1 , label : assocConfig ?. label ?? `Unnamed group ${ group }` , multiChannel } ) ; } } return ret ; } /**
     * Returns all association groups that exist on a node and all its endpoints.
     * The returned map uses the endpoint index as keys and its values are maps of group IDs to their definition
     */ public getAllAssociationGroups ( nodeId ) { const node = this . nodes . getOrThrow ( nodeId ) ; const ret = new Map < number , ReadonlyMap < number , AssociationGroup > > ( ) ; for ( const endpoint of node . getAllEndpoints ( ) ) { if ( endpoint . supportsCC ( CommandClasses . Association ) ) { ret . set ( endpoint . index , this . getAssociationGroups ( { nodeId , endpoint : endpoint . index } ) ) ; } } return ret ; } /**
     * Returns all associations (Multi Channel or normal) that are configured on the root device or an endpoint of a node.
     * If no endpoint is given, the associations of the root device (endpoint 0) are returned.
     */ public getAssociations ( source ) ; deprecated public getAssociations ( nodeId ) ; public getAssociations ( source ) { const nodeId = typeof source === "number" ? source : source . nodeId ; const endpointIndex = typeof source === "number" ? 0 : source . endpoint ?? 0 ; const node = this . nodes . getOrThrow ( nodeId ) ; const endpoint = node . getEndpointOrThrow ( endpointIndex ) ; const ret = new Map < number , readonly AssociationAddress [ ] > ( ) ; if ( endpoint . supportsCC ( CommandClasses . Association ) ) { const cc = endpoint . createCCInstanceUnsafe < AssociationCC > ( CommandClasses . Association ) ! ; const destinations = cc . getAllDestinationsCached ( ) ; for ( const [ groupId , assocs ] of destinations ) { ret . set ( groupId , assocs ) ; } } else { throw new ZWaveError ( `Node ${ nodeId }${ endpointIndex > 0 ? `, endpoint ${ endpointIndex }` : "" } does not support associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } if ( endpoint . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { const cc = endpoint . createCCInstanceUnsafe < MultiChannelAssociationCC > ( CommandClasses [ "Multi Channel Association" ] ) ! ; const destinations = cc . getAllDestinationsCached ( ) ; for ( const [ groupId , assocs ] of destinations ) { if ( ret . has ( groupId ) ) { const normalAssociations = ret . get ( groupId ) ! ; ret . set ( groupId , [ ... normalAssociations , ... assocs . filter ( ( a1 ) => normalAssociations . findIndex ( ( a2 ) => a1 . nodeId === a2 . nodeId && a1 . endpoint === a2 . endpoint ) === - 1 ) , ] ) ; } else { ret . set ( groupId , assocs ) ; } } } return ret ; } /**
     * Returns all associations (Multi Channel or normal) that are configured on a node and all its endpoints.
     * The returned map uses the source node+endpoint as keys and its values are a map of association group IDs to target node+endpoint.
     */ public getAllAssociations ( nodeId ) { const node = this . nodes . getOrThrow ( nodeId ) ; const ret = new ObjectKeyMap < AssociationAddress , ReadonlyMap < number , readonly AssociationAddress [ ] > > ( ) ; for ( const endpoint of node . getAllEndpoints ( ) ) { const address = { nodeId , endpoint : endpoint . index } ; if ( endpoint . supportsCC ( CommandClasses . Association ) ) { ret . set ( address , this . getAssociations ( address ) ) ; } } return ret ; } /**
     * Checks if a given association is allowed.
     */ public isAssociationAllowed ( source , group , destination ) ; deprecated public isAssociationAllowed ( nodeId , group , destination ) ; public isAssociationAllowed ( source , group , destination ) { if ( typeof source === "number" ) { source = { nodeId : source } ; } const node = this . nodes . getOrThrow ( source . nodeId ) ; const endpoint = node . getEndpointOrThrow ( source . endpoint ?? 0 ) ; const targetNode = this . nodes . getOrThrow ( destination . nodeId ) ; const targetEndpoint = destination . nodeId === this . _ownNodeId ? targetNode : targetNode . getEndpointOrThrow ( destination . endpoint ?? 0 ) ; if ( ! endpoint . supportsCC ( CommandClasses . Association ) && ! endpoint . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { throw new ZWaveError ( `Node ${ node . id }${ endpoint . index > 0 ? `, endpoint ${ endpoint . index }` : "" } does not support associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } else if ( ! endpoint . supportsCC ( CommandClasses [ "Association Group Information" ] ) ) { return true ; } if ( destination . nodeId === this . _ownNodeId ) return true ; const groupCommandList = endpoint . createCCInstanceInternal < AssociationGroupInfoCC > ( CommandClasses [ "Association Group Information" ] ) ! . getIssuedCommandsCached ( group ) ; if ( ! groupCommandList || ! groupCommandList . size ) { return true ; } const groupCCs = [ ... groupCommandList . keys ( ) ] ; if ( groupCCs . includes ( CommandClasses . Basic ) && actuatorCCs . some ( ( cc ) => targetEndpoint ?. supportsCC ( cc ) ) ) { return true ; } return groupCCs . some ( ( cc ) => targetEndpoint ?. supportsCC ( cc ) ) ; } /**
     * Adds associations to a node or endpoint
     */ public addAssociations ( source , group , destinations ) ; deprecated public addAssociations ( nodeId , group , destinations ) ; /**
     * Adds associations to a node or endpoint
     */ public async addAssociations ( source , group , destinations ) { if ( typeof source === "number" ) { source = { nodeId : source } ; } const node = this . nodes . getOrThrow ( source . nodeId ) ; const endpoint = node . getEndpointOrThrow ( source . endpoint ?? 0 ) ; const nodeAndEndpointString = `${ node . id }${ endpoint . index > 0 ? `, endpoint ${ endpoint . index }` : "" }` ; let assocInstance ; let mcInstance ; const nodeAssociations = distinct ( destinations . filter ( ( a ) => a . endpoint == undefined ) . map ( ( a ) => a . nodeId ) ) ; const endpointAssociations = destinations . filter ( ( a ) => a . endpoint != undefined ) as EndpointAddress [ ] ; if ( endpoint . supportsCC ( CommandClasses . Association ) ) { assocInstance = endpoint . createCCInstanceUnsafe < AssociationCC > ( CommandClasses . Association ) ; } else if ( nodeAssociations . length > 0 ) { throw new ZWaveError ( `Node ${ nodeAndEndpointString } does not support associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } if ( endpoint . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { mcInstance = endpoint . createCCInstanceUnsafe < MultiChannelAssociationCC > ( CommandClasses [ "Multi Channel Association" ] ) ; } else if ( endpointAssociations . length > 0 ) { throw new ZWaveError ( `Node ${ nodeAndEndpointString } does not support multi channel associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } const assocGroupCount = assocInstance ?. getGroupCountCached ( ) ?? 0 ; const mcGroupCount = mcInstance ?. getGroupCountCached ( ) ?? 0 ; const groupCount = Math . max ( assocGroupCount , mcGroupCount ) ; if ( group > groupCount ) { throw new ZWaveError ( `Group ${ group } does not exist on node ${ nodeAndEndpointString }` , ZWaveErrorCodes . AssociationCC_InvalidGroup ) ; } const groupIsMultiChannel = ! ! mcInstance && group <= mcGroupCount && node . deviceConfig ?. associations ?. get ( group ) ?. multiChannel !== false ; if ( groupIsMultiChannel ) { const disallowedAssociations = destinations . filter ( ( a ) => ! this . isAssociationAllowed ( source as AssociationAddress , group , a ) ) ; if ( disallowedAssociations . length ) { let message = `The following associations are not allowed:` ; message += disallowedAssociations . map ( ( a ) => `\n· Node ${ a . nodeId }${ a . endpoint ? `, endpoint ${ a . endpoint }` : "" }` ) . join ( "" ) ; throw new ZWaveError ( message , ZWaveErrorCodes . AssociationCC_NotAllowed ) ; } await endpoint . commandClasses [ "Multi Channel Association" ] . addDestinations ( { groupId : group , nodeIds : nodeAssociations , endpoints : endpointAssociations } ) ; await endpoint . commandClasses [ "Multi Channel Association" ] . getGroup ( group ) ; } else { if ( destinations . some ( ( a ) => a . endpoint != undefined ) ) { throw new ZWaveError ( `Node ${ nodeAndEndpointString }, group ${ group } does not support multi channel associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } const disallowedAssociations = destinations . filter ( ( a ) => ! this . isAssociationAllowed ( source as AssociationAddress , group , a ) ) ; if ( disallowedAssociations . length ) { throw new ZWaveError ( `The associations to the following nodes are not allowed: ${ disallowedAssociations . map ( ( a ) => a . nodeId ) . join ( ", " ) }` , ZWaveErrorCodes . AssociationCC_NotAllowed ) ; } await endpoint . commandClasses . Association . addNodeIds ( group , ... destinations . map ( ( a ) => a . nodeId ) ) ; await endpoint . commandClasses . Association . getGroup ( group ) ; } } /**
     * Removes the given associations from a node or endpoint
     */ public removeAssociations ( source , group , destinations ) ; deprecated public removeAssociations ( nodeId , group , destinations ) ; /**
     * Removes the given associations from a node or endpoint
     */ public async removeAssociations ( source , group , destinations ) { if ( typeof source === "number" ) { source = { nodeId : source } ; } const node = this . nodes . getOrThrow ( source . nodeId ) ; const endpoint = node . getEndpointOrThrow ( source . endpoint ?? 0 ) ; const nodeAndEndpointString = `${ node . id }${ endpoint . index > 0 ? `, endpoint ${ endpoint . index }` : "" }` ; const nodeAssociations = distinct ( destinations . filter ( ( a ) => a . endpoint == undefined ) . map ( ( a ) => a . nodeId ) ) ; const endpointAssociations = destinations . filter ( ( a ) => a . endpoint != undefined ) as EndpointAddress [ ] ; let groupExistsAsMultiChannel = false ; let groupExistsAsNodeAssociation = false ; let mcInstance ; let assocInstance ; if ( endpoint . supportsCC ( CommandClasses [ "Multi Channel Association" ] ) ) { mcInstance = endpoint . createCCInstanceUnsafe < MultiChannelAssociationCC > ( CommandClasses [ "Multi Channel Association" ] ) ! ; if ( group <= mcInstance . getGroupCountCached ( ) ) { groupExistsAsMultiChannel = true ; } } else if ( endpointAssociations . length > 0 ) { throw new ZWaveError ( `Node ${ nodeAndEndpointString } does not support multi channel associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } if ( endpoint . supportsCC ( CommandClasses . Association ) ) { assocInstance = endpoint . createCCInstanceUnsafe < AssociationCC > ( CommandClasses . Association ) ! ; if ( group <= assocInstance . getGroupCountCached ( ) ) { groupExistsAsNodeAssociation = true ; } } if ( ! mcInstance && ! assocInstance ) { throw new ZWaveError ( `Node ${ nodeAndEndpointString } does not support associations!` , ZWaveErrorCodes . CC_NotSupported ) ; } if ( ! groupExistsAsMultiChannel && ! groupExistsAsNodeAssociation ) { throw new ZWaveError ( ` Association group ${ group } does not exist for node ${ nodeAndEndpointString }` , ZWaveErrorCodes . AssociationCC_InvalidGroup ) ; } if ( endpointAssociations . length > 0 && ! groupExistsAsMultiChannel ) { throw new ZWaveError ( `Node ${ nodeAndEndpointString }, association group ${ group } does not support multi channel associations!` , ZWaveErrorCodes . AssociationCC_InvalidGroup ) ; } if ( assocInstance && nodeAssociations . length > 0 && groupExistsAsNodeAssociation ) { await endpoint . commandClasses . Association . removeNodeIds ( { groupId : group , nodeIds : nodeAssociations } ) ; await endpoint . commandClasses . Association . getGroup ( group ) ; } if ( mcInstance && groupExistsAsMultiChannel ) { await endpoint . commandClasses [ "Multi Channel Association" ] . removeDestinations ( { groupId : group , nodeIds : nodeAssociations , endpoints : endpointAssociations } ) ; await endpoint . commandClasses [ "Multi Channel Association" ] . getGroup ( group ) ; } } /**
     * Removes a node from all other nodes' associations
     * WARNING: It is not recommended to await this method
     */ public async removeNodeFromAllAssociations ( nodeId ) { const tasks = [ ] ; for ( const node of this . nodes . values ( ) ) { if ( node . id === this . _ownNodeId || node . id === nodeId ) continue ; for ( const endpoint of node . getAllEndpoints ( ) ) { if ( endpoint . commandClasses [ "Multi Channel Association" ] . isSupported ( ) ) { return endpoint . commandClasses [ "Multi Channel Association" ] . removeDestinations ( { nodeIds : [ nodeId ] } ) ; } else if ( endpoint . commandClasses . Association . isSupported ( ) ) { return endpoint . commandClasses . Association . removeNodeIdsFromAllGroups ( [ nodeId ] ) ; } } } await Promise . all ( tasks ) ; } param nodeId public async isFailedNode ( nodeId ) { const result = await this . driver . sendMessage < IsFailedNodeResponse > ( new IsFailedNodeRequest ( this . driver , { failedNodeId : nodeId } ) ) ; return result . result ; } param nodeId public async removeFailedNode ( nodeId ) { const node = this . nodes . getOrThrow ( nodeId ) ; if ( await node . ping ( ) ) { throw new ZWaveError ( `The node removal process could not be started because the node responded to a ping.` , ZWaveErrorCodes . ReplaceFailedNode_Failed ) ; } const result = await this . driver . sendMessage < RemoveFailedNodeRequestStatusReport | RemoveFailedNodeResponse > ( new RemoveFailedNodeRequest ( this . driver , { failedNodeId : nodeId } ) ) ; if ( result instanceof RemoveFailedNodeResponse ) { let message = `The node removal process could not be started due to the following reasons:` ; if ( ! ! ( result . removeStatus & RemoveFailedNodeStartFlags . NotPrimaryController ) ) { message += "\n· This controller is not the primary controller" ; } if ( ! ! ( result . removeStatus & RemoveFailedNodeStartFlags . NodeNotFound ) ) { message += `\n· Node ${ nodeId } is not in the list of failed nodes` ; } if ( ! ! ( result . removeStatus & RemoveFailedNodeStartFlags . RemoveProcessBusy ) ) { message += `\n· The node removal process is currently busy` ; } if ( ! ! ( result . removeStatus & RemoveFailedNodeStartFlags . RemoveFailed ) ) { message += `\n· The controller is busy or the node has responded` ; } throw new ZWaveError ( message , ZWaveErrorCodes . RemoveFailedNode_Failed ) ; } else { switch ( result . removeStatus ) { case RemoveFailedNodeStatus . NodeOK : throw new ZWaveError ( `The node could not be removed because it has responded` , ZWaveErrorCodes . RemoveFailedNode_NodeOK ) ; case RemoveFailedNodeStatus . NodeNotRemoved : throw new ZWaveError ( `The removal process could not be completed` , ZWaveErrorCodes . RemoveFailedNode_Failed ) ; default : this . emit ( "node removed" , this . nodes . get ( nodeId ) ! , false ) ; this . unmarkNodeOnProvisioningList ( nodeId ) ; this . _nodes . delete ( nodeId ) ; return ; } } } param nodeId param options public async replaceFailedNode ( nodeId , options ) ; param nodeId param includeNonSecure deprecated public async replaceFailedNode ( nodeId , includeNonSecure ? ) ; public async replaceFailedNode ( nodeId , options ? ) { if ( this . _inclusionState === InclusionState . Including || this . _inclusionState === InclusionState . Excluding || this . _inclusionState === InclusionState . Busy ) { return false ; } if ( this . _inclusionState === InclusionState . SmartStart ) { await this . stopInclusion ( ) ; } this . setInclusionState ( InclusionState . Busy ) ; if ( options == undefined ) { options = { strategy : InclusionStrategy . Security_S0 } ; } else if ( typeof options === "boolean" ) { options = { strategy : options ? InclusionStrategy . Insecure : InclusionStrategy . Security_S0 } ; } this . driver . controllerLog . print ( `starting replace failed node process...` ) ; const node = this . nodes . getOrThrow ( nodeId ) ; if ( await node . ping ( ) ) { this . setInclusionState ( InclusionState . Idle ) ; throw new ZWaveError ( `The node replace process could not be started because the node responded to a ping.` , ZWaveErrorCodes . ReplaceFailedNode_Failed ) ; } this . _inclusionOptions = options ; const result = await this . driver . sendMessage < ReplaceFailedNodeResponse > ( new ReplaceFailedNodeRequest ( this . driver , { failedNodeId : nodeId } ) ) ; if ( ! result . isOK ( ) ) { let message = `The node replace process could not be started due to the following reasons:` ; if ( ! ! ( result . replaceStatus & ReplaceFailedNodeStartFlags . NotPrimaryController ) ) { message += "\n· This controller is not the primary controller" ; } if ( ! ! ( result . replaceStatus & ReplaceFailedNodeStartFlags . NodeNotFound ) ) { message += `\n· Node ${ nodeId } is not in the list of failed nodes` ; } if ( ! ! ( result . replaceStatus & ReplaceFailedNodeStartFlags . ReplaceProcessBusy ) ) { message += `\n· The node replace process is currently busy` ; } if ( ! ! ( result . replaceStatus & ReplaceFailedNodeStartFlags . ReplaceFailed ) ) { message += `\n· The controller is busy or the node has responded` ; } this . setInclusionState ( InclusionState . Idle ) ; throw new ZWaveError ( message , ZWaveErrorCodes . ReplaceFailedNode_Failed ) ; } else { this . _nodePendingReplace = this . nodes . get ( nodeId ) ; this . _replaceFailedPromise = createDeferredPromise ( ) ; return this . _replaceFailedPromise ; } } /** Configure the RF region at the Z-Wave API Module */ public async setRFRegion ( region ) { const result = await this . driver . sendMessage < SerialAPISetup_SetRFRegionResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_SetRFRegionRequest ( this . driver , { region } ) ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support setting the RF region!` , ZWaveErrorCodes . Driver_NotSupported ) ; } if ( result . success ) await this . driver . trySoftReset ( ) ; return result . success ; } /** Request the current RF region configured at the Z-Wave API Module */ public async getRFRegion ( ) { const result = await this . driver . sendMessage < SerialAPISetup_GetRFRegionResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_GetRFRegionRequest ( this . driver ) ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support getting the RF region!` , ZWaveErrorCodes . Driver_NotSupported ) ; } return result . region ; } /** Configure the Powerlevel setting of the Z-Wave API */ public async setPowerlevel ( powerlevel , measured0dBm ) { const result = await this . driver . sendMessage < SerialAPISetup_SetPowerlevelResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_SetPowerlevelRequest ( this . driver , { powerlevel , measured0dBm } ) ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support setting the powerlevel!` , ZWaveErrorCodes . Driver_NotSupported ) ; } return result . success ; } /** Request the Powerlevel setting of the Z-Wave API */ public async getPowerlevel ( ) { const result = await this . driver . sendMessage < SerialAPISetup_GetPowerlevelResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_GetPowerlevelRequest ( this . driver ) ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support getting the powerlevel!` , ZWaveErrorCodes . Driver_NotSupported ) ; } return pick ( result , [ "powerlevel" , "measured0dBm" ] ) ; } internal public async setNodeIDType ( nodeIdType ) { const result = await this . driver . sendMessage < SerialAPISetup_SetNodeIDTypeResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_SetNodeIDTypeRequest ( this . driver , { nodeIdType } ) ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support switching between short and long node IDs!` , ZWaveErrorCodes . Driver_NotSupported ) ; } return result . success ; } internal public async getMaxPayloadSize ( ) { const result = await this . driver . sendMessage < SerialAPISetup_GetMaximumPayloadSizeResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_GetMaximumPayloadSizeRequest ( this . driver ) , { supportCheck : false } ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support getting the max. payload size!` , ZWaveErrorCodes . Driver_NotSupported ) ; } return result . maxPayloadSize ; } internal public async getMaxPayloadSizeLongRange ( ) { const result = await this . driver . sendMessage < SerialAPISetup_GetLRMaximumPayloadSizeResponse | SerialAPISetup_CommandUnsupportedResponse > ( new SerialAPISetup_GetLRMaximumPayloadSizeRequest ( this . driver ) ) ; if ( result instanceof SerialAPISetup_CommandUnsupportedResponse ) { throw new ZWaveError ( `Your hardware does not support getting the max. long range payload size!` , ZWaveErrorCodes . Driver_NotSupported ) ; } return result . maxPayloadSize ; } /**
     * Returns the known list of neighbors for a node
     */ public async getNodeNeighbors ( nodeId , onlyRepeaters = false ) { this . driver . controllerLog . logNode ( nodeId , { message : "requesting node neighbors..." , direction : "outbound" } ) ; try { const resp = await this . driver . sendMessage < GetRoutingInfoResponse > ( new GetRoutingInfoRequest ( this . driver , { nodeId , removeBadLinks : false , removeNonRepeaters : onlyRepeaters } ) ) ; this . driver . controllerLog . logNode ( nodeId , { message : `node neighbors received: ${ resp . nodeIds . join ( ", " ) }` , direction : "inbound" } ) ; return resp . nodeIds ; } catch ( e ) { this . driver . controllerLog . logNode ( nodeId , `requesting the node neighbors failed: ${ getErrorMessage ( e ) }` , "error" ) ; throw e ; } } internal public serialize ( ) { return { controller : { supportsSoftReset : this . supportsSoftReset , provisioningList : this . provisioningList . map ( ( e ) => { const { dsk , securityClasses , nodeId , ... rest } = e ; return { dsk , securityClasses : securityClasses . map ( ( s ) => SecurityClass [ s ] ) , ... rest } ; } ) } , nodes : composeObject ( [ ... this . nodes . entries ( ) ] . map ( ( [ id , node ] ) => [ id . toString ( ) , node . serialize ( ) ] as [ string , unknown ] ) ) } ; } internal public async deserialize ( serialized ) { if ( isObject ( serialized . controller ) ) { if ( typeof serialized . controller . supportsSoftReset === "boolean" ) { this . supportsSoftReset = serialized . controller . supportsSoftReset ; } if ( isArray ( serialized . controller . provisioningList ) ) { entries : for ( const entry of serialized . controller . provisioningList ) { if ( ! isObject ( entry ) ) continue ; const { dsk , securityClasses : secClasses , nodeId , ... rest } = entry ; if ( typeof entry . dsk !== "string" ) continue ; if ( ! isArray ( entry . securityClasses ) ) continue ; if ( ! isValidDSK ( entry . dsk ) ) continue ; const securityClasses = [ ] ; for ( const s of secClasses ) { if ( typeof s !== "string" ) continue entries ; const secClass = ( SecurityClass as any ) [ s ] ; if ( typeof secClass !== "number" ) continue entries ; securityClasses . push ( secClass ) ; } this . _provisioningList . push ( { dsk : entry . dsk , securityClasses , ... rest } ) ; } } } if ( isObject ( serialized . nodes ) ) { for ( const nodeId of Object . keys ( serialized . nodes ) ) { const serializedNode = serialized . nodes [ nodeId ] ; if ( ! serializedNode || typeof serializedNode . id !== "number" || serializedNode . id . toString ( ) !== nodeId ) { throw new ZWaveError ( "The cache file is invalid" , ZWaveErrorCodes . Driver_InvalidCache ) ; } if ( this . nodes . has ( serializedNode . id ) ) { const node = this . nodes . getOrThrow ( serializedNode . id ) ; await node . deserialize ( serializedNode ) ; this . markNodeOnProvisioningList ( node ) ; } } } } /** Turns the Z-Wave radio on or off */ public async toggleRF ( enabled ) { try { this . driver . controllerLog . print ( `Turning RF ${ enabled ? "on" : "off" }...` ) ; const ret = await this . driver . sendMessage < SetRFReceiveModeResponse > ( new SetRFReceiveModeRequest ( this . driver , { enabled } ) ) ; return ret . isOK ( ) ; } catch ( e ) { this . driver . controllerLog . print ( `Error turning RF ${ enabled ? "on" : "off" }: ${ getErrorMessage ( e ) }` , "error" ) ; return false ; } } /**
     * **Z-Wave 500 series only**
     *
     * Returns information of the controller's external NVM
     */ public async getNVMId ( ) { const ret = await this . driver . sendMessage < GetNVMIdResponse > ( new GetNVMIdRequest ( this . driver ) ) ; return pick ( ret , [ "nvmManufacturerId" , "memoryType" , "memorySize" ] ) ; } /**
     * **Z-Wave 500 series only**
     *
     * Reads a byte from the external NVM at the given offset
     */ public async externalNVMReadByte ( offset ) { const ret = await this . driver . sendMessage < ExtNVMReadLongByteResponse > ( new ExtNVMReadLongByteRequest ( this . driver , { offset } ) ) ; return ret . byte ; } returns public async externalNVMWriteByte ( offset , data ) { const ret = await this . driver . sendMessage < ExtNVMWriteLongByteResponse > ( new ExtNVMWriteLongByteRequest ( this . driver , { offset , byte : data } ) ) ; return ret . success ; } /**
     * **Z-Wave 500 series only**
     *
     * Reads a buffer from the external NVM at the given offset
     */ public async externalNVMReadBuffer ( offset , length ) { const ret = await this . driver . sendMessage < ExtNVMReadLongBufferResponse > ( new ExtNVMReadLongBufferRequest ( this . driver , { offset , length } ) ) ; return ret . buffer ; } /**
     * **Z-Wave 700 series only**
     *
     * Reads a buffer from the external NVM at the given offset
     */ public async externalNVMReadBuffer700 ( offset , length ) { const ret = await this . driver . sendMessage < NVMOperationsResponse > ( new NVMOperationsReadRequest ( this . driver , { offset , length } ) ) ; if ( ! ret . isOK ( ) ) { let message = "Could not read from the external NVM" ; if ( ret . status === NVMOperationStatus . Error_OperationInterference ) { message += ": interference between read and write operation." ; } else if ( ret . status === NVMOperationStatus . Error_OperationMismatch ) { message += ": wrong operation requested." ; } throw new ZWaveError ( message , ZWaveErrorCodes . Controller_CommandError ) ; } return { buffer : ret . buffer , endOfFile : ret . status === NVMOperationStatus . EndOfFile } ; } returns public async externalNVMWriteBuffer ( offset , buffer ) { const ret = await this . driver . sendMessage < ExtNVMWriteLongBufferResponse > ( new ExtNVMWriteLongBufferRequest ( this . driver , { offset , buffer } ) ) ; return ret . success ; } /**
     * **Z-Wave 700 series only**
     *
     * Writes a buffer to the external NVM at the given offset
     * **WARNING:** This function can write in the full NVM address space and is not offset to start at the application area.
     * Take care not to accidentally overwrite the protocol NVM area!
     */ public async externalNVMWriteBuffer700 ( offset , buffer ) { const ret = await this . driver . sendMessage < NVMOperationsResponse > ( new NVMOperationsWriteRequest ( this . driver , { offset , buffer } ) ) ; if ( ! ret . isOK ( ) ) { let message = "Could not write to the external NVM" ; if ( ret . status === NVMOperationStatus . Error_OperationInterference ) { message += ": interference between read and write operation." ; } else if ( ret . status === NVMOperationStatus . Error_OperationMismatch ) { message += ": wrong operation requested." ; } throw new ZWaveError ( message , ZWaveErrorCodes . Controller_CommandError ) ; } return { endOfFile : ret . status === NVMOperationStatus . EndOfFile } ; } /**
     * **Z-Wave 700 series only**
     *
     * Opens the controller's external NVM for reading/writing and returns the NVM size
     */ public async externalNVMOpen ( ) { const ret = await this . driver . sendMessage < NVMOperationsResponse > ( new NVMOperationsOpenRequest ( this . driver ) ) ; if ( ! ret . isOK ( ) ) { throw new ZWaveError ( "Failed to open the external NVM" , ZWaveErrorCodes . Controller_CommandError ) ; } return ret . offsetOrSize ; } /**
     * **Z-Wave 700 series only**
     *
     * Closes the controller's external NVM
     */ public async externalNVMClose ( ) { const ret = await this . driver . sendMessage < NVMOperationsResponse > ( new NVMOperationsCloseRequest ( this . driver ) ) ; if ( ! ret . isOK ( ) ) { throw new ZWaveError ( "Failed to close the external NVM" , ZWaveErrorCodes . Controller_CommandError ) ; } } param onProgress returns public async backupNVMRaw ( onProgress ? ) { this . driver . controllerLog . print ( "Backing up NVM..." ) ; if ( ! ( await this . toggleRF ( false ) ) ) { throw new ZWaveError ( "Could not turn off the Z-Wave radio before creating NVM backup!" , ZWaveErrorCodes . Controller_ResponseNOK ) ; } let ret ; try { if ( this . serialApiGte ( "7.0" ) ) { ret = await this . backupNVMRaw700 ( onProgress ) ; } else { ret = await this . backupNVMRaw500 ( onProgress ) ; } this . driver . controllerLog . print ( "NVM backup completed" ) ; } finally { await this . toggleRF ( true ) ; } return ret ; } private async backupNVMRaw500 ( onProgress ? ) { const size = nvmSizeToBufferSize ( ( await this . getNVMId ( ) ) . memorySize ) ; if ( ! size ) { throw new ZWaveError ( "Unknown NVM size - cannot backup!" , ZWaveErrorCodes . Controller_NotSupported ) ; } const ret = Buffer . allocUnsafe ( size ) ; let offset = 0 ; let chunkSize = Math . min ( 0xffff , ret . length ) ; while ( offset < ret . length ) { const chunk = await this . externalNVMReadBuffer ( offset , Math . min ( chunkSize , ret . length - offset ) ) ; if ( chunk . length === 0 ) { chunkSize = 48 ; continue ; } chunk . copy ( ret , offset ) ; offset += chunk . length ; if ( chunkSize > chunk . length ) chunkSize = chunk . length ; if ( onProgress ) setImmediate ( ( ) => onProgress ( offset , size ) ) ; } return ret ; } private async backupNVMRaw700 ( onProgress ? ) { const size = await this . externalNVMOpen ( ) ; const ret = Buffer . allocUnsafe ( size ) ; let offset = 0 ; let chunkSize = Math . min ( 0xff , ret . length ) ; try { while ( offset < ret . length ) { const { buffer : chunk , endOfFile } = await this . externalNVMReadBuffer700 ( offset , Math . min ( chunkSize , ret . length - offset ) ) ; if ( chunkSize === 0xff && chunk . length === 0 ) { chunkSize = 48 ; continue ; } chunk . copy ( ret , offset ) ; offset += chunk . length ; if ( chunkSize > chunk . length ) chunkSize = chunk . length ; if ( onProgress ) setImmediate ( ( ) => onProgress ( offset , size ) ) ; if ( endOfFile ) break ; } } finally { await this . externalNVMClose ( ) ; } return ret ; } param nvmData param convertProgress param restoreProgress public async restoreNVM ( nvmData , convertProgress ? , restoreProgress ? ) { if ( ! ( await this . toggleRF ( false ) ) ) { throw new ZWaveError ( "Could not turn off the Z-Wave radio before restoring NVM backup!" , ZWaveErrorCodes . Controller_ResponseNOK ) ; } try { this . driver . controllerLog . print ( "Converting NVM to target format..." ) ; let targetNVM ; if ( this . serialApiGte ( "7.0" ) ) { targetNVM = await this . backupNVMRaw700 ( convertProgress ) ; } else { targetNVM = await this . backupNVMRaw500 ( convertProgress ) ; } const convertedNVM = migrateNVM ( nvmData , targetNVM ) ; this . driver . controllerLog . print ( "Restoring NVM backup..." ) ; if ( this . serialApiGte ( "7.0" ) ) { await this . restoreNVMRaw700 ( convertedNVM , restoreProgress ) ; } else { await this . restoreNVMRaw500 ( convertedNVM , restoreProgress ) ; } this . driver . controllerLog . print ( "NVM backup restored" ) ; } finally { await this . toggleRF ( true ) ; } this . _supportsSoftReset = undefined ; this . _nodes . clear ( ) ; await this . driver . saveNetworkToCache ( ) ; await this . driver . softResetAndRestart ( "Restarting driver to activate restored NVM backup..." , "Applying the NVM backup requires a driver restart!" ) ; } /**
     * Restores an NVM backup that was created with `backupNVMRaw`. The Z-Wave radio is turned off/on automatically.
     *
     * **WARNING:** The given buffer is NOT checked for compatibility with the current stick. To have Z-Wave JS do that, use the  restoreNVM method instead.
     *
     * **WARNING:** A failure during this process may brick your controller. Use at your own risk!
     *  param nvmData param onProgress public async restoreNVMRaw ( nvmData , onProgress ? ) { this . driver . controllerLog . print ( "Restoring NVM..." ) ; if ( ! ( await this . toggleRF ( false ) ) ) { throw new ZWaveError ( "Could not turn off the Z-Wave radio before restoring NVM backup!" , ZWaveErrorCodes . Controller_ResponseNOK ) ; } try { if ( this . serialApiGte ( "7.0" ) ) { await this . restoreNVMRaw700 ( nvmData , onProgress ) ; } else { await this . restoreNVMRaw500 ( nvmData , onProgress ) ; } this . driver . controllerLog . print ( "NVM backup restored" ) ; } finally { await this . toggleRF ( true ) ; } this . _supportsSoftReset = undefined ; this . driver . controllerLog . print ( "Restarting driver to activate restored NVM backup..." ) ; this . driver . emit ( "error" , new ZWaveError ( "Activating the NVM backup requires a driver restart!" , ZWaveErrorCodes . Driver_Failed ) ) ; await this . driver . destroy ( ) ; } private async restoreNVMRaw500 ( nvmData , onProgress ? ) { const size = nvmSizeToBufferSize ( ( await this . getNVMId ( ) ) . memorySize ) ; if ( ! size ) { throw new ZWaveError ( "Unknown NVM size - cannot restore!" , ZWaveErrorCodes . Controller_NotSupported ) ; } else if ( size !== nvmData . length ) { const actualSize = 1 + nvmData . readUInt16BE ( 0 ) ; if ( actualSize !== nvmData . length ) { throw new ZWaveError ( "The given data does not match the NVM size - cannot restore!" , ZWaveErrorCodes . Argument_Invalid ) ; } const oldSize = 1 + ( await this . externalNVMReadBuffer ( 0 , 2 ) ) . readUInt16BE ( 0 ) ; if ( oldSize > actualSize ) { nvmData = Buffer . concat ( [ nvmData , Buffer . alloc ( oldSize - actualSize , 0xff ) , ] ) ; } } const chunkSize = ( await this . externalNVMReadBuffer ( 0 , 0xffff ) ) . length - 5 ; for ( let offset = 0 ; offset < nvmData . length ; offset += chunkSize ) { await this . externalNVMWriteBuffer ( offset , nvmData . slice ( offset , offset + chunkSize ) ) ; if ( onProgress ) setImmediate ( ( ) => onProgress ( offset , nvmData . length ) ) ; } } private async restoreNVMRaw700 ( nvmData , onProgress ? ) { const size = await this . externalNVMOpen ( ) ; if ( size !== nvmData . length ) { throw new ZWaveError ( "The given data does not match the NVM size - cannot restore!" , ZWaveErrorCodes . Argument_Invalid ) ; } const chunkSize = ( await this . externalNVMReadBuffer700 ( 0 , 0xff ) ) . buffer . length || 48 ; await this . externalNVMClose ( ) ; await this . externalNVMOpen ( ) ; for ( let offset = 0 ; offset < nvmData . length ; offset += chunkSize ) { const { endOfFile } = await this . externalNVMWriteBuffer700 ( offset , nvmData . slice ( offset , offset + chunkSize ) ) ; if ( onProgress ) setImmediate ( ( ) => onProgress ( offset , size ) ) ; if ( endOfFile ) break ; } await this . externalNVMClose ( ) ; } /**
     * Request the most recent background RSSI levels detected by the controller.
     *
     * **Note:** This only returns useful values if something was transmitted recently.
     */ public async getBackgroundRSSI ( ) { const ret = await this . driver . sendMessage < GetBackgroundRSSIResponse > ( new GetBackgroundRSSIRequest ( this . driver ) ) ; return pick ( ret , [ "rssiChannel0" , "rssiChannel1" , "rssiChannel2" ] ) ; } }