import type { DeviceConfig , Notification , NotificationValueDefinition , } from "@zwave-js/config" ; import { actuatorCCs , applicationCCs , CacheMetadata , CacheValue , CommandClasses , CommandClassInfo , CRC16_CCITT , DataRate , dskFromString , dskToString , FLiRS , getCCName , getNodeMetaValueID , isTransmissionError , isZWaveError , Maybe , MetadataUpdatedArgs , NodeType , NodeUpdatePayload , nonApplicationCCs , normalizeValueID , ProtocolVersion , SecurityClass , securityClassIsS2 , securityClassOrder , SecurityClassOwner , sensorCCs , timespan , topologicalSort , unknownBoolean , ValueDB , ValueID , valueIdToString , ValueMetadata , ValueMetadataNumeric , ValueRemovedArgs , ValueUpdatedArgs , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { discreteBinarySearch , formatId , getEnumMemberName , getErrorMessage , JSONObject , Mixin , num2hex , ObjectKeyMap , pick , stringify , TypedEventEmitter , } from "@zwave-js/shared" ; import { padStart } from "alcalzone-shared/strings" ; import { isArray , isObject } from "alcalzone-shared/typeguards" ; import { randomBytes } from "crypto" ; import { EventEmitter } from "events" ; import { PowerlevelCCTestNodeReport } from "../commandclass" ; import type { CCAPI , PollValueImplementation , SetValueAPIOptions , } from "../commandclass/API" ; import { getHasLifelineValueId } from "../commandclass/AssociationCC" ; import { BasicCC , BasicCCReport , BasicCCSet , getCompatEventValueId as getBasicCCCompatEventValueId , getCurrentValueValueId as getBasicCCCurrentValueValueId , } from "../commandclass/BasicCC" ; import { CentralSceneCCNotification , CentralSceneKeys , getSceneValueId , getSlowRefreshValueId , } from "../commandclass/CentralSceneCC" ; import { ClockCCReport } from "../commandclass/ClockCC" ; import { CommandClass , getCCValueMetadata } from "../commandclass/CommandClass" ; import { DoorLockMode , getCurrentModeValueId as getCurrentLockModeValueId , } from "../commandclass/DoorLockCC" ; import { EntryControlCCNotification } from "../commandclass/EntryControlCC" ; import { FirmwareUpdateCapabilities , FirmwareUpdateMetaDataCC , FirmwareUpdateMetaDataCCGet , FirmwareUpdateMetaDataCCReport , FirmwareUpdateMetaDataCCStatusReport , FirmwareUpdateRequestStatus , FirmwareUpdateStatus , } from "../commandclass/FirmwareUpdateMetaDataCC" ; import { HailCC } from "../commandclass/HailCC" ; import { isCommandClassContainer } from "../commandclass/ICommandClassContainer" ; import { getLockedValueId } from "../commandclass/LockCC" ; import { getManufacturerIdValueId , getProductIdValueId , getProductTypeValueId , } from "../commandclass/ManufacturerSpecificCC" ; import { getEndpointCCsValueId , getEndpointDeviceClassValueId , getEndpointIndizesValueId , } from "../commandclass/MultiChannelCC" ; import { getNodeLocationValueId , getNodeNameValueId , } from "../commandclass/NodeNamingCC" ; import { getNotificationValueMetadata , getNotificationValueMetadataUnknownType , NotificationCC , NotificationCCReport , } from "../commandclass/NotificationCC" ; import { Powerlevel , PowerlevelTestStatus } from "../commandclass/PowerlevelCC" ; import { SceneActivationCCSet } from "../commandclass/SceneActivationCC" ; import { Security2CCNonceGet , Security2CCNonceReport , } from "../commandclass/Security2CC" ; import { SecurityCCNonceGet , SecurityCCNonceReport , } from "../commandclass/SecurityCC" ; import { getFirmwareVersionsValueId } from "../commandclass/VersionCC" ; import { getWakeUpIntervalValueId , getWakeUpOnDemandSupportedValueId , WakeUpCCWakeUpNotification , } from "../commandclass/WakeUpCC" ; import { getNodeTypeValueId , getRoleTypeValueId , getZWavePlusVersionValueId , ZWavePlusNodeType , ZWavePlusRoleType , } from "../commandclass/ZWavePlusCC" ; import { ApplicationUpdateRequest , ApplicationUpdateRequestNodeInfoReceived , ApplicationUpdateRequestNodeInfoRequestFailed , } from "../controller/ApplicationUpdateRequest" ; import { GetNodeProtocolInfoRequest , GetNodeProtocolInfoResponse , } from "../controller/GetNodeProtocolInfoMessages" ; import { RSSI , RssiError , TXReport } from "../controller/SendDataShared" ; import type { Driver , SendCommandOptions } from "../driver/Driver" ; import { Extended , interpretEx } from "../driver/StateMachineShared" ; import type { StatisticsEventCallbacksWithSelf } from "../driver/Statistics" ; import type { Transaction } from "../driver/Transaction" ; import { MessagePriority } from "../message/Constants" ; import { DeviceClass } from "./DeviceClass" ; import { Endpoint } from "./Endpoint" ; import { formatLifelineHealthCheckSummary , formatRouteHealthCheckSummary , healthCheckTestFrameCount , } from "./HealthCheck" ; import { createNodeReadyMachine , NodeReadyInterpreter , } from "./NodeReadyMachine" ; import { NodeStatistics , NodeStatisticsHost } from "./NodeStatistics" ; import { createNodeStatusMachine , NodeStatusInterpreter , nodeStatusMachineStateToNodeStatus , } from "./NodeStatusMachine" ; import { RequestNodeInfoRequest , RequestNodeInfoResponse , } from "./RequestNodeInfoMessages" ; import type { LifelineHealthCheckResult , LifelineHealthCheckSummary , RefreshInfoOptions , RouteHealthCheckResult , RouteHealthCheckSummary , TranslatedValueID , ZWaveNodeEventCallbacks , ZWaveNodeValueEventCallbacks , } from "./Types" ; import { InterviewStage , NodeStatus } from "./Types" ; export interface ZWaveNode extends TypedEventEmitter < ZWaveNodeEventCallbacks & StatisticsEventCallbacksWithSelf < ZWaveNode , NodeStatistics > > , NodeStatisticsHost { } /**
 * A ZWaveNode represents a node in a Z-Wave network. It is also an instance
 * of its root endpoint (index 0)
 */ @ Mixin ( [ EventEmitter , NodeStatisticsHost ] ) export class ZWaveNode extends Endpoint implements SecurityClassOwner { public constructor ( public readonly id , driver , deviceClass ? , supportedCCs = [ ] , controlledCCs = [ ] , valueDB ? ) { super ( id , driver , 0 , deviceClass , supportedCCs ) ; this . _valueDB = valueDB ?? new ValueDB ( id , driver . valueDB ! , driver . metadataDB ! ) ; for ( const event of [ "value added" , "value updated" , "value removed" , "value notification" , "metadata updated" , ] as const ) { this . _valueDB . on ( event , this . translateValueEvent . bind ( this , event ) ) ; } for ( const event of [ "value updated" , "value removed" ] as const ) { this . _valueDB . on ( event , ( args ) => { if ( "source" in args && args . source === "driver" ) return ; if ( this . cancelScheduledPoll ( args ) ) { this . driver . controllerLog . logNode ( this . nodeId , "Scheduled poll canceled because value was updated" , "verbose" ) ; } } ) ; } for ( const cc of controlledCCs ) this . addCC ( cc , { isControlled : true } ) ; this . statusMachine = interpretEx ( createNodeStatusMachine ( this ) ) ; this . statusMachine . onTransition ( ( state ) => { if ( state . changed ) { this . onStatusChange ( nodeStatusMachineStateToNodeStatus ( state . value as any ) ) ; } } ) ; this . statusMachine . start ( ) ; this . readyMachine = interpretEx ( createNodeReadyMachine ( ) ) ; this . readyMachine . onTransition ( ( state ) => { if ( state . changed ) { this . onReadyChange ( state . value === "ready" ) ; } } ) ; this . readyMachine . start ( ) ; } /**
     * Cleans up all resources used by this node
     */ public destroy ( ) { this . statusMachine . stop ( ) ; this . readyMachine . stop ( ) ; for ( const timeout of [ this . centralSceneKeyHeldDownContext ?. timeout , ... this . notificationIdleTimeouts . values ( ) , ... this . manualRefreshTimers . values ( ) , ] ) { if ( timeout ) clearTimeout ( timeout ) ; } for ( const valueId of this . scheduledPolls . keys ( ) ) { this . cancelScheduledPoll ( valueId ) ; } } /**
     * Enhances a value id so it can be consumed better by applications
     */ private translateValueID < T extends ValueID > ( valueId ) { const commandClassName = getCCName ( valueId . commandClass ) ; const ret = { commandClassName , ... valueId } ; const ccInstance = this . createCCInstanceInternal ( valueId . commandClass ) ; if ( ! ccInstance ) { throw new ZWaveError ( `Cannot translate a value ID for the non-implemented CC ${ getCCName ( valueId . commandClass ) }` , ZWaveErrorCodes . CC_NotImplemented ) ; } ret . propertyName = ccInstance . translateProperty ( valueId . property , valueId . propertyKey ) ; if ( valueId . propertyKey != undefined ) { const propertyKey = ccInstance . translatePropertyKey ( valueId . property , valueId . propertyKey ) ; ret . propertyKeyName = propertyKey ; } return ret ; } /**
     * Enhances the raw event args of the ValueDB so it can be consumed better by applications
     */ private translateValueEvent < T extends ValueID > ( eventName , arg ) { const outArg = this . translateValueID ( arg ) ; if ( "source" in outArg ) delete outArg . source ; if ( "metadata" in outArg ) { ( outArg as unknown as MetadataUpdatedArgs ) . metadata = this . getValueMetadata ( arg ) ; } const ccInstance = this . createCCInstanceInternal ( arg . commandClass ) ; const isInternalValue = ccInstance && ccInstance . isInternalValue ( arg . property as any ) ; if ( ( arg as any as ValueUpdatedArgs ) . source !== "driver" ) { const [ changeTarget , changeType ] = eventName . split ( " " ) ; const logArgument = { ... outArg , nodeId : this . nodeId , internal : isInternalValue } ; if ( changeTarget === "value" ) { this . driver . controllerLog . value ( changeType as any , logArgument as any ) ; } else if ( changeTarget === "metadata" ) { this . driver . controllerLog . metadataUpdated ( logArgument ) ; } } if ( isInternalValue ) return ; if ( ! arg . endpoint && applicationCCs . includes ( arg . commandClass ) && this . shouldHideRootApplicationCCValues ( ) ) { for ( const endpoint of this . getEndpointIndizes ( ) ) { const possiblyMirroredValueID = { ... pick ( arg , [ "commandClass" , "property" , "propertyKey" ] ) , endpoint } ; if ( this . valueDB . hasValue ( possiblyMirroredValueID ) ) return ; } } this . emit ( eventName , this , outArg as any ) ; } private statusMachine : Extended < NodeStatusInterpreter > ; private _status : NodeStatus = NodeStatus . Unknown ; private onStatusChange ( newStatus ) { if ( newStatus === this . _status ) return ; const oldStatus = this . _status ; this . _status = newStatus ; if ( this . _status === NodeStatus . Asleep ) { this . emit ( "sleep" , this , oldStatus ) ; } else if ( this . _status === NodeStatus . Awake ) { this . emit ( "wake up" , this , oldStatus ) ; } else if ( this . _status === NodeStatus . Dead ) { this . emit ( "dead" , this , oldStatus ) ; } else if ( this . _status === NodeStatus . Alive ) { this . emit ( "alive" , this , oldStatus ) ; } this . readyMachine . send ( this . _status !== NodeStatus . Unknown && this . _status !== NodeStatus . Dead ? "NOT_DEAD" : "MAYBE_DEAD" ) ; } /**
     * Which status the node is believed to be in
     */ public get status ( ) : NodeStatus { return this . _status ; } internal public markAsDead ( ) { this . statusMachine . send ( "DEAD" ) ; } internal public markAsAlive ( ) { this . statusMachine . send ( "ALIVE" ) ; } internal public markAsAsleep ( ) { this . statusMachine . send ( "ASLEEP" ) ; } internal public markAsAwake ( ) { this . statusMachine . send ( "AWAKE" ) ; } /** Returns a promise that resolves when the node wakes up the next time or immediately if the node is already awake. */ public waitForWakeup ( ) { if ( ! this . canSleep || ! this . supportsCC ( CommandClasses [ "Wake Up" ] ) ) { throw new ZWaveError ( `Node ${ this . id } does not support wakeup!` , ZWaveErrorCodes . CC_NotSupported ) ; } else if ( this . _status === NodeStatus . Awake ) { return Promise . resolve ( ) ; } return new Promise ( ( resolve ) => { this . once ( "wake up" , ( ) => resolve ( ) ) ; } ) ; } private readyMachine : Extended < NodeReadyInterpreter > ; private _ready : boolean = false ; private onReadyChange ( ready ) { if ( ready === this . _ready ) return ; this . _ready = ready ; if ( ready ) this . emit ( "ready" , this ) ; } /**
     * Whether the node is ready to be used
     */ public get ready ( ) : boolean { return this . _ready ; } private _isListening : boolean | undefined ; /** Whether this node is always listening or not */ public get isListening ( ) : boolean | undefined { return this . _isListening ; } private _isFrequentListening : FLiRS | undefined ; /** Indicates the wakeup interval if this node is a FLiRS node. `false` if it isn't. */ public get isFrequentListening ( ) : FLiRS | undefined { return this . _isFrequentListening ; } public get canSleep ( ) : boolean | undefined { if ( this . _isListening == undefined ) return undefined ; if ( this . _isFrequentListening == undefined ) return undefined ; return ! this . _isListening && ! this . _isFrequentListening ; } private _isRouting : boolean | undefined ; /** Whether the node supports routing/forwarding messages. */ public get isRouting ( ) : boolean | undefined { return this . _isRouting ; } private _supportedDataRates : readonly DataRate [ ] | undefined ; public get supportedDataRates ( ) : readonly DataRate [ ] | undefined { return this . _supportedDataRates ; } public get maxDataRate ( ) : DataRate | undefined { if ( this . _supportedDataRates ) { return Math . max ( ... this . _supportedDataRates ) as DataRate ; } } internal public readonly securityClasses = new Map < SecurityClass , boolean > ( ) ; private _dsk : Buffer | undefined ; /**
     * The device specific key (DSK) of this node in binary format.
     * This is only set if included with Security S2.
     */ public get dsk ( ) : Buffer | undefined { return this . _dsk ; } internal public set dsk ( value ) { this . _dsk = value ; } /** Whether the node was granted at least one security class */ public get isSecure ( ) : Maybe < boolean > { const securityClass = this . getHighestSecurityClass ( ) ; if ( securityClass == undefined ) return unknownBoolean ; if ( securityClass === SecurityClass . None ) return false ; return true ; } public hasSecurityClass ( securityClass ) { return this . securityClasses . get ( securityClass ) ?? unknownBoolean ; } /** Returns the highest security class this node was granted or `undefined` if that information isn't known yet */ public getHighestSecurityClass ( ) { if ( this . securityClasses . size === 0 ) return undefined ; let missingSome = false ; for ( const secClass of securityClassOrder ) { if ( this . securityClasses . get ( secClass ) === true ) return secClass ; if ( ! this . securityClasses . has ( secClass ) ) { missingSome = true ; } } return missingSome ? undefined : SecurityClass . None ; } private _protocolVersion : ProtocolVersion | undefined ; /** The Z-Wave protocol version this node implements */ public get protocolVersion ( ) : ProtocolVersion | undefined { return this . _protocolVersion ; } private _nodeType : NodeType | undefined ; /** Whether this node is a controller (can calculate routes) or an end node (relies on route info) */ public get nodeType ( ) : NodeType | undefined { return this . _nodeType ; } private _supportsSecurity : boolean | undefined ; /**
     * Whether this node supports security (S0 or S2).
     * **WARNING:** Nodes often report this incorrectly - do not blindly trust it.
     */ public get supportsSecurity ( ) : boolean | undefined { return this . _supportsSecurity ; } private _supportsBeaming : boolean | undefined ; /** Whether this node can issue wakeup beams to FLiRS nodes */ public get supportsBeaming ( ) : boolean | undefined { return this . _supportsBeaming ; } public get manufacturerId ( ) : number | undefined { return this . getValue ( getManufacturerIdValueId ( ) ) ; } public get productId ( ) : number | undefined { return this . getValue ( getProductIdValueId ( ) ) ; } public get productType ( ) : number | undefined { return this . getValue ( getProductTypeValueId ( ) ) ; } public get firmwareVersion ( ) : string | undefined { return this . getValue < string [ ] > ( getFirmwareVersionsValueId ( ) ) ?. [ 0 ] ; } public get zwavePlusVersion ( ) : number | undefined { return this . getValue ( getZWavePlusVersionValueId ( ) ) ; } public get zwavePlusNodeType ( ) : ZWavePlusNodeType | undefined { return this . getValue ( getNodeTypeValueId ( ) ) ; } public get zwavePlusRoleType ( ) : ZWavePlusRoleType | undefined { return this . getValue ( getRoleTypeValueId ( ) ) ; } public get supportsWakeUpOnDemand ( ) : boolean | undefined { return this . getValue ( getWakeUpOnDemandSupportedValueId ( ) ) ; } public get shouldRequestWakeUpOnDemand ( ) : boolean { return ( ! ! this . supportsWakeUpOnDemand && this . status === NodeStatus . Asleep && this . driver . hasPendingTransactions ( ( t ) => t . requestWakeUpOnDemand && t . message . getNodeId ( ) === this . id ) ) ; } /**
     * The user-defined name of this node. Uses the value reported by `Node Naming and Location CC` if it exists.
     *
     * **Note:** Setting this value only updates the name locally. To permanently change the name of the node, use
     * the `commandClasses` API.
     */ public get name ( ) : string | undefined { return this . getValue ( getNodeNameValueId ( ) ) ; } public set name ( value ) { if ( value != undefined ) { this . _valueDB . setValue ( getNodeNameValueId ( ) , value ) ; } else { this . _valueDB . removeValue ( getNodeNameValueId ( ) ) ; } } /**
     * The user-defined location of this node. Uses the value reported by `Node Naming and Location CC` if it exists.
     *
     * **Note:** Setting this value only updates the location locally. To permanently change the location of the node, use
     * the `commandClasses` API.
     */ public get location ( ) : string | undefined { return this . getValue ( getNodeLocationValueId ( ) ) ; } public set location ( value ) { if ( value != undefined ) { this . _valueDB . setValue ( getNodeLocationValueId ( ) , value ) ; } else { this . _valueDB . removeValue ( getNodeLocationValueId ( ) ) ; } } /** Whether a SUC return route was configured for this node */ public get hasSUCReturnRoute ( ) : boolean { return ! ! this . valueDB . getValue < boolean > ( getNodeMetaValueID ( "hasSUCReturnRoute" ) ) ; } public set hasSUCReturnRoute ( value ) { this . valueDB . setValue ( getNodeMetaValueID ( "hasSUCReturnRoute" ) , value ) ; } private _deviceConfig : DeviceConfig | undefined ; /**
     * Contains additional information about this node, loaded from a config file
     */ public get deviceConfig ( ) : DeviceConfig | undefined { return this . _deviceConfig ; } public get label ( ) : string | undefined { return this . _deviceConfig ?. label ; } public get deviceDatabaseUrl ( ) : string | undefined { if ( this . manufacturerId != undefined && this . productType != undefined && this . productId != undefined ) { const manufacturerId = formatId ( this . manufacturerId ) ; const productType = formatId ( this . productType ) ; const productId = formatId ( this . productId ) ; const firmwareVersion = this . firmwareVersion || "0.0" ; return `https://devices.zwave-js.io/?jumpTo=${ manufacturerId }:${ productType }:${ productId }:${ firmwareVersion }` ; } } private _valueDB : ValueDB ; internal public get valueDB ( ) : ValueDB { return this . _valueDB ; } /**
     * Retrieves a stored value for a given value id.
     * This does not request an updated value from the node!
     */ public getValue < T = unknown > ( valueId ) { return this . _valueDB . getValue ( valueId ) ; } /**
     * Retrieves metadata for a given value id.
     * This can be used to enhance the user interface of an application
     */ public getValueMetadata ( valueId ) { const { commandClass , property } = valueId ; return { ... getCCValueMetadata ( commandClass , property ) , ... this . _valueDB . getMetadata ( valueId ) } ; } /** Returns a list of all value names that are defined on all endpoints of this node */ public getDefinedValueIDs ( ) { let ret = [ ] ; const allowControlled = [ CommandClasses [ "Scene Activation" ] , ] ; for ( const endpoint of this . getAllEndpoints ( ) ) { for ( const [ cc , info ] of endpoint . implementedCommandClasses ) { if ( info . isSupported || ( info . isControlled && allowControlled . includes ( cc ) ) ) { const ccInstance = endpoint . createCCInstanceUnsafe ( cc ) ; if ( ccInstance ) { ret . push ( ... ccInstance . getDefinedValueIDs ( ) ) ; } } } } if ( this . shouldHideRootApplicationCCValues ( ) ) { ret = this . filterRootApplicationCCValueIDs ( ret ) ; } return ret . map ( ( id ) => this . translateValueID ( id ) ) ; } /** Determines whether the root application CC values should be hidden in favor of endpoint values */ private shouldHideRootApplicationCCValues ( ) { if ( this . _deviceConfig ?. compat ?. preserveRootApplicationCCValueIDs ) return false ; const endpointIndizes = this . getEndpointIndizes ( ) ; if ( endpointIndizes . length === 0 ) return false ; const preserveEndpoints = this . _deviceConfig ?. compat ?. preserveEndpoints ; if ( preserveEndpoints != undefined && preserveEndpoints !== "*" && preserveEndpoints . length !== endpointIndizes . length ) { return false ; } return true ; } private shouldHideRootValueID ( valueId , allValueIds ) { if ( ! ! valueId . endpoint ) return false ; if ( ! applicationCCs . includes ( valueId . commandClass ) ) return false ; const valueExistsOnAnotherEndpoint = allValueIds . some ( ( other ) => other . commandClass === valueId . commandClass && ! ! other . endpoint && other . property === valueId . property && other . propertyKey === valueId . propertyKey ) ; return valueExistsOnAnotherEndpoint ; } /**
     * Removes all Value IDs from an array that belong to a root endpoint and have a corresponding
     * Value ID on a non-root endpoint
     */ private filterRootApplicationCCValueIDs ( allValueIds ) { return allValueIds . filter ( ( vid ) => ! this . shouldHideRootValueID ( vid , allValueIds ) ) ; } /**
     * Updates a value for a given property of a given CommandClass on the node.
     * This will communicate with the node!
     */ public async setValue ( valueId , value , options ? ) { try { const endpointInstance = this . getEndpoint ( valueId . endpoint || 0 ) ; if ( ! endpointInstance ) return false ; const api = ( endpointInstance . commandClasses as any ) [ valueId . commandClass ] as CCAPI ; if ( ! api . setValue ) return false ; await api . setValue ( { property : valueId . property , propertyKey : valueId . propertyKey } , value , options ) ; if ( api . isSetValueOptimistic ( valueId ) ) { this . _valueDB . setValue ( valueId , value , ! ! this . driver . options . emitValueUpdateAfterSetValue ? { source : "driver" } : { noEvent : true } ) ; } return true ; } catch ( e ) { if ( isZWaveError ( e ) ) { let handled = false ; let emitErrorEvent = false ; switch ( e . code ) { case ZWaveErrorCodes . CC_NotImplemented : case ZWaveErrorCodes . CC_NoAPI : handled = true ; break ; case ZWaveErrorCodes . Argument_Invalid : handled = true ; emitErrorEvent = true ; break ; } if ( emitErrorEvent ) this . driver . emit ( "error" , e ) ; if ( handled ) return false ; } throw e ; } } /**
     * Requests a value for a given property of a given CommandClass by polling the node.
     * **Warning:** Some value IDs share a command, so make sure not to blindly call this for every property
     */ public pollValue < T = unknown > ( valueId , sendCommandOptions = { } ) { const endpointInstance = this . getEndpoint ( valueId . endpoint || 0 ) ; if ( ! endpointInstance ) { throw new ZWaveError ( `Endpoint ${ valueId . endpoint } does not exist on Node ${ this . id }` , ZWaveErrorCodes . Argument_Invalid ) ; } const api = ( ( endpointInstance . commandClasses as any ) [ valueId . commandClass ] as CCAPI ) . withOptions ( { maxSendAttempts : 1 , priority : MessagePriority . Poll , ... sendCommandOptions } ) ; if ( ! api . pollValue ) { throw new ZWaveError ( `The pollValue API is not implemented for CC ${ getCCName ( valueId . commandClass ) }!` , ZWaveErrorCodes . CC_NoAPI ) ; } return ( api . pollValue as PollValueImplementation < T > ) ( { property : valueId . property , propertyKey : valueId . propertyKey } ) ; } internal public scheduledPolls = new ObjectKeyMap < ValueID , NodeJS . Timeout > ( ) ; internal returns public schedulePoll ( valueId , timeoutMs = this . driver . options . timeouts . refreshValue ) { valueId = normalizeValueID ( valueId ) ; const endpointInstance = this . getEndpoint ( valueId . endpoint || 0 ) ; if ( ! endpointInstance ) return false ; const api = ( ( endpointInstance . commandClasses as any ) [ valueId . commandClass ] as CCAPI ) . withOptions ( { maxSendAttempts : 1 , priority : MessagePriority . Poll } ) ; if ( ! api . pollValue ) return false ; this . cancelScheduledPoll ( valueId ) ; this . scheduledPolls . set ( valueId , setTimeout ( async ( ) => { this . cancelScheduledPoll ( valueId ) ; try { await api . pollValue ! ( valueId ) ; } catch { } } , timeoutMs ) . unref ( ) ) ; return true ; } internal public cancelScheduledPoll ( valueId ) { valueId = normalizeValueID ( valueId ) ; if ( this . scheduledPolls . has ( valueId ) ) { clearTimeout ( this . scheduledPolls . get ( valueId ) ! ) ; this . scheduledPolls . delete ( valueId ) ; return true ; } return false ; } public get endpointCountIsDynamic ( ) : boolean | undefined { return this . getValue ( { commandClass : CommandClasses [ "Multi Channel" ] , property : "countIsDynamic" } ) ; } public get endpointsHaveIdenticalCapabilities ( ) : boolean | undefined { return this . getValue ( { commandClass : CommandClasses [ "Multi Channel" ] , property : "identicalCapabilities" } ) ; } public get individualEndpointCount ( ) : number | undefined { return this . getValue ( { commandClass : CommandClasses [ "Multi Channel" ] , property : "individualCount" } ) ; } public get aggregatedEndpointCount ( ) : number | undefined { return this . getValue ( { commandClass : CommandClasses [ "Multi Channel" ] , property : "aggregatedCount" } ) ; } /** Returns the device class of an endpoint. Falls back to the node's device class if the information is not known. */ private getEndpointDeviceClass ( index ) { const deviceClass = this . getValue < { generic : number ; specific : number ; } > ( getEndpointDeviceClassValueId ( this . endpointsHaveIdenticalCapabilities ? 1 : index ) ) ; if ( deviceClass && this . _deviceClass ) { return new DeviceClass ( this . driver . configManager , this . _deviceClass . basic . key , deviceClass . generic , deviceClass . specific ) ; } return this . _deviceClass ; } private getEndpointCCs ( index ) { const ret = this . getValue ( getEndpointCCsValueId ( this . endpointsHaveIdenticalCapabilities ? 1 : index ) ) ; if ( isArray ( ret ) ) { return ret as CommandClasses [ ] ; } else if ( isObject ( ret ) && "supportedCCs" in ret ) { return ret . supportedCCs as CommandClasses [ ] ; } } /**
     * Returns the current endpoint count of this node.
     *
     * If you want to enumerate the existing endpoints, use `getEndpointIndizes` instead.
     * Some devices are known to contradict themselves.
     */ public getEndpointCount ( ) { return ( ( this . individualEndpointCount || 0 ) + ( this . aggregatedEndpointCount || 0 ) ) ; } /**
     * Returns indizes of all endpoints on the node.
     */ public getEndpointIndizes ( ) { let ret = this . getValue < number [ ] > ( getEndpointIndizesValueId ( ) ) ; if ( ! ret ) { ret = [ ] ; for ( let i = 1 ; i <= this . getEndpointCount ( ) ; i ++ ) { ret . push ( i ) ; } } return ret ; } /** Whether the Multi Channel CC has been interviewed and all endpoint information is known */ private get isMultiChannelInterviewComplete ( ) : boolean { return ! ! this . getValue ( { commandClass : CommandClasses [ "Multi Channel" ] , endpoint : 0 , property : "interviewComplete" } ) ; } /** Cache for this node's endpoint instances */ private _endpointInstances = new Map < number , Endpoint > ( ) ; /**
     * Returns an endpoint of this node with the given index. 0 returns the node itself.
     */ public getEndpoint ( index ) ; public getEndpoint ( index ) ; public getEndpoint ( index ) { if ( index < 0 ) throw new ZWaveError ( "The endpoint index must be positive!" , ZWaveErrorCodes . Argument_Invalid ) ; if ( index === 0 ) return this ; if ( ! this . isMultiChannelInterviewComplete ) { this . driver . driverLog . print ( `Node ${ this . nodeId }, Endpoint ${ index }: Trying to access endpoint instance before Multi Channel interview` , "error" ) ; return undefined ; } if ( ! this . getEndpointIndizes ( ) . includes ( index ) ) return undefined ; if ( ! this . _endpointInstances . has ( index ) ) { this . _endpointInstances . set ( index , new Endpoint ( this . id , this . driver , index , this . getEndpointDeviceClass ( index ) , this . getEndpointCCs ( index ) ) ) ; } return this . _endpointInstances . get ( index ) ! ; } public getEndpointOrThrow ( index ) { const ret = this . getEndpoint ( index ) ; if ( ! ret ) { throw new ZWaveError ( `Endpoint ${ index } does not exist on Node ${ this . id }` , ZWaveErrorCodes . Controller_EndpointNotFound ) ; } return ret ; } /** Returns a list of all endpoints of this node, including the root endpoint (index 0) */ public getAllEndpoints ( ) { const ret = [ this ] ; if ( this . isMultiChannelInterviewComplete ) { for ( const i of this . getEndpointIndizes ( ) ) { const endpoint = this . getEndpoint ( i ) ; if ( endpoint ) ret . push ( endpoint ) ; } } return ret ; } /**
     * This tells us which interview stage was last completed
     */ public interviewStage : InterviewStage = InterviewStage . None ; private _interviewAttempts : number = 0 ; /** How many attempts to interview this node have already been made */ public get interviewAttempts ( ) : number { return this . _interviewAttempts ; } private _hasEmittedNoS2NetworkKeyError : boolean = false ; private _hasEmittedNoS0NetworkKeyError : boolean = false ; /** Utility function to check if this node is the controller */ public isControllerNode ( ) { return this . id === this . driver . controller . ownNodeId ; } /**
     * Resets all information about this node and forces a fresh interview.
     * **Note:** This does nothing for the controller node.
     *
     * **WARNING:** Take care NOT to call this method when the node is already being interviewed.
     * Otherwise the node information may become inconsistent.
     */ public async refreshInfo ( options = { } ) { if ( this . isControllerNode ( ) ) return ; const { resetSecurityClasses = false , waitForWakeup = true } = options ; if ( waitForWakeup && this . canSleep && this . supportsCC ( CommandClasses [ "Wake Up" ] ) ) { await this . waitForWakeup ( ) . catch ( ( ) => { } ) ; } const name = this . name ; const location = this . location ; if ( resetSecurityClasses ) this . securityClasses . clear ( ) ; this . _interviewAttempts = 0 ; this . interviewStage = InterviewStage . None ; this . _ready = false ; this . _deviceClass = undefined ; this . _isListening = undefined ; this . _isFrequentListening = undefined ; this . _isRouting = undefined ; this . _supportedDataRates = undefined ; this . _protocolVersion = undefined ; this . _nodeType = undefined ; this . _supportsSecurity = undefined ; this . _supportsBeaming = undefined ; this . _deviceConfig = undefined ; this . _hasEmittedNoS0NetworkKeyError = false ; this . _hasEmittedNoS2NetworkKeyError = false ; this . _valueDB . clear ( { noEvent : true } ) ; this . _endpointInstances . clear ( ) ; super . reset ( ) ; this . readyMachine . restart ( ) ; this . statusMachine . restart ( ) ; for ( const valueId of this . scheduledPolls . keys ( ) ) { this . cancelScheduledPoll ( valueId ) ; } if ( name != undefined ) this . name = name ; if ( location != undefined ) this . location = location ; await this . driver . saveNetworkToCache ( ) ; this . keepAwake = false ; void this . driver . interviewNode ( this ) ; } internal public async interview ( ) { if ( this . interviewStage === InterviewStage . Complete ) { this . driver . controllerLog . logNode ( this . id , `skipping interview because it is already completed` ) ; return true ; } else { this . driver . controllerLog . interviewStart ( this ) ; } this . _interviewAttempts ++ ; const tryInterviewStage = async ( method ) : Promise < boolean > => { try { await method ( ) ; return true ; } catch ( e ) { if ( isTransmissionError ( e ) ) { return false ; } throw e ; } } ; if ( this . interviewStage === InterviewStage . None ) { this . driver . controllerLog . logNode ( this . id , `new node, doing a full interview...` ) ; this . emit ( "interview started" , this ) ; await this . queryProtocolInfo ( ) ; } if ( ! this . isControllerNode ( ) ) { if ( ( this . isListening || this . isFrequentListening ) && this . status !== NodeStatus . Alive ) { await this . ping ( ) ; } if ( this . interviewStage === InterviewStage . ProtocolInfo ) { if ( ! ( await tryInterviewStage ( ( ) => this . queryNodeInfo ( ) ) ) ) { return false ; } } if ( this . interviewStage === InterviewStage . NodeInfo ) { if ( await this . interviewCCs ( ) ) { await this . setInterviewStage ( InterviewStage . CommandClasses ) ; } else { return false ; } } } if ( ( this . isControllerNode ( ) && this . interviewStage === InterviewStage . ProtocolInfo ) || ( ! this . isControllerNode ( ) && this . interviewStage === InterviewStage . CommandClasses ) ) { await this . overwriteConfig ( ) ; } await this . setInterviewStage ( InterviewStage . Complete ) ; this . readyMachine . send ( "INTERVIEW_DONE" ) ; this . emit ( "interview completed" , this ) ; return true ; } /** Updates this node's interview stage and saves to cache when appropriate */ private async setInterviewStage ( completedStage ) { this . interviewStage = completedStage ; this . emit ( "interview stage completed" , this , getEnumMemberName ( InterviewStage , completedStage ) ) ; switch ( completedStage ) { case InterviewStage . ProtocolInfo : case InterviewStage . NodeInfo : case InterviewStage . CommandClasses : case InterviewStage . Complete : await this . driver . saveNetworkToCache ( ) ; } this . driver . controllerLog . interviewStage ( this ) ; } /** Step #1 of the node interview */ protected async queryProtocolInfo ( ) { this . driver . controllerLog . logNode ( this . id , { message : "querying protocol info..." , direction : "outbound" } ) ; const resp = await this . driver . sendMessage < GetNodeProtocolInfoResponse > ( new GetNodeProtocolInfoRequest ( this . driver , { requestedNodeId : this . id } ) ) ; this . _isListening = resp . isListening ; this . _isFrequentListening = resp . isFrequentListening ; this . _isRouting = resp . isRouting ; this . _supportedDataRates = resp . supportedDataRates ; this . _protocolVersion = resp . protocolVersion ; this . _nodeType = resp . nodeType ; this . _supportsSecurity = resp . supportsSecurity ; this . _supportsBeaming = resp . supportsBeaming ; this . applyDeviceClass ( resp . deviceClass ) ; const logMessage = `received response for protocol info:
basic device class:    ${ this . deviceClass ! . basic . label }
generic device class:  ${ this . deviceClass ! . generic . label }
specific device class: ${ this . deviceClass ! . specific . label }
node type:             ${ getEnumMemberName ( NodeType , this . _nodeType ) }
is always listening:   ${ this . isListening }
is frequent listening: ${ this . isFrequentListening }
can route messages:    ${ this . isRouting }
supports security:     ${ this . _supportsSecurity }
supports beaming:      ${ this . _supportsBeaming }
maximum data rate:     ${ this . maxDataRate } kbps
protocol version:      ${ this . _protocolVersion }` ; this . driver . controllerLog . logNode ( this . id , { message : logMessage , direction : "inbound" } ) ; if ( this . canSleep ) { if ( this . status === NodeStatus . Alive ) { this . markAsAwake ( ) ; } else { this . markAsAsleep ( ) ; } } await this . setInterviewStage ( InterviewStage . ProtocolInfo ) ; } /** Node interview: pings the node to see if it responds */ public async ping ( ) { if ( this . isControllerNode ( ) ) { this . driver . controllerLog . logNode ( this . id , "is the controller node, cannot ping" , "warn" ) ; return true ; } this . driver . controllerLog . logNode ( this . id , { message : "pinging the node..." , direction : "outbound" } ) ; try { await this . commandClasses [ "No Operation" ] . send ( ) ; this . driver . controllerLog . logNode ( this . id , { message : "ping successful" , direction : "inbound" } ) ; return true ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , `ping failed: ${ getErrorMessage ( e ) }` ) ; return false ; } } /**
     * Step #5 of the node interview
     * Request node info
     */ protected async queryNodeInfo ( ) { if ( this . isControllerNode ( ) ) { this . driver . controllerLog . logNode ( this . id , "is the controller node, cannot query node info" , "warn" ) ; return ; } this . driver . controllerLog . logNode ( this . id , { message : "querying node info..." , direction : "outbound" } ) ; const resp = await this . driver . sendMessage < RequestNodeInfoResponse | ApplicationUpdateRequest > ( new RequestNodeInfoRequest ( this . driver , { nodeId : this . id } ) ) ; if ( resp instanceof RequestNodeInfoResponse && ! resp . wasSent ) { this . driver . controllerLog . logNode ( this . id , `Querying the node info failed` , "error" ) ; throw new ZWaveError ( `Querying the node info failed` , ZWaveErrorCodes . Controller_ResponseNOK ) ; } else if ( resp instanceof ApplicationUpdateRequestNodeInfoRequestFailed ) { this . driver . controllerLog . logNode ( this . id , `Querying the node info failed` , "error" ) ; throw new ZWaveError ( `Querying the node info failed` , ZWaveErrorCodes . Controller_CallbackNOK ) ; } else if ( resp instanceof ApplicationUpdateRequestNodeInfoReceived ) { const logLines = [ "node info received" , "supported CCs:" ] ; for ( const cc of resp . nodeInformation . supportedCCs ) { const ccName = CommandClasses [ cc ] ; logLines . push ( `· ${ ccName ? ccName : num2hex ( cc ) }` ) ; } logLines . push ( "controlled CCs:" ) ; for ( const cc of resp . nodeInformation . controlledCCs ) { const ccName = CommandClasses [ cc ] ; logLines . push ( `· ${ ccName ? ccName : num2hex ( cc ) }` ) ; } this . driver . controllerLog . logNode ( this . id , { message : logLines . join ( "\n" ) , direction : "inbound" } ) ; this . updateNodeInfo ( resp . nodeInformation ) ; } await this . setInterviewStage ( InterviewStage . NodeInfo ) ; } /**
     * Loads the device configuration for this node from a config file
     */ protected async loadDeviceConfig ( ) { if ( this . manufacturerId != undefined && this . productType != undefined && this . productId != undefined ) { this . _deviceConfig = await this . driver . configManager . lookupDevice ( this . manufacturerId , this . productType , this . productId , this . firmwareVersion ) ; if ( this . _deviceConfig ) { this . driver . controllerLog . logNode ( this . id , `${ this . _deviceConfig . isEmbedded ? "Embedded" : "User-provided" } device config loaded` ) ; } else { this . driver . controllerLog . logNode ( this . id , "No device config found" , "warn" ) ; } } } /** Step #? of the node interview */ protected async interviewCCs ( ) { if ( this . isControllerNode ( ) ) { this . driver . controllerLog . logNode ( this . id , "is the controller node, cannot interview CCs" , "warn" ) ; return true ; } const interviewEndpoint = async ( endpoint , cc ) : Promise < "continue" | false | void > => { let instance ; try { instance = endpoint . createCCInstance ( cc ) ! ; } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . CC_NotSupported ) { return "continue" ; } throw e ; } if ( endpoint . isCCSecure ( cc ) && ! this . driver . securityManager && ! this . driver . securityManager2 ) { this . driver . controllerLog . logNode ( this . id , `Skipping interview for secure CC ${ getCCName ( cc ) } because no network key is configured!` , "error" ) ; return "continue" ; } if ( instance . interviewComplete ) return "continue" ; try { await instance . interview ( ) ; } catch ( e ) { if ( isTransmissionError ( e ) ) { return false ; } throw e ; } try { await this . driver . saveNetworkToCache ( ) ; } catch ( e ) { this . driver . controllerLog . print ( `${ getCCName ( cc ) }: Error after interview:\n${ getErrorMessage ( e ) }` , "error" ) ; } } ; if ( this . supportsCC ( CommandClasses [ "Security 2" ] ) ) { this . addCC ( CommandClasses [ "Security 2" ] , { secure : true } ) ; const securityClass = this . getHighestSecurityClass ( ) ; if ( securityClass == undefined || securityClassIsS2 ( securityClass ) ) { if ( ! this . driver . securityManager2 ) { if ( ! this . _hasEmittedNoS2NetworkKeyError ) { const errorMessage = `supports Security S2, but no S2 network keys were configured. The interview might not include all functionality.` ; this . driver . controllerLog . logNode ( this . nodeId , errorMessage , "error" ) ; this . driver . emit ( "error" , new ZWaveError ( `Node ${ padStart ( this . id . toString ( ) , 3 , "0" ) } ${ errorMessage }` , ZWaveErrorCodes . Controller_NodeInsecureCommunication ) ) ; this . _hasEmittedNoS2NetworkKeyError = true ; } } else { await interviewEndpoint ( this , CommandClasses [ "Security 2" ] ) ; } } } else { for ( const secClass of [ SecurityClass . S2_AccessControl , SecurityClass . S2_Authenticated , SecurityClass . S2_Unauthenticated , ] as const ) { if ( this . hasSecurityClass ( secClass ) === unknownBoolean ) { this . securityClasses . set ( secClass , false ) ; } } } if ( this . supportsCC ( CommandClasses . Security ) ) { this . addCC ( CommandClasses . Security , { secure : true } ) ; if ( this . hasSecurityClass ( SecurityClass . S0_Legacy ) !== false ) { if ( ! this . driver . securityManager ) { if ( ! this . _hasEmittedNoS0NetworkKeyError ) { const errorMessage = `supports Security S0, but the S0 network key was not configured. The interview might not include all functionality.` ; this . driver . controllerLog . logNode ( this . nodeId , errorMessage , "error" ) ; this . driver . emit ( "error" , new ZWaveError ( `Node ${ padStart ( this . id . toString ( ) , 3 , "0" ) } ${ errorMessage }` , ZWaveErrorCodes . Controller_NodeInsecureCommunication ) ) ; this . _hasEmittedNoS0NetworkKeyError = true ; } } else { await interviewEndpoint ( this , CommandClasses . Security ) ; } } } else { if ( this . hasSecurityClass ( SecurityClass . S0_Legacy ) === unknownBoolean ) { this . securityClasses . set ( SecurityClass . S0_Legacy , false ) ; } } if ( this . supportsCC ( CommandClasses [ "Manufacturer Specific" ] ) ) { await interviewEndpoint ( this , CommandClasses [ "Manufacturer Specific" ] ) ; } if ( this . supportsCC ( CommandClasses . Version ) ) { await interviewEndpoint ( this , CommandClasses . Version ) ; await this . loadDeviceConfig ( ) ; this . applyCommandClassesCompatFlag ( ) ; } const compat = this . _deviceConfig ?. compat ; if ( compat ?. treatBasicSetAsEvent ) { this . addCC ( CommandClasses . Basic , { isSupported : true , version : 1 } ) ; } else if ( ! compat ?. disableBasicMapping ) { this . hideBasicCCInFavorOfActuatorCCs ( ) ; } const rootInterviewGraphBeforeEndpoints = this . buildCCInterviewGraph ( [ CommandClasses . Security , CommandClasses [ "Security 2" ] , CommandClasses [ "Manufacturer Specific" ] , CommandClasses . Version , ... applicationCCs , ] ) ; let rootInterviewOrderBeforeEndpoints ; const rootInterviewGraphAfterEndpoints = this . buildCCInterviewGraph ( [ CommandClasses . Security , CommandClasses [ "Security 2" ] , CommandClasses [ "Manufacturer Specific" ] , CommandClasses . Version , ... nonApplicationCCs , ] ) ; let rootInterviewOrderAfterEndpoints ; try { rootInterviewOrderBeforeEndpoints = topologicalSort ( rootInterviewGraphBeforeEndpoints ) ; rootInterviewOrderAfterEndpoints = topologicalSort ( rootInterviewGraphAfterEndpoints ) ; } catch ( e ) { throw new ZWaveError ( "The CC interview cannot be completed because there are circular dependencies between CCs!" , ZWaveErrorCodes . CC_Invalid ) ; } for ( const cc of rootInterviewOrderBeforeEndpoints ) { const action = await interviewEndpoint ( this , cc ) ; if ( action === "continue" ) continue ; else if ( typeof action === "boolean" ) return action ; } this . applyCommandClassesCompatFlag ( ) ; for ( const endpointIndex of this . getEndpointIndizes ( ) ) { const endpoint = this . getEndpoint ( endpointIndex ) ; if ( ! endpoint ) continue ; const securityClass = this . getHighestSecurityClass ( ) ; if ( endpoint . supportsCC ( CommandClasses [ "Security 2" ] ) ) { endpoint . addCC ( CommandClasses [ "Security 2" ] , { secure : true } ) ; if ( securityClass != undefined && securityClassIsS2 ( securityClass ) && ! ! this . driver . securityManager2 ) { const action = await interviewEndpoint ( endpoint , CommandClasses [ "Security 2" ] ) ; if ( typeof action === "boolean" ) return action ; } } if ( endpoint . supportsCC ( CommandClasses . Security ) ) { endpoint . addCC ( CommandClasses . Security , { secure : true } ) ; if ( securityClass === SecurityClass . S0_Legacy && ! ! this . driver . securityManager ) { const action = await interviewEndpoint ( endpoint , CommandClasses . Security ) ; if ( typeof action === "boolean" ) return action ; } } if ( endpoint . supportsCC ( CommandClasses . Security ) && this . hasSecurityClass ( SecurityClass . S0_Legacy ) === true && this . driver . securityManager ) { endpoint . addCC ( CommandClasses . Security , { secure : true } ) ; } this . applyCommandClassesCompatFlag ( endpoint . index ) ; if ( ! compat ?. disableBasicMapping && ! compat ?. treatBasicSetAsEvent ) { endpoint . hideBasicCCInFavorOfActuatorCCs ( ) ; } const endpointInterviewGraph = endpoint . buildCCInterviewGraph ( [ CommandClasses . Security , CommandClasses [ "Security 2" ] , ] ) ; let endpointInterviewOrder ; try { endpointInterviewOrder = topologicalSort ( endpointInterviewGraph ) ; } catch ( e ) { throw new ZWaveError ( "The CC interview cannot be completed because there are circular dependencies between CCs!" , ZWaveErrorCodes . CC_Invalid ) ; } for ( const cc of endpointInterviewOrder ) { const action = await interviewEndpoint ( endpoint , cc ) ; if ( action === "continue" ) continue ; else if ( typeof action === "boolean" ) return action ; } } for ( const cc of rootInterviewOrderAfterEndpoints ) { const action = await interviewEndpoint ( this , cc ) ; if ( action === "continue" ) continue ; else if ( typeof action === "boolean" ) return action ; } return true ; } internal public updateNodeInfo ( nodeInfo ) { if ( this . interviewStage < InterviewStage . NodeInfo ) { for ( const cc of nodeInfo . supportedCCs ) this . addCC ( cc , { isSupported : true } ) ; for ( const cc of nodeInfo . controlledCCs ) this . addCC ( cc , { isControlled : true } ) ; } this . markAsAwake ( ) ; if ( this . requiresManualValueRefresh ( ) ) { const delay = this . deviceConfig ?. compat ?. manualValueRefreshDelayMs || 0 ; this . driver . controllerLog . logNode ( this . nodeId , { message : `Node does not send unsolicited updates; refreshing actuator and sensor values${ delay > 0 ? ` in ${ delay } ms` : "" }...` } ) ; setTimeout ( ( ) => this . refreshValues ( ) , delay ) ; } } /** Returns whether a manual refresh of non-static values is likely necessary for this node */ public requiresManualValueRefresh ( ) { return ( this . interviewStage === InterviewStage . Complete && ! this . supportsCC ( CommandClasses [ "Z-Wave Plus Info" ] ) && ! this . valueDB . getValue ( getHasLifelineValueId ( ) ) ) ; } internal public scheduleManualValueRefreshes ( ) { if ( ! this . canSleep ) return ; if ( ! this . requiresManualValueRefresh ( ) ) return ; this . scheduleManualValueRefresh ( CommandClasses . Battery , timespan . days ( 7 ) ) ; this . scheduleManualValueRefresh ( CommandClasses . Meter , timespan . hours ( 6 ) ) ; this . scheduleManualValueRefresh ( CommandClasses [ "Multilevel Sensor" ] , timespan . hours ( 6 ) ) ; if ( this . supportsCC ( CommandClasses . Notification ) && this . createCCInstance ( NotificationCC ) ?. notificationMode === "pull" ) { this . scheduleManualValueRefresh ( CommandClasses . Notification , timespan . hours ( 6 ) ) ; } } private manualRefreshTimers = new Map < CommandClasses , NodeJS . Timeout > ( ) ; /**
     * Is used to schedule a manual value refresh for nodes that don't send unsolicited commands
     */ private scheduleManualValueRefresh ( cc , timeout ) { this . manualRefreshTimers . set ( cc , setInterval ( ( ) => { void this . refreshCCValues ( cc ) ; } , timeout ) . unref ( ) ) ; } private cancelManualValueRefresh ( cc ) { if ( this . manualRefreshTimers . has ( cc ) ) { const timeout = this . manualRefreshTimers . get ( cc ) ! ; clearTimeout ( timeout ) ; this . manualRefreshTimers . delete ( cc ) ; } } /**
     * Rediscovers all capabilities of a single CC on this node and all endpoints.
     * This can be considered a more targeted variant of `refreshInfo`.
     *
     * WARNING: It is not recommended to await this method!
     */ public async interviewCC ( cc ) { const endpoints = this . getAllEndpoints ( ) ; endpoints . push ( endpoints . shift ( ) ! ) ; for ( const endpoint of endpoints ) { const instance = endpoint . createCCInstanceUnsafe ( cc ) ; if ( instance ) { try { await instance . interview ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , `failed to interview CC ${ getCCName ( cc ) }, endpoint ${ endpoint . index }: ${ getErrorMessage ( e ) }` , "error" ) ; } } } } /**
     * Refreshes all non-static values of a single CC from this node (all endpoints).
     * WARNING: It is not recommended to await this method!
     */ public async refreshCCValues ( cc ) { for ( const endpoint of this . getAllEndpoints ( ) ) { const instance = endpoint . createCCInstanceUnsafe ( cc ) ; if ( instance ) { try { await instance . refreshValues ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , `failed to refresh values for ${ getCCName ( cc ) }, endpoint ${ endpoint . index }: ${ getErrorMessage ( e ) }` , "error" ) ; } } } } /**
     * Refreshes all non-static values from this node's actuator and sensor CCs.
     * WARNING: It is not recommended to await this method!
     */ public async refreshValues ( ) { for ( const endpoint of this . getAllEndpoints ( ) ) { for ( const cc of endpoint . getSupportedCCInstances ( ) ) { if ( ! actuatorCCs . includes ( cc . ccId ) && ! sensorCCs . includes ( cc . ccId ) ) { continue ; } try { await cc . refreshValues ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , `failed to refresh values for ${ getCCName ( cc . ccId ) }, endpoint ${ endpoint . index }: ${ getErrorMessage ( e ) }` , "error" ) ; } } } } param endpointIndex private applyCommandClassesCompatFlag ( endpointIndex ? ) { if ( this . deviceConfig ) { const addCCs = this . deviceConfig . compat ?. addCCs ; if ( addCCs ) { for ( const [ cc , { endpoints } ] of addCCs ) { if ( endpointIndex === undefined ) { for ( const [ ep , info ] of endpoints ) { this . getEndpoint ( ep ) ?. addCC ( cc , info ) ; } } else if ( endpoints . has ( endpointIndex ) ) { this . getEndpoint ( endpointIndex ) ?. addCC ( cc , endpoints . get ( endpointIndex ) ! ) ; } } } const removeCCs = this . deviceConfig . compat ?. removeCCs ; if ( removeCCs ) { for ( const [ cc , endpoints ] of removeCCs ) { if ( endpoints === "*" ) { if ( endpointIndex === undefined ) { for ( const ep of this . getAllEndpoints ( ) ) { ep . removeCC ( cc ) ; } } else { this . getEndpoint ( endpointIndex ) ?. removeCC ( cc ) ; } } else { if ( endpointIndex === undefined ) { for ( const ep of endpoints ) { this . getEndpoint ( ep ) ?. removeCC ( cc ) ; } } else if ( endpoints . includes ( endpointIndex ) ) { this . getEndpoint ( endpointIndex ) ?. removeCC ( cc ) ; } } } } } } /** Overwrites the reported configuration with information from a config file */ protected async overwriteConfig ( ) { if ( this . isControllerNode ( ) ) { await this . loadDeviceConfig ( ) ; } if ( this . deviceConfig ) { const addCCs = this . deviceConfig . compat ?. addCCs ; if ( addCCs ) { for ( const [ cc , { endpoints } ] of addCCs ) { for ( const [ ep , info ] of endpoints ) { this . getEndpoint ( ep ) ?. addCC ( cc , info ) ; } } } const removeCCs = this . deviceConfig . compat ?. removeCCs ; if ( removeCCs ) { for ( const [ cc , endpoints ] of removeCCs ) { if ( endpoints === "*" ) { for ( const ep of this . getAllEndpoints ( ) ) { ep . removeCC ( cc ) ; } } else { for ( const ep of endpoints ) { this . getEndpoint ( ep ) ?. removeCC ( cc ) ; } } } } } await this . setInterviewStage ( InterviewStage . OverwriteConfig ) ; } internal public handleCommand ( command ) { this . cancelManualValueRefresh ( command . ccId ) ; if ( command . endpointIndex === 0 && command . constructor . name . endsWith ( "Report" ) && this . getEndpointCount ( ) >= 1 && this . _deviceConfig ?. compat ?. mapRootReportsToEndpoint != undefined ) { const endpoint = this . getEndpoint ( this . _deviceConfig ?. compat ?. mapRootReportsToEndpoint ) ; if ( endpoint && endpoint . supportsCC ( command . ccId ) ) { this . driver . controllerLog . logNode ( this . nodeId , `Mapping unsolicited report from root device to endpoint #${ endpoint . index }` ) ; command . endpointIndex = endpoint . index ; command . persistValues ( ) ; } } if ( command instanceof BasicCC ) { return this . handleBasicCommand ( command ) ; } else if ( command instanceof CentralSceneCCNotification ) { return this . handleCentralSceneNotification ( command ) ; } else if ( command instanceof WakeUpCCWakeUpNotification ) { return this . handleWakeUpNotification ( ) ; } else if ( command instanceof NotificationCCReport ) { return this . handleNotificationReport ( command ) ; } else if ( command instanceof ClockCCReport ) { return this . handleClockReport ( command ) ; } else if ( command instanceof SecurityCCNonceGet ) { return this . handleSecurityNonceGet ( ) ; } else if ( command instanceof SecurityCCNonceReport ) { return this . handleSecurityNonceReport ( command ) ; } else if ( command instanceof Security2CCNonceGet ) { return this . handleSecurity2NonceGet ( ) ; } else if ( command instanceof Security2CCNonceReport ) { return this . handleSecurity2NonceReport ( command ) ; } else if ( command instanceof HailCC ) { return this . handleHail ( command ) ; } else if ( command instanceof FirmwareUpdateMetaDataCCGet ) { return this . handleFirmwareUpdateGet ( command ) ; } else if ( command instanceof FirmwareUpdateMetaDataCCStatusReport ) { return this . handleFirmwareUpdateStatusReport ( command ) ; } else if ( command instanceof EntryControlCCNotification ) { return this . handleEntryControlNotification ( command ) ; } else if ( command instanceof PowerlevelCCTestNodeReport ) { return this . handlePowerlevelTestNodeReport ( command ) ; } switch ( true ) { case command . constructor . name . endsWith ( "Report" ) : case command instanceof SceneActivationCCSet : return ; } this . driver . controllerLog . logNode ( this . id , { message : `TODO: no handler for application command` , direction : "inbound" } ) ; } private hasLoggedNoNetworkKey = false ; internal public async handleSecurityNonceGet ( ) { if ( ! this . driver . securityManager ) { if ( ! this . hasLoggedNoNetworkKey ) { this . hasLoggedNoNetworkKey = true ; this . driver . controllerLog . logNode ( this . id , { message : `cannot reply to NonceGet because no network key was configured!` , direction : "inbound" , level : "warn" } ) ; } return ; } this . addCC ( CommandClasses . Security , { isSupported : true , version : 1 , secure : true } ) ; const isNonceReport = ( t ) => t . message . getNodeId ( ) === this . nodeId && isCommandClassContainer ( t . message ) && t . message . command instanceof SecurityCCNonceReport ; if ( this . driver . hasPendingTransactions ( isNonceReport ) ) { this . driver . controllerLog . logNode ( this . id , { message : "in the process of replying to a NonceGet, won't send another NonceReport" , level : "warn" } ) ; return ; } this . driver . securityManager . deleteAllNoncesForReceiver ( this . id ) ; try { await this . commandClasses . Security . sendNonce ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , { message : `failed to send nonce: ${ getErrorMessage ( e ) }` , direction : "inbound" } ) ; } } /**
     * Is called when a nonce report is received that does not belong to any transaction.
     * The received nonce reports are stored as "free" nonces
     */ private handleSecurityNonceReport ( command ) { const secMan = this . driver . securityManager ; if ( ! secMan ) return ; secMan . setNonce ( { issuer : this . id , nonceId : secMan . getNonceId ( command . nonce ) } , { nonce : command . nonce , receiver : this . driver . controller . ownNodeId ! } , { free : true } ) ; } internal public async handleSecurity2NonceGet ( ) { if ( ! this . driver . securityManager2 ) { if ( ! this . hasLoggedNoNetworkKey ) { this . hasLoggedNoNetworkKey = true ; this . driver . controllerLog . logNode ( this . id , { message : `cannot reply to NonceGet (S2) because no network key was configured!` , direction : "inbound" , level : "warn" } ) ; } return ; } this . addCC ( CommandClasses [ "Security 2" ] , { isSupported : true , version : 1 , secure : true } ) ; const isNonceReport = ( t ) => t . message . getNodeId ( ) === this . nodeId && isCommandClassContainer ( t . message ) && t . message . command instanceof Security2CCNonceReport ; if ( this . driver . hasPendingTransactions ( isNonceReport ) ) { this . driver . controllerLog . logNode ( this . id , { message : "in the process of replying to a NonceGet, won't send another NonceReport" , level : "warn" } ) ; return ; } try { await this . commandClasses [ "Security 2" ] . sendNonce ( ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , { message : `failed to send nonce: ${ getErrorMessage ( e ) }` , direction : "inbound" } ) ; } } /**
     * Is called when a nonce report is received that does not belong to any transaction.
     */ private handleSecurity2NonceReport ( command ) { const secMan = this . driver . securityManager2 ; if ( ! secMan ) return ; if ( command . SOS && command . receiverEI ) { secMan . storeRemoteEI ( this . nodeId , command . receiverEI ) ; } this . driver . controllerLog . logNode ( this . id , { message : `received S2 nonce, not sure what to do with it` , level : "warn" , direction : "inbound" } ) ; } private busyPollingAfterHail : boolean = false ; private async handleHail ( _command ) { this . markAsAwake ( ) ; if ( this . busyPollingAfterHail ) { this . driver . controllerLog . logNode ( this . nodeId , { message : `Hail received from node, but still busy with previous one...` } ) ; return ; } this . busyPollingAfterHail = true ; this . driver . controllerLog . logNode ( this . nodeId , { message : `Hail received from node, refreshing actuator and sensor values...` } ) ; try { await this . refreshValues ( ) ; } catch { } this . busyPollingAfterHail = false ; } /** Stores information about a currently held down key */ private centralSceneKeyHeldDownContext : { timeout : NodeJS . Timeout ; sceneNumber : number ; } | undefined ; private lastCentralSceneNotificationSequenceNumber : number | undefined ; private centralSceneForcedKeyUp = false ; /** Handles the receipt of a Central Scene notifification */ private handleCentralSceneNotification ( command ) { if ( command . sequenceNumber === this . lastCentralSceneNotificationSequenceNumber ) { return ; } else { this . lastCentralSceneNotificationSequenceNumber = command . sequenceNumber ; } const setSceneValue = ( sceneNumber , key ) : void => { const valueId = getSceneValueId ( sceneNumber ) ; this . valueDB . setValue ( valueId , key , { stateful : false } ) ; } ; const forceKeyUp = ( ) : void => { this . centralSceneForcedKeyUp = true ; setSceneValue ( this . centralSceneKeyHeldDownContext ! . sceneNumber , CentralSceneKeys . KeyReleased ) ; clearTimeout ( this . centralSceneKeyHeldDownContext ! . timeout ) ; this . centralSceneKeyHeldDownContext = undefined ; } ; if ( this . centralSceneKeyHeldDownContext && this . centralSceneKeyHeldDownContext . sceneNumber !== command . sceneNumber ) { forceKeyUp ( ) ; } if ( command . keyAttribute === CentralSceneKeys . KeyHeldDown ) { this . centralSceneForcedKeyUp = false ; if ( this . centralSceneKeyHeldDownContext ) { clearTimeout ( this . centralSceneKeyHeldDownContext . timeout ) ; } const slowRefresh = command . slowRefresh ?? this . valueDB . getValue < boolean > ( getSlowRefreshValueId ( ) ) ; this . centralSceneKeyHeldDownContext = { sceneNumber : command . sceneNumber , timeout : setTimeout ( forceKeyUp , slowRefresh ? 60000 : 400 ) . unref ( ) } ; } else if ( command . keyAttribute === CentralSceneKeys . KeyReleased ) { if ( this . centralSceneKeyHeldDownContext ) { clearTimeout ( this . centralSceneKeyHeldDownContext . timeout ) ; this . centralSceneKeyHeldDownContext = undefined ; } else if ( this . centralSceneForcedKeyUp ) { this . valueDB . setValue ( getSlowRefreshValueId ( ) , true ) ; return ; } } setSceneValue ( command . sceneNumber , command . keyAttribute ) ; this . driver . controllerLog . logNode ( this . id , { message : `received CentralScene notification ${ stringify ( command ) }` , direction : "inbound" } ) ; } /** The timestamp of the last received wakeup notification */ private lastWakeUp : number | undefined ; /** Handles the receipt of a Wake Up notification */ private handleWakeUpNotification ( ) { this . driver . controllerLog . logNode ( this . id , { message : `received wakeup notification` , direction : "inbound" } ) ; this . addCC ( CommandClasses [ "Wake Up" ] , { isSupported : true , version : 1 } ) ; this . markAsAwake ( ) ; const now = Date . now ( ) ; if ( this . lastWakeUp ) { const wakeUpInterval = this . getValue < number > ( getWakeUpIntervalValueId ( ) ) ?? 1 ; if ( wakeUpInterval > 0 && ( now - this . lastWakeUp ) / 1000 > wakeUpInterval + 5 * 60 ) { this . commandClasses [ "Wake Up" ] . getInterval ( ) . catch ( ( ) => { } ) ; } } this . lastWakeUp = now ; if ( this . _deviceConfig ?. compat ?. queryOnWakeup ) { void this . compatDoWakeupQueries ( ) ; } this . driver . debounceSendNodeToSleep ( this ) ; } private async compatDoWakeupQueries ( ) { if ( ! this . _deviceConfig ?. compat ?. queryOnWakeup ) return ; this . driver . controllerLog . logNode ( this . id , { message : `expects some queries after wake up, so it shall receive` , direction : "none" } ) ; for ( const [ ccName , apiMethod , ... args ] of this . _deviceConfig . compat . queryOnWakeup ) { this . driver . controllerLog . logNode ( this . id , { message : `compat query "${ ccName }"::${ apiMethod }(${ args . map ( ( arg ) => JSON . stringify ( arg ) ) . join ( ", " ) })` , direction : "none" } ) ; let API ; try { API = ( ( this . commandClasses as any ) [ ccName ] as CCAPI ) . withOptions ( { tag : "compat" , maxSendAttempts : 1 , expire : 10000 } ) ; } catch { this . driver . controllerLog . logNode ( this . id , { message : `could not access API, skipping query` , direction : "none" , level : "warn" } ) ; continue ; } if ( ! API . isSupported ( ) ) { this . driver . controllerLog . logNode ( this . id , { message : `API not supported, skipping query` , direction : "none" , level : "warn" } ) ; continue ; } else if ( ! ( API as any ) [ apiMethod ] ) { this . driver . controllerLog . logNode ( this . id , { message : `method ${ apiMethod } not found on API, skipping query` , direction : "none" , level : "warn" } ) ; continue ; } const method = ( API as any ) [ apiMethod ] . bind ( API ) as Function ; const methodArgs = args . map < unknown > ( ( arg ) => { if ( isObject ( arg ) ) { const valueId = { commandClass : API . ccId , ... arg } ; return this . getValue ( valueId ) ; } return arg ; } ) ; try { await method ( ... methodArgs ) ; this . driver . controllerLog . logNode ( this . id , { message : `API call successful` , direction : "none" } ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , { message : `error during API call: ${ getErrorMessage ( e ) }` , direction : "none" , level : "warn" } ) ; if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_MessageExpired ) { return ; } } } } /** Handles the receipt of a BasicCC Set or Report */ private handleBasicCommand ( command ) { const sourceEndpoint = this . getEndpoint ( command . endpointIndex ?? 0 ) ?? this ; let mappedTargetCC ; if ( ! this . _deviceConfig ?. compat ?. disableBasicMapping ) { switch ( sourceEndpoint . deviceClass ?. generic . key ) { case 0x20 : mappedTargetCC = sourceEndpoint . createCCInstanceUnsafe ( CommandClasses [ "Binary Sensor" ] ) ; break ; case 0x10 : mappedTargetCC = sourceEndpoint . createCCInstanceUnsafe ( CommandClasses [ "Binary Switch" ] ) ; break ; case 0x11 : mappedTargetCC = sourceEndpoint . createCCInstanceUnsafe ( CommandClasses [ "Multilevel Switch" ] ) ; break ; } } if ( command instanceof BasicCCReport ) { const didSetMappedValue = typeof command . currentValue === "number" && mappedTargetCC ?. setMappedBasicValue ( command . currentValue ) ; if ( ! didSetMappedValue ) { command . persistValues ( ) ; if ( ! actuatorCCs . some ( ( cc ) => sourceEndpoint . supportsCC ( cc ) ) ) { sourceEndpoint . addCC ( CommandClasses . Basic , { isControlled : true } ) ; } } } else if ( command instanceof BasicCCSet ) { if ( this . _deviceConfig ?. compat ?. treatBasicSetAsEvent ) { this . driver . controllerLog . logNode ( this . id , { endpoint : command . endpointIndex , message : "treating BasicCC::Set as a value event" } ) ; this . _valueDB . setValue ( getBasicCCCompatEventValueId ( command . endpointIndex ) , command . targetValue , { stateful : false } ) ; return ; } else { this . driver . controllerLog . logNode ( this . id , { endpoint : command . endpointIndex , message : "treating BasicCC::Set as a report" } ) ; const didSetMappedValue = ! ! this . _deviceConfig ?. compat ?. enableBasicSetMapping && ! ! mappedTargetCC ?. setMappedBasicValue ( command . targetValue ) ; if ( ! didSetMappedValue ) { this . _valueDB . setValue ( getBasicCCCurrentValueValueId ( command . endpointIndex ) , command . targetValue ) ; if ( ! sourceEndpoint . controlsCC ( CommandClasses . Basic ) ) { sourceEndpoint . addCC ( CommandClasses . Basic , { isControlled : true } ) ; } } } } } /**
     * Allows automatically resetting notification values to idle if the node does not do it itself
     */ private notificationIdleTimeouts = new Map < string , NodeJS . Timeout > ( ) ; /** Schedules a notification value to be reset */ private scheduleNotificationIdleReset ( valueId , handler ) { this . clearNotificationIdleReset ( valueId ) ; const key = valueIdToString ( valueId ) ; this . notificationIdleTimeouts . set ( key , setTimeout ( handler , 5 * 60 * 1000 ) . unref ( ) ) ; } /** Removes a scheduled notification reset */ private clearNotificationIdleReset ( valueId ) { const key = valueIdToString ( valueId ) ; if ( this . notificationIdleTimeouts . has ( key ) ) { clearTimeout ( this . notificationIdleTimeouts . get ( key ) ! ) ; this . notificationIdleTimeouts . delete ( key ) ; } } /**
     * Handles the receipt of a Notification Report
     */ private handleNotificationReport ( command ) { if ( command . notificationType == undefined ) { if ( command . alarmType == undefined ) { this . driver . controllerLog . logNode ( this . id , { message : `received unsupported notification ${ stringify ( command ) }` , direction : "inbound" } ) ; } return ; } const extendValueMetadata = ( valueId , notificationConfig , valueConfig ) => { if ( command . version === 2 || ! this . valueDB . hasMetadata ( valueId ) ) { const metadata = getNotificationValueMetadata ( this . valueDB . getMetadata ( valueId ) as ValueMetadataNumeric | undefined , notificationConfig , valueConfig ) ; this . valueDB . setMetadata ( valueId , metadata ) ; } } ; const ensureValueMetadataUnknownType = ( valueId , notificationConfig ) => { if ( command . version === 2 && ! this . valueDB . hasMetadata ( valueId ) ) { const metadata = getNotificationValueMetadataUnknownType ( notificationConfig . id ) ; this . valueDB . setMetadata ( valueId , metadata ) ; } } ; const notificationConfig = this . driver . configManager . lookupNotification ( command . notificationType ) ; if ( notificationConfig ) { const property = notificationConfig . name ; /** Returns a single notification state to idle */ const setStateIdle = ( prevValue ) : void => { const valueConfig = notificationConfig . lookupValue ( prevValue ) ; if ( ! valueConfig || valueConfig . type !== "state" ) return ; if ( ! valueConfig . idle ) return ; const propertyKey = valueConfig . variableName ; const valueId = { commandClass : command . ccId , endpoint : command . endpointIndex , property , propertyKey } ; this . clearNotificationIdleReset ( valueId ) ; extendValueMetadata ( valueId , notificationConfig , valueConfig ) ; this . valueDB . setValue ( valueId , 0 ) ; } ; const value = command . notificationEvent ! ; if ( value === 0 ) { if ( Buffer . isBuffer ( command . eventParameters ) && command . eventParameters . length ) { setStateIdle ( command . eventParameters [ 0 ] ) ; } else { const nonIdleValues = this . valueDB . getValues ( CommandClasses . Notification ) . filter ( ( v ) => ( v . endpoint || 0 ) === command . endpointIndex && v . property === property && typeof v . value === "number" && v . value !== 0 ) ; for ( const v of nonIdleValues ) { setStateIdle ( v . value as number ) ; } } return ; } let propertyKey ; const valueConfig = notificationConfig . lookupValue ( value ) ; this . handleKnownNotification ( command ) ; let allowIdleReset ; if ( ! valueConfig ) { propertyKey = "unknown" ; allowIdleReset = false ; } else if ( valueConfig . type === "state" ) { propertyKey = valueConfig . variableName ; allowIdleReset = valueConfig . idle ; } else { this . emit ( "notification" , this , CommandClasses . Notification , { type : command . notificationType , event : value , label : notificationConfig . name , eventLabel : valueConfig . label , parameters : command . eventParameters } ) ; return ; } const valueId = { commandClass : command . ccId , endpoint : command . endpointIndex , property , propertyKey } ; if ( valueConfig ) { extendValueMetadata ( valueId , notificationConfig , valueConfig ) ; } else { ensureValueMetadataUnknownType ( valueId , notificationConfig ) ; } this . valueDB . setValue ( valueId , value ) ; if ( allowIdleReset && ! ! this . _deviceConfig ?. compat ?. forceNotificationIdleReset ) { this . scheduleNotificationIdleReset ( valueId , ( ) => setStateIdle ( value ) ) ; } } else { const property = `UNKNOWN_${ num2hex ( command . notificationType ) }` ; const valueId = { commandClass : command . ccId , endpoint : command . endpointIndex , property } ; this . valueDB . setValue ( valueId , command . notificationEvent ) ; } } private handleKnownNotification ( command ) { const lockEvents = [ 0x01 , 0x03 , 0x05 , 0x09 ] ; const unlockEvents = [ 0x02 , 0x04 , 0x06 ] ; if ( command . notificationType === 0x06 && ( lockEvents . includes ( command . notificationEvent as number ) || unlockEvents . includes ( command . notificationEvent as number ) ) && ( this . supportsCC ( CommandClasses [ "Door Lock" ] ) || this . supportsCC ( CommandClasses . Lock ) ) ) { const isLocked = lockEvents . includes ( command . notificationEvent as number ) ; if ( this . supportsCC ( CommandClasses [ "Door Lock" ] ) ) { this . valueDB . setValue ( getCurrentLockModeValueId ( command . endpointIndex ) , isLocked ? DoorLockMode . Secured : DoorLockMode . Unsecured ) ; } if ( this . supportsCC ( CommandClasses . Lock ) ) { this . valueDB . setValue ( getLockedValueId ( command . endpointIndex ) , isLocked ) ; } } } private busySettingClock : boolean = false ; private async handleClockReport ( command ) { if ( this . busySettingClock ) return ; const now = new Date ( ) ; const hours = now . getHours ( ) ; let minutes = now . getMinutes ( ) ; if ( now . getSeconds ( ) >= 30 ) { minutes = ( minutes + 1 ) % 60 ; } let weekday = now . getDay ( ) ; if ( weekday === 0 ) weekday = 7 ; if ( command . weekday !== weekday || command . hour !== hours || command . minute !== minutes ) { const endpoint = command . getEndpoint ( ) ; if ( ! endpoint || ! endpoint . commandClasses . Clock . isSupported ( ) ) { return ; } this . driver . controllerLog . logNode ( this . nodeId , `detected a deviation of the node's clock, updating it...` ) ; this . busySettingClock = true ; try { await endpoint . commandClasses . Clock . set ( hours , minutes , weekday ) ; } catch { } this . busySettingClock = false ; } } private _firmwareUpdateStatus : { progress : number ; fragmentSize : number ; numFragments : number ; data : Buffer ; abort : boolean ; /** This is set when waiting for the update status */ statusTimeout ? : NodeJS . Timeout ; /** This is set when waiting for a get request */ getTimeout ? : NodeJS . Timeout ; } | undefined ; /** Retrieves the firmware update capabilities of a node to decide which options to offer a user prior to the update */ public async getFirmwareUpdateCapabilities ( ) { const api = this . commandClasses [ "Firmware Update Meta Data" ] ; const meta = await api . getMetaData ( ) ; if ( ! meta ) { throw new ZWaveError ( `Failed to request firmware update capabilities: The node did not respond in time!` , ZWaveErrorCodes . Controller_NodeTimeout ) ; } else if ( ! meta . firmwareUpgradable ) { return { firmwareUpgradable : false } ; } return { firmwareUpgradable : true , firmwareTargets : [ 0 , ... meta . additionalFirmwareIDs ] , continuesToFunction : meta . continuesToFunction , supportsActivation : meta . supportsActivation } ; } param data param target public async beginFirmwareUpdate ( data , target = 0 ) { if ( this . _firmwareUpdateStatus ) { throw new ZWaveError ( `Failed to start the update: A firmware upgrade is already in progress!` , ZWaveErrorCodes . FirmwareUpdateCC_Busy ) ; } this . _firmwareUpdateStatus = { data , fragmentSize : 0 , numFragments : 0 , progress : 0 , abort : false } ; const version = this . getCCVersion ( CommandClasses [ "Firmware Update Meta Data" ] ) ; const api = this . commandClasses [ "Firmware Update Meta Data" ] ; const meta = await api . getMetaData ( ) ; if ( ! meta ) { this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: The node did not respond in time!` , ZWaveErrorCodes . Controller_NodeTimeout ) ; } if ( target === 0 ) { if ( ! meta . firmwareUpgradable ) { this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: The Z-Wave chip firmware is not upgradable!` , ZWaveErrorCodes . FirmwareUpdateCC_NotUpgradable ) ; } } else { if ( version < 3 ) { this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: The node does not support upgrading a different firmware target than 0!` , ZWaveErrorCodes . FirmwareUpdateCC_TargetNotFound ) ; } else if ( meta . additionalFirmwareIDs [ target - 1 ] == undefined ) { this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: Firmware target #${ target } not found on this node!` , ZWaveErrorCodes . FirmwareUpdateCC_TargetNotFound ) ; } } const fcc = new FirmwareUpdateMetaDataCC ( this . driver , { nodeId : this . id } ) ; const maxNetPayloadSize = this . driver . computeNetCCPayloadSize ( fcc ) - 2 - ( version >= 2 ? 2 : 0 ) ; this . _firmwareUpdateStatus . fragmentSize = Math . min ( maxNetPayloadSize , meta . maxFragmentSize ?? Number . POSITIVE_INFINITY ) ; this . _firmwareUpdateStatus . numFragments = Math . ceil ( data . length / this . _firmwareUpdateStatus . fragmentSize ) ; this . driver . controllerLog . logNode ( this . id , { message : `Starting firmware update...` , direction : "outbound" } ) ; const status = await api . requestUpdate ( { manufacturerId : meta . manufacturerId , firmwareId : target == 0 ? meta . firmwareId : meta . additionalFirmwareIDs [ target - 1 ] , firmwareTarget : target , fragmentSize : this . _firmwareUpdateStatus . fragmentSize , checksum : CRC16_CCITT ( data ) } ) ; switch ( status ) { case FirmwareUpdateRequestStatus . Error_AuthenticationExpected : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: A manual authentication event (e.g. button push) was expected!` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToStart ) ; case FirmwareUpdateRequestStatus . Error_BatteryLow : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: The battery level is too low!` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToStart ) ; case FirmwareUpdateRequestStatus . Error_FirmwareUpgradeInProgress : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: A firmware upgrade is already in progress!` , ZWaveErrorCodes . FirmwareUpdateCC_Busy ) ; case FirmwareUpdateRequestStatus . Error_InvalidManufacturerOrFirmwareID : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: Invalid manufacturer or firmware id!` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToStart ) ; case FirmwareUpdateRequestStatus . Error_InvalidHardwareVersion : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: Invalid hardware version!` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToStart ) ; case FirmwareUpdateRequestStatus . Error_NotUpgradable : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: Firmware target #${ target } is not upgradable!` , ZWaveErrorCodes . FirmwareUpdateCC_NotUpgradable ) ; case FirmwareUpdateRequestStatus . Error_FragmentSizeTooLarge : this . resetFirmwareUpdateStatus ( ) ; throw new ZWaveError ( `Failed to start the update: The chosen fragment size is too large!` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToStart ) ; case FirmwareUpdateRequestStatus . OK : this . keepAwake = true ; this . _firmwareUpdateStatus . getTimeout = setTimeout ( ( ) => this . timeoutFirmwareUpdate ( ) , 30000 ) . unref ( ) ; return ; } } /**
     * Aborts an active firmware update process
     */ public async abortFirmwareUpdate ( ) { if ( ! this . _firmwareUpdateStatus || this . _firmwareUpdateStatus . abort ) { return ; } else if ( this . _firmwareUpdateStatus . numFragments > 0 && this . _firmwareUpdateStatus . progress === this . _firmwareUpdateStatus . numFragments ) { throw new ZWaveError ( `The firmware update was transmitted completely, cannot abort anymore.` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToAbort ) ; } this . driver . controllerLog . logNode ( this . id , { message : `Aborting firmware update...` , direction : "outbound" } ) ; this . _firmwareUpdateStatus . abort = true ; try { await this . driver . waitForCommand < FirmwareUpdateMetaDataCCStatusReport > ( ( cc ) => cc . nodeId === this . nodeId && cc instanceof FirmwareUpdateMetaDataCCStatusReport && cc . status === FirmwareUpdateStatus . Error_TransmissionFailed , 5000 ) ; this . driver . controllerLog . logNode ( this . id , { message : `Firmware update aborted` , direction : "inbound" } ) ; this . resetFirmwareUpdateStatus ( ) ; } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_NodeTimeout ) { throw new ZWaveError ( `The node did not acknowledge the aborted update` , ZWaveErrorCodes . FirmwareUpdateCC_FailedToAbort ) ; } throw e ; } } private async sendCorruptedFirmwareUpdateReport ( reportNum , fragment ? ) { if ( ! fragment ) { let fragmentSize = this . _firmwareUpdateStatus ?. fragmentSize ; if ( ! fragmentSize ) { const fcc = new FirmwareUpdateMetaDataCC ( this . driver , { nodeId : this . id } ) ; fragmentSize = this . driver . computeNetCCPayloadSize ( fcc ) - 2 - ( fcc . version >= 2 ? 2 : 0 ) ; } fragment = randomBytes ( fragmentSize ) ; } else { for ( let i = 0 ; i < fragment . length ; i ++ ) { fragment [ i ] = fragment [ i ] ^ 0xff ; } } await this . commandClasses [ "Firmware Update Meta Data" ] . sendFirmwareFragment ( reportNum , true , fragment ) ; } private handleFirmwareUpdateGet ( command ) { if ( this . _firmwareUpdateStatus == undefined ) { this . driver . controllerLog . logNode ( this . id , { message : `Received Firmware Update Get, but no firmware update is in progress. Forcing the node to abort...` , direction : "inbound" } ) ; this . sendCorruptedFirmwareUpdateReport ( command . reportNumber ) . catch ( ( ) => { } ) ; return ; } else if ( command . reportNumber > this . _firmwareUpdateStatus . numFragments ) { this . driver . controllerLog . logNode ( this . id , { message : `Received Firmware Update Get for an out-of-bounds fragment. Forcing the node to abort...` , direction : "inbound" } ) ; this . sendCorruptedFirmwareUpdateReport ( command . reportNumber ) . catch ( ( ) => { } ) ; return ; } if ( this . _firmwareUpdateStatus . getTimeout ) { this . _firmwareUpdateStatus . getTimeout = this . _firmwareUpdateStatus . getTimeout . refresh ( ) . unref ( ) ; } try { this . markAsAwake ( ) ; } catch { } void ( async ( ) => { const { numFragments , data , fragmentSize , abort } = this . _firmwareUpdateStatus ! ; for ( let num = command . reportNumber ; num < command . reportNumber + command . numReports ; num ++ ) { if ( num > numFragments ) { break ; } const fragment = data . slice ( ( num - 1 ) * fragmentSize , num * fragmentSize ) ; if ( abort ) { await this . sendCorruptedFirmwareUpdateReport ( num , fragment ) ; return ; } else { const isCurrentFirmwareFragment = ( t ) => t . message . getNodeId ( ) === this . nodeId && isCommandClassContainer ( t . message ) && t . message . command instanceof FirmwareUpdateMetaDataCCReport && t . message . command . reportNumber === num ; if ( this . driver . hasPendingTransactions ( isCurrentFirmwareFragment ) ) { this . driver . controllerLog . logNode ( this . id , { message : `Firmware fragment ${ num } already queued` , level : "warn" } ) ; continue ; } this . driver . controllerLog . logNode ( this . id , { message : `Sending firmware fragment ${ num } / ${ numFragments }` , direction : "outbound" } ) ; const isLast = num === numFragments ; if ( isLast ) { this . keepAwake = true ; } await this . commandClasses [ "Firmware Update Meta Data" ] . sendFirmwareFragment ( num , isLast , fragment ) ; this . _firmwareUpdateStatus ! . progress = num ; this . emit ( "firmware update progress" , this , num , numFragments ) ; if ( isLast ) { if ( this . _firmwareUpdateStatus ?. getTimeout ) { clearTimeout ( this . _firmwareUpdateStatus . getTimeout ) ; this . _firmwareUpdateStatus . getTimeout = undefined ; } void this . finishFirmwareUpdate ( ) . catch ( ( ) => { } ) ; } } } } ) ( ) . catch ( ( e ) => { this . driver . controllerLog . logNode ( this . nodeId , `Error while sending firmware fragment: ${ e . message }` , "error" ) ; } ) ; } private timeoutFirmwareUpdate ( ) { this . driver . controllerLog . logNode ( this . id , { message : `Firmware update timed out` , direction : "none" , level : "warn" } ) ; this . resetFirmwareUpdateStatus ( ) ; this . emit ( "firmware update finished" , this , FirmwareUpdateStatus . Error_Timeout ) ; } private handleFirmwareUpdateStatusReport ( report ) { if ( ! this . _firmwareUpdateStatus ) return ; const { status , waitTime } = report ; const success = status >= FirmwareUpdateStatus . OK_WaitingForActivation ; this . driver . controllerLog . logNode ( this . id , { message : `Firmware update ${ success ? "completed" : "failed" } with status ${ getEnumMemberName ( FirmwareUpdateStatus , status ) }` , direction : "inbound" } ) ; this . resetFirmwareUpdateStatus ( ) ; this . emit ( "firmware update finished" , this , status , success ? waitTime : undefined ) ; } private async finishFirmwareUpdate ( ) { try { const report = await this . driver . waitForCommand < FirmwareUpdateMetaDataCCStatusReport > ( ( cc ) => cc . nodeId === this . nodeId && cc instanceof FirmwareUpdateMetaDataCCStatusReport , 5 * 60000 ) ; this . handleFirmwareUpdateStatusReport ( report ) ; } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_NodeTimeout ) { this . driver . controllerLog . logNode ( this . id , `The node did not acknowledge the completed update` , "warn" ) ; this . resetFirmwareUpdateStatus ( ) ; this . emit ( "firmware update finished" , this , FirmwareUpdateStatus . Error_Timeout ) ; } throw e ; } } private resetFirmwareUpdateStatus ( ) { this . _firmwareUpdateStatus = undefined ; this . keepAwake = false ; } private recentEntryControlNotificationSequenceNumbers : number [ ] = [ ] ; private handleEntryControlNotification ( command ) { if ( ! this . _deviceConfig ?. compat ?. disableStrictEntryControlDataValidation ) { if ( this . recentEntryControlNotificationSequenceNumbers . includes ( command . sequenceNumber ) ) { this . driver . controllerLog . logNode ( this . id , `Received duplicate Entry Control Notification (sequence number ${ command . sequenceNumber }), ignoring...` , "warn" ) ; return ; } this . recentEntryControlNotificationSequenceNumbers . unshift ( command . sequenceNumber ) ; if ( this . recentEntryControlNotificationSequenceNumbers . length > 5 ) { this . recentEntryControlNotificationSequenceNumbers . pop ( ) ; } } this . emit ( "notification" , this , CommandClasses [ "Entry Control" ] , pick ( command , [ "eventType" , "dataType" , "eventData" ] ) ) ; } private handlePowerlevelTestNodeReport ( command ) { this . emit ( "notification" , this , CommandClasses . Powerlevel , pick ( command , [ "testNodeId" , "status" , "acknowledgedFrames" ] ) ) ; } internal public serialize ( ) { const ret = { id : this . id , interviewStage : InterviewStage [ this . interviewStage ] , deviceClass : this . deviceClass && { basic : this . deviceClass . basic . key , generic : this . deviceClass . generic . key , specific : this . deviceClass . specific . key } , isListening : this . isListening , isFrequentListening : this . isFrequentListening , isRouting : this . isRouting , supportedDataRates : this . supportedDataRates , protocolVersion : this . protocolVersion , nodeType : this . nodeType != undefined ? NodeType [ this . nodeType ] : undefined , supportsSecurity : this . supportsSecurity , supportsBeaming : this . supportsBeaming , securityClasses : { } as JSONObject , dsk : this . dsk ? dskToString ( this . dsk ) : undefined , commandClasses : { } as JSONObject } ; for ( const secClass of securityClassOrder ) { const hasClass = this . hasSecurityClass ( secClass ) ; if ( typeof hasClass === "boolean" ) { ret . securityClasses [ SecurityClass [ secClass ] ] = hasClass ; } } const sortedCCs = [ ... this . implementedCommandClasses . keys ( ) ] . sort ( ( a , b ) => Math . sign ( a - b ) ) ; for ( const cc of sortedCCs ) { const serializedCC = { name : CommandClasses [ cc ] , endpoints : { } as Record < string , CommandClassInfo > } ; for ( const endpoint of this . getAllEndpoints ( ) ) { if ( endpoint . implementedCommandClasses . has ( cc ) ) { serializedCC . endpoints [ endpoint . index . toString ( ) ] = endpoint . implementedCommandClasses . get ( cc ) ! ; } } ret . commandClasses [ num2hex ( cc ) ] = serializedCC as any ; } return ret as any as JSONObject ; } internal public async deserialize ( obj ) { if ( obj . interviewStage in InterviewStage ) { this . interviewStage = typeof obj . interviewStage === "number" ? obj . interviewStage : InterviewStage [ obj . interviewStage ] ; if ( this . interviewStage === InterviewStage . Complete ) { this . readyMachine . send ( "RESTART_FROM_CACHE" ) ; } } if ( isObject ( obj . deviceClass ) ) { const { basic , generic , specific } = obj . deviceClass ; if ( typeof basic === "number" && typeof generic === "number" && typeof specific === "number" ) { this . _deviceClass = new DeviceClass ( this . driver . configManager , basic , generic , specific ) ; } } const tryParse = ( key , type ) : void => { if ( typeof obj [ key ] === type ) this [ `_${ key }` as keyof this ] = obj [ key ] ; } ; const tryParseLegacy = ( keys , types ) : void => { for ( const key of keys ) { if ( types . includes ( typeof obj [ key ] as any ) ) { this [ `_${ keys [ 0 ] }` as keyof this ] = obj [ key ] ; return ; } } } ; tryParse ( "isListening" , "boolean" ) ; tryParseLegacy ( [ "isFrequentListening" ] , [ "string" , "boolean" ] ) ; if ( ( this . _isFrequentListening as any ) === true ) { this . _isFrequentListening = "1000ms" ; } tryParse ( "isRouting" , "boolean" ) ; if ( isObject ( obj . securityClasses ) ) { for ( const [ key , val ] of Object . entries ( obj . securityClasses ) ) { if ( key in SecurityClass && typeof ( SecurityClass as any ) [ key ] === "number" && typeof val === "boolean" ) { this . securityClasses . set ( ( SecurityClass as any ) [ key ] , val ) ; } } } else if ( typeof obj . isSecure === "boolean" ) { this . securityClasses . set ( SecurityClass . S0_Legacy , obj . isSecure ) ; this . securityClasses . set ( SecurityClass . S2_AccessControl , false ) ; this . securityClasses . set ( SecurityClass . S2_Authenticated , false ) ; this . securityClasses . set ( SecurityClass . S2_Unauthenticated , false ) ; } if ( typeof obj . dsk === "string" ) { try { this . _dsk = dskFromString ( obj . dsk ) ; } catch { } } tryParse ( "supportsSecurity" , "boolean" ) ; tryParse ( "supportsBeaming" , "boolean" ) ; tryParseLegacy ( [ "supportsBeaming" , "isBeaming" ] , [ "string" ] ) ; tryParse ( "protocolVersion" , "number" ) ; if ( ! this . _protocolVersion ) { if ( typeof obj . version === "number" ) { this . _protocolVersion = obj . version - 1 ; } } if ( obj . nodeType in NodeType ) { this . _nodeType = NodeType [ obj . nodeType ] as any ; } if ( typeof obj . maxBaudRate === "number" ) { this . _supportedDataRates = [ obj . maxBaudRate ] ; } if ( isArray ( obj . supportedDataRates ) && obj . supportedDataRates . every ( ( r ) => typeof r === "number" ) ) { this . _supportedDataRates = obj . supportedDataRates ; } function enforceType ( val , type ) { return typeof val === type ? val : undefined ; } const endpointCCSupport = new Map < number , Map < number , Partial < CommandClassInfo > > > ( ) ; if ( isObject ( obj . commandClasses ) ) { const ccDict = obj . commandClasses ; for ( const ccHex of Object . keys ( ccDict ) ) { if ( ! /^0x[0-9a-fA-F]+$/ . test ( ccHex ) ) continue ; const ccNum = parseInt ( ccHex ) ; if ( ! ( ccNum in CommandClasses ) ) continue ; const { values , metadata , endpoints , isSupported , isControlled , version , } = ccDict [ ccHex ] ; if ( isObject ( endpoints ) ) { const support = new Map < number , Partial < CommandClassInfo > > ( ) ; for ( const endpointIndex of Object . keys ( endpoints ) ) { if ( ! /^\d+$/ . test ( endpointIndex ) ) continue ; const numEndpointIndex = parseInt ( endpointIndex , 10 ) ; const info = endpoints [ endpointIndex ] as CommandClassInfo ; info . isSupported = enforceType ( info . isSupported , "boolean" ) ; info . isControlled = enforceType ( info . isControlled , "boolean" ) ; info . version = enforceType ( info . version , "number" ) ; if ( numEndpointIndex === 0 ) { this . addCC ( ccNum , info ) ; } else { support . set ( numEndpointIndex , info ) ; } } endpointCCSupport . set ( ccNum , support ) ; } else { this . addCC ( ccNum , { isSupported : enforceType ( isSupported , "boolean" ) , isControlled : enforceType ( isControlled , "boolean" ) , version : enforceType ( version , "number" ) } ) ; } if ( isArray ( metadata ) && metadata . length > 0 ) { const ccInstance = this . createCCInstanceUnsafe ( ccNum ) ; if ( ccInstance ) { for ( const m of metadata ) { if ( "propertyName" in m ) { m . property = m . propertyName ; delete m . propertyName ; } } try { ccInstance . deserializeMetadataFromCache ( metadata as CacheMetadata [ ] ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , { message : `Error during deserialization of CC value metadata from cache:\n${ getErrorMessage ( e , true ) }` , level : "error" } ) ; } } } if ( isArray ( values ) && values . length > 0 ) { const ccInstance = this . createCCInstanceUnsafe ( ccNum ) ; if ( ccInstance ) { for ( const v of values ) { if ( "propertyName" in v ) { v . property = v . propertyName ; delete v . propertyName ; } } try { ccInstance . deserializeValuesFromCache ( values as CacheValue [ ] ) ; } catch ( e ) { this . driver . controllerLog . logNode ( this . id , { message : `Error during deserialization of CC values from cache:\n${ getErrorMessage ( e , true ) }` , level : "error" } ) ; } } } } } for ( const [ cc , support ] of endpointCCSupport ) { for ( const [ endpointIndex , info ] of support ) { const endpoint = this . getEndpoint ( endpointIndex ) ; if ( ! endpoint ) continue ; endpoint . addCC ( cc , info ) ; } } await this . loadDeviceConfig ( ) ; const compat = this . _deviceConfig ?. compat ; if ( ! compat ?. disableBasicMapping && ! compat ?. treatBasicSetAsEvent ) { for ( const endpoint of this . getAllEndpoints ( ) ) { endpoint . hideBasicCCInFavorOfActuatorCCs ( ) ; } } } /**
     * Whether the node should be kept awake when there are no pending messages.
     */ public keepAwake : boolean = false ; private isSendingNoMoreInformation : boolean = false ; internal public async sendNoMoreInformation ( ) { if ( this . keepAwake ) return false ; if ( this . isSendingNoMoreInformation ) return false ; this . isSendingNoMoreInformation = true ; let msgSent = false ; if ( this . status === NodeStatus . Awake && this . interviewStage === InterviewStage . Complete ) { this . driver . controllerLog . logNode ( this . id , { message : "Sending node back to sleep..." , direction : "outbound" } ) ; try { await this . commandClasses [ "Wake Up" ] . sendNoMoreInformation ( ) ; msgSent = true ; } catch { } finally { this . markAsAsleep ( ) ; } } this . isSendingNoMoreInformation = false ; return msgSent ; } /**
     * Instructs the node to send powerlevel test frames to the other node using the given powerlevel. Returns how many frames were acknowledged during the test.
     *
     * **Note:** Depending on the number of test frames, this may take a while
     */ public async testPowerlevel ( testNodeId , powerlevel , healthCheckTestFrameCount , onProgress ? ) { const api = this . commandClasses . Powerlevel ; const wasKeptAwake = this . keepAwake ; if ( this . canSleep ) this . keepAwake = true ; const result = < T > ( value ) => { this . keepAwake = wasKeptAwake ; return value ; } ; await api . startNodeTest ( testNodeId , powerlevel , healthCheckTestFrameCount ) ; const expectedDurationMs = Math . round ( ( healthCheckTestFrameCount / 5 ) * 1000 ) ; const pollFrequencyMs = expectedDurationMs >= 60000 ? 10000 : expectedDurationMs >= 10000 ? 5000 : 1000 ; let continuousErrors = 0 ; while ( true ) { const report = await this . driver . waitForCommand < PowerlevelCCTestNodeReport > ( ( cc ) => cc . nodeId === this . id && cc instanceof PowerlevelCCTestNodeReport , pollFrequencyMs ) . catch ( ( ) => undefined ) ; const status = report ? pick ( report , [ "status" , "acknowledgedFrames" ] ) : await api . getNodeTestStatus ( ) . catch ( ( ) => undefined ) ; if ( ! status ) { if ( continuousErrors > 5 ) return result ( 0 ) ; continuousErrors ++ ; continue ; } else { continuousErrors = 0 ; } if ( status . status === PowerlevelTestStatus . Failed ) { return result ( 0 ) ; } else if ( status . status === PowerlevelTestStatus . Success ) { return result ( status . acknowledgedFrames ) ; } else if ( onProgress ) { onProgress ( status . acknowledgedFrames , healthCheckTestFrameCount ) ; } } } /**
     * Checks the health of connection between the controller and this node and returns the results.
     */ public async checkLifelineHealth ( rounds = 5 , onProgress ? ) { if ( rounds > 10 || rounds < 1 ) { throw new ZWaveError ( "The number of health check rounds must be between 1 and 10!" , ZWaveErrorCodes . Argument_Invalid ) ; } const start = Date . now ( ) ; /** Computes a health rating from a health check result */ const computeRating = ( result ) => { const failedPings = Math . max ( result . failedPingsController ?? 0 , result . failedPingsNode ) ; const numNeighbors = result . numNeighbors ; const minPowerlevel = result . minPowerlevel ?? Powerlevel [ "-6 dBm" ] ; const snrMargin = result . snrMargin ?? 17 ; const latency = result . latency ; if ( failedPings === 10 ) return 0 ; if ( failedPings > 2 ) return 1 ; if ( failedPings === 2 || latency > 1000 ) return 2 ; if ( failedPings === 1 || latency > 500 ) return 3 ; if ( latency > 250 ) return 4 ; if ( latency > 100 ) return 5 ; if ( minPowerlevel < Powerlevel [ "-6 dBm" ] || snrMargin < 17 ) { return numNeighbors > 2 ? 7 : 6 ; } if ( numNeighbors <= 2 ) return 8 ; if ( latency > 50 ) return 9 ; return 10 ; } ; this . driver . controllerLog . logNode ( this . id , `Starting lifeline health check (${ rounds } round${ rounds !== 1 ? "s" : "" })...` ) ; const results = [ ] ; for ( let round = 1 ; round <= rounds ; round ++ ) { const numNeighbors = ( await this . driver . controller . getNodeNeighbors ( this . nodeId , true ) ) . length ; let txReport ; let routeChanges ; let rssi ; let channel ; let snrMargin ; let failedPingsNode = 0 ; let latency = 0 ; const pingAPI = this . commandClasses [ "No Operation" ] . withOptions ( { onTXReport : ( report ) => { txReport = report ; } } ) ; for ( let i = 1 ; i <= healthCheckTestFrameCount ; i ++ ) { const start = Date . now ( ) ; const pingResult = await pingAPI . send ( ) . then ( ( ) => true , ( ) => false ) ; const rtt = Date . now ( ) - start ; latency = Math . max ( latency , txReport ? txReport . txTicks * 10 : rtt ) ; if ( ! pingResult ) { failedPingsNode ++ ; } else if ( txReport ) { routeChanges ??= 0 ; if ( txReport . routingAttempts > 1 ) { routeChanges ++ ; } rssi = txReport . ackRSSI ; channel = txReport . ackChannelNo ; } } if ( rssi != undefined && rssi < RssiError . NoSignalDetected && channel != undefined ) { const backgroundRSSI = await this . driver . controller . getBackgroundRSSI ( ) ; if ( `rssiChannel${ channel }` in backgroundRSSI ) { snrMargin = rssi - ( backgroundRSSI as any ) [ `rssiChannel${ channel }` ] ; } } const ret = { latency , failedPingsNode , numNeighbors , routeChanges , snrMargin , rating : 0 } ; if ( this . supportsCC ( CommandClasses . Powerlevel ) ) { let failedPingsController = 0 ; const executor = async ( powerlevel ) => { this . driver . controllerLog . logNode ( this . id , `Sending ${ healthCheckTestFrameCount } pings to controller at ${ getEnumMemberName ( Powerlevel , powerlevel ) }...` ) ; const result = await this . testPowerlevel ( this . driver . controller . ownNodeId ! , powerlevel , healthCheckTestFrameCount ) ; failedPingsController = healthCheckTestFrameCount - result ; this . driver . controllerLog . logNode ( this . id , `At ${ getEnumMemberName ( Powerlevel , powerlevel ) }, ${ result }/${ healthCheckTestFrameCount } pings were acknowledged...` ) ; return failedPingsController === 0 ; } ; try { const powerlevel = await discreteBinarySearch ( Powerlevel [ "Normal Power" ] , Powerlevel [ "-9 dBm" ] , executor ) ; if ( powerlevel == undefined ) { ret . minPowerlevel = Powerlevel [ "Normal Power" ] ; ret . failedPingsController = failedPingsController ; } else { ret . minPowerlevel = powerlevel ; } } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_CallbackNOK ) { ret . minPowerlevel = Powerlevel [ "Normal Power" ] ; ret . failedPingsController = healthCheckTestFrameCount ; } else { throw e ; } } } ret . rating = computeRating ( ret ) ; results . push ( ret ) ; onProgress ?. ( round , rounds , ret . rating ) ; } const duration = Date . now ( ) - start ; const rating = Math . min ( ... results . map ( ( r ) => r . rating ) ) ; const summary = { results , rating } ; this . driver . controllerLog . logNode ( this . id , `Lifeline health check complete in ${ duration } ms
${ formatLifelineHealthCheckSummary ( summary ) }` ) ; return summary ; } /**
     * Checks the health of connection between this node and the target node and returns the results.
     */ public async checkRouteHealth ( targetNodeId , rounds = 5 , onProgress ? ) { if ( rounds > 10 || rounds < 1 ) { throw new ZWaveError ( "The number of health check rounds must be between 1 and 10!" , ZWaveErrorCodes . Argument_Invalid ) ; } const otherNode = this . driver . controller . nodes . getOrThrow ( targetNodeId ) ; if ( ! this . supportsCC ( CommandClasses . Powerlevel ) && ! otherNode . supportsCC ( CommandClasses . Powerlevel ) ) { throw new ZWaveError ( "For a route health check, at least one of the nodes must support Powerlevel CC!" , ZWaveErrorCodes . CC_NotSupported ) ; } const healthCheckTestFrameCount = 10 ; const start = Date . now ( ) ; /** Computes a health rating from a health check result */ const computeRating = ( result ) => { const failedPings = Math . max ( result . failedPingsToSource ?? 0 , result . failedPingsToTarget ?? 0 ) ; const numNeighbors = result . numNeighbors ; const minPowerlevel = Math . max ( result . minPowerlevelSource ?? Powerlevel [ "-6 dBm" ] , result . minPowerlevelTarget ?? Powerlevel [ "-6 dBm" ] ) ; if ( failedPings === 10 ) return 0 ; if ( failedPings > 2 ) return 1 ; if ( failedPings === 2 ) return 2 ; if ( failedPings === 1 ) return 3 ; if ( minPowerlevel < Powerlevel [ "-6 dBm" ] ) { return numNeighbors > 2 ? 7 : 6 ; } if ( numNeighbors <= 2 ) return 8 ; return 10 ; } ; this . driver . controllerLog . logNode ( this . id , `Starting route health check to node ${ targetNodeId } (${ rounds } round${ rounds !== 1 ? "s" : "" })...` ) ; const results = [ ] ; for ( let round = 1 ; round <= rounds ; round ++ ) { const numNeighbors = Math . min ( ( await this . driver . controller . getNodeNeighbors ( this . nodeId , true ) ) . length , ( await this . driver . controller . getNodeNeighbors ( targetNodeId , true ) ) . length ) ; let failedPings = 0 ; let failedPingsToSource ; let minPowerlevelSource ; let failedPingsToTarget ; let minPowerlevelTarget ; const executor = ( node , otherNode ) => async ( powerlevel ) => { this . driver . controllerLog . logNode ( node . id , `Sending ${ healthCheckTestFrameCount } pings to node ${ otherNode . id } at ${ getEnumMemberName ( Powerlevel , powerlevel ) }...` ) ; const result = await node . testPowerlevel ( otherNode . id , powerlevel , healthCheckTestFrameCount ) ; failedPings = healthCheckTestFrameCount - result ; this . driver . controllerLog . logNode ( node . id , `At ${ getEnumMemberName ( Powerlevel , powerlevel ) }, ${ result }/${ healthCheckTestFrameCount } pings were acknowledged by node ${ otherNode . id }...` ) ; return failedPings === 0 ; } ; if ( this . supportsCC ( CommandClasses . Powerlevel ) ) { try { const powerlevel = await discreteBinarySearch ( Powerlevel [ "Normal Power" ] , Powerlevel [ "-9 dBm" ] , executor ( this , otherNode ) ) ; if ( powerlevel == undefined ) { minPowerlevelSource = Powerlevel [ "Normal Power" ] ; failedPingsToTarget = failedPings ; } else { minPowerlevelSource = powerlevel ; } } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_CallbackNOK ) { minPowerlevelSource = Powerlevel [ "Normal Power" ] ; failedPingsToTarget = healthCheckTestFrameCount ; } else { throw e ; } } } if ( otherNode . supportsCC ( CommandClasses . Powerlevel ) ) { try { const powerlevel = await discreteBinarySearch ( Powerlevel [ "Normal Power" ] , Powerlevel [ "-9 dBm" ] , executor ( otherNode , this ) ) ; if ( powerlevel == undefined ) { minPowerlevelTarget = Powerlevel [ "Normal Power" ] ; failedPingsToSource = failedPings ; } else { minPowerlevelTarget = powerlevel ; } } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_CallbackNOK ) { minPowerlevelTarget = Powerlevel [ "Normal Power" ] ; failedPingsToSource = healthCheckTestFrameCount ; } else { throw e ; } } } const ret = { numNeighbors , failedPingsToSource , failedPingsToTarget , minPowerlevelSource , minPowerlevelTarget , rating : 0 } ; ret . rating = computeRating ( ret ) ; results . push ( ret ) ; onProgress ?. ( round , rounds , ret . rating ) ; } const duration = Date . now ( ) - start ; const rating = Math . min ( ... results . map ( ( r ) => r . rating ) ) ; const summary = { results , rating } ; this . driver . controllerLog . logNode ( this . id , `Route health check to node ${ otherNode . id } complete in ${ duration } ms
${ formatRouteHealthCheckSummary ( this . id , otherNode . id , summary ) }` ) ; return summary ; } }