UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

1,075 lines 325 kB
import { ApplicationStatusCCRejectedRequest, CRC16CC, CRC16CCCommandEncapsulation, CommandClass, InclusionControllerCCInitiate, InclusionControllerStep, InvalidCC, KEXFailType, MultiChannelCC, NoOperationCC, Security2CC, Security2CCCommandsSupportedGet, Security2CCCommandsSupportedReport, Security2CCMessageEncapsulation, Security2CCNonceGet, Security2CCNonceReport, Security2Command, SecurityCC, SecurityCCCommandEncapsulation, SecurityCCCommandEncapsulationNonceGet, SecurityCCCommandsSupportedGet, SecurityCCCommandsSupportedReport, SecurityCCNonceGet, SecurityCCNonceReport, SecurityCommand, SupervisionCC, SupervisionCCReport, TransportServiceCC, TransportServiceCCFirstSegment, TransportServiceCCSegmentComplete, TransportServiceCCSegmentRequest, TransportServiceCCSegmentWait, TransportServiceTimeouts, VersionCommand, WakeUpCCNoMoreInformation, WakeUpCCValues, getImplementedVersion, isEncapsulatingCommandClass, isMultiEncapsulatingCommandClass, isTransportServiceEncapsulation, registerCCs, } from "@zwave-js/cc"; import { userCodeToLogString } from "@zwave-js/cc/UserCodeCC"; import { ConfigManager } from "@zwave-js/config"; import { CommandClasses, ControllerLogger, ControllerRole, ControllerStatus, Duration, EncapsulationFlags, MAX_SUPERVISION_SESSION_ID, MAX_TRANSPORT_SERVICE_SESSION_ID, MPANState, MessagePriority, NUM_NODEMASK_BYTES, NodeIDType, RFRegion, SPANState, SecurityClass, SecurityManager, SecurityManager2, SupervisionStatus, TransactionState, TransmitOptions, TransmitStatus, ZWaveError, ZWaveErrorCodes, allCCs, deserializeCacheValue, encapsulationCCs, generateECDHKeyPair, getCCName, isEncapsulationCC, isLongRangeNodeId, isMissingControllerACK, isMissingControllerCallback, isMissingControllerResponse, isZWaveError, keyPairFromRawECDHPrivateKey, messageRecordToLines, randomBytes, securityClassIsS2, securityClassOrder, serializeCacheValue, stripUndefined, timespan, wasControllerReset, } from "@zwave-js/core"; import { BootloaderChunkType, CLIChunkType, FunctionType, Message, MessageHeaders, MessageType, XModemMessageHeaders, ZWaveSerialFrameType, ZWaveSerialMode, ZWaveSerialStreamFactory, getDefaultPriority, hasNodeId, isSuccessIndicator, isZWaveSerialBindingFactory, isZWaveSerialPortImplementation, wrapLegacySerialBinding, } from "@zwave-js/serial"; import { ApplicationUpdateRequest, EnterBootloaderRequest, GetControllerVersionRequest, MAX_SEND_ATTEMPTS, SendDataAbort, SendDataBridgeRequest, SendDataMulticastBridgeRequest, SendDataMulticastRequest, SendDataRequest, SendTestFrameRequest, SendTestFrameTransmitReport, SerialAPIStartedRequest, SerialAPIWakeUpReason, SoftResetRequest, containsCC, containsSerializedCC, hasTXReport, isAnySendDataResponse, isCommandRequest, isSendData, isSendDataSinglecast, isSendDataTransmitReport, isTransmitReport, } from "@zwave-js/serial/serialapi"; import { AsyncQueue, Bytes, TypedEventTarget, areUint8ArraysEqual, buffer2hex, cloneDeep, createWrappingCounter, getErrorMessage, getenv, isAbortError, isUint8Array, mergeDeep, noop, num2hex, pick, setInterval, setTimer, } from "@zwave-js/shared"; import { distinct } from "alcalzone-shared/arrays"; import { wait } from "alcalzone-shared/async"; import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; import { roundTo } from "alcalzone-shared/math"; import { isArray, isObject } from "alcalzone-shared/typeguards"; import path from "pathe"; import { PACKAGE_NAME, PACKAGE_VERSION } from "../_version.js"; import { ZWaveController } from "../controller/Controller.js"; import { downloadFirmwareUpdate } from "../controller/FirmwareUpdateService.js"; import { InclusionState, RemoveNodeReason, } from "../controller/Inclusion.js"; import { determineNIF } from "../controller/NodeInformationFrame.js"; import { isFirmwareUpdateInfo, } from "../controller/_Types.js"; import { DriverLogger } from "../log/Driver.js"; import { InterviewStage, NodeStatus, zWaveNodeEvents, } from "../node/_Types.js"; import { reportMissingDeviceConfig } from "../telemetry/deviceConfig.js"; import { compileStatistics, sendStatistics, } from "../telemetry/statistics.js"; import { Bootloader } from "./Bootloader.js"; import { DriverMode } from "./DriverMode.js"; import { EndDeviceCLI } from "./EndDeviceCLI.js"; import { createMessageGenerator } from "./MessageGenerators.js"; import { cacheKeys, deserializeNetworkCacheValue, migrateLegacyNetworkCache, serializeNetworkCacheValue, } from "./NetworkCache.js"; import { TransactionQueue } from "./Queue.js"; import { createSerialAPICommandMachine, } from "./SerialAPICommandMachine.js"; import { createMessageDroppedUnexpectedError, serialAPICommandErrorToZWaveError, } from "./StateMachineShared.js"; import { TaskScheduler } from "./Task.js"; import { throttlePresets } from "./ThrottlePresets.js"; import { Transaction } from "./Transaction.js"; import { createTransportServiceRXMachine, } from "./TransportServiceMachine.js"; import { checkForConfigUpdates, installConfigUpdate } from "./UpdateConfig.js"; import { mergeUserAgent, userAgentComponentsToString } from "./UserAgent.js"; import { OTWFirmwareUpdateStatus, } from "./_Types.js"; import { discoverRemoteSerialPorts } from "./mDNSDiscovery.js"; // Force-load all Command Classes: registerCCs(); export const libVersion = PACKAGE_VERSION; export const libName = PACKAGE_NAME; // This is made with cfonts: const libNameString = ` ███████╗ ██╗ ██╗ █████╗ ██╗ ██╗ ███████╗ ██╗ ███████╗ ╚══███╔╝ ██║ ██║ ██╔══██╗ ██║ ██║ ██╔════╝ ██║ ██╔════╝ ███╔╝ █████╗ ██║ █╗ ██║ ███████║ ██║ ██║ █████╗ ██║ ███████╗ ███╔╝ ╚════╝ ██║███╗██║ ██╔══██║ ╚██╗ ██╔╝ ██╔══╝ ██ ██║ ╚════██║ ███████╗ ╚███╔███╔╝ ██║ ██║ ╚████╔╝ ███████╗ ╚█████╔╝ ███████║ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝ ╚════╝ ╚══════╝ `; const defaultOptions = { timeouts: { ack: 1600, // A sending interface MUST wait for 1600ms or more for an ACK Frame after transmitting a Data Frame. byte: 150, // Ideally we'd want to have this as low as possible, but some // 500 series controllers can take several seconds to respond sometimes. response: 10000, report: 1000, // ReportTime timeout SHOULD be set to CommandTime + 1 second nonce: 5000, sendDataAbort: 20000, // If a controller takes over 20 seconds to reach a node, it's probably not going to happen sendDataCallback: 30000, // INS13954 defines this to be 65000 ms, but waiting that long causes issues with reporting devices sendToSleep: 250, // The default should be enough time for applications to react to devices waking up retryJammed: 1000, refreshValue: 5000, // Default should handle most slow devices until we have a better solution refreshValueAfterTransition: 1000, // To account for delays in the device serialAPIStarted: 5000, pollTime: 10000, // PollTime SHOULD be 10 seconds + CommandTime as per the spec }, attempts: { openSerialPort: 10, controller: 3, sendData: 3, sendDataJammed: 5, nodeInterview: 5, smartStartInclusion: 5, firmwareUpdateOTW: 3, }, disableOptimisticValueUpdate: false, features: { // By default enable soft reset unless the env variable is set softReset: !getenv("ZWAVEJS_DISABLE_SOFT_RESET"), // By default enable the unresponsive controller recovery unless the env variable is set unresponsiveControllerRecovery: !getenv("ZWAVEJS_DISABLE_UNRESPONSIVE_CONTROLLER_RECOVERY"), // By default disable the watchdog watchdog: false, // Support all CCs unless specified otherwise disableCommandClasses: [], }, // By default, try to recover from bootloader mode bootloaderMode: "recover", interview: { queryAllUserCodes: false, applyRecommendedConfigParamValues: false, }, storage: { cacheDir: typeof process !== "undefined" ? path.join(process.cwd(), "cache") : "/cache", lockDir: getenv("ZWAVEJS_LOCK_DIRECTORY"), throttle: "normal", }, preferences: { scales: {}, lookupUserIdInNotificationEvents: false, }, }; /** 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 > 60000) { throw new ZWaveError(`The Response timeout must be between 500 and 60000 milliseconds!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (options.timeouts.report < 500 || options.timeouts.report > 10000) { throw new ZWaveError(`The Report timeout must be between 500 and 10000 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.retryJammed < 10 || options.timeouts.retryJammed > 5000) { throw new ZWaveError(`The timeout for retrying while jammed must be between 10 and 5000 milliseconds!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (options.timeouts.sendToSleep < 10 || options.timeouts.sendToSleep > 5000) { throw new ZWaveError(`The Send To Sleep timeout must be between 10 and 5000 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.pollTime < 1000 || options.timeouts.pollTime > 30000) { throw new ZWaveError(`The Poll Time must be between 1000 and 30000 milliseconds!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (options.timeouts.sendDataAbort < 5000 || options.timeouts.sendDataAbort > options.timeouts.sendDataCallback - 5000) { throw new ZWaveError(`The Send Data Abort Callback timeout must be between 5000 and ${options.timeouts.sendDataCallback - 5000} 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) { 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]) => areUint8ArraysEqual(k, key)) !== i) { throw new ZWaveError(`The security key for class ${secClass} was used multiple times!`, 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.sendDataJammed < 1 || options.attempts.sendDataJammed > 10) { throw new ZWaveError(`The SendData attempts while jammed must be between 1 and 10!`, 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); } if (options.attempts.smartStartInclusion < 1 || options.attempts.smartStartInclusion > 25) { throw new ZWaveError(`The SmartStart inclusion attempts must be between 1 and 25!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (options.attempts.firmwareUpdateOTW < 1 || options.attempts.firmwareUpdateOTW > 5) { throw new ZWaveError(`The OTW firmware update attempts must be between 1 and 5!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (options.inclusionUserCallbacks) { if (!isObject(options.inclusionUserCallbacks)) { throw new ZWaveError(`The inclusionUserCallbacks must be an object!`, ZWaveErrorCodes.Driver_InvalidOptions); } else if (typeof options.inclusionUserCallbacks.grantSecurityClasses !== "function" || typeof options.inclusionUserCallbacks.validateDSKAndEnterPIN !== "function" || typeof options.inclusionUserCallbacks.abort !== "function") { throw new ZWaveError(`The inclusionUserCallbacks must contain the following functions: grantSecurityClasses, validateDSKAndEnterPIN, abort!`, ZWaveErrorCodes.Driver_InvalidOptions); } } if (options.joinNetworkUserCallbacks) { if (!isObject(options.joinNetworkUserCallbacks)) { throw new ZWaveError(`The joinNetworkUserCallbacks must be an object!`, ZWaveErrorCodes.Driver_InvalidOptions); } else if (typeof options.joinNetworkUserCallbacks.showDSK !== "function" || typeof options.joinNetworkUserCallbacks.done !== "function") { throw new ZWaveError(`The joinNetworkUserCallbacks must contain the following functions: showDSK, done!`, ZWaveErrorCodes.Driver_InvalidOptions); } } if (options.rf != undefined) { if (options.rf.region != undefined) { if (typeof options.rf.region !== "number" || !(options.rf.region in RFRegion) || options.rf.region === RFRegion.Unknown) { throw new ZWaveError(`${options.rf.region} is not a valid RF region!`, ZWaveErrorCodes.Driver_InvalidOptions); } } if (options.rf.txPower != undefined) { if (!isObject(options.rf.txPower)) { throw new ZWaveError(`rf.txPower must be an object!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (typeof options.rf.txPower.powerlevel !== "number" && options.rf.txPower.powerlevel !== "auto") { throw new ZWaveError(`rf.txPower.powerlevel must be a number or "auto"!`, ZWaveErrorCodes.Driver_InvalidOptions); } if (options.rf.txPower.measured0dBm != undefined && typeof options.rf.txPower.measured0dBm !== "number") { throw new ZWaveError(`rf.txPower.measured0dBm must be a number!`, ZWaveErrorCodes.Driver_InvalidOptions); } } if (options.features.disableCommandClasses?.length) { // Ensure that all CCs may be disabled const mandatory = new Set([ // Encapsulation CCs are always supported ...encapsulationCCs, // All Root Devices or nodes MUST support CommandClasses.Association, CommandClasses["Association Group Information"], CommandClasses["Device Reset Locally"], CommandClasses["Firmware Update Meta Data"], CommandClasses.Indicator, CommandClasses["Manufacturer Specific"], CommandClasses["Multi Channel Association"], CommandClasses.Powerlevel, CommandClasses.Version, CommandClasses["Z-Wave Plus Info"], ]); const mandatoryDisabled = options.features.disableCommandClasses .filter((cc) => mandatory.has(cc)); if (mandatoryDisabled.length > 0) { throw new ZWaveError(`The following CCs are mandatory and cannot be disabled using features.disableCommandClasses: ${mandatoryDisabled.map((cc) => getCCName(cc)).join(", ")}!`, ZWaveErrorCodes.Driver_InvalidOptions); } } } } function messageIsPing(msg) { return containsCC(msg) && msg.command instanceof NoOperationCC; } function assertValidCCs(container) { if (container.command instanceof InvalidCC) { if (typeof container.command.reason === "number") { throw new ZWaveError("The message payload failed validation!", container.command.reason); } else { throw new ZWaveError("The message payload is invalid!", ZWaveErrorCodes.PacketFormat_InvalidPayload, container.command.reason); } } else if (containsCC(container.command)) { assertValidCCs(container.command); } } function wrapLegacyFSDriverForCacheMigrationOnly(legacy) { // This usage only needs readFile and checking if a file exists // Every other usage will throw! return { async readFile(path) { const text = await legacy.readFile(path, "utf8"); return Bytes.from(text, "utf8"); }, async stat(path) { if (await legacy.pathExists(path)) { return { isDirectory() { return false; }, isFile() { return true; }, mtime: new Date(), size: 0, }; } else { throw new Error("File not found"); } }, readDir(_path) { return Promise.reject(new Error("Not implemented for the legacy FS driver")); }, }; } /** * 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 TypedEventTarget { port; constructor(port, ...optionsAndPresets) { super(); this.port = port; // Ensure the given serial port is valid if (typeof port !== "string" && !isZWaveSerialPortImplementation(port) && !isZWaveSerialBindingFactory(port)) { throw new ZWaveError(`The port must be a string or a valid custom serial port implementation!`, ZWaveErrorCodes.Driver_InvalidOptions); } // Deep-Merge all given options/presets const definedOptionsAndPresets = optionsAndPresets.filter((o) => !!o); let mergedOptions = {}; for (const preset of definedOptionsAndPresets) { mergedOptions = mergeDeep(mergedOptions, preset, true); } // Finally apply the defaults, without overwriting any existing settings this._options = mergeDeep(mergedOptions, cloneDeep(defaultOptions)); // And make sure they contain valid values checkOptions(this._options); if (this._options.userAgent) { if (!isObject(this._options.userAgent)) { throw new ZWaveError(`The userAgent property must be an object!`, ZWaveErrorCodes.Driver_InvalidOptions); } this.updateUserAgent(this._options.userAgent); } // Initialize the cache this.cacheDir = this._options.storage.cacheDir; const self = this; this.messageEncodingContext = { getHighestSecurityClass: (nodeId) => this.getHighestSecurityClass(nodeId), hasSecurityClass: (nodeId, securityClass) => this.hasSecurityClass(nodeId, securityClass), setSecurityClass: (nodeId, securityClass, granted) => this.setSecurityClass(nodeId, securityClass, granted), getDeviceConfig: (nodeId) => this.getDeviceConfig(nodeId), // These are evaluated lazily, so we cannot spread messageParsingContext unfortunately get securityManager() { return self.securityManager; }, get securityManager2() { return self.securityManager2; }, get securityManagerLR() { return self.securityManagerLR; }, getSupportedCCVersion: (cc, nodeId, endpointIndex) => this.getSupportedCCVersion(cc, nodeId, endpointIndex), }; this._scheduler = new TaskScheduler(() => { return new ZWaveError("Task was removed", ZWaveErrorCodes.Driver_TaskRemoved); }); } serialFactory; /** The serial port instance */ serial; messageEncodingContext; getEncodingContext() { return { ...this.messageEncodingContext, ownNodeId: this.controller.ownNodeId, homeId: this.controller.homeId, nodeIdType: this._controller?.nodeIdType ?? NodeIDType.Short, }; } getMessageParsingContext() { return { getDeviceConfig: (nodeId) => this.getDeviceConfig(nodeId), sdkVersion: this._controller?.sdkVersion, requestStorage: this._requestStorage, ownNodeId: this._controller?.ownNodeId ?? 0, // Unspecified node ID homeId: this._controller?.homeId ?? 0x55555555, // Invalid home ID nodeIdType: this._controller?.nodeIdType ?? NodeIDType.Short, }; } getCCParsingContext() { return { ...this.messageEncodingContext, ownNodeId: this.controller.ownNodeId, homeId: this.controller.homeId, }; } // We have multiple queues to achieve multiple "layers" of communication priority: // The default queue for most messages queue; // Is initialized in initTransactionQueues() // An immediate queue for handling queries that need to be handled ASAP, e.g. Nonce Get immediateQueue; // Is initialized in initTransactionQueues() // And all of them feed into the serial API queue, which contains commands that will be sent ASAP serialAPIQueue; // Is initialized in initControllerAndNodes() // Timers for delayed transaction re-queuing requeueTimers = new Map(); // Poll timing state per the Z-Wave specification. // After any transaction completes, we must wait at least pollTime // before starting the next poll transaction. _lastTransactionEnd = 0; // CommandTime is measured from when the poll command is sent to when // the successful transmit report is received. The required wait before // the next poll is pollTime + commandTime. _lastPollCommandTime = 0; _pollDelayTimer; /** Gives access to the transaction queues, ordered by priority */ get queues() { return [this.immediateQueue, this.queue]; } initTransactionQueues() { this.immediateQueue = new TransactionQueue({ name: "immediate", mayStartNextTransaction: (t) => { // While the controller is unresponsive, only soft resetting is allowed. // Since we use GetControllerVersionRequest to check if the controller responds after soft-reset, // allow that too. if (this.controller.status === ControllerStatus.Unresponsive) { return t.message instanceof SoftResetRequest || t.message instanceof GetControllerVersionRequest; } // While the controller is jammed, only soft resetting is allowed if (this.controller.status === ControllerStatus.Jammed) { return t.message instanceof SoftResetRequest; } // All other messages on the immediate queue may always be sent as long as the controller is ready to send return !this.queuePaused && this.controller.status === ControllerStatus.Ready; }, }); this.queue = new TransactionQueue({ name: "normal", mayStartNextTransaction: (t) => this.mayStartTransaction(t), }); this._queueIdle = false; // Start draining the queues for (const queue of this.queues) { void this.drainTransactionQueue(queue); } } async destroyTransactionQueues(reason, errorCode) { // Clear all delayed requeue timers for (const set of this.requeueTimers.values()) { for (const timer of set) { timer.clear(); } } this.requeueTimers.clear(); // Clear the poll delay timer this._pollDelayTimer?.clear(); this._pollDelayTimer = undefined; // The queues might not have been initialized yet for (const queue of this.queues) { if (!queue) return; } // Reject pending transactions, but not during integration tests if (getenv("NODE_ENV") !== "test") { await this.rejectTransactions((_t) => true, reason, errorCode ?? ZWaveErrorCodes.Driver_TaskRemoved); } for (const queue of this.queues) { queue.abort(); } } _scheduler; get scheduler() { return this._scheduler; } queuePaused = false; /** Used to immediately abort ongoing Serial API commands */ abortSerialAPICommand; initSerialAPIQueue() { this.serialAPIQueue = new AsyncQueue(); // Start draining the queue void this.drainSerialAPIQueue(); } destroySerialAPIQueue(reason, errorCode) { // The queue might not have been initialized yet if (!this.serialAPIQueue) return; this.serialAPIQueue.abort(); // Abort the currently executed serial API command, so the queue does not lock up this.abortSerialAPICommand?.reject(new ZWaveError(reason, errorCode ?? ZWaveErrorCodes.Driver_Destroyed)); } // Keep track of which queues are currently busy _queuesBusyFlags = 0; _queueIdle = false; /** Whether the queue is currently idle */ get queueIdle() { return this._queueIdle; } set queueIdle(value) { if (this._queueIdle !== value) { this.driverLog.print(value ? "all queues idle" : "one or more queues busy"); this._queueIdle = value; this.handleQueueIdleChange(value); } } /** A map of handlers for all sorts of requests */ requestHandlers = new Map(); /** A list of awaited message headers */ awaitedMessageHeaders = []; /** A list of awaited messages */ awaitedMessages = []; /** A list of awaited commands */ awaitedCommands = []; /** A list of awaited chunks from the bootloader */ awaitedBootloaderChunks = []; /** A list of awaited chunks from the end device CLI */ awaitedCLIChunks = []; /** A list of promises waiting for the queues to become idle */ awaitedIdle = []; /** A map of Node ID -> ongoing sessions */ nodeSessions = new Map(); ensureNodeSessions(nodeId) { if (!this.nodeSessions.has(nodeId)) { this.nodeSessions.set(nodeId, { transportService: new Map(), supervision: new Map(), }); } return this.nodeSessions.get(nodeId); } _requestStorage = new Map(); /** * @internal * Stores data from Serial API command requests to be used by their responses */ get requestStorage() { return this._requestStorage; } cacheDir; _valueDB; /** @internal */ get valueDB() { return this._valueDB; } _metadataDB; /** @internal */ get metadataDB() { return this._metadataDB; } _networkCache; /** @internal */ get networkCache() { if (this._networkCache == undefined) { throw new ZWaveError("The network cache was not yet initialized!", ZWaveErrorCodes.Driver_NotReady); } return this._networkCache; } // This is set during `start()` and should not be accessed before _configManager; get configManager() { return this._configManager; } get configVersion() { return (this.configManager?.configVersion ?? require("zwave-js/package.json")?.dependencies?.["@zwave-js/config"] ?? libVersion); } // This is set during `start()` and should not be accessed before _logContainer; // This is set during `start()` and should not be accessed before _driverLog; /** @internal */ get driverLog() { return this._driverLog; } // This is set during `start()` and should not be accessed before _controllerLog; /** @internal */ get controllerLog() { return this._controllerLog; } logNode(...args) { // @ts-expect-error this._controllerLog.logNode(...args); } _controller; /** Encapsulates information about the Z-Wave controller and provides access to its nodes */ get controller() { if (this._controller == undefined) { throw new ZWaveError("The controller is not yet ready!", ZWaveErrorCodes.Driver_NotReady); } return this._controller; } /** While in bootloader mode, this encapsulates information about the bootloader and its state */ _bootloader; get bootloader() { if (this._bootloader == undefined) { throw new ZWaveError("The controller is not in bootloader mode!", ZWaveErrorCodes.Driver_NotReady); } return this._bootloader; } _cli; /** While in end device CLI mode, this encapsulates information about the CLI and its state */ get cli() { if (this._cli == undefined) { throw new ZWaveError("The Z-Wave module is not in CLI mode!", ZWaveErrorCodes.Driver_NotReady); } return this._cli; } /** Determines which kind of Z-Wave application the driver is currently communicating with */ get mode() { if (this._bootloader) return DriverMode.Bootloader; if (this._cli) return DriverMode.CLI; if (this._controller) return DriverMode.SerialAPI; return DriverMode.Unknown; } _recoveryPhase = 0 /* ControllerRecoveryPhase.None */; _securityManager; /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ get securityManager() { return this._securityManager; } _securityManager2; /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ get securityManager2() { return this._securityManager2; } _securityManagerLR; /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ get securityManagerLR() { return this._securityManagerLR; } /** @internal */ getSecurityManager2(destination) { const nodeId = isArray(destination) ? destination[0] : destination; const isLongRange = isLongRangeNodeId(nodeId); return isLongRange ? this.securityManagerLR : this.securityManager2; } _learnModeAuthenticatedKeyPair; /** @internal */ async getLearnModeAuthenticatedKeyPair() { if (this._learnModeAuthenticatedKeyPair == undefined) { // Try restoring from cache const privateKey = this.cacheGet(cacheKeys.controller.privateKey); if (privateKey) { this._learnModeAuthenticatedKeyPair = await keyPairFromRawECDHPrivateKey(privateKey); } else { // Not found in cache, create a new one and cache it this._learnModeAuthenticatedKeyPair = await generateECDHKeyPair(); this.cacheSet(cacheKeys.controller.privateKey, this._learnModeAuthenticatedKeyPair.privateKey); } } return this._learnModeAuthenticatedKeyPair; } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications. Use `controller.homeId` instead! */ get homeId() { // This is needed for the ZWaveHost interface return this.controller.homeId; } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications. Use `controller.ownNodeId` instead! */ get ownNodeId() { // This is needed for the ZWaveHost interface return this.controller.ownNodeId; } /** @internal Used for compatibility with the CCAPIHost interface */ getNode(nodeId) { return this.controller.nodes.get(nodeId); } /** @internal Used for compatibility with the CCAPIHost interface */ getNodeOrThrow(nodeId) { return this.controller.nodes.getOrThrow(nodeId); } /** @internal Used for compatibility with the CCAPIHost interface */ getAllNodes() { return [...this.controller.nodes.values()]; } tryGetNode(msg) { const nodeId = msg.getNodeId(); if (nodeId != undefined) return this.controller.nodes.get(nodeId); } tryGetEndpoint(cc) { if (cc.isSinglecast()) { return this.controller.nodes .get(cc.nodeId) ?.getEndpoint(cc.endpointIndex); } } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ getValueDB(nodeId) { // This is needed for the ZWaveHost interface const node = this.controller.nodes.getOrThrow(nodeId); return node.valueDB; } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ tryGetValueDB(nodeId) { // This is needed for the ZWaveHost interface const node = this.controller.nodes.get(nodeId); return node?.valueDB; } getDeviceConfig(nodeId) { // This is needed for the ZWaveHost interface return this.controller.nodes.get(nodeId)?.deviceConfig; } lookupManufacturer(manufacturerId) { return this.configManager.lookupManufacturer(manufacturerId); } getHighestSecurityClass(nodeId) { // This is needed for the ZWaveHost interface const node = this.controller.nodes.getOrThrow(nodeId); return node.getHighestSecurityClass(); } hasSecurityClass(nodeId, securityClass) { // This is needed for the ZWaveHost interface const node = this.controller.nodes.getOrThrow(nodeId); return node.hasSecurityClass(securityClass); } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ setSecurityClass(nodeId, securityClass, granted) { // This is needed for the ZWaveHost interface const node = this.controller.nodes.getOrThrow(nodeId); node.setSecurityClass(securityClass, granted); } /** Updates the logging configuration without having to restart the driver. */ updateLogConfig(config) { this._logContainer.updateConfiguration(config); } /** Returns the current logging configuration. */ getLogConfig() { return this._logContainer.getConfiguration(); } /** Updates the preferred sensor scales to use for node queries */ setPreferredScales(scales) { this._options.preferences.scales = mergeDeep(defaultOptions.preferences.scales, scales); } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ getUserPreferences() { return this._options.preferences; } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ getInterviewOptions() { return this._options.interview; } /** * **!!! INTERNAL !!!** * * Not intended to be used by applications */ getRefreshValueTimeouts() { return { refreshValue: this._options.timeouts.refreshValue, refreshValueAfterTransition: this._options.timeouts.refreshValueAfterTransition, }; } /** * Enumerates all existing serial ports. * @param local Whether to include local serial ports * @param remote Whether to discover remote serial ports using an mDNS query for the `_zwave._tcp` domain */ static async enumerateSerialPorts({ local = true, remote = true, } = {}) { const ret = []; // Ideally we'd use the host bindings used by the driver, but we can't access them in a static method const bindings = // oxlint-disable-next-line typescript/ban-ts-comment // @ts-ignore - For some reason, VSCode does not like this import, although tsc is fine with it (await import("#default_bindings/serial")).serial; if (local && typeof bindings.list === "function") { for (const port of await bindings.list()) { if (port.type === "custom") continue; ret.push(port); } } if (remote) { const ports = await discoverRemoteSerialPorts(); if (ports) { ret.push(...ports.map((p) => ({ type: "socket", path: p.port, }))); } } const portOrder = ["link", "socket", "tty"]; ret.sort((a, b) => { const typeA = portOrder.indexOf(a.type); const typeB = portOrder.indexOf(b.type); if (typeA !== typeB) return typeA - typeB; return a.path.localeCompare(b.path); }); return distinct(ret.map((p) => p.path)); } /** Updates a subset of the driver options on the fly */ updateOptions(options) { // This code is called from user code, so we need to make sure no options were passed // which we are not able to update on the fly const safeOptions = pick(options, [ "attempts", "disableOptimisticValueUpdate", "emitValueUpdateAfterSetValue", "inclusionUserCallbacks", "joinNetworkUserCallbacks", "interview", "preferences", "vendor", ]); // Create a new deep-merged copy of the options so we can check them for validity // without affecting our own options. // The following options are potentially unsafe to clone, so just preserve them: // - logConfig // - host (could contain classes) const { logConfig, host, ...rest } = this._options; const newOptions = mergeDeep(cloneDeep(rest), safeOptions, true); newOptions.logConfig = logConfig; newOptions.host = host; checkOptions(newOptions); if (options.userAgent && !isObject(options.userAgent)) { throw new ZWaveError(`The userAgent property must be an object!`, ZWaveErrorCodes.Driver_InvalidOptions); } // All good, update the options this._options = newOptions; if (options.logConfig) { this.updateLogConfig(options.logConfig); } if (options.userAgent) { this.updateUserAgent(options.userAgent); } } _options; get options() { return this._options; } /** * The host bindings used to access file system etc. */ // This is set during `start()` and should not be accessed before bindings; _wasStarted = false; _isOpen = false; /** Start the driver */ async start() { // avoid starting twice 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; // Populate default bindings. This has to happen asynchronously, so the driver does not have a hard dependency // on Node.js internals this.bindings = { fs: this._options.host?.fs ?? (await import("#default_bindings/fs")).fs, serial: this._options.host?.serial ?? (await import("#default_bindings/serial")).serial, db: this._options.host?.db // oxlint-disable-next-line typescript/ban-ts-comment // @ts-ignore - For some reason, VSCode does not like this import, although tsc is fine with it ?? (await import("#default_bindings/db")).db, log: this._options.host?.log ?? (await import("#default_bindings/log")).log, }; // Initialize logging this._logContainer = this.bindings.log(this._options.logConfig); this._driverLog = new DriverLogger(this, this._logContainer); this._controllerLog = new ControllerLogger(this._logContainer); // Initialize config manager this._configManager = new ConfigManager({ bindings: this.bindings.fs, logContainer: this._logContainer, deviceConfigPriorityDir: this._options.storage.deviceConfigPriorityDir, deviceConfigExternalDir: this._options.storage.deviceConfigExternalDir, }); const spOpenPromise = createDeferredPromise(); // Log which version is running if (this._options.logConfig?.showLogo !== false) { this.driverLog.print(libNameString, "info"); } this.driverLog.print(`version ${libVersion}`, "info"); this.driverLog.print("", "info"); this.driverLog.print("starting driver..."); // Open the serial port let binding; if (typeof this.port === "string") { if (typeof this.bindings.serial.createFactoryByPath === "function") { this.driverLog.print(`opening serial port ${this.port}`); binding = await this.bindings.serial.createFactoryByPath(this.port); } else { spOpenPromise.reject(new ZWaveError("This platform does not support creating a serial connection by path", ZWaveErrorCodes.Driver_Failed)); void this.destroy(); return; } } else if (isZWaveSerialPortImplementation(this.port)) { this.driverLog.print("opening serial port using the provided custom implementation"); this.driverLog.print("This is deprecated! Switch to the factory pattern instead.", "warn"); binding = wrapLegacySerialBinding(this.port); } else { this.driverLog.print("opening serial port using the provided custom factory"); binding = this.port; } this.serialFactory = new ZWaveSerialStreamFactory(binding, this._logContainer); // IMPORTANT: Test code expects the open promise to be created and returned synchronously // Everything async (including opening the serial port) must happen in the setImmediate callback // asynchronously open the serial port 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(); // Start the task scheduler this._scheduler.start(); if (typeof this._options.testingHooks?.onSerialPortOpen === "function") { await this._options.testingHooks.onSerialPortOpen(this.serial); } // Perform initialization sequence if (this._options.testingHooks?.skipFirmwareIdentification) { // No identification desired, just send a NAK and assume it's a // Serial API controller await this.writeHeader(MessageHeaders.NAK); if (getenv("NODE_ENV") !== "test") { await wait(1000); } } else { const mode = await this.detectMode(); if (mode === DriverMode.CLI) { this.emit("cli ready"); return; } if (mode === DriverMode.Bootloader) { if (this._options.bootloaderMode === "stay") { this.driverLog.print("Controller is in bootloader mode. Staying in bootloader as requested.", "warn"); this.emit("bootloader ready"); return; } this.driverLog.print("Controller is in bootloader, attempting to recover...", "warn"); await this.leaveBootloaderInternal(); // Wait a short time again. If we're in bootloader mode again, we're stuck await wait(1000); // FIXME: Leaving the bootloader may end up in the CLI if (this._bootloader) { if (this._options.bootloaderMode === "allow") { this.driverLog.print("Failed to recover from bootloader. Staying in bootloader mode as requested.", "warn"); this.emit("bootloader ready"); } else { // bootloaderMode === "recover" void this.destroyWithMessage("Failed to recover from bootloader. Please flash a new firmware to continue..."); } return; } } } // Try to create the cache directory. This can fail, in which case we should expose a good error message try { if (this._options.storage.driver) { await this._options.storage.driver.ensureDir(this.cacheDir); } else { await this.bindings.fs.ensureDir(this.cacheDir); } } catch (e) { let message; if (/\.yarn[/\\]cache[/\\]zwave-js/i.test(getErrorMessage(e, true))) { message = `Failed to create the cache directory ${this.cacheDir}. When using Yarn PnP, you need to change the location with the "storage.cacheDir" driver option.`; } else { message = `Failed to create the cache directory ${this.cacheDir}. Please make sure that it is writable or change the location with the "storage.cacheDir" driver option.`; } void this.destroyWithMessage(message); return; } // Load the necessary configuration if (this._options.testingHooks?.loadConfiguration !== false) { this.driverLog.print("loading configuration..."); try { await this.configManager.loadAll(); } catch (e) { const message = `Failed to load the configuration: ${getErrorMessage(e)}`; void this.destroyWithMessage(message); 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; } async detectMode() { // We re-use the NAK that should be used to reset the communication on // Serial API startup to detect which kind of application we are talking to const incomingNAK = this.waitForMessageHeader((h) => h === MessageHeaders.NAK, 500) .then(() => true) .catch(() => false); await this.writeHeader(MessageHeaders.NAK); // The response to this NAK helps determine whether the Z-Wave module is... // ...stuck in the bootloader, // ...running a SoC end device firmware with CLI // ...or a "normal" Serial API if (await incomingNAK) { // This is possibly a CLI. It should respond with a prompt after we // send a newline. await this.writeSerial(Bytes.from("\n", "ascii")); } // If there was no NAK, it may be a bootloader, but it may also be a CLI // on a device that just started. In this case it can happen that the // NAK is not answered, but a CLI prompt is received. // In this case, the CLI is also detected by the serial parsers. // In any case, wait another 500ms to give