zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
1,075 lines • 325 kB
JavaScript
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