import { JsonlDB , JsonlDBOptions } from "@alcalzone/jsonl-db" ; import * as Sentry from "@sentry/node" ; import { ConfigManager , externalConfigDir } from "@zwave-js/config" ; import { CommandClasses , deserializeCacheValue , dskFromString , Duration , highResTimestamp , isZWaveError , LogConfig , nwiHomeIdFromDSK , SecurityClass , securityClassIsS2 , SecurityManager , SecurityManager2 , serializeCacheValue , SPANState , timespan , ValueMetadata , ZWaveError , ZWaveErrorCodes , ZWaveLogContainer , } from "@zwave-js/core" ; import { MessageHeaders , ZWaveSerialPort , ZWaveSerialPortBase , ZWaveSocket , } from "@zwave-js/serial" ; import { DeepPartial , getErrorMessage , isDocker , mergeDeep , num2hex , pick , stringify , TypedEventEmitter , } from "@zwave-js/shared" ; import { wait } from "alcalzone-shared/async" ; import { createDeferredPromise , DeferredPromise , } from "alcalzone-shared/deferred-promise" ; import { isArray , isObject } from "alcalzone-shared/typeguards" ; import { randomBytes } from "crypto" ; import type { EventEmitter } from "events" ; import fsExtra from "fs-extra" ; import path from "path" ; import SerialPort from "serialport" ; import { URL } from "url" ; import * as util from "util" ; import { interpret } from "xstate" ; import { FirmwareUpdateStatus , Security2CC , Security2CCNonceReport , } from "../commandclass" ; import { assertValidCCs , CommandClass , getImplementedVersion , InvalidCC , } from "../commandclass/CommandClass" ; import { DeviceResetLocallyCCNotification } from "../commandclass/DeviceResetLocallyCC" ; import { isEncapsulatingCommandClass , isMultiEncapsulatingCommandClass , } from "../commandclass/EncapsulatingCommandClass" ; import { ICommandClassContainer , isCommandClassContainer , } from "../commandclass/ICommandClassContainer" ; import { MultiChannelCC } from "../commandclass/MultiChannelCC" ; import { messageIsPing } from "../commandclass/NoOperationCC" ; import { KEXFailType } from "../commandclass/Security2/shared" ; import { SecurityCC , SecurityCCCommandEncapsulationNonceGet , } from "../commandclass/SecurityCC" ; import { SupervisionCC , SupervisionCCGet , SupervisionCCReport , SupervisionResult , SupervisionStatus , } from "../commandclass/SupervisionCC" ; import { isTransportServiceEncapsulation , TransportServiceCCFirstSegment , TransportServiceCCSegmentComplete , TransportServiceCCSegmentRequest , TransportServiceCCSegmentWait , TransportServiceCCSubsequentSegment , TransportServiceTimeouts , } from "../commandclass/TransportServiceCC" ; import { getWakeUpIntervalValueId , WakeUpCCNoMoreInformation , } from "../commandclass/WakeUpCC" ; import { ApplicationCommandRequest } from "../controller/ApplicationCommandRequest" ; import { ApplicationUpdateRequest , ApplicationUpdateRequestNodeInfoReceived , ApplicationUpdateRequestSmartStartHomeIDReceived , } from "../controller/ApplicationUpdateRequest" ; import { BridgeApplicationCommandRequest } from "../controller/BridgeApplicationCommandRequest" ; import { ZWaveController } from "../controller/Controller" ; import { GetControllerVersionRequest } from "../controller/GetControllerVersionMessages" ; import { InclusionState } from "../controller/Inclusion" ; import { SendDataBridgeRequest , SendDataMulticastBridgeRequest , } from "../controller/SendDataBridgeMessages" ; import { MAX_SEND_ATTEMPTS , SendDataAbort , SendDataMulticastRequest , SendDataRequest , } from "../controller/SendDataMessages" ; import { hasTXReport , isSendData , isSendDataSinglecast , isSendDataTransmitReport , isTransmitReport , SendDataMessage , TransmitOptions , TXReport , } from "../controller/SendDataShared" ; import { SoftResetRequest } from "../controller/SoftResetRequest" ; import { ControllerLogger } from "../log/Controller" ; import { DriverLogger } from "../log/Driver" ; import { FunctionType , MessagePriority , MessageType , } from "../message/Constants" ; import { getDefaultPriority , Message } from "../message/Message" ; import { isSuccessIndicator } from "../message/SuccessIndicator" ; import { INodeQuery , isNodeQuery } from "../node/INodeQuery" ; import type { ZWaveNode } from "../node/Node" ; import { InterviewStage , NodeStatus } from "../node/Types" ; import type { SerialAPIStartedRequest } from "../serialapi/misc/SerialAPIStartedRequest" ; import { reportMissingDeviceConfig } from "../telemetry/deviceConfig" ; import { AppInfo , compileStatistics , sendStatistics , } from "../telemetry/statistics" ; import { createMessageGenerator } from "./MessageGenerators" ; import { createSendThreadMachine , SendThreadInterpreter , TransactionReducer , TransactionReducerResult , } from "./SendThreadMachine" ; import { throttlePresets } from "./ThrottlePresets" ; import { Transaction } from "./Transaction" ; import { createTransportServiceRXMachine , TransportServiceRXInterpreter , } from "./TransportServiceMachine" ; import { checkForConfigUpdates , installConfigUpdate , installConfigUpdateInDocker , } from "./UpdateConfig" ; import type { ZWaveOptions } from "./ZWaveOptions" ; const packageJsonPath = require . resolve ( "zwave-js/package.json" ) ; const packageJson = require ( packageJsonPath ) ; const libraryRootDir = path . dirname ( packageJsonPath ) ; const libVersion = packageJson . version ; const libNameString = `
███████╗ ██╗    ██╗  █████╗  ██╗   ██╗ ███████╗             ██╗ ███████╗
╚══███╔╝ ██║    ██║ ██╔══██╗ ██║   ██║ ██╔════╝             ██║ ██╔════╝
  ███╔╝  ██║ █╗ ██║ ███████║ ██║   ██║ █████╗   █████╗      ██║ ███████╗
 ███╔╝   ██║███╗██║ ██╔══██║ ╚██╗ ██╔╝ ██╔══╝   ╚════╝ ██   ██║ ╚════██║
███████╗ ╚███╔███╔╝ ██║  ██║  ╚████╔╝  ███████╗        ╚█████╔╝ ███████║
╚══════╝  ╚══╝╚══╝  ╚═╝  ╚═╝   ╚═══╝   ╚══════╝         ╚════╝  ╚══════╝
` ; const defaultOptions = { timeouts : { ack : 1000 , byte : 150 , response : 1600 , report : 10000 , nonce : 5000 , sendDataCallback : 65000 , refreshValue : 5000 , refreshValueAfterTransition : 1000 , serialAPIStarted : 5000 } , attempts : { openSerialPort : 10 , controller : 3 , sendData : 3 , nodeInterview : 5 } , preserveUnknownValues : false , disableOptimisticValueUpdate : false , enableSoftReset : ! process . env . ZWAVEJS_DISABLE_SOFT_RESET , interview : { skipInterview : false , queryAllUserCodes : false } , storage : { driver : fsExtra , cacheDir : path . resolve ( libraryRootDir , "cache" ) , lockDir : process . env . ZWAVEJS_LOCK_DIRECTORY , throttle : "normal" } , preferences : { scales : { temperature : "Celsius" } } } ; /** Ensures that the options are valid */ function checkOptions ( options ) { if ( options . timeouts . ack < 1 ) { throw new ZWaveError ( `The ACK timeout must be positive!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . timeouts . byte < 1 ) { throw new ZWaveError ( `The BYTE timeout must be positive!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . timeouts . response < 500 || options . timeouts . response > 5000 ) { throw new ZWaveError ( `The Response timeout must be between 500 and 5000 milliseconds!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . timeouts . report < 1000 || options . timeouts . report > 40000 ) { throw new ZWaveError ( `The Report timeout must be between 1000 and 40000 milliseconds!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . timeouts . nonce < 3000 || options . timeouts . nonce > 20000 ) { throw new ZWaveError ( `The Nonce timeout must be between 3000 and 20000 milliseconds!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . timeouts . sendDataCallback < 10000 ) { throw new ZWaveError ( `The Send Data Callback timeout must be at least 10000 milliseconds!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . timeouts . serialAPIStarted < 1000 || options . timeouts . serialAPIStarted > 30000 ) { throw new ZWaveError ( `The Serial API started timeout must be between 1000 and 30000 milliseconds!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . securityKeys != undefined ) { if ( options . networkKey != undefined ) { throw new ZWaveError ( `The deprecated networkKey option may not be used together with the new securityKeys option!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } const keys = Object . entries ( options . securityKeys ) ; for ( let i = 0 ; i < keys . length ; i ++ ) { const [ secClass , key ] = keys [ i ] ; if ( key . length !== 16 ) { throw new ZWaveError ( `The security key for class ${ secClass } must be a buffer with length 16!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( keys . findIndex ( ( [ , k ] ) => k . equals ( key ) ) !== i ) { throw new ZWaveError ( `The security key for class ${ secClass } was used multiple times!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } } } else if ( options . networkKey != undefined ) { if ( options . networkKey . length !== 16 ) { throw new ZWaveError ( `The network key must be a buffer with length 16!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } } if ( options . attempts . controller < 1 || options . attempts . controller > 3 ) { throw new ZWaveError ( `The Controller attempts must be between 1 and 3!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . attempts . sendData < 1 || options . attempts . sendData > MAX_SEND_ATTEMPTS ) { throw new ZWaveError ( `The SendData attempts must be between 1 and ${ MAX_SEND_ATTEMPTS }!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } if ( options . attempts . nodeInterview < 1 || options . attempts . nodeInterview > 10 ) { throw new ZWaveError ( `The Node interview attempts must be between 1 and 10!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } } /**
 * Function signature for a message handler. The return type signals if the
 * message was handled (`true`) or further handlers should be called (`false`)
 */ export type RequestHandler < T extends Message = Message > = ( msg : T ) => boolean | Promise < boolean > ; interface RequestHandlerEntry < T extends Message = Message > { invoke : RequestHandler < T > ; oneTime : boolean ; } interface AwaitedMessageEntry { promise : DeferredPromise < Message > ; timeout ? : NodeJS . Timeout ; predicate : ( msg : Message ) => boolean ; } interface AwaitedCommandEntry { promise : DeferredPromise < CommandClass > ; timeout ? : NodeJS . Timeout ; predicate : ( cc : CommandClass ) => boolean ; } export interface SendMessageOptions { /** The priority of the message to send. If none is given, the defined default priority of the message class will be used. */ priority ? : MessagePriority ; /** If an exception should be thrown when the message to send is not supported. Setting this to false is is useful if the capabilities haven't been determined yet. Default: true */ supportCheck ? : boolean ; /**
     * Whether the driver should update the node status to asleep or dead when a transaction is not acknowledged (repeatedly).
     * Setting this to false will cause the simply transaction to be rejected on failure.
     * Default: true
     */ changeNodeStatusOnMissingACK ? : boolean ; /** Sets the number of milliseconds after which a message expires. When the expiration timer elapses, the promise is rejected with the error code `Controller_MessageExpired`. */ expire ? : number ; internal tag ? : any ; internal pauseSendThread ? : boolean ; /** If a Wake Up On Demand should be requested for the target node. */ requestWakeUpOnDemand ? : boolean ; /**
     * When a message sent to a node results in a TX report to be received, this callback will be called.
     * For multi-stage messages, the callback may be called multiple times.
     */ onTXReport ? : ( report : TXReport ) => void ; } export interface SendCommandOptions extends SendMessageOptions { /** How many times the driver should try to send the message. Defaults to the configured Driver option */ maxSendAttempts ? : number ; /** Whether the driver should automatically handle the encapsulation. Default: true */ autoEncapsulate ? : boolean ; /** Overwrite the default transmit options */ transmitOptions ? : TransmitOptions ; } export type SupervisionUpdateHandler = ( status : SupervisionStatus , remainingDuration ? : Duration ) => void ; export type SendSupervisedCommandOptions = SendCommandOptions & ( { requestStatusUpdates : false ; } | { requestStatusUpdates : true ; onUpdate : SupervisionUpdateHandler ; } ) ; interface TransportServiceSession { fragmentSize : number ; interpreter : TransportServiceRXInterpreter ; } interface Sessions { /** A map of all current Transport Service sessions that may still receive updates */ transportService : Map < number , TransportServiceSession > ; /** A map of all current supervision sessions that may still receive updates */ supervision : Map < number , SupervisionUpdateHandler > ; } export interface DriverEventCallbacks { "driver ready" : ( ) => void ; "all nodes ready" : ( ) => void ; error : ( err : Error ) => void ; } export type DriverEvents = Extract < keyof DriverEventCallbacks , string > ; /**
 * The driver is the core of this library. It controls the serial interface,
 * handles transmission and receipt of messages and manages the network cache.
 * Any action you want to perform on the Z-Wave network must go through a driver
 * instance or its associated nodes.
 */ export class Driver extends TypedEventEmitter < DriverEventCallbacks > { public constructor ( private port , options ? ) { super ( ) ; this . options = mergeDeep ( options , defaultOptions ) as ZWaveOptions ; checkOptions ( this . options ) ; this . _logContainer = new ZWaveLogContainer ( this . options . logConfig ) ; this . _driverLog = new DriverLogger ( this . _logContainer ) ; this . _controllerLog = new ControllerLogger ( this . _logContainer ) ; this . cacheDir = this . options . storage . cacheDir ; this . configManager = new ConfigManager ( { logContainer : this . _logContainer , deviceConfigPriorityDir : this . options . storage . deviceConfigPriorityDir } ) ; const sendThreadMachine = createSendThreadMachine ( { sendData : this . writeSerial . bind ( this ) , createSendDataAbort : ( ) => new SendDataAbort ( this ) , notifyUnsolicited : ( msg ) => { void this . handleUnsolicitedMessage ( msg ) ; } , notifyRetry : ( command , lastError , message , attempts , maxAttempts , delay ) => { if ( command === "SendData" ) { this . controllerLog . logNode ( message . getNodeId ( ) ?? 255 , `did not respond after ${ attempts }/${ maxAttempts } attempts. Scheduling next try in ${ delay } ms.` , "warn" ) ; } else { let errorReason ; switch ( lastError ) { case "response timeout" : errorReason = "No response from controller" ; this . _controller ?. incrementStatistics ( "timeoutResponse" ) ; break ; case "callback timeout" : errorReason = "No callback from controller" ; this . _controller ?. incrementStatistics ( "timeoutCallback" ) ; break ; case "response NOK" : errorReason = "The controller response indicated failure" ; break ; case "callback NOK" : errorReason = "The controller callback indicated failure" ; break ; case "ACK timeout" : this . _controller ?. incrementStatistics ( "timeoutACK" ) ; case "CAN" : case "NAK" : default : errorReason = "Failed to execute controller command" ; break ; } this . controllerLog . print ( `${ errorReason } after ${ attempts }/${ maxAttempts } attempts. Scheduling next try in ${ delay } ms.` , "warn" ) ; } } , timestamp : highResTimestamp , rejectTransaction : ( transaction , error ) => { if ( this . isMissingNodeACK ( transaction , error ) ) { if ( this . handleMissingNodeACK ( transaction as any ) ) return ; } if ( transaction . parts . self ) { transaction . parts . self . throw ( error ) . catch ( ( ) => { } ) ; } else { transaction . promise . reject ( error ) ; } } , resolveTransaction : ( transaction , result ) => { if ( transaction . parts . self ) { transaction . parts . self . throw ( result ) . catch ( ( ) => { } ) ; } else { transaction . promise . resolve ( result ) ; } } , logOutgoingMessage : ( msg ) => { this . driverLog . logMessage ( msg , { direction : "outbound" } ) ; if ( process . env . NODE_ENV !== "test" ) { Sentry . addBreadcrumb ( { category : "message" , timestamp : Date . now ( ) / 1000 , type : "debug" , data : { direction : "outbound" , msgType : msg . type , functionType : msg . functionType , name : msg . constructor . name , nodeId : msg . getNodeId ( ) , ... msg . toLogEntry ( ) } } ) ; } } , log : this . driverLog . print . bind ( this . driverLog ) , logQueue : this . driverLog . sendQueue . bind ( this . driverLog ) } , pick ( this . options , [ "timeouts" , "attempts" ] ) ) ; this . sendThread = interpret ( sendThreadMachine ) ; } /** The serial port instance */ private serial : ZWaveSerialPortBase | undefined ; /** An instance of the Send Thread state machine */ private sendThread : SendThreadInterpreter ; /** A map of handlers for all sorts of requests */ private requestHandlers = new Map < FunctionType , RequestHandlerEntry [ ] > ( ) ; /** A map of awaited messages */ private awaitedMessages : AwaitedMessageEntry [ ] = [ ] ; /** A map of awaited commands */ private awaitedCommands : AwaitedCommandEntry [ ] = [ ] ; /** A map of Node ID -> ongoing sessions */ private nodeSessions = new Map < number , Sessions > ( ) ; private ensureNodeSessions ( nodeId ) { if ( ! this . nodeSessions . has ( nodeId ) ) { this . nodeSessions . set ( nodeId , { transportService : new Map ( ) , supervision : new Map ( ) } ) ; } return this . nodeSessions . get ( nodeId ) ! ; } public readonly cacheDir : string ; private _valueDB : JsonlDB | undefined ; internal public get valueDB ( ) : JsonlDB | undefined { return this . _valueDB ; } private _metadataDB : JsonlDB < ValueMetadata > | undefined ; internal public get metadataDB ( ) : JsonlDB < ValueMetadata > | undefined { return this . _metadataDB ; } public readonly configManager : ConfigManager ; public get configVersion ( ) : string { return ( this . configManager ?. configVersion ?? packageJson ?. dependencies ?. [ "@zwave-js/config" ] ?? libVersion ) ; } private _logContainer : ZWaveLogContainer ; private _driverLog : DriverLogger ; internal public get driverLog ( ) : DriverLogger { return this . _driverLog ; } private _controllerLog : ControllerLogger ; internal public get controllerLog ( ) : ControllerLogger { return this . _controllerLog ; } private _controller : ZWaveController | undefined ; /** Encapsulates information about the Z-Wave controller and provides access to its nodes */ public get controller ( ) : ZWaveController { if ( this . _controller == undefined ) { throw new ZWaveError ( "The controller is not yet ready!" , ZWaveErrorCodes . Driver_NotReady ) ; } return this . _controller ; } private _securityManager : SecurityManager | undefined ; internal public get securityManager ( ) : SecurityManager | undefined { return this . _securityManager ; } private _securityManager2 : SecurityManager2 | undefined ; internal public get securityManager2 ( ) : SecurityManager2 | undefined { return this . _securityManager2 ; } /** Updates the logging configuration without having to restart the driver. */ public updateLogConfig ( config ) { this . _logContainer . updateConfiguration ( config ) ; } /** Returns the current logging configuration. */ public getLogConfig ( ) { return this . _logContainer . getConfiguration ( ) ; } /** Updates the preferred sensor scales to use for node queries */ public setPreferredScales ( scales ) { this . options . preferences . scales = mergeDeep ( defaultOptions . preferences . scales , scales ) ; } /** Enumerates all existing serial ports */ public static async enumerateSerialPorts ( ) { const ports = await SerialPort . list ( ) ; return ports . map ( ( port ) => port . path ) ; } internal public options : ZWaveOptions ; private _wasStarted : boolean = false ; private _isOpen : boolean = false ; /** Start the driver */ public async start ( ) { if ( this . wasDestroyed ) { throw new ZWaveError ( "The driver was destroyed. Create a new instance and start that one." , ZWaveErrorCodes . Driver_Destroyed ) ; } if ( this . _wasStarted ) return Promise . resolve ( ) ; this . _wasStarted = true ; if ( ( this as unknown as EventEmitter ) . listenerCount ( "error" ) === 0 ) { throw new ZWaveError ( `Before starting the driver, a handler for the "error" event must be attached.` , ZWaveErrorCodes . Driver_NoErrorHandler ) ; } const spOpenPromise = createDeferredPromise ( ) ; this . driverLog . print ( libNameString , "info" ) ; this . driverLog . print ( `version ${ libVersion }` , "info" ) ; this . driverLog . print ( "" , "info" ) ; this . driverLog . print ( "starting driver..." ) ; this . sendThread . start ( ) ; if ( this . port . startsWith ( "tcp://" ) ) { const url = new URL ( this . port ) ; this . driverLog . print ( `opening serial port ${ this . port }` ) ; this . serial = new ZWaveSocket ( { host : url . hostname , port : parseInt ( url . port ) } , this . _logContainer ) ; } else { this . driverLog . print ( `opening serial port ${ this . port }` ) ; this . serial = new ZWaveSerialPort ( this . port , this . _logContainer ) ; } this . serial . on ( "data" , this . serialport_onData . bind ( this ) ) . on ( "error" , ( err ) => { if ( this . isSoftResetting && ! this . serial ?. isOpen ) { return ; } else if ( ! this . _isOpen ) { return ; } const message = `Serial port errored: ${ err . message }` ; this . driverLog . print ( message , "error" ) ; const error = new ZWaveError ( message , ZWaveErrorCodes . Driver_Failed ) ; this . emit ( "error" , error ) ; void this . destroy ( ) ; } ) ; if ( this . serial . isOpen ) await this . serial . close ( ) ; setImmediate ( async ( ) => { try { await this . openSerialport ( ) ; } catch ( e ) { spOpenPromise . reject ( e ) ; void this . destroy ( ) ; return ; } this . driverLog . print ( "serial port opened" ) ; this . _isOpen = true ; spOpenPromise . resolve ( ) ; await this . writeHeader ( MessageHeaders . NAK ) ; try { await this . options . storage . driver . ensureDir ( this . cacheDir ) ; } catch ( e ) { let message ; if ( /\.yarn[/\\]cache[/\\]zwave-js/i . test ( getErrorMessage ( e , true ) ) ) { message = `Failed to create the cache directory. When using Yarn PnP, you need to change the location with the "storage.cacheDir" driver option.` ; } else { message = `Failed to create the cache directory. Please make sure that it is writable or change the location with the "storage.cacheDir" driver option.` ; } this . driverLog . print ( message , "error" ) ; this . emit ( "error" , new ZWaveError ( message , ZWaveErrorCodes . Driver_Failed ) ) ; void this . destroy ( ) ; return ; } this . driverLog . print ( "loading configuration..." ) ; try { await this . configManager . loadAll ( ) ; } catch ( e ) { const message = `Failed to load the configuration: ${ getErrorMessage ( e ) }` ; this . driverLog . print ( message , "error" ) ; this . emit ( "error" , new ZWaveError ( message , ZWaveErrorCodes . Driver_Failed ) ) ; void this . destroy ( ) ; return ; } this . driverLog . print ( "beginning interview..." ) ; try { await this . initializeControllerAndNodes ( ) ; } catch ( e ) { let message ; if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_MessageDropped ) { message = `Failed to initialize the driver, no response from the controller. Are you sure this is a Z-Wave controller?` ; } else { message = `Failed to initialize the driver: ${ getErrorMessage ( e , true ) }` ; } this . driverLog . print ( message , "error" ) ; this . emit ( "error" , new ZWaveError ( message , ZWaveErrorCodes . Driver_Failed ) ) ; void this . destroy ( ) ; return ; } } ) ; return spOpenPromise ; } private _controllerInterviewed : boolean = false ; private _nodesReady = new Set < number > ( ) ; private _nodesReadyEventEmitted : boolean = false ; private async openSerialport ( ) { let lastError ; for ( let attempt = 1 ; attempt <= this . options . attempts . openSerialPort ; attempt ++ ) { try { await this . serial ! . open ( ) ; return ; } catch ( e ) { lastError = e ; } if ( attempt < this . options . attempts . openSerialPort ) { await wait ( 1000 ) ; } } const message = `Failed to open the serial port: ${ getErrorMessage ( lastError ) }` ; this . driverLog . print ( message , "error" ) ; throw new ZWaveError ( message , ZWaveErrorCodes . Driver_Failed ) ; } /** Indicates whether all nodes are ready, i.e. the "all nodes ready" event has been emitted */ public get allNodesReady ( ) : boolean { return this . _nodesReadyEventEmitted ; } private async initValueDBs ( homeId ) { const options = { ignoreReadErrors : true , ... throttlePresets [ this . options . storage . throttle ] } ; if ( this . options . storage . lockDir ) { options . lockfileDirectory = this . options . storage . lockDir ; } const valueDBFile = path . join ( this . cacheDir , `${ homeId . toString ( 16 ) }.values.jsonl` ) ; this . _valueDB = new JsonlDB ( valueDBFile , { ... options , reviver : ( key , value ) => deserializeCacheValue ( value ) , serializer : ( key , value ) => serializeCacheValue ( value ) } ) ; await this . _valueDB . open ( ) ; const metadataDBFile = path . join ( this . cacheDir , `${ homeId . toString ( 16 ) }.metadata.jsonl` ) ; this . _metadataDB = new JsonlDB ( metadataDBFile , options ) ; await this . _metadataDB . open ( ) ; if ( process . env . NO_CACHE === "true" ) { this . _valueDB . clear ( ) ; this . _metadataDB . clear ( ) ; } } /**
     * Initializes the variables for controller and nodes,
     * adds event handlers and starts the interview process.
     */ private async initializeControllerAndNodes ( ) { if ( this . _controller == undefined ) { this . _controller = new ZWaveController ( this ) ; this . _controller . on ( "node added" , this . onNodeAdded . bind ( this ) ) . on ( "node removed" , this . onNodeRemoved . bind ( this ) ) ; } if ( ! this . options . interview . skipInterview ) { await this . controller . identify ( ) ; if ( this . options . enableSoftReset && ! ( await this . maySoftReset ( ) ) ) { this . driverLog . print ( `Soft reset is enabled through config, but this stick does not support it.` , "warn" ) ; this . options . enableSoftReset = false ; } if ( this . options . enableSoftReset ) { try { await this . softResetInternal ( false ) ; } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Driver_Failed ) { await this . rememberNoSoftReset ( ) ; await this . destroy ( ) ; return ; } } } if ( this . controller . ownNodeId === 0 && this . options . enableSoftReset ) { this . driverLog . print ( `Controller identification returned invalid node ID 0 - trying again...` , "warn" ) ; await this . controller . identify ( ) ; } if ( this . controller . ownNodeId === 0 ) { this . driverLog . print ( `Controller identification returned invalid node ID 0` , "error" ) ; await this . destroy ( ) ; return ; } await this . initValueDBs ( this . controller . homeId ! ) ; await this . _controller . interview ( async ( ) => { if ( process . env . NO_CACHE !== "true" ) { await this . restoreNetworkStructureFromCache ( ) ; } } ) ; this . _controller . autoProvisionSmartStart ( ) ; } const S0Key = this . options . securityKeys ?. S0_Legacy ?? this . options . networkKey ; if ( S0Key ) { this . driverLog . print ( "Network key for S0 configured, enabling S0 security manager..." ) ; this . _securityManager = new SecurityManager ( { networkKey : S0Key , ownNodeId : this . _controller . ownNodeId ! , nonceTimeout : this . options . timeouts . nonce } ) ; } else { this . driverLog . print ( "No network key for S0 configured, communication with secure (S0) devices won't work!" , "warn" ) ; } if ( this . options . securityKeys && Object . keys ( this . options . securityKeys ) . some ( ( key ) => key . startsWith ( "S2_" ) && key in SecurityClass && securityClassIsS2 ( ( SecurityClass as any ) [ key ] ) ) ) { this . driverLog . print ( "At least one network key for S2 configured, enabling S2 security manager..." ) ; this . _securityManager2 = new SecurityManager2 ( ) ; for ( const secClass of [ "S2_Unauthenticated" , "S2_Authenticated" , "S2_AccessControl" , "S0_Legacy" , ] as const ) { const key = this . options . securityKeys [ secClass ] ; if ( key ) { this . _securityManager2 . setKey ( SecurityClass [ secClass ] , key ) ; } } } else { this . driverLog . print ( "No network key for S2 configured, communication with secure (S2) devices won't work!" , "warn" ) ; } this . _controllerInterviewed = true ; this . driverLog . print ( "driver ready" ) ; this . emit ( "driver ready" ) ; for ( const node of this . _controller . nodes . values ( ) ) { this . addNodeEventHandlers ( node ) ; } this . _nodesReady . clear ( ) ; this . _nodesReadyEventEmitted = false ; if ( ! this . options . interview . skipInterview ) { const controllerNode = this . _controller . nodes . get ( this . _controller . ownNodeId ! ) ! ; await this . interviewNode ( controllerNode ) ; for ( const node of this . _controller . nodes . values ( ) ) { if ( node . id === this . _controller . ownNodeId ) { node . markAsAlive ( ) ; continue ; } else if ( node . canSleep ) { node . markAsAsleep ( ) ; } void ( async ( ) => { if ( node . interviewStage < InterviewStage . Complete ) { await this . interviewNode ( node ) ; } else if ( node . isListening || node . isFrequentListening ) { await node . ping ( ) ; } if ( ! node . hasSUCReturnRoute && node . status !== NodeStatus . Dead ) { node . hasSUCReturnRoute = await this . controller . assignSUCReturnRoute ( node . id ) ; } } ) ( ) ; } } } private retryNodeInterviewTimeouts = new Map < number , NodeJS . Timeout > ( ) ; internal public async interviewNode ( node ) { if ( node . interviewStage === InterviewStage . Complete ) { return ; } if ( this . retryNodeInterviewTimeouts . has ( node . id ) ) { clearTimeout ( this . retryNodeInterviewTimeouts . get ( node . id ) ! ) ; this . retryNodeInterviewTimeouts . delete ( node . id ) ; } this . rejectTransactions ( ( t ) => t . message . getNodeId ( ) === node . id && ( t . priority === MessagePriority . NodeQuery || t . tag === "interview" ) , "The interview was restarted" , ZWaveErrorCodes . Controller_InterviewRestarted ) ; const maxInterviewAttempts = this . options . attempts . nodeInterview ; try { if ( ! ( await node . interview ( ) ) ) { if ( node . status === NodeStatus . Dead ) { this . controllerLog . logNode ( node . id , `Interview attempt (${ node . interviewAttempts }/${ maxInterviewAttempts }) failed, node is dead.` , "warn" ) ; node . emit ( "interview failed" , node , { errorMessage : "The node is dead" , isFinal : true } ) ; } else if ( node . interviewAttempts < maxInterviewAttempts ) { const retryTimeout = Math . min ( 30000 , node . interviewAttempts * 5000 ) ; this . controllerLog . logNode ( node . id , `Interview attempt ${ node . interviewAttempts }/${ maxInterviewAttempts } failed, retrying in ${ retryTimeout } ms...` , "warn" ) ; node . emit ( "interview failed" , node , { errorMessage : `Attempt ${ node . interviewAttempts }/${ maxInterviewAttempts } failed` , isFinal : false , attempt : node . interviewAttempts , maxAttempts : maxInterviewAttempts } ) ; this . retryNodeInterviewTimeouts . set ( node . id , setTimeout ( ( ) => { this . retryNodeInterviewTimeouts . delete ( node . id ) ; void this . interviewNode ( node ) ; } , retryTimeout ) . unref ( ) ) ; } else { this . controllerLog . logNode ( node . id , `Failed all interview attempts, giving up.` , "warn" ) ; node . emit ( "interview failed" , node , { errorMessage : `Maximum interview attempts reached` , isFinal : true , attempt : maxInterviewAttempts , maxAttempts : maxInterviewAttempts } ) ; } } else if ( node . manufacturerId != undefined && node . productType != undefined && node . productId != undefined && node . firmwareVersion != undefined && ! node . deviceConfig && process . env . NODE_ENV !== "test" ) { void reportMissingDeviceConfig ( node as any ) . catch ( ( ) => { } ) ; } } catch ( e ) { if ( isZWaveError ( e ) ) { if ( e . code === ZWaveErrorCodes . Driver_NotReady || e . code === ZWaveErrorCodes . Controller_NodeRemoved ) { return ; } else if ( e . code === ZWaveErrorCodes . Controller_InterviewRestarted ) { return ; } this . controllerLog . logNode ( node . id , `Error during node interview: ${ e . message }` , "error" ) ; } else { throw e ; } } } /** Adds the necessary event handlers for a node instance */ private addNodeEventHandlers ( node ) { node . on ( "wake up" , this . onNodeWakeUp . bind ( this ) ) . on ( "sleep" , this . onNodeSleep . bind ( this ) ) . on ( "alive" , this . onNodeAlive . bind ( this ) ) . on ( "dead" , this . onNodeDead . bind ( this ) ) . on ( "interview completed" , this . onNodeInterviewCompleted . bind ( this ) ) . on ( "ready" , this . onNodeReady . bind ( this ) ) . on ( "firmware update finished" , this . onNodeFirmwareUpdated . bind ( this ) ) ; } /** Removes a node's event handlers that were added with addNodeEventHandlers */ private removeNodeEventHandlers ( node ) { node . removeAllListeners ( "wake up" ) . removeAllListeners ( "sleep" ) . removeAllListeners ( "alive" ) . removeAllListeners ( "dead" ) . removeAllListeners ( "interview completed" ) . removeAllListeners ( "ready" ) . removeAllListeners ( "firmware update finished" ) ; } /** Is called when a node wakes up */ private onNodeWakeUp ( node , oldStatus ) { this . controllerLog . logNode ( node . id , `The node is ${ oldStatus === NodeStatus . Unknown ? "" : "now " }awake.` ) ; if ( oldStatus === NodeStatus . Asleep ) { this . sendThread . send ( { type : "reduce" , reducer : ( { message } ) => { if ( message . getNodeId ( ) !== node . id ) return { type : "keep" } ; if ( messageIsPing ( message ) ) return { type : "resolve" , message : undefined } ; return { type : "requeue" } ; } } ) ; } } /** Is called when a node goes to sleep */ private onNodeSleep ( node , oldStatus ) { this . controllerLog . logNode ( node . id , `The node is ${ oldStatus === NodeStatus . Unknown ? "" : "now " }asleep.` ) ; this . moveMessagesToWakeupQueue ( node . id ) ; } /** Is called when a previously dead node starts communicating again */ private onNodeAlive ( node , oldStatus ) { this . controllerLog . logNode ( node . id , `The node is ${ oldStatus === NodeStatus . Unknown ? "" : "now " }alive.` ) ; if ( oldStatus === NodeStatus . Dead && node . interviewStage !== InterviewStage . Complete ) { void this . interviewNode ( node ) ; } } /** Is called when a node is marked as dead */ private onNodeDead ( node , oldStatus ) { this . controllerLog . logNode ( node . id , `The node is ${ oldStatus === NodeStatus . Unknown ? "" : "now " }dead.` ) ; this . checkAllNodesReady ( ) ; } /** Is called when a node is ready to be used */ private onNodeReady ( node ) { this . _nodesReady . add ( node . id ) ; this . controllerLog . logNode ( node . id , "The node is ready to be used" ) ; node . scheduleManualValueRefreshes ( ) ; this . checkAllNodesReady ( ) ; } /** Checks if all nodes are ready and emits the "all nodes ready" event if they are */ private checkAllNodesReady ( ) { if ( this . _nodesReadyEventEmitted ) return ; for ( const [ id , node ] of this . controller . nodes ) { if ( node . status === NodeStatus . Dead ) continue ; if ( ! this . _nodesReady . has ( id ) ) return ; } this . controllerLog . print ( "All nodes are ready to be used" ) ; this . emit ( "all nodes ready" ) ; this . _nodesReadyEventEmitted = true ; void this . compileAndSendStatistics ( ) . catch ( ( ) => { } ) ; } private _statisticsEnabled : boolean = false ; /** Whether reporting usage statistics is currently enabled */ public get statisticsEnabled ( ) : boolean { return this . _statisticsEnabled ; } private statisticsAppInfo : Pick < AppInfo , "applicationName" | "applicationVersion" > | undefined ; /**
     * Enable sending usage statistics. Although this does not include any sensitive information, we expect that you
     * inform your users before enabling statistics.
     */ public enableStatistics ( appInfo ) { if ( this . _statisticsEnabled ) return ; this . _statisticsEnabled = true ; if ( ! isObject ( appInfo ) || typeof appInfo . applicationName !== "string" || typeof appInfo . applicationVersion !== "string" ) { throw new ZWaveError ( `The application statistics must be an object with two string properties "applicationName" and "applicationVersion"!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } else if ( appInfo . applicationName . length > 100 ) { throw new ZWaveError ( `The applicationName for statistics must be maximum 100 characters long!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } else if ( appInfo . applicationVersion . length > 100 ) { throw new ZWaveError ( `The applicationVersion for statistics must be maximum 100 characters long!` , ZWaveErrorCodes . Driver_InvalidOptions ) ; } this . statisticsAppInfo = appInfo ; if ( this . _nodesReadyEventEmitted ) { void this . compileAndSendStatistics ( ) . catch ( ( ) => { } ) ; } } /**
     * Disable sending usage statistics
     */ public disableStatistics ( ) { this . _statisticsEnabled = false ; this . statisticsAppInfo = undefined ; if ( this . statisticsTimeout ) { clearTimeout ( this . statisticsTimeout ) ; this . statisticsTimeout = undefined ; } } internal public async getUUID ( ) { if ( ! this . _valueDB ! . has ( "uuid" ) ) { this . _valueDB ! . set ( "uuid" , randomBytes ( 32 ) . toString ( "hex" ) ) ; } const ret = this . _valueDB ! . get ( "uuid" ) as string ; return ret ; } private statisticsTimeout : NodeJS . Timeout | undefined ; private async compileAndSendStatistics ( ) { if ( ! this . statisticsEnabled || ! this . statisticsAppInfo ) return ; if ( this . statisticsTimeout ) { clearTimeout ( this . statisticsTimeout ) ; this . statisticsTimeout = undefined ; } let success = false ; try { const statistics = await compileStatistics ( this , { driverVersion : libVersion , ... this . statisticsAppInfo , nodeVersion : process . versions . node , os : process . platform , arch : process . arch } ) ; success = await sendStatistics ( statistics ) ; } catch { success = false ; } finally { if ( typeof success === "number" ) { this . driverLog . print ( `Sending usage statistics was rate limited - next attempt scheduled in ${ success } seconds.` , "verbose" ) ; const retryMs = Math . max ( timespan . minutes ( 1 ) , Math . min ( success * 1000 , timespan . hours ( 6 ) ) ) ; this . statisticsTimeout = setTimeout ( ( ) => { void this . compileAndSendStatistics ( ) ; } , retryMs ) . unref ( ) ; } else { this . driverLog . print ( success ? `Usage statistics sent - next transmission scheduled in 23 hours.` : `Failed to send usage statistics - next transmission scheduled in 6 hours.` , "verbose" ) ; this . statisticsTimeout = setTimeout ( ( ) => { void this . compileAndSendStatistics ( ) ; } , timespan . hours ( success ? 23 : 6 ) ) . unref ( ) ; } } } /** Is called when a node interview is completed */ private onNodeInterviewCompleted ( node ) { this . debounceSendNodeToSleep ( node ) ; } /** This is called when a new node has been added to the network */ private onNodeAdded ( node ) { this . addNodeEventHandlers ( node ) ; if ( ! this . options . interview . skipInterview ) { void this . interviewNode ( node ) ; } } /** This is called when a node was removed from the network */ private onNodeRemoved ( node , replaced ) { this . removeNodeEventHandlers ( node ) ; node . valueDB . clear ( ) ; this . rejectAllTransactionsForNode ( node . id , "The node was removed from the network" , ZWaveErrorCodes . Controller_NodeRemoved ) ; if ( ! replaced ) { this . controller . removeNodeFromAllAssociations ( node . id ) . catch ( ( err ) => { this . driverLog . print ( `Failed to remove node ${ node . id } from all associations: ${ err . message }` , "error" ) ; } ) ; } node . destroy ( ) ; this . checkAllNodesReady ( ) ; } /** This is called when a node's firmware was updated */ private onNodeFirmwareUpdated ( node , status , waitTime ? ) { if ( status < FirmwareUpdateStatus . OK_WaitingForActivation ) return ; if ( ! waitTime ) waitTime = 5 ; this . controllerLog . logNode ( node . id , `Firmware updated, scheduling interview in ${ waitTime } seconds...` ) ; this . retryNodeInterviewTimeouts . set ( node . id , setTimeout ( ( ) => { this . retryNodeInterviewTimeouts . delete ( node . id ) ; void node . refreshInfo ( { waitForWakeup : false } ) ; } , waitTime * 1000 ) . unref ( ) ) ; } /** Checks if there are any pending messages for the given node */ private hasPendingMessages ( node ) { if ( this . hasPendingTransactions ( ( t ) => t . message . getNodeId ( ) === node . id ) ) { return true ; } return node . scheduledPolls . size > 0 ; } /** Checks if there are any pending transactions that match the given predicate */ public hasPendingTransactions ( predicate ) { const { queue , activeTransactions } = this . sendThread . state . context ; if ( ! ! queue . find ( ( t ) => predicate ( t ) ) ) return true ; for ( const { transaction } of activeTransactions . values ( ) ) { if ( predicate ( transaction ) ) return true ; } return false ; } param cc param nodeId param endpointIndex public getSupportedCCVersionForEndpoint ( cc , nodeId , endpointIndex = 0 ) { if ( ! this . _controller ?. nodes . has ( nodeId ) ) { return 0 ; } const node = this . controller . nodes . get ( nodeId ) ! ; const endpoint = node . getEndpoint ( endpointIndex ) ; if ( endpoint ) return endpoint . getCCVersion ( cc ) ; return node . getCCVersion ( cc ) ; } param cc param nodeId param endpointIndex public getSafeCCVersionForNode ( cc , nodeId , endpointIndex = 0 ) { const supportedVersion = this . getSupportedCCVersionForEndpoint ( cc , nodeId , endpointIndex ) ; if ( supportedVersion === 0 ) { return 1 ; } else { const implementedVersion = getImplementedVersion ( cc ) ; if ( implementedVersion !== 0 && implementedVersion !== Number . POSITIVE_INFINITY ) { return Math . min ( supportedVersion , implementedVersion ) ; } throw new ZWaveError ( "Cannot retrieve the version of a CC that is not implemented" , ZWaveErrorCodes . CC_NotSupported ) ; } } private isSoftResetting : boolean = false ; private async maySoftReset ( ) { let supportsSoftReset ; if ( this . _controllerInterviewed ) { supportsSoftReset = this . controller . supportsSoftReset ; } else { const fs = this . options . storage . driver ; const cacheFile = path . join ( this . cacheDir , this . controller . homeId ! . toString ( 16 ) + ".json" ) ; let json ; if ( await fs . pathExists ( cacheFile ) ) { try { json = JSON . parse ( await fs . readFile ( cacheFile , "utf8" ) ) ; } catch { } } supportsSoftReset = json ?. controller ?. supportsSoftReset ; } if ( supportsSoftReset === false ) return false ; const { manufacturerId , productType , productId } = this . controller ; if ( manufacturerId === 0x0115 && productType === 0x0000 && productId === 0x0000 ) { return false ; } if ( manufacturerId === 0x0115 && productType === 0x0400 && productId === 0x0001 ) { return false ; } if ( manufacturerId === 0x0109 && productType === 0x1001 && productId === 0x0201 ) { return false ; } return true ; } private async rememberNoSoftReset ( ) { this . driverLog . print ( "Soft reset seems not to be supported by this stick, disabling it." , "warn" ) ; this . controller . supportsSoftReset = false ; if ( this . _controllerInterviewed ) { await this . saveNetworkToCacheInternal ( ) ; } else { const fs = this . options . storage . driver ; await fs . ensureDir ( this . cacheDir ) ; const cacheFile = path . join ( this . cacheDir , this . controller . homeId ! . toString ( 16 ) + ".json" ) ; let json ; if ( await fs . pathExists ( cacheFile ) ) { try { json = JSON . parse ( await fs . readFile ( cacheFile , "utf8" ) ) ; } catch { } } json ??= { } ; json . controller ??= { } ; json . controller . supportsSoftReset = false ; const jsonString = stringify ( json ) ; await this . options . storage . driver . writeFile ( cacheFile , jsonString , "utf8" ) ; } } /**
     * Soft-resets the controller if the feature is enabled
     */ public async trySoftReset ( ) { if ( this . options . enableSoftReset ) { await this . softReset ( ) ; } else { const message = `The soft reset feature is not enabled, skipping API call.` ; this . controllerLog . print ( message , "warn" ) ; } } /**
     * Instruct the controller to soft-reset.
     *
     * **Warning:** USB modules will reconnect, meaning that they might get a new address.
     *
     * **Warning:** This call will throw if soft-reset is not enabled.
     */ public async softReset ( ) { if ( ! this . options . enableSoftReset ) { const message = `The soft reset feature has been disabled with a config option or the ZWAVEJS_DISABLE_SOFT_RESET environment variable.` ; this . controllerLog . print ( message , "error" ) ; throw new ZWaveError ( message , ZWaveErrorCodes . Driver_FeatureDisabled ) ; } return this . softResetInternal ( true ) ; } private async softResetInternal ( destroyOnError ) { this . controllerLog . print ( "Performing soft reset..." ) ; try { this . isSoftResetting = true ; await this . sendMessage ( new SoftResetRequest ( this ) , { supportCheck : false , pauseSendThread : true } ) ; } catch ( e ) { this . controllerLog . print ( `Soft reset failed: ${ getErrorMessage ( e ) }` , "error" ) ; } if ( ! ( await this . ensureSerialAPI ( ) ) ) { if ( destroyOnError ) { await this . destroy ( ) ; } else { throw new ZWaveError ( "The Serial API did not respond after soft-reset" , ZWaveErrorCodes . Driver_Failed ) ; } } this . isSoftResetting = false ; this . unpauseSendThread ( ) ; this . _controller ?. setInclusionState ( InclusionState . Idle ) ; } internal public async softResetAndRestart ( restartLogMessage , restartReason ) { this . controllerLog . print ( "Performing soft reset..." ) ; try { this . isSoftResetting = true ; await this . sendMessage ( new SoftResetRequest ( this ) , { supportCheck : false , pauseSendThread : true } ) ; } catch ( e ) { this . controllerLog . print ( `Soft reset failed: ${ getErrorMessage ( e ) }` , "error" ) ; } this . isSoftResetting = false ; this . controllerLog . print ( restartLogMessage ) ; this . emit ( "error" , new ZWaveError ( restartReason , ZWaveErrorCodes . Driver_Failed ) ) ; await this . destroy ( ) ; } private async ensureSerialAPI ( ) { this . controllerLog . print ( "Waiting for the controller to reconnect..." ) ; let waitResult = await this . waitForMessage < SerialAPIStartedRequest > ( ( msg ) => msg . functionType === FunctionType . SerialAPIStarted , 1500 ) . catch ( ( ) => false as const ) ; if ( waitResult ) { this . controllerLog . print ( "reconnected and restarted" ) ; return true ; } if ( ! this . serial ! . isOpen ) { this . controllerLog . print ( "Re-opening serial port..." ) ; try { await this . openSerialport ( ) ; } catch { return false ; } } this . controllerLog . print ( "Waiting for the Serial API to start..." ) ; waitResult = await this . waitForMessage < SerialAPIStartedRequest > ( ( msg ) => msg . functionType === FunctionType . SerialAPIStarted , this . options . timeouts . serialAPIStarted ) . catch ( ( ) => false as const ) ; if ( waitResult ) { this . controllerLog . print ( "Serial API started" ) ; return true ; } this . controllerLog . print ( "Did not receive notification that Serial API has started, checking if it responds..." ) ; const pollController = async ( ) => { try { this . unpauseSendThread ( ) ; await this . sendMessage ( new GetControllerVersionRequest ( this ) , { supportCheck : false } ) ; this . pauseSendThread ( ) ; this . controllerLog . print ( "Serial API responded" ) ; return true ; } catch { return false ; } } ; if ( await pollController ( ) ) return true ; for ( const backoff of [ 2 , 5 , 10 , 15 ] ) { this . controllerLog . print ( `Serial API did not respond, trying again in ${ backoff } seconds...` ) ; await wait ( backoff * 1000 ) ; if ( await pollController ( ) ) return true ; } this . controllerLog . print ( "Serial API did not respond, giving up" , "error" ) ; return false ; } /**
     * Performs a hard reset on the controller. This wipes out all configuration!
     *
     * The returned Promise resolves when the hard reset has been performed.
     * It does not wait for the initialization process which is started afterwards.
     */ public async hardReset ( ) { this . ensureReady ( true ) ; await this . _controller ! . hardReset ( ) ; this . rejectTransactions ( ( ) => true , `The controller was hard-reset` ) ; this . sendNodeToSleepTimers . forEach ( ( timeout ) => clearTimeout ( timeout ) ) ; this . sendNodeToSleepTimers . clear ( ) ; this . retryNodeInterviewTimeouts . forEach ( ( timeout ) => clearTimeout ( timeout ) ) ; this . retryNodeInterviewTimeouts . clear ( ) ; this . _controllerInterviewed = false ; void this . initializeControllerAndNodes ( ) ; } private _destroyPromise : DeferredPromise < void > | undefined ; private get wasDestroyed ( ) : boolean { return ! ! this . _destroyPromise ; } /**
     * Ensures that the driver is ready to communicate (serial port open and not destroyed).
     * If desired, also checks that the controller interview has been completed.
     */ private ensureReady ( includingController = false ) { if ( ! this . _wasStarted || ! this . _isOpen || this . wasDestroyed ) { throw new ZWaveError ( "The driver is not ready or has been destroyed" , ZWaveErrorCodes . Driver_NotReady ) ; } if ( includingController && ! this . _controllerInterviewed ) { throw new ZWaveError ( "The controller is not ready yet" , ZWaveErrorCodes . Driver_NotReady ) ; } } /** Indicates whether the driver is ready, i.e. the "driver ready" event was emitted */ public get ready ( ) : boolean { return ( this . _wasStarted && this . _isOpen && ! this . wasDestroyed && this . _controllerInterviewed ) ; } /**
     * Terminates the driver instance and closes the underlying serial connection.
     * Must be called under any circumstances.
     */ public async destroy ( ) { if ( this . _destroyPromise ) return this . _destroyPromise ; this . _destroyPromise = createDeferredPromise ( ) ; this . driverLog . print ( "destroying driver instance..." ) ; if ( this . sendThread . initialized ) this . sendThread . stop ( ) ; if ( this . serial != undefined ) { this . serial . removeAllListeners ( ) ; if ( this . serial . isOpen ) await this . serial . close ( ) ; this . serial = undefined ; } try { await this . saveNetworkToCacheInternal ( ) ; } catch ( e ) { this . driverLog . print ( `Saving the network to cache failed: ${ getErrorMessage ( e ) }` , "error" ) ; } try { await this . _valueDB ?. close ( ) ; await this . _metadataDB ?. close ( ) ; } catch ( e ) { this . driverLog . print ( `Closing the value DBs failed: ${ getErrorMessage ( e ) }` , "error" ) ; } for ( const timeout of [ this . saveToCacheTimer , ... this . sendNodeToSleepTimers . values ( ) , ... this . retryNodeInterviewTimeouts . values ( ) , this . statisticsTimeout , ... this . awaitedCommands . map ( ( c ) => c . timeout ) , ... this . awaitedMessages . map ( ( m ) => m . timeout ) , ] ) { if ( timeout ) clearTimeout ( timeout ) ; } this . _controller ?. nodes . forEach ( ( n ) => n . destroy ( ) ) ; this . driverLog . print ( `driver instance destroyed` ) ; this . _logContainer . destroy ( ) ; this . _destroyPromise . resolve ( ) ; } /**
     * Is called when the serial port has received a single-byte message or a complete message buffer
     */ private async serialport_onData ( data ) { if ( typeof data === "number" ) { switch ( data ) { case MessageHeaders . ACK : { this . sendThread . send ( "ACK" ) ; return ; } case MessageHeaders . NAK : { this . sendThread . send ( "NAK" ) ; this . _controller ?. incrementStatistics ( "NAK" ) ; return ; } case MessageHeaders . CAN : { this . sendThread . send ( "CAN" ) ; this . _controller ?. incrementStatistics ( "CAN" ) ; return ; } } } let msg ; try { msg = Message . from ( this , data ) ; if ( isCommandClassContainer ( msg ) ) { assertValidCCs ( msg ) ; } if ( ! ! this . _controller ) { if ( isCommandClassContainer ( msg ) ) { msg . getNodeUnsafe ( ) ?. incrementStatistics ( "commandsRX" ) ; } else { this . _controller . incrementStatistics ( "messagesRX" ) ; } } await this . writeHeader ( MessageHeaders . ACK ) ; } catch ( e ) { try { if ( await this . handleSecurityS2DecodeError ( e , msg ) ) { } else { const response = this . handleDecodeError ( e , data , msg ) ; if ( response ) await this . writeHeader ( response ) ; if ( ! ! this . _controller ) { if ( isCommandClassContainer ( msg ) ) { msg . getNodeUnsafe ( ) ?. incrementStatistics ( "commandsDroppedRX" ) ; const supervisionSessionId = SupervisionCC . getSessionId ( msg . command ) ; if ( supervisionSessionId !== undefined && msg . command instanceof InvalidCC ) { await msg . getNodeUnsafe ( ) ?. createAPI ( CommandClasses . Supervision , false ) . sendReport ( { sessionId : supervisionSessionId , moreUpdatesFollow : false , status : SupervisionStatus . NoSupport , secure : msg . command . secure } ) ; return ; } } else { this . _controller . incrementStatistics ( "messagesDroppedRX" ) ; } } } } catch ( ee ) { if ( ee instanceof Error ) { if ( /serial port is not open/ . test ( ee . message ) ) { this . emit ( "error" , ee ) ; void this . destroy ( ) ; return ; } this . _driverLog . print ( ee . stack ?? ee . message , "error" ) ; } } msg = undefined ; } if ( msg ) { let wasMessageLogged = false ; if ( isCommandClassContainer ( msg ) ) { if ( msg . command instanceof SecurityCCCommandEncapsulationNonceGet ) { void msg . getNodeUnsafe ( ) ?. handleSecurityNonceGet ( ) ; } if ( isTransportServiceEncapsulation ( msg . command ) ) { this . driverLog . logMessage ( msg , { secondaryTags : [ "partial" ] , direction : "inbound" } ) ; wasMessageLogged = true ; void this . handleTransportServiceCommand ( msg . command ) . catch ( ( ) => { } ) ; } if ( ! this . assemblePartialCCs ( msg ) ) return ; if ( isTransportServiceEncapsulation ( msg . command ) ) { msg . command = msg . command . encapsulated ; wasMessageLogged = false ; } } try { if ( ! wasMessageLogged ) { this . driverLog . logMessage ( msg , { direction : "inbound" } ) ; } if ( process . env . NODE_ENV !== "test" ) { Sentry . addBreadcrumb ( { category : "message" , timestamp : Date . now ( ) / 1000 , type : "debug" , data : { direction : "inbound" , msgType : msg . type , functionType : msg . functionType , name : msg . constructor . name , nodeId : msg . getNodeId ( ) , ... msg . toLogEntry ( ) } } ) ; } } catch ( e ) { this . driverLog . print ( `Logging a message failed: ${ getErrorMessage ( e ) }` ) ; } this . sendThread . send ( { type : "message" , message : msg } ) ; } } /** Handles a decoding error and returns the desired reply to the stick */ private handleDecodeError ( e , data , msg ) { if ( isZWaveError ( e ) ) { switch ( e . code ) { case ZWaveErrorCodes . PacketFormat_Invalid : case ZWaveErrorCodes . PacketFormat_Checksum : case ZWaveErrorCodes . PacketFormat_Truncated : this . driverLog . print ( `Dropping message because it contains invalid data` , "warn" ) ; return MessageHeaders . NAK ; case ZWaveErrorCodes . Deserialization_NotImplemented : case ZWaveErrorCodes . CC_NotImplemented : this . driverLog . print ( `Dropping message because it could not be deserialized: ${ e . message }` , "warn" ) ; return MessageHeaders . ACK ; case ZWaveErrorCodes . Driver_NotReady : this . driverLog . print ( `Dropping message because the driver is not ready to handle it yet.` , "warn" ) ; return MessageHeaders . ACK ; case ZWaveErrorCodes . PacketFormat_InvalidPayload : if ( msg ) { this . driverLog . print ( `Dropping message with invalid payload` , "warn" ) ; try { this . driverLog . logMessage ( msg , { direction : "inbound" } ) ; } catch ( e ) { this . driverLog . print ( `Logging a message failed: ${ getErrorMessage ( e ) }` ) ; } } else { this . driverLog . print ( `Dropping message with invalid payload${ typeof e . context === "string" ? ` (Reason: ${ e . context })` : "" }:
0x${ data . toString ( "hex" ) }` , "warn" ) ; } return MessageHeaders . ACK ; case ZWaveErrorCodes . Driver_NoSecurity : case ZWaveErrorCodes . Security2CC_NotInitialized : this . driverLog . print ( `Dropping message because network keys are not set or the driver is not yet ready to receive secure messages.` , "warn" ) ; return MessageHeaders . ACK ; } } else { if ( /database is not open/ . test ( e . message ) ) { this . driverLog . print ( `Dropping message because the driver is not ready to handle it yet.` , "warn" ) ; return MessageHeaders . ACK ; } } throw e ; } private async handleSecurityS2DecodeError ( e , msg ) { if ( ! isZWaveError ( e ) ) return false ; if ( ( e . code === ZWaveErrorCodes . Security2CC_NoSPAN || e . code === ZWaveErrorCodes . Security2CC_CannotDecode ) && isCommandClassContainer ( msg ) ) { const nodeId = msg . getNodeId ( ) ! ; const node = this . _controller ?. nodes . get ( nodeId ) ; if ( ! node ) return false ; await this . writeHeader ( MessageHeaders . ACK ) ; this . driverLog . logMessage ( msg , { direction : "inbound" } ) ; node . incrementStatistics ( "commandsDroppedRX" ) ; if ( node . interviewStage < InterviewStage . NodeInfo ) { node . addCC ( CommandClasses [ "Security 2" ] , { isSupported : true , version : 1 } ) ; } const isS2NonceReport = ( t ) => t . message . getNodeId ( ) === nodeId && isCommandClassContainer ( t . message ) && t . message . command instanceof Security2CCNonceReport ; const message = e . code === ZWaveErrorCodes . Security2CC_CannotDecode ? "Message authentication failed" : "No SPAN is established yet" ; if ( this . controller . bootstrappingS2NodeId === nodeId ) { if ( this . securityManager2 ?. tempKeys . has ( nodeId ) ) { if ( this . securityManager2 . getSPANState ( nodeId ) . type === SPANState . None ) { this . controllerLog . logNode ( nodeId , { message : `${ message }, cannot decode command. Requesting a nonce...` , level : "verbose" , direction : "outbound" } ) ; node . commandClasses [ "Security 2" ] . sendNonce ( ) . catch ( ( ) => { } ) ; } else { this . controllerLog . logNode ( nodeId , { message : `${ message }, cannot decode command. Aborting the S2 bootstrapping process...` , level : "error" , direction : "inbound" } ) ; this . controller . cancelSecureBootstrapS2 ( KEXFailType . BootstrappingCanceled ) ; } } else { this . controllerLog . logNode ( nodeId , { message : `Ignoring KEXSet because the DSK has not been verified yet` , level : "verbose" , direction : "inbound" } ) ; } } else if ( ! this . hasPendingTransactions ( isS2NonceReport ) ) { this . controllerLog . logNode ( nodeId , { message : `${ message }, cannot decode command. Requesting a nonce...` , level : "verbose" , direction : "outbound" } ) ; node . commandClasses [ "Security 2" ] . sendNonce ( ) . catch ( ( ) => { } ) ; } else { this . controllerLog . logNode ( nodeId , { message : `${ message }, cannot decode command.` , level : "verbose" , direction : "none" } ) ; } return true ; } return false ; } /** Checks if a transaction failed because a node didn't respond in time */ private isMissingNodeACK ( transaction , e ) { return ( e . code === ZWaveErrorCodes . Controller_CallbackNOK && ( transaction . message instanceof SendDataRequest || transaction . message instanceof SendDataBridgeRequest ) ) ; } internal public handleMissingNodeACK ( transaction ) { const node = transaction . message . getNodeUnsafe ( ) ; if ( ! node ) return false ; const messagePart1 = isSendData ( transaction . message ) ? `The node did not respond after ${ transaction . message . maxSendAttempts } attempts` : `The node did not respond` ; if ( ! transaction . changeNodeStatusOnTimeout ) { return false ; } else if ( node . canSleep ) { this . controllerLog . logNode ( node . id , `${ messagePart1 }. It is probably asleep, moving its messages to the wakeup queue.` , "warn" ) ; node . markAsAsleep ( ) ; return this . mayMoveToWakeupQueue ( transaction ) ; } else { const errorMsg = `${ messagePart1 }, it is presumed dead` ; this . controllerLog . logNode ( node . id , errorMsg , "warn" ) ; node . markAsDead ( ) ; this . rejectAllTransactionsForNode ( node . id , errorMsg ) ; return false ; } } private partialCCSessions = new Map < string , CommandClass [ ] > ( ) ; private getPartialCCSession ( command , createIfMissing ) ; private getPartialCCSession ( command , createIfMissing ) ; private getPartialCCSession ( command , createIfMissing ) { const sessionId = command . getPartialCCSessionId ( ) ; if ( sessionId ) { const partialSessionKey = JSON . stringify ( { nodeId : command . nodeId , ccId : command . ccId , ccCommand : command . ccCommand ! , ... sessionId } ) ; if ( createIfMissing && ! this . partialCCSessions . has ( partialSessionKey ) ) { this . partialCCSessions . set ( partialSessionKey , [ ] ) ; } return { partialSessionKey , session : this . partialCCSessions . get ( partialSessionKey ) } ; } } /**
     * Assembles partial CCs of in a message body. Returns `true` when the message is complete and can be handled further.
     * If the message expects another partial one, this returns `false`.
     */ private assemblePartialCCs ( msg ) { let command = msg . command ; while ( true ) { const { partialSessionKey , session } = this . getPartialCCSession ( command , true ) ?? { } ; if ( session ) { if ( command . expectMoreMessages ( session ) ) { session . push ( command ) ; if ( ! isTransportServiceEncapsulation ( msg . command ) ) { this . driverLog . logMessage ( msg , { secondaryTags : [ "partial" ] , direction : "inbound" } ) ; } return false ; } else { this . partialCCSessions . delete ( partialSessionKey ! ) ; try { command . mergePartialCCs ( session ) ; } catch ( e ) { if ( isZWaveError ( e ) ) { switch ( e . code ) { case ZWaveErrorCodes . Deserialization_NotImplemented : case ZWaveErrorCodes . CC_NotImplemented : this . driverLog . print ( `Dropping message because it could not be deserialized: ${ e . message }` , "warn" ) ; return false ; case ZWaveErrorCodes . PacketFormat_InvalidPayload : this . driverLog . print ( `Could not assemble partial CCs because the payload is invalid. Dropping them.` , "warn" ) ; return false ; case ZWaveErrorCodes . Driver_NotReady : this . driverLog . print ( `Could not assemble partial CCs because the driver is not ready yet. Dropping them` , "warn" ) ; return false ; } } throw e ; } } } else { } if ( isEncapsulatingCommandClass ( command ) ) { command = command . encapsulated ; } else { break ; } } return true ; } /** Is called when a Transport Service command is received */ private async handleTransportServiceCommand ( command ) { const nodeSessions = this . ensureNodeSessions ( command . nodeId ) ; const transportSession = nodeSessions . transportService . get ( command . sessionId ) ; if ( command instanceof TransportServiceCCFirstSegment ) { if ( transportSession ) { transportSession . interpreter . stop ( ) ; } nodeSessions . transportService . clear ( ) ; this . controllerLog . logNode ( command . nodeId , { message : `Beginning Transport Service RX session #${ command . sessionId }...` , level : "debug" , direction : "inbound" } ) ; const RXStateMachine = createTransportServiceRXMachine ( { requestMissingSegment : async ( index ) => { this . controllerLog . logNode ( command . nodeId , { message : `Transport Service RX session #${ command . sessionId }: Segment ${ index } missing - requesting it...` , level : "debug" , direction : "outbound" } ) ; const cc = new TransportServiceCCSegmentRequest ( this , { nodeId : command . nodeId , sessionId : command . sessionId , datagramOffset : index * transportSession ! . fragmentSize } ) ; await this . sendCommand ( cc , { maxSendAttempts : 1 , priority : MessagePriority . Nonce } ) ; } , sendSegmentsComplete : async ( ) => { this . controllerLog . logNode ( command . nodeId , { message : `Transport Service RX session #${ command . sessionId } complete` , level : "debug" , direction : "outbound" } ) ; const cc = new TransportServiceCCSegmentComplete ( this , { nodeId : command . nodeId , sessionId : command . sessionId } ) ; await this . sendCommand ( cc , { maxSendAttempts : 1 , priority : MessagePriority . Nonce } ) ; } } , { missingSegmentTimeout : TransportServiceTimeouts . requestMissingSegmentR2 , numSegments : Math . ceil ( command . datagramSize / command . partialDatagram . length ) } ) ; const interpreter = interpret ( RXStateMachine ) . start ( ) ; nodeSessions . transportService . set ( command . sessionId , { fragmentSize : command . partialDatagram . length , interpreter } ) ; interpreter . onTransition ( ( state ) => { if ( state . changed && state . value === "failure" ) { this . controllerLog . logNode ( command . nodeId , { message : `Transport Service RX session #${ command . sessionId } failed` , level : "error" , direction : "none" } ) ; interpreter . stop ( ) ; nodeSessions . transportService . delete ( command . sessionId ) ; } } ) ; } else { if ( transportSession ) { transportSession . interpreter . send ( { type : "segment" , index : Math . floor ( command . datagramOffset / transportSession . fragmentSize ) } ) ; } else { const cc = new TransportServiceCCSegmentWait ( this , { nodeId : command . nodeId , pendingSegments : 0 } ) ; await this . sendCommand ( cc , { maxSendAttempts : 1 , priority : MessagePriority . Nonce } ) ; } } } param msg private async handleUnsolicitedMessage ( msg ) { if ( msg . type === MessageType . Request ) { try { await this . handleRequest ( msg ) ; } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Driver_NotReady ) { this . driverLog . print ( `Cannot handle message because the driver is not ready to handle it yet.` , "warn" ) ; } else { throw e ; } } } else { this . driverLog . transactionResponse ( msg , undefined , "unexpected" ) ; this . driverLog . print ( "unexpected response, discarding..." , "warn" ) ; } } param fnType param handler param oneTime public registerRequestHandler < T extends Message > ( fnType , handler , oneTime = false ) { const handlers = this . requestHandlers . has ( fnType ) ? this . requestHandlers . get ( fnType ) ! : [ ] ; const entry = { invoke : handler , oneTime } ; handlers . push ( entry ) ; this . driverLog . print ( `added${ oneTime ? " one-time" : "" } request handler for ${ FunctionType [ fnType ] } (${ num2hex ( fnType ) })...
${ handlers . length } registered` ) ; this . requestHandlers . set ( fnType , handlers as RequestHandlerEntry [ ] ) ; } param fnType param handler public unregisterRequestHandler ( fnType , handler ) { const handlers = this . requestHandlers . has ( fnType ) ? this . requestHandlers . get ( fnType ) ! : [ ] ; for ( let i = 0 , entry = handlers [ i ] ; i < handlers . length ; i ++ ) { if ( entry . invoke === handler ) { handlers . splice ( i , 1 ) ; break ; } } this . driverLog . print ( `removed request handler for ${ FunctionType [ fnType ] } (${ fnType })...
${ handlers . length } left` ) ; this . requestHandlers . set ( fnType , handlers ) ; } /**
     * Checks whether a CC may be handled or should be ignored.
     * This method expects `cc` to be unwrapped.
     */ private mayHandleUnsolicitedCommand ( cc ) { if ( cc . secure && cc . ccId !== CommandClasses . Security && cc . ccId !== CommandClasses [ "Security 2" ] ) { const commandName = cc . constructor . name ; if ( commandName . endsWith ( "Report" ) || commandName . endsWith ( "Notification" ) ) { if ( cc . isEncapsulatedWith ( CommandClasses . Security ) || cc . isEncapsulatedWith ( CommandClasses [ "Security 2" ] ) ) { return true ; } this . controllerLog . logNode ( cc . nodeId as number , `command must be encrypted but was received without Security encapsulation - discarding it...` , "warn" ) ; return false ; } } return true ; } /**
     * Is called when a Request-type message was received
     */ private async handleRequest ( msg ) { let handlers ; if ( isNodeQuery ( msg ) || isCommandClassContainer ( msg ) ) { const node = msg . getNodeUnsafe ( ) ; if ( node ) { if ( node . status === NodeStatus . Dead ) { node . markAsAlive ( ) ; } } } for ( const entry of this . awaitedMessages ) { if ( entry . predicate ( msg ) ) { entry . promise . resolve ( msg ) ; return ; } } if ( isCommandClassContainer ( msg ) ) { this . unwrapCommands ( msg ) ; const node = msg . getNodeUnsafe ( ) ; if ( node ?. isSecure === false && ( msg . command . ccId === CommandClasses . Security || msg . command . isEncapsulatedWith ( CommandClasses . Security ) ) ) { node . securityClasses . set ( SecurityClass . S0_Legacy , true ) ; void node . refreshInfo ( ) ; } if ( ! this . mayHandleUnsolicitedCommand ( msg . command ) ) return ; } if ( msg instanceof ApplicationCommandRequest || msg instanceof BridgeApplicationCommandRequest ) { const nodeId = msg . command . nodeId ; if ( this . _controller == undefined ) { this . driverLog . print ( `  the controller is not ready yet, discarding...` , "warn" ) ; return ; } else if ( ! this . controller . nodes . has ( nodeId ) ) { this . driverLog . print ( `  the node is unknown or not initialized yet, discarding...` , "warn" ) ; return ; } const node = this . controller . nodes . get ( nodeId ) ! ; const nodeSessions = this . nodeSessions . get ( nodeId ) ; if ( msg . command . ccId === CommandClasses [ "Device Reset Locally" ] && msg . command instanceof DeviceResetLocallyCCNotification ) { this . controllerLog . logNode ( msg . command . nodeId , { message : `The node was reset locally, removing it` , direction : "inbound" } ) ; try { await this . controller . removeFailedNode ( msg . command . nodeId ) ; } catch ( e ) { this . controllerLog . logNode ( msg . command . nodeId , { message : `removing the node failed: ${ getErrorMessage ( e ) }` , level : "error" } ) ; } } else if ( msg . command . ccId === CommandClasses . Supervision && msg . command instanceof SupervisionCCReport && nodeSessions ?. supervision . has ( msg . command . sessionId ) ) { this . controllerLog . logNode ( msg . command . nodeId , { message : `Received update for a Supervision session` , direction : "inbound" } ) ; nodeSessions . supervision . get ( msg . command . sessionId ) ! ( msg . command . status , msg . command . duration ) ; if ( ! msg . command . moreUpdatesFollow ) { nodeSessions . supervision . delete ( msg . command . sessionId ) ; } } else { const supervisionSessionId = SupervisionCC . getSessionId ( msg . command ) ; const secure = msg . command . secure ; for ( const entry of this . awaitedCommands ) { if ( entry . predicate ( msg . command ) ) { entry . promise . resolve ( msg . command ) ; if ( supervisionSessionId !== undefined ) { await node . createAPI ( CommandClasses . Supervision , false ) . sendReport ( { sessionId : supervisionSessionId , moreUpdatesFollow : false , status : SupervisionStatus . Success , secure } ) ; } return ; } } if ( supervisionSessionId !== undefined ) { try { await node . handleCommand ( msg . command ) ; await node . createAPI ( CommandClasses . Supervision , false ) . sendReport ( { sessionId : supervisionSessionId , moreUpdatesFollow : false , status : SupervisionStatus . Success , secure } ) ; } catch ( e ) { await node . createAPI ( CommandClasses . Supervision , false ) . sendReport ( { sessionId : supervisionSessionId , moreUpdatesFollow : false , status : SupervisionStatus . Fail , secure } ) ; throw e ; } } else { await node . handleCommand ( msg . command ) ; } } return ; } else if ( msg instanceof ApplicationUpdateRequest ) { if ( msg instanceof ApplicationUpdateRequestNodeInfoReceived ) { const node = msg . getNodeUnsafe ( ) ; if ( node ) { this . controllerLog . logNode ( node . id , { message : "Received updated node info" , direction : "inbound" } ) ; node . updateNodeInfo ( msg . nodeInformation ) ; this . sendThread . send ( { type : "NIF" , nodeId : node . id } ) ; if ( node . canSleep && node . supportsCC ( CommandClasses [ "Wake Up" ] ) ) { this . debounceSendNodeToSleep ( node ) ; } return ; } } else if ( msg instanceof ApplicationUpdateRequestSmartStartHomeIDReceived ) { this . controllerLog . print ( "Received Smart Start inclusion request" ) ; if ( this . controller . inclusionState !== InclusionState . Idle && this . controller . inclusionState !== InclusionState . SmartStart ) { this . controllerLog . print ( "Controller is busy and cannot handle this inclusion request right now..." ) ; return ; } const provisioningEntry = this . controller . provisioningList . find ( ( entry ) => nwiHomeIdFromDSK ( dskFromString ( entry . dsk ) ) . equals ( msg . nwiHomeId ) ) ; if ( ! provisioningEntry ) { this . controllerLog . print ( "NWI Home ID not found in provisioning list, ignoring request..." ) ; return ; } this . controllerLog . print ( "NWI Home ID found in provisioning list, including node..." ) ; try { const result = await this . controller . beginInclusionSmartStart ( provisioningEntry ) ; if ( ! result ) { this . controllerLog . print ( "Smart Start inclusion could not be started" , "error" ) ; } } catch ( e ) { this . controllerLog . print ( `Smart Start inclusion could not be started: ${ getErrorMessage ( e ) }` , "error" ) ; } } } else { this . driverLog . print ( `handling request ${ FunctionType [ msg . functionType ] } (${ msg . functionType })` ) ; handlers = this . requestHandlers . get ( msg . functionType ) ; } if ( handlers != undefined && handlers . length > 0 ) { this . driverLog . print ( `  ${ handlers . length } handler${ handlers . length !== 1 ? "s" : "" } registered!` ) ; for ( let i = 0 ; i < handlers . length ; i ++ ) { this . driverLog . print ( `  invoking handler #${ i }` ) ; const handler = handlers [ i ] ; let handlerResult = handler . invoke ( msg ) ; if ( handlerResult instanceof Promise ) { handlerResult = await handlerResult ; } if ( handlerResult ) { this . driverLog . print ( `    the message was handled` ) ; if ( handler . oneTime ) { this . driverLog . print ( "  one-time handler was successfully called, removing it..." ) ; handlers . splice ( i , 1 ) ; } break ; } } } else { this . driverLog . print ( "  no handlers registered!" , "warn" ) ; } } private lastCallbackId = 0xff ; /**
     * Returns the next callback ID. Callback IDs are used to correllate requests
     * to the controller/nodes with its response
     */ public getNextCallbackId ( ) { this . lastCallbackId = ( this . lastCallbackId + 1 ) & 0xff ; if ( this . lastCallbackId < 1 ) this . lastCallbackId = 1 ; return this . lastCallbackId ; } private encapsulateCommands ( msg ) { if ( SupervisionCC . requiresEncapsulation ( msg . command ) ) { msg . command = SupervisionCC . encapsulate ( this , msg . command ) ; } if ( MultiChannelCC . requiresEncapsulation ( msg . command ) ) { msg . command = MultiChannelCC . encapsulate ( this , msg . command ) ; } const node = msg . command . getNode ( ) ; if ( node ?. supportsCC ( CommandClasses [ "Security 2" ] ) ) { const securityClass = node . getHighestSecurityClass ( ) ; if ( ( ( securityClass != undefined && securityClass !== SecurityClass . S0_Legacy ) || this . securityManager2 ?. tempKeys . has ( node . id ) ) && Security2CC . requiresEncapsulation ( msg . command ) ) { msg . command = Security2CC . encapsulate ( this , msg . command ) ; } } if ( SecurityCC . requiresEncapsulation ( msg . command ) ) { msg . command = SecurityCC . encapsulate ( this , msg . command ) ; } } public unwrapCommands ( msg ) { while ( isEncapsulatingCommandClass ( msg . command ) || isMultiEncapsulatingCommandClass ( msg . command ) ) { const unwrapped = msg . command . encapsulated ; if ( isArray ( unwrapped ) ) { this . driverLog . print ( `Received a command that contains multiple CommandClasses. This is not supported yet! Discarding the message...` , "warn" ) ; return ; } msg . command = unwrapped ; } } /**
     * Gets called whenever any Serial API command succeeded or a SendData command had a negative callback.
     */ private handleSerialAPICommandResult ( msg , options , result ) { const node = msg . getNodeUnsafe ( ) ; let success = true ; if ( isSendData ( msg ) || isNodeQuery ( msg ) ) { if ( ! node ) return ; if ( isTransmitReport ( result ) ) { if ( ! result . isOK ( ) ) { success = false ; node . incrementStatistics ( "commandsDroppedTX" ) ; } else { node . incrementStatistics ( "commandsTX" ) ; } if ( hasTXReport ( result ) ) { options . onTXReport ?. ( result . txReport ) ; } } if ( success ) { if ( node . canSleep ) { if ( options . priority !== MessagePriority . Nonce && options . priority !== MessagePriority . Supervision ) { if ( ! node . keepAwake ) { setImmediate ( ( ) => this . debounceSendNodeToSleep ( node ) ) ; } node . markAsAwake ( ) ; } } else if ( node . status !== NodeStatus . Alive ) { node . markAsAlive ( ) ; } } } else { this . _controller ?. incrementStatistics ( "messagesTX" ) ; } } param msg param options public async sendMessage < TResponse extends Message = Message > ( msg , options = { } ) { this . ensureReady ( ) ; let node ; if ( isNodeQuery ( msg ) || isCommandClassContainer ( msg ) ) { node = msg . getNodeUnsafe ( ) ; if ( ! messageIsPing ( msg ) && node ?. status === NodeStatus . Dead ) { if ( ! ( await node . ping ( ) ) ) { throw new ZWaveError ( `The message cannot be sent because node ${ node . id } is dead` , ZWaveErrorCodes . Controller_MessageDropped ) ; } } } if ( options . priority == undefined ) options . priority = getDefaultPriority ( msg ) ; if ( options . priority == undefined ) { const className = msg . constructor . name ; const msgTypeName = FunctionType [ msg . functionType ] ; throw new ZWaveError ( `No default priority has been defined for ${ className } (${ msgTypeName }), so you have to provide one for your message` , ZWaveErrorCodes . Driver_NoPriority ) ; } if ( options . supportCheck == undefined ) options . supportCheck = true ; if ( options . supportCheck && this . _controller != undefined && ! this . _controller . isFunctionSupported ( msg . functionType ) ) { throw new ZWaveError ( `Your hardware does not support the ${ FunctionType [ msg . functionType ] } function` , ZWaveErrorCodes . Driver_NotSupported ) ; } if ( ! ! node && ! messageIsPing ( msg ) && node . canSleep && ( node . supportsCC ( CommandClasses [ "Wake Up" ] ) || node . interviewStage < InterviewStage . NodeInfo ) && ! ( msg instanceof SendDataMulticastRequest ) && ! ( msg instanceof SendDataMulticastBridgeRequest ) && options . priority !== MessagePriority . Nonce && options . priority !== MessagePriority . Supervision ) { if ( options . priority === MessagePriority . NodeQuery ) { options . tag = "interview" ; } options . priority = MessagePriority . WakeUp ; } const { generator , resultPromise } = createMessageGenerator ( this , msg , ( msg , _result ) => { this . handleSerialAPICommandResult ( msg , options , _result ) ; } ) ; const transaction = new Transaction ( this , { message : msg , priority : options . priority , parts : generator , promise : resultPromise } ) ; if ( options . changeNodeStatusOnMissingACK != undefined ) { transaction . changeNodeStatusOnTimeout = options . changeNodeStatusOnMissingACK ; } if ( options . pauseSendThread === true ) { transaction . pauseSendThread = true ; } transaction . requestWakeUpOnDemand = ! ! options . requestWakeUpOnDemand ; transaction . tag = options . tag ; this . sendThread . send ( { type : "add" , transaction } ) ; let expirationTimeout ; if ( options . expire ) { expirationTimeout = setTimeout ( ( ) => { this . sendThread . send ( { type : "reduce" , reducer : ( t ) => { if ( t === transaction ) return { type : "reject" , message : `The message has expired` , code : ZWaveErrorCodes . Controller_MessageExpired } ; return { type : "keep" } ; } } ) ; } , options . expire ) . unref ( ) ; } try { const result = ( await resultPromise ) as TResponse ; let maybeSendToSleep ; if ( isSendData ( msg ) ) { maybeSendToSleep = options . priority !== MessagePriority . Nonce && options . priority !== MessagePriority . Supervision && result && ( result . functionType === FunctionType . BridgeApplicationCommand || result . functionType === FunctionType . ApplicationCommand || ( isSendDataTransmitReport ( result ) && result . isOK ( ) ) ) ; } else { maybeSendToSleep = isNodeQuery ( msg ) && result && isSuccessIndicator ( result ) && result . isOK ( ) ; } if ( maybeSendToSleep && node && node . canSleep && ! node . keepAwake ) { setImmediate ( ( ) => this . debounceSendNodeToSleep ( node ! ) ) ; } return result ; } catch ( e ) { if ( isZWaveError ( e ) ) { if ( ( e . code === ZWaveErrorCodes . Controller_ResponseNOK || e . code === ZWaveErrorCodes . Controller_CallbackNOK ) && e . context instanceof Message && e . context . functionType !== FunctionType . SendData && e . context . functionType !== FunctionType . SendDataMulticast && e . context . functionType !== FunctionType . SendDataBridge && e . context . functionType !== FunctionType . SendDataMulticastBridge ) { this . _controller ?. incrementStatistics ( "messagesDroppedTX" ) ; return e . context as TResponse ; } else if ( e . code === ZWaveErrorCodes . Controller_NodeTimeout ) { node ?. incrementStatistics ( "timeoutResponse" ) ; } if ( ! e . transactionSource ) { throw new ZWaveError ( e . message , e . code , e . context , transaction . stack ) ; } } throw e ; } finally { if ( expirationTimeout ) clearTimeout ( expirationTimeout ) ; } } /** Wraps a CC in the correct SendData message to use for sending */ public createSendDataMessage ( command , options = { } ) { let msg ; if ( command . isSinglecast ( ) ) { if ( this . controller . isFunctionSupported ( FunctionType . SendDataBridge ) ) { msg = new SendDataBridgeRequest ( this , { command } ) ; } else { msg = new SendDataRequest ( this , { command } ) ; } } else if ( command . isMulticast ( ) ) { if ( this . controller . isFunctionSupported ( FunctionType . SendDataMulticastBridge ) ) { msg = new SendDataMulticastBridgeRequest ( this , { command } ) ; } else { msg = new SendDataMulticastRequest ( this , { command } ) ; } } else { throw new ZWaveError ( `A CC must either be singlecast or multicast` , ZWaveErrorCodes . Argument_Invalid ) ; } if ( options . maxSendAttempts != undefined ) { msg . maxSendAttempts = options . maxSendAttempts ; } if ( options . transmitOptions != undefined ) { msg . transmitOptions = options . transmitOptions ; if ( ! ( options . transmitOptions & TransmitOptions . ACK ) ) { msg . callbackId = 0 ; } } if ( options . autoEncapsulate !== false ) this . encapsulateCommands ( msg ) ; return msg ; } param command param options public async sendCommand < TResponse extends CommandClass = CommandClass > ( command , options = { } ) { const msg = this . createSendDataMessage ( command , options ) ; try { const resp = await this . sendMessage ( msg , options ) ; if ( isCommandClassContainer ( resp ) ) { this . unwrapCommands ( resp ) ; return resp . command as TResponse ; } } catch ( e ) { if ( isZWaveError ( e ) && e . code === ZWaveErrorCodes . Controller_NodeTimeout ) { if ( command . isSinglecast ( ) ) { this . controllerLog . logNode ( command . nodeId , e . message , "warn" ) ; } } else { throw e ; } } } param command param options public async sendSupervisedCommand ( command , options = { requestStatusUpdates : false } ) { const nodeId = command . nodeId ; if ( typeof nodeId !== "number" ) { throw new ZWaveError ( `Sending a supervised command is only supported with singlecast!` , ZWaveErrorCodes . CC_NotSupported ) ; } if ( ! command . getNode ( ) ?. supportsCC ( CommandClasses . Supervision ) ) { throw new ZWaveError ( `Node ${ nodeId } does not support the Supervision CC!` , ZWaveErrorCodes . CC_NotSupported ) ; } command = SupervisionCC . encapsulate ( this , command , options . requestStatusUpdates ) ; const resp = await this . sendCommand < SupervisionCCReport > ( command , options ) ; if ( ! resp ) return ; if ( options . requestStatusUpdates && resp . moreUpdatesFollow ) { this . ensureNodeSessions ( nodeId ) . supervision . set ( ( command as SupervisionCCGet ) . sessionId , options . onUpdate ) ; } return { status : resp . status , remainingDuration : resp . duration } ; } param command param options public async trySendCommandSupervised ( command , options ? ) { if ( command . getNode ( ) ?. supportsCC ( CommandClasses . Supervision ) ) { return this . sendSupervisedCommand ( command , options ) ; } else { await this . sendCommand ( command , options ) ; } } param message private writeHeader ( header ) { return this . writeSerial ( Buffer . from ( [ header ] ) ) ; } /** Sends a raw datagram to the serialport (if that is open) */ private async writeSerial ( data ) { return this . serial ?. writeAsync ( data ) ; } /**
     * Waits until an unsolicited serial message is received or a timeout has elapsed. Returns the received message.
     *
     * **Note:** To wait for a certain CommandClass, better use  waitForCommand .
     *  param timeout param predicate public waitForMessage < T extends Message > ( predicate , timeout ) { return new Promise < T > ( ( resolve , reject ) => { const entry = { predicate , promise : createDeferredPromise < Message > ( ) , timeout : undefined } ; this . awaitedMessages . push ( entry ) ; const removeEntry = ( ) => { if ( entry . timeout ) clearTimeout ( entry . timeout ) ; const index = this . awaitedMessages . indexOf ( entry ) ; if ( index !== - 1 ) this . awaitedMessages . splice ( index , 1 ) ; } ; entry . timeout = setTimeout ( ( ) => { removeEntry ( ) ; reject ( new ZWaveError ( `Received no matching message within the provided timeout!` , ZWaveErrorCodes . Controller_Timeout ) ) ; } , timeout ) ; void entry . promise . then ( ( cc ) => { removeEntry ( ) ; resolve ( cc as T ) ; } ) ; } ) ; } param timeout param predicate public waitForCommand < T extends CommandClass > ( predicate , timeout ) { return new Promise < T > ( ( resolve , reject ) => { const entry = { predicate , promise : createDeferredPromise < CommandClass > ( ) , timeout : undefined } ; this . awaitedCommands . push ( entry ) ; const removeEntry = ( ) => { if ( entry . timeout ) clearTimeout ( entry . timeout ) ; const index = this . awaitedCommands . indexOf ( entry ) ; if ( index !== - 1 ) this . awaitedCommands . splice ( index , 1 ) ; } ; entry . timeout = setTimeout ( ( ) => { removeEntry ( ) ; reject ( new ZWaveError ( `Received no matching command within the provided timeout!` , ZWaveErrorCodes . Controller_NodeTimeout ) ) ; } , timeout ) ; void entry . promise . then ( ( cc ) => { removeEntry ( ) ; resolve ( cc as T ) ; } ) ; } ) ; } /** Checks if a message is allowed to go into the wakeup queue */ private mayMoveToWakeupQueue ( transaction ) { const msg = transaction . message ; switch ( true ) { case messageIsPing ( msg ) : case transaction . priority === MessagePriority . Nonce : case transaction . priority === MessagePriority . Supervision : case isCommandClassContainer ( msg ) && msg . command instanceof WakeUpCCNoMoreInformation : case transaction . tag === "compat" : return false ; } return true ; } /** Moves all messages for a given node into the wakeup queue */ private moveMessagesToWakeupQueue ( nodeId ) { const reject = { type : "reject" , message : `The node is asleep` , code : ZWaveErrorCodes . Controller_MessageDropped } ; const requeue = { type : "requeue" , priority : MessagePriority . WakeUp } ; const requeueAndTagAsInterview = { ... requeue , tag : "interview" } ; const reducer = ( transaction , _source ) => { const msg = transaction . message ; if ( msg . getNodeId ( ) !== nodeId ) return { type : "keep" } ; return this . mayMoveToWakeupQueue ( transaction ) ? transaction . priority === MessagePriority . NodeQuery ? requeueAndTagAsInterview : requeue : reject ; } ; this . sendThread . send ( { type : "reduce" , reducer } ) ; } internal public rejectTransactions ( predicate , errorMsg = `The message has been removed from the queue` , errorCode = ZWaveErrorCodes . Controller_MessageDropped ) { const reducer = ( transaction ) => { if ( predicate ( transaction ) ) { return { type : "reject" , message : errorMsg , code : errorCode } ; } else { return { type : "keep" } ; } } ; this . sendThread . send ( { type : "reduce" , reducer } ) ; } internal public rejectAllTransactionsForNode ( nodeId , errorMsg = `The node is dead` , errorCode = ZWaveErrorCodes . Controller_MessageDropped ) { this . rejectTransactions ( ( t ) => t . message . getNodeId ( ) === nodeId , errorMsg , errorCode ) ; } internal public pauseSendThread ( ) { this . sendThread . send ( { type : "pause" } ) ; } internal public unpauseSendThread ( ) { this . sendThread . send ( { type : "unpause" } ) ; } /** Re-sorts the send queue */ private sortSendQueue ( ) { this . sendThread . send ( "sortQueue" ) ; } private lastSaveToCache : number = 0 ; private readonly saveToCacheInterval : number = 150 ; private saveToCacheTimer : NodeJS . Timer | undefined ; private isSavingToCache : boolean = false ; /**
     * Does the work for saveNetworkToCache. This is not throttled, so any call
     * to this method WILL save the network.
     */ private async saveNetworkToCacheInternal ( ) { if ( ! this . _controller || this . _controller . homeId == undefined || ! this . _controllerInterviewed ) return ; await this . options . storage . driver . ensureDir ( this . cacheDir ) ; const cacheFile = path . join ( this . cacheDir , this . _controller . homeId . toString ( 16 ) + ".json" ) ; const serializedObj = this . _controller . serialize ( ) ; const jsonString = stringify ( serializedObj ) ; await this . options . storage . driver . writeFile ( cacheFile , jsonString , "utf8" ) ; } /**
     * Saves the current configuration and collected data about the controller and all nodes to a cache file.
     * For performance reasons, these calls may be throttled.
     */ public async saveNetworkToCache ( ) { if ( ! this . _controller || this . _controller . homeId == undefined ) return ; if ( this . isSavingToCache || Date . now ( ) - this . lastSaveToCache < this . saveToCacheInterval ) { if ( ! this . saveToCacheTimer ) { this . saveToCacheTimer = setTimeout ( ( ) => void this . saveNetworkToCache ( ) , this . saveToCacheInterval ) ; } return ; } else { this . saveToCacheTimer = undefined ; } this . isSavingToCache = true ; await this . saveNetworkToCacheInternal ( ) ; this . isSavingToCache = false ; this . lastSaveToCache = Date . now ( ) ; } /**
     * Restores a previously stored Z-Wave network state from cache to speed up the startup process
     */ public async restoreNetworkStructureFromCache ( ) { if ( ! this . _controller || ! this . controller . homeId ) return ; const cacheFile = path . join ( this . cacheDir , `${ this . controller . homeId . toString ( 16 ) }.json` ) ; if ( ! ( await this . options . storage . driver . pathExists ( cacheFile ) ) ) return ; try { this . driverLog . print ( `Cache file for homeId ${ num2hex ( this . controller . homeId ) } found, attempting to restore the network from cache...` ) ; const cacheString = await this . options . storage . driver . readFile ( cacheFile , "utf8" ) ; await this . controller . deserialize ( JSON . parse ( cacheString ) ) ; this . driverLog . print ( `Restoring the network from cache was successful!` ) ; } catch ( e ) { const message = `Restoring the network from cache failed: ${ getErrorMessage ( e , true ) }` ; this . emit ( "error" , new ZWaveError ( message , ZWaveErrorCodes . Driver_InvalidCache ) ) ; this . driverLog . print ( message , "error" ) ; } } private sendNodeToSleepTimers = new Map < number , NodeJS . Timeout > ( ) ; internal public debounceSendNodeToSleep ( node ) { if ( this . sendNodeToSleepTimers . has ( node . id ) ) { clearTimeout ( this . sendNodeToSleepTimers . get ( node . id ) ! ) ; } const sendNodeToSleep = ( node ) : void => { this . sendNodeToSleepTimers . delete ( node . id ) ; if ( ! this . hasPendingMessages ( node ) ) { void node . sendNoMoreInformation ( ) . catch ( ( ) => { } ) ; } } ; if ( node . supportsCC ( CommandClasses [ "Wake Up" ] ) && ! this . hasPendingMessages ( node ) ) { const wakeUpInterval = node . getValue < number > ( getWakeUpIntervalValueId ( ) ) ; if ( wakeUpInterval !== 0 ) { this . sendNodeToSleepTimers . set ( node . id , setTimeout ( ( ) => sendNodeToSleep ( node ) , 1000 ) . unref ( ) ) ; } } } /** Computes the maximum net CC payload size for the given CC or SendDataRequest */ public computeNetCCPayloadSize ( commandOrMsg ) { let msg ; if ( isSendDataSinglecast ( commandOrMsg ) ) { msg = commandOrMsg ; } else { const SendDataConstructor = this . getSendDataSinglecastConstructor ( ) ; msg = new SendDataConstructor ( this , { command : commandOrMsg } ) ; } this . encapsulateCommands ( msg ) ; return msg . command . getMaxPayloadLength ( msg . getMaxPayloadLength ( ) ) ; } /** Returns the preferred constructor to use for singlecast SendData commands */ public getSendDataSinglecastConstructor ( ) { return this . _controller ?. isFunctionSupported ( FunctionType . SendDataBridge ) ? SendDataBridgeRequest : SendDataRequest ; } /** Returns the preferred constructor to use for multicast SendData commands */ public getSendDataMulticastConstructor ( ) { return this . _controller ?. isFunctionSupported ( FunctionType . SendDataMulticastBridge ) ? SendDataMulticastBridgeRequest : SendDataMulticastRequest ; } public [ util . inspect . custom ] ( ) { return "[Driver]" ; } /**
     * Checks whether there is a compatible update for the currently installed config package.
     * Returns the new version if there is an update, `undefined` otherwise.
     */ public async checkForConfigUpdates ( silent = false ) { this . ensureReady ( ) ; try { if ( ! silent ) this . driverLog . print ( "Checking for configuration updates..." ) ; const ret = await checkForConfigUpdates ( this . configVersion ) ; if ( ret ) { if ( ! silent ) this . driverLog . print ( `Configuration update available: ${ ret }` ) ; } else { if ( ! silent ) this . driverLog . print ( "No configuration update available..." ) ; } return ret ; } catch ( e ) { this . driverLog . print ( getErrorMessage ( e ) , "error" ) ; } } /**
     * Installs an update for the embedded configuration DB if there is a compatible one.
     * Returns `true` when an update was installed, `false` otherwise.
     *
     * **Note:** Bugfixes and changes to device configuration generally require a restart or re-interview to take effect.
     */ public async installConfigUpdate ( ) { this . ensureReady ( ) ; const newVersion = await this . checkForConfigUpdates ( true ) ; if ( ! newVersion ) return false ; try { this . driverLog . print ( `Installing version ${ newVersion } of configuration DB...` ) ; const extConfigDir = externalConfigDir ( ) ; if ( this . configManager . useExternalConfig && extConfigDir ) { await installConfigUpdateInDocker ( newVersion , { cacheDir : this . options . storage . cacheDir , configDir : extConfigDir } ) ; } else if ( isDocker ( ) ) { await installConfigUpdateInDocker ( newVersion ) ; } else { await installConfigUpdate ( newVersion ) ; } } catch ( e ) { this . driverLog . print ( getErrorMessage ( e ) , "error" ) ; return false ; } this . driverLog . print ( `Configuration DB updated to version ${ newVersion }, activating...` ) ; await this . configManager . loadAll ( ) ; if ( this . _controller ) { for ( const node of this . _controller . nodes . values ( ) ) { if ( node . ready ) await node [ "loadDeviceConfig" ] ( ) ; } } return true ; } }