UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

1,056 lines 116 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var Node_exports = {}; __export(Node_exports, { ZWaveNode: () => ZWaveNode }); module.exports = __toCommonJS(Node_exports); var import_tslib = require("tslib"); var import_cc = require("@zwave-js/cc"); var import_ApplicationStatusCC = require("@zwave-js/cc/ApplicationStatusCC"); var import_AssociationCC = require("@zwave-js/cc/AssociationCC"); var import_AssociationGroupInfoCC = require("@zwave-js/cc/AssociationGroupInfoCC"); var import_BasicCC = require("@zwave-js/cc/BasicCC"); var import_BinarySwitchCC = require("@zwave-js/cc/BinarySwitchCC"); var import_CentralSceneCC = require("@zwave-js/cc/CentralSceneCC"); var import_ClockCC = require("@zwave-js/cc/ClockCC"); var import_EntryControlCC = require("@zwave-js/cc/EntryControlCC"); var import_FirmwareUpdateMetaDataCC = require("@zwave-js/cc/FirmwareUpdateMetaDataCC"); var import_HailCC = require("@zwave-js/cc/HailCC"); var import_ManufacturerSpecificCC = require("@zwave-js/cc/ManufacturerSpecificCC"); var import_MultilevelSwitchCC = require("@zwave-js/cc/MultilevelSwitchCC"); var import_NodeNamingCC = require("@zwave-js/cc/NodeNamingCC"); var import_NotificationCC = require("@zwave-js/cc/NotificationCC"); var import_PowerlevelCC = require("@zwave-js/cc/PowerlevelCC"); var import_SceneActivationCC = require("@zwave-js/cc/SceneActivationCC"); var import_Security2CC = require("@zwave-js/cc/Security2CC"); var import_SecurityCC = require("@zwave-js/cc/SecurityCC"); var import_ThermostatModeCC = require("@zwave-js/cc/ThermostatModeCC"); var import_VersionCC = require("@zwave-js/cc/VersionCC"); var import_WakeUpCC = require("@zwave-js/cc/WakeUpCC"); var import_ZWavePlusCC = require("@zwave-js/cc/ZWavePlusCC"); var import_config = require("@zwave-js/config"); var import_core = require("@zwave-js/core"); var import_serial = require("@zwave-js/serial"); var import_serialapi = require("@zwave-js/serial/serialapi"); var import_shared = require("@zwave-js/shared"); var import_async = require("alcalzone-shared/async"); var import_deferred_promise = require("alcalzone-shared/deferred-promise"); var import_math = require("alcalzone-shared/math"); var import_pathe = __toESM(require("pathe"), 1); var import_NetworkCache = require("../driver/NetworkCache.js"); var import_ApplicationStatusCC2 = require("./CCHandlers/ApplicationStatusCC.js"); var import_AssociationCC2 = require("./CCHandlers/AssociationCC.js"); var import_AssociationGroupInformationCC = require("./CCHandlers/AssociationGroupInformationCC.js"); var import_BasicCC2 = require("./CCHandlers/BasicCC.js"); var import_BatteryCC = require("./CCHandlers/BatteryCC.js"); var import_BinarySwitchCC2 = require("./CCHandlers/BinarySwitchCC.js"); var import_CentralSceneCC2 = require("./CCHandlers/CentralSceneCC.js"); var import_ClockCC2 = require("./CCHandlers/ClockCC.js"); var import_DeviceResetLocallyCC = require("./CCHandlers/DeviceResetLocallyCC.js"); var import_EntryControlCC2 = require("./CCHandlers/EntryControlCC.js"); var import_HailCC2 = require("./CCHandlers/HailCC.js"); var import_IndicatorCC = require("./CCHandlers/IndicatorCC.js"); var import_ManufacturerSpecificCC2 = require("./CCHandlers/ManufacturerSpecificCC.js"); var import_MultiChannelAssociationCC = require("./CCHandlers/MultiChannelAssociationCC.js"); var import_MultilevelSwitchCC2 = require("./CCHandlers/MultilevelSwitchCC.js"); var import_NotificationCC2 = require("./CCHandlers/NotificationCC.js"); var import_PowerlevelCC2 = require("./CCHandlers/PowerlevelCC.js"); var import_SoundSwitchCC = require("./CCHandlers/SoundSwitchCC.js"); var import_ThermostatModeCC2 = require("./CCHandlers/ThermostatModeCC.js"); var import_TimeCC = require("./CCHandlers/TimeCC.js"); var import_VersionCC2 = require("./CCHandlers/VersionCC.js"); var import_WakeUpCC2 = require("./CCHandlers/WakeUpCC.js"); var import_ZWavePlusCC2 = require("./CCHandlers/ZWavePlusCC.js"); var import_DeviceClass = require("./DeviceClass.js"); var import_HealthCheck = require("./HealthCheck.js"); var import_NodeStatistics = require("./NodeStatistics.js"); var import_Types = require("./_Types.js"); var import_mixins = require("./mixins/index.js"); var nodeUtils = __toESM(require("./utils.js"), 1); let ZWaveNode = (() => { let _classDecorators = [(0, import_shared.Mixin)([import_shared.TypedEventTarget, import_NodeStatistics.NodeStatisticsHost])]; let _classDescriptor; let _classExtraInitializers = []; let _classThis; let _classSuper = import_mixins.ZWaveNodeMixins; var ZWaveNode2 = class extends _classSuper { static { __name(this, "ZWaveNode"); } static { _classThis = this; } static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; (0, import_tslib.__esDecorate)(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); ZWaveNode2 = _classThis = _classDescriptor.value; if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); (0, import_tslib.__runInitializers)(_classThis, _classExtraInitializers); } constructor(id, driver, deviceClass, supportedCCs = [], controlledCCs = [], valueDB) { super( id, driver, // Define this node's intrinsic endpoint as the root device (0) 0, deviceClass, supportedCCs, valueDB ); for (const cc of controlledCCs) this.addCC(cc, { isControlled: true }); } /** * Cleans up all resources used by this node */ destroy() { for (const timeout of [ this.centralSceneHandlerStore.keyHeldDownContext?.timeout, ...this.notificationHandlerStore.idleTimeouts.values(), ...this.soundSwitchHandlerStore.autoResetTimers.values() ]) { timeout?.clear(); } this.removeAllListeners(); this.cancelAllScheduledPolls(); } /** * The device specific key (DSK) of this node in binary format. * This is only set if included with Security S2. */ get dsk() { return this.driver.cacheGet(import_NetworkCache.cacheKeys.node(this.id).dsk); } /** @internal */ set dsk(value) { const cacheKey = import_NetworkCache.cacheKeys.node(this.id).dsk; this.driver.cacheSet(cacheKey, value); } /** * The user-defined name of this node. Uses the value reported by `Node Naming and Location CC` if it exists. * * **Note:** Setting this value only updates the name locally. To permanently change the name of the node, use * the `commandClasses` API. */ get name() { return this.getValue(import_NodeNamingCC.NodeNamingAndLocationCCValues.name.id); } set name(value) { if (value != void 0) { this._valueDB.setValue(import_NodeNamingCC.NodeNamingAndLocationCCValues.name.id, value); } else { this._valueDB.removeValue(import_NodeNamingCC.NodeNamingAndLocationCCValues.name.id); } } /** * The user-defined location of this node. Uses the value reported by `Node Naming and Location CC` if it exists. * * **Note:** Setting this value only updates the location locally. To permanently change the location of the node, use * the `commandClasses` API. */ get location() { return this.getValue(import_NodeNamingCC.NodeNamingAndLocationCCValues.location.id); } set location(value) { if (value != void 0) { this._valueDB.setValue(import_NodeNamingCC.NodeNamingAndLocationCCValues.location.id, value); } else { this._valueDB.removeValue(import_NodeNamingCC.NodeNamingAndLocationCCValues.location.id); } } /** Whether a SUC return route was configured for this node */ get hasSUCReturnRoute() { return !!this.driver.cacheGet(import_NetworkCache.cacheKeys.node(this.id).hasSUCReturnRoute); } set hasSUCReturnRoute(value) { this.driver.cacheSet(import_NetworkCache.cacheKeys.node(this.id).hasSUCReturnRoute, value); } /** The last time a message was received from this node */ get lastSeen() { return this.driver.cacheGet(import_NetworkCache.cacheKeys.node(this.id).lastSeen); } /** @internal */ set lastSeen(value) { this.driver.cacheSet(import_NetworkCache.cacheKeys.node(this.id).lastSeen, value); this.updateStatistics((cur) => ({ ...cur, lastSeen: value })); } /** * The default volume level to be used for activating a Sound Switch. * Can be overridden by command-specific options. */ get defaultVolume() { return this.driver.cacheGet(import_NetworkCache.cacheKeys.node(this.id).defaultVolume); } set defaultVolume(value) { if (value != void 0 && (value < 0 || value > 100)) { throw new import_core.ZWaveError(`The default volume must be a number between 0 and 100!`, import_core.ZWaveErrorCodes.Argument_Invalid); } this.driver.cacheSet(import_NetworkCache.cacheKeys.node(this.id).defaultVolume, value); } /** * The default transition duration to be used for transitions like dimming lights or activating scenes. * Can be overridden by command-specific options. */ get defaultTransitionDuration() { return this.driver.cacheGet(import_NetworkCache.cacheKeys.node(this.id).defaultTransitionDuration); } set defaultTransitionDuration(value) { value = import_core.Duration.from(value)?.toString(); this.driver.cacheSet(import_NetworkCache.cacheKeys.node(this.id).defaultTransitionDuration, value); } /** Returns a list of all value names that are defined on all endpoints of this node */ getDefinedValueIDs() { if (this.isControllerNode) { const proprietary = this.driver.controller.proprietary; for (const impl of Object.values(proprietary)) { if (typeof impl.getDefinedValueIDs === "function") { return impl.getDefinedValueIDs(); } } return []; } else { return nodeUtils.getDefinedValueIDs(this.driver, this); } } /** * Updates a value for a given property of a given CommandClass on the node. * This will communicate with the node! */ async setValue(valueId, value, options) { valueId = (0, import_core.normalizeValueID)(valueId); if (this.isControllerNode) { const proprietary = this.driver.controller.proprietary; for (const impl of Object.values(proprietary)) { if (typeof impl.setValue === "function") { return impl.setValue(valueId, value); } } } const loglevel = this.driver.getLogConfig().level; try { const endpointInstance = this.getEndpoint(valueId.endpoint || 0); if (!endpointInstance) { return { status: import_cc.SetValueStatus.EndpointNotFound, message: `Endpoint ${valueId.endpoint} does not exist on Node ${this.id}` }; } let api = endpointInstance.commandClasses[valueId.commandClass]; if (!api.setValue) { return { status: import_cc.SetValueStatus.NotImplemented, message: `The ${(0, import_core.getCCName)(valueId.commandClass)} CC does not support setting values` }; } if (loglevel === "silly") { this.driver.controllerLog.logNode(this.id, { endpoint: valueId.endpoint, message: `[setValue] calling SET_VALUE API ${api.constructor.name}: property: ${valueId.property} property key: ${valueId.propertyKey} optimistic: ${api.isSetValueOptimistic(valueId)}`, level: "silly" }); } options ??= {}; options.transitionDuration ??= this.defaultTransitionDuration; options.volume ??= this.defaultVolume; const valueIdProps = { property: valueId.property, propertyKey: valueId.propertyKey }; const hooks = api.setValueHooks?.(valueIdProps, value, options); if (hooks?.supervisionDelayedUpdates) { api = api.withOptions({ requestStatusUpdates: true, onUpdate: /* @__PURE__ */ __name(async (update) => { try { if (update.status === import_core.SupervisionStatus.Success) { await hooks.supervisionOnSuccess(); } else if (update.status === import_core.SupervisionStatus.Fail) { await hooks.supervisionOnFailure(); } } catch { } }, "onUpdate") }); } if (typeof options.onProgress === "function") { api = api.withOptions({ onProgress: options.onProgress }); } const result = await api.setValue.call(api, valueIdProps, value, options); if (loglevel === "silly") { let message = `[setValue] result of SET_VALUE API call for ${api.constructor.name}:`; if (result) { if ((0, import_core.isSupervisionResult)(result)) { message += ` (SupervisionResult) status: ${(0, import_shared.getEnumMemberName)(import_core.SupervisionStatus, result.status)}`; if (result.remainingDuration) { message += ` duration: ${result.remainingDuration.toString()}`; } } else { message += " (other) " + JSON.stringify(result, null, 2); } } else { message += " undefined"; } this.driver.controllerLog.logNode(this.id, { endpoint: valueId.endpoint, message, level: "silly" }); } const isAPIOptimistic = api.isSetValueOptimistic(valueId); const isSlowDeviceClass = endpointInstance.deviceClass?.specific.supportsOptimisticValueUpdate === false; const supervisedAndAccepted = (0, import_core.supervisedCommandSucceeded)(result); const supervisedAndCompletedSuccessfully = (0, import_core.isSupervisionResult)(result) && result.status === import_core.SupervisionStatus.Success; const unsupervisedAndOptimisticValueUpdateEnabled = !this.driver.options.disableOptimisticValueUpdate && result == void 0; const shouldUpdateActualValueOptimistically = isAPIOptimistic && (!isSlowDeviceClass || hooks?.isSplitStateTargetValue) && (supervisedAndAccepted || unsupervisedAndOptimisticValueUpdateEnabled); const shouldUpdateRelatedValuesOptimistically = isAPIOptimistic && !isSlowDeviceClass && (supervisedAndCompletedSuccessfully || unsupervisedAndOptimisticValueUpdateEnabled); if (shouldUpdateActualValueOptimistically) { const emitEvent = !!result || !!this.driver.options.emitValueUpdateAfterSetValue; if (loglevel === "silly") { const message = emitEvent ? "updating value with event" : "updating value without event"; this.driver.controllerLog.logNode(this.id, { endpoint: valueId.endpoint, message: `[setValue] ${message}`, level: "silly" }); } const options2 = {}; if (emitEvent) { options2.source = "driver"; } else { options2.noEvent = true; } options2.updateTimestamp = (0, import_core.supervisedCommandSucceeded)(result); this._valueDB.setValue(valueId, value, options2); } else if (loglevel === "silly") { this.driver.controllerLog.logNode(this.id, { endpoint: valueId.endpoint, message: `[setValue] not updating value`, level: "silly" }); } if (hooks) { if (shouldUpdateRelatedValuesOptimistically) { hooks.optimisticallyUpdateRelatedValues?.(supervisedAndCompletedSuccessfully); } if (!(0, import_core.supervisedCommandSucceeded)(result) || isSlowDeviceClass || hooks.forceVerifyChanges?.()) { await hooks.verifyChanges?.(result); } } if ((0, import_core.isUnsupervisedOrSucceeded)(result)) { this.handleSetValueSideEffects(valueId, value); } return (0, import_cc.supervisionResultToSetValueResult)(result); } catch (e) { if ((0, import_core.isZWaveError)(e)) { let result; switch (e.code) { // This CC or API is not implemented case import_core.ZWaveErrorCodes.CC_NotImplemented: case import_core.ZWaveErrorCodes.CC_NoAPI: result = { status: import_cc.SetValueStatus.NotImplemented, message: e.message }; break; // A user tried to set an invalid value case import_core.ZWaveErrorCodes.Argument_Invalid: result = { status: import_cc.SetValueStatus.InvalidValue, message: e.message }; break; } if (loglevel === "silly") { this.driver.controllerLog.logNode(this.id, { endpoint: valueId.endpoint, message: `[setValue] raised ZWaveError (${!!result ? "handled" : "not handled"}, code ${(0, import_shared.getEnumMemberName)(import_core.ZWaveErrorCodes, e.code)}): ${e.message}`, level: "silly" }); } if (result) return result; } throw e; } } /** * Handles CC-specific side effects after a value has been set successfully. */ handleSetValueSideEffects(valueId, value) { if (import_cc.SoundSwitchCCValues.toneId.is(valueId) && typeof value === "number") { (0, import_SoundSwitchCC.handleSoundSwitchSetValue)(this.driver, this, this.soundSwitchHandlerStore, valueId.endpoint ?? 0, value); } } /** * Requests a value for a given property of a given CommandClass by polling the node. * **Warning:** Some value IDs share a command, so make sure not to blindly call this for every property */ pollValue(valueId, sendCommandOptions = {}) { valueId = (0, import_core.normalizeValueID)(valueId); if (this.isControllerNode) { const proprietary = this.driver.controller.proprietary; for (const impl of Object.values(proprietary)) { if (typeof impl.pollValue === "function") { return impl.pollValue(valueId); } } } const endpointInstance = this.getEndpoint(valueId.endpoint || 0); if (!endpointInstance) { throw new import_core.ZWaveError(`Endpoint ${valueId.endpoint} does not exist on Node ${this.id}`, import_core.ZWaveErrorCodes.Argument_Invalid); } const api = endpointInstance.commandClasses[valueId.commandClass].withOptions({ // We do not want to delay more important communication by polling, so... // ...don't retry maxSendAttempts: 1, // ...and give it the lowest priority for user interactions priority: import_core.MessagePriority.NodeQuery, // ...unless overwritten by the options ...sendCommandOptions }); if (!api.pollValue) { throw new import_core.ZWaveError(`The pollValue API is not implemented for CC ${(0, import_core.getCCName)(valueId.commandClass)}!`, import_core.ZWaveErrorCodes.CC_NoAPI); } return api.pollValue.call(api, { property: valueId.property, propertyKey: valueId.propertyKey }); } /** Returns a list of all `"notification"` event arguments that are known to be supported by this node */ getSupportedNotificationEvents() { return nodeUtils.getSupportedNotificationEvents(this.driver, this); } _interviewAttempts = 0; /** How many attempts to interview this node have already been made */ get interviewAttempts() { return this._interviewAttempts; } _hasEmittedNoS2NetworkKeyError = false; _hasEmittedNoS0NetworkKeyError = false; /** * Starts or resumes a deferred initial interview of this node. * * **WARNING:** This is only allowed when the initial interview was deferred using the * `interview.disableOnNodeAdded` option. Otherwise, this method will throw an error. * * **NOTE:** It is advised to NOT await this method as it can take a very long time (minutes to hours)! */ async interview() { if (this.isControllerNode) return; if (!this.driver.options.interview?.disableOnNodeAdded) { throw new import_core.ZWaveError(`Calling ZWaveNode.interview() is not allowed because automatic node interviews are enabled. Wait for the driver to interview the node or use ZWaveNode.refreshInfo() to re-interview a node.`, import_core.ZWaveErrorCodes.Driver_FeatureDisabled); } return this.driver.interviewNodeInternal(this); } _refreshInfoPending = false; /** * Resets all information about this node and forces a fresh interview. * **Note:** This does nothing for the controller node. * * **WARNING:** Take care NOT to call this method when the node is already being interviewed. * Otherwise the node information may become inconsistent. */ async refreshInfo(options = {}) { if (this.isControllerNode) return; if (this._refreshInfoPending) return; this._refreshInfoPending = true; const { resetSecurityClasses = false, waitForWakeup = true } = options; let didWakeUp = false; const wasAwake = this.status === import_Types.NodeStatus.Awake; if (waitForWakeup && this.canSleep && !wasAwake && this.supportsCC(import_core.CommandClasses["Wake Up"])) { this.driver.controllerLog.logNode(this.id, "Re-interview scheduled, waiting for node to wake up..."); didWakeUp = await this.waitForWakeup().then(() => true).catch(() => false); } const name = this.name; const location = this.location; const preservedValues = []; const preservedMetadata = []; if (this.supportsCC(import_core.CommandClasses["User Code"]) && !this.driver.options.interview.queryAllUserCodes) { const mustBackup = /* @__PURE__ */ __name((v) => import_cc.UserCodeCCValues.userCode.is(v) || import_cc.UserCodeCCValues.userIdStatus.is(v) || import_cc.UserCodeCCValues.userCodeChecksum.is(v), "mustBackup"); const values = this.valueDB.getValues(import_core.CommandClasses["User Code"]).filter(mustBackup); preservedValues.push(...values); const meta = this.valueDB.getAllMetadata(import_core.CommandClasses["User Code"]).filter(mustBackup); preservedMetadata.push(...meta); } if (resetSecurityClasses) this.securityClasses.clear(); this._interviewAttempts = 0; this.interviewStage = import_Types.InterviewStage.None; this.ready = false; this.deviceClass = void 0; this.isListening = void 0; this.isFrequentListening = void 0; this.isRouting = void 0; this.supportedDataRates = void 0; this.protocolVersion = void 0; this.nodeType = void 0; this.supportsSecurity = void 0; this.supportsBeaming = void 0; this.deviceConfig = void 0; this.currentDeviceConfigHash = void 0; this.cachedDeviceConfigHash = void 0; this._hasEmittedNoS0NetworkKeyError = false; this._hasEmittedNoS2NetworkKeyError = false; for (const ep of this.getAllEndpoints()) { ep["reset"](); } this._valueDB.clear({ noEvent: true }); this._endpointInstances.clear(); super.reset(); this.restartReadyMachine(); this.restartStatusMachine(); this.cancelAllScheduledPolls(); if (name != void 0) this.name = name; if (location != void 0) this.location = location; for (const { value, ...valueId } of preservedValues) { this.valueDB.setValue(valueId, value, { noEvent: true }); } for (const { metadata, ...valueId } of preservedMetadata) { this.valueDB.setMetadata(valueId, metadata, { noEvent: true }); } this.keepAwake = false; if (didWakeUp || wasAwake) { this.isListening = false; this.isFrequentListening = false; this.markAsAwake(); } void this.driver.interviewNodeInternal(this); this._refreshInfoPending = false; } /** * @internal * Interviews this node. Returns true when it succeeded, false otherwise * * WARNING: Do not call this method from application code. To refresh the information * for a specific node, use `node.refreshInfo()` instead */ async interviewInternal() { if (this.interviewStage === import_Types.InterviewStage.Complete) { this.driver.controllerLog.logNode(this.id, `skipping interview because it is already completed`); return true; } else { this.driver.controllerLog.interviewStart(this); } this._interviewAttempts++; const tryInterviewStage = /* @__PURE__ */ __name(async (method) => { try { await method(); return true; } catch (e) { if ((0, import_core.isTransmissionError)(e)) { return false; } throw e; } }, "tryInterviewStage"); if (this.interviewStage === import_Types.InterviewStage.None) { this.driver.controllerLog.logNode(this.id, `new node, doing a full interview...`); this.emit("interview started", this); await this.queryProtocolInfo(); } if (!this.isControllerNode) { if ((this.isListening || this.isFrequentListening) && this.status !== import_Types.NodeStatus.Alive) { if (!await this.ping()) { return false; } } if (this.interviewStage === import_Types.InterviewStage.ProtocolInfo) { if (!await tryInterviewStage(() => this.interviewNodeInfo())) { return false; } } if (this.interviewStage === import_Types.InterviewStage.NodeInfo) { if (await this.interviewCCs()) { this.setInterviewStage(import_Types.InterviewStage.CommandClasses); } else { return false; } } } if (this.isControllerNode && this.interviewStage === import_Types.InterviewStage.ProtocolInfo || !this.isControllerNode && this.interviewStage === import_Types.InterviewStage.CommandClasses) { await this.overwriteConfig(); } this.cachedDeviceConfigHash = await this.deviceConfig?.getHash(); this.setInterviewStage(import_Types.InterviewStage.Complete); this.updateReadyMachine({ value: "INTERVIEW_DONE" }); this.emit("interview completed", this); return true; } /** Updates this node's interview stage and saves to cache when appropriate */ setInterviewStage(completedStage) { this.interviewStage = completedStage; this.emit("interview stage completed", this, (0, import_shared.getEnumMemberName)(import_Types.InterviewStage, completedStage)); this.driver.controllerLog.interviewStage(this); } /** Step #1 of the node interview */ async queryProtocolInfo() { this.driver.controllerLog.logNode(this.id, { message: "querying protocol info...", direction: "outbound" }); this.driver.requestStorage.set(import_serial.FunctionType.GetNodeProtocolInfo, { nodeId: this.id }); const resp = await this.driver.sendMessage(new import_serialapi.GetNodeProtocolInfoRequest({ requestedNodeId: this.id })); this.isListening = resp.isListening; this.isFrequentListening = resp.isFrequentListening; this.isRouting = resp.isRouting; this.supportedDataRates = resp.supportedDataRates; this.protocolVersion = resp.protocolVersion; this.nodeType = resp.nodeType; this.supportsSecurity = resp.supportsSecurity; this.supportsBeaming = resp.supportsBeaming; this.deviceClass = new import_DeviceClass.DeviceClass(resp.basicDeviceClass, resp.genericDeviceClass, resp.specificDeviceClass); const logMessage = `received response for protocol info: basic device class: ${(0, import_shared.getEnumMemberName)(import_core.BasicDeviceClass, this.deviceClass.basic)} generic device class: ${this.deviceClass.generic.label} specific device class: ${this.deviceClass.specific.label} node type: ${(0, import_shared.getEnumMemberName)(import_core.NodeType, this.nodeType)} is always listening: ${this.isListening} is frequent listening: ${this.isFrequentListening} can route messages: ${this.isRouting} supports security: ${this.supportsSecurity} supports beaming: ${this.supportsBeaming} maximum data rate: ${this.maxDataRate} kbps protocol version: ${this.protocolVersion}`; this.driver.controllerLog.logNode(this.id, { message: logMessage, direction: "inbound" }); if (this.canSleep) { if (this.status === import_Types.NodeStatus.Alive) { this.markAsAwake(); } else if (this.status !== import_Types.NodeStatus.Awake) { this.markAsAsleep(); } } this.setInterviewStage(import_Types.InterviewStage.ProtocolInfo); } /** * Pings the node to see if it responds * @param tryReallyHard Whether the controller should resort to route resolution * and explorer frames if the communication fails. Setting this option to `true` * can result in multi-second delays. */ async ping(tryReallyHard = false) { if (this.isControllerNode) { this.driver.controllerLog.logNode(this.id, "is the controller node, cannot ping", "warn"); return true; } this.driver.controllerLog.logNode(this.id, { message: "pinging the node...", direction: "outbound" }); try { let api = this.commandClasses["No Operation"]; if (tryReallyHard) { api = api.withOptions({ transmitOptions: import_core.TransmitOptions.DEFAULT }); } await api.send(); this.driver.controllerLog.logNode(this.id, { message: "ping successful", direction: "inbound" }); return true; } catch (e) { this.driver.controllerLog.logNode(this.id, `ping failed: ${(0, import_shared.getErrorMessage)(e)}`); return false; } } /** * Step #5 of the node interview * Request node info */ async interviewNodeInfo() { if (this.isControllerNode) { this.driver.controllerLog.logNode(this.id, "is the controller node, cannot query node info", "warn"); return; } for (let attempts = 1; attempts <= 2; attempts++) { this.driver.controllerLog.logNode(this.id, { message: "querying node info...", direction: "outbound" }); try { const nodeInfo = await this.requestNodeInfo(); const logLines = [ "node info received", "supported CCs:" ]; for (const cc of nodeInfo.supportedCCs) { logLines.push(`\xB7 ${(0, import_core.getCCName)(cc)}`); } this.driver.controllerLog.logNode(this.id, { message: logLines.join("\n"), direction: "inbound" }); this.updateNodeInfo(nodeInfo); break; } catch (e) { if ((0, import_core.isZWaveError)(e)) { if (attempts === 1 && this.canSleep && this.status !== import_Types.NodeStatus.Asleep && e.code === import_core.ZWaveErrorCodes.Controller_CallbackNOK) { this.driver.controllerLog.logNode(this.id, `Querying the node info failed, the node is probably asleep. Retrying after wakeup...`, "error"); this.markAsAsleep(); continue; } if (e.code === import_core.ZWaveErrorCodes.Controller_ResponseNOK || e.code === import_core.ZWaveErrorCodes.Controller_CallbackNOK) { this.driver.controllerLog.logNode(this.id, `Querying the node info failed`, "error"); } throw e; } } } this.setInterviewStage(import_Types.InterviewStage.NodeInfo); } async requestNodeInfo() { const resp = await this.driver.sendMessage(new import_serialapi.RequestNodeInfoRequest({ nodeId: this.id })); if (resp instanceof import_serialapi.RequestNodeInfoResponse && !resp.wasSent) { throw new import_core.ZWaveError(`Querying the node info failed`, import_core.ZWaveErrorCodes.Controller_ResponseNOK); } else if (resp instanceof import_serialapi.ApplicationUpdateRequestNodeInfoRequestFailed) { throw new import_core.ZWaveError(`Querying the node info failed`, import_core.ZWaveErrorCodes.Controller_CallbackNOK); } else if (resp instanceof import_serialapi.ApplicationUpdateRequestNodeInfoReceived) { const logLines = ["node info received", "supported CCs:"]; for (const cc of resp.nodeInformation.supportedCCs) { logLines.push(`\xB7 ${(0, import_core.getCCName)(cc)}`); } this.driver.controllerLog.logNode(this.id, { message: logLines.join("\n"), direction: "inbound" }); return resp.nodeInformation; } throw new import_core.ZWaveError(`Received unexpected response to RequestNodeInfoRequest`, import_core.ZWaveErrorCodes.Controller_CommandError); } /** Step #? of the node interview */ async interviewCCs() { if (this.isControllerNode) { this.driver.controllerLog.logNode(this.id, "is the controller node, cannot interview CCs", "warn"); return true; } const securityManager2 = this.driver.getSecurityManager2(this.id); const interviewEndpoint = /* @__PURE__ */ __name(async (endpoint, cc, force = false) => { let instance; try { if (force) { instance = import_cc.CommandClass.createInstanceUnchecked(endpoint, cc); } else { instance = endpoint.createCCInstance(cc); } } catch (e) { if ((0, import_core.isZWaveError)(e) && e.code === import_core.ZWaveErrorCodes.CC_NotSupported) { return "continue"; } throw e; } if (endpoint.isCCSecure(cc) && !this.driver.securityManager && !securityManager2) { this.driver.controllerLog.logNode(this.id, `Skipping interview for secure CC ${(0, import_core.getCCName)(cc)} because no network key is configured!`, "error"); return "continue"; } if (instance.isInterviewComplete(this.driver)) return "continue"; try { await instance.interview(this.driver); } catch (e) { if ((0, import_core.isTransmissionError)(e)) { return false; } throw e; } }, "interviewEndpoint"); if (this.supportsCC(import_core.CommandClasses["Security 2"])) { this.addCC(import_core.CommandClasses["Security 2"], { secure: true }); const securityClass = this.getHighestSecurityClass(); if (securityClass == void 0 || (0, import_core.securityClassIsS2)(securityClass)) { this.driver.controllerLog.logNode(this.id, "Root device interview: Security S2", "silly"); if (!securityManager2) { if (!this._hasEmittedNoS2NetworkKeyError) { const errorMessage = `supports Security S2, but no S2 network keys were configured. The interview might not include all functionality.`; this.driver.controllerLog.logNode(this.id, errorMessage, "error"); this.driver.emit("error", new import_core.ZWaveError(`Node ${this.id.toString().padStart(3, "0")} ${errorMessage}`, import_core.ZWaveErrorCodes.Controller_NodeInsecureCommunication)); this._hasEmittedNoS2NetworkKeyError = true; } } else { const action = await interviewEndpoint(this, import_core.CommandClasses["Security 2"]); if (typeof action === "boolean") return action; } } } else { for (const secClass of [ import_core.SecurityClass.S2_AccessControl, import_core.SecurityClass.S2_Authenticated, import_core.SecurityClass.S2_Unauthenticated ]) { if (this.hasSecurityClass(secClass) === import_core.NOT_KNOWN) { this.securityClasses.set(secClass, false); } } } if (this.supportsCC(import_core.CommandClasses.Security)) { this.addCC(import_core.CommandClasses.Security, { secure: true }); if (this.hasSecurityClass(import_core.SecurityClass.S0_Legacy) !== false) { this.driver.controllerLog.logNode(this.id, "Root device interview: Security S0", "silly"); if (!this.driver.securityManager) { if (!this._hasEmittedNoS0NetworkKeyError) { const errorMessage = `supports Security S0, but the S0 network key was not configured. The interview might not include all functionality.`; this.driver.controllerLog.logNode(this.id, errorMessage, "error"); this.driver.emit("error", new import_core.ZWaveError(`Node ${this.id.toString().padStart(3, "0")} ${errorMessage}`, import_core.ZWaveErrorCodes.Controller_NodeInsecureCommunication)); this._hasEmittedNoS0NetworkKeyError = true; } } else { const action = await interviewEndpoint(this, import_core.CommandClasses.Security); if (typeof action === "boolean") return action; } } } else { if (this.hasSecurityClass(import_core.SecurityClass.S0_Legacy) === import_core.NOT_KNOWN) { this.securityClasses.set(import_core.SecurityClass.S0_Legacy, false); } } if (this.supportsCC(import_core.CommandClasses["Manufacturer Specific"])) { this.driver.controllerLog.logNode(this.id, "Root device interview: Manufacturer Specific", "silly"); const action = await interviewEndpoint(this, import_core.CommandClasses["Manufacturer Specific"]); if (typeof action === "boolean") return action; } if (this.supportsCC(import_core.CommandClasses.Version)) { this.driver.controllerLog.logNode(this.id, "Root device interview: Version", "silly"); const action = await interviewEndpoint(this, import_core.CommandClasses.Version); if (typeof action === "boolean") return action; await this.loadDeviceConfig(); this.applyCommandClassesCompatFlag(0); } else { this.driver.controllerLog.logNode(this.id, "Version CC is not supported. Using the highest implemented version for each CC", "debug"); for (const [ccId, info] of this.getCCs()) { if (info.isSupported || ccId === import_core.CommandClasses.Basic) { this.addCC(ccId, { version: (0, import_cc.getImplementedVersion)(ccId) }); } } } if (this.supportsCC(import_core.CommandClasses["Wake Up"])) { this.driver.controllerLog.logNode(this.id, "Root device interview: Wake Up", "silly"); const action = await interviewEndpoint(this, import_core.CommandClasses["Wake Up"]); if (typeof action === "boolean") return action; } this.modifySupportedCCBeforeInterview(this); const specialCCs = [ import_core.CommandClasses.Security, import_core.CommandClasses["Security 2"], import_core.CommandClasses["Manufacturer Specific"], import_core.CommandClasses.Version, import_core.CommandClasses["Wake Up"], // Basic CC is interviewed last import_core.CommandClasses.Basic ]; const rootInterviewGraphBeforeEndpoints = this.buildCCInterviewGraph([ ...specialCCs, ...import_core.applicationCCs ]); let rootInterviewOrderBeforeEndpoints; const rootInterviewGraphAfterEndpoints = this.buildCCInterviewGraph([ ...specialCCs, ...import_core.nonApplicationCCs ]); let rootInterviewOrderAfterEndpoints; try { rootInterviewOrderBeforeEndpoints = (0, import_core.topologicalSort)(rootInterviewGraphBeforeEndpoints); rootInterviewOrderAfterEndpoints = (0, import_core.topologicalSort)(rootInterviewGraphAfterEndpoints); } catch { throw new import_core.ZWaveError("The CC interview cannot be completed because there are circular dependencies between CCs!", import_core.ZWaveErrorCodes.CC_Invalid); } this.driver.controllerLog.logNode(this.id, `Root device interviews before endpoints: ${rootInterviewOrderBeforeEndpoints.map((cc) => ` \xB7 ${(0, import_core.getCCName)(cc)}`).join("")}`, "silly"); this.driver.controllerLog.logNode(this.id, `Root device interviews after endpoints: ${rootInterviewOrderAfterEndpoints.map((cc) => ` \xB7 ${(0, import_core.getCCName)(cc)}`).join("")}`, "silly"); for (const cc of rootInterviewOrderBeforeEndpoints) { this.driver.controllerLog.logNode(this.id, `Root device interview: ${(0, import_core.getCCName)(cc)}`, "silly"); const action = await interviewEndpoint(this, cc); if (action === "continue") continue; else if (typeof action === "boolean") return action; } this.applyCommandClassesCompatFlag(); for (const endpointIndex of this.getEndpointIndizes()) { const endpoint = this.getEndpoint(endpointIndex); if (!endpoint) continue; const securityClass = this.getHighestSecurityClass(); const endpointMissingS2 = (0, import_core.securityClassIsS2)(securityClass) && this.supportsCC(import_core.CommandClasses["Security 2"]) && !endpoint.supportsCC(import_core.CommandClasses["Security 2"]); if (endpointMissingS2) { endpoint.addCC(import_core.CommandClasses["Security 2"], this.implementedCommandClasses.get(import_core.CommandClasses["Security 2"])); } if (endpoint.supportsCC(import_core.CommandClasses["Security 2"])) { endpoint.addCC(import_core.CommandClasses["Security 2"], { secure: true }); if ((0, import_core.securityClassIsS2)(securityClass) && !!securityManager2) { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `Endpoint ${endpoint.index} interview: Security S2`, level: "silly" }); const action = await interviewEndpoint(endpoint, import_core.CommandClasses["Security 2"]); if (typeof action === "boolean") return action; } } if (endpoint.supportsCC(import_core.CommandClasses.Security)) { endpoint.addCC(import_core.CommandClasses.Security, { secure: true }); if (securityClass === import_core.SecurityClass.S0_Legacy && !!this.driver.securityManager) { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `Endpoint ${endpoint.index} interview: Security S0`, level: "silly" }); const action = await interviewEndpoint(endpoint, import_core.CommandClasses.Security); if (typeof action === "boolean") return action; } } const endpointMissingS0 = securityClass === import_core.SecurityClass.S0_Legacy && this.supportsCC(import_core.CommandClasses.Security) && !endpoint.supportsCC(import_core.CommandClasses.Security); if (endpointMissingS0) { const possibleTests = [ { ccId: import_core.CommandClasses["Z-Wave Plus Info"], test: /* @__PURE__ */ __name(() => endpoint.commandClasses["Z-Wave Plus Info"].get(), "test") }, { ccId: import_core.CommandClasses["Binary Switch"], test: /* @__PURE__ */ __name(() => endpoint.commandClasses["Binary Switch"].get(), "test") }, { ccId: import_core.CommandClasses["Binary Sensor"], test: /* @__PURE__ */ __name(() => endpoint.commandClasses["Binary Sensor"].get(), "test") }, { ccId: import_core.CommandClasses["Multilevel Switch"], test: /* @__PURE__ */ __name(() => endpoint.commandClasses["Multilevel Switch"].get(), "test") }, { ccId: import_core.CommandClasses["Multilevel Sensor"], test: /* @__PURE__ */ __name(() => endpoint.commandClasses["Multilevel Sensor"].get(), "test") } // TODO: add other tests if necessary ]; const foundTest = possibleTests.find((t) => endpoint.supportsCC(t.ccId)); if (foundTest) { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `is included using Security S0, but endpoint ${endpoint.index} does not list the CC. Testing if it accepts secure commands anyways.`, level: "silly" }); const { ccId, test } = foundTest; endpoint.addCC(ccId, { secure: true }); const success = !!await test().catch(() => false); if (success) { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `Endpoint ${endpoint.index} accepts/expects secure commands`, level: "silly" }); for (const [ccId2] of endpoint.getCCs()) { endpoint.addCC(ccId2, { secure: true }); } } else { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `Endpoint ${endpoint.index} is actually not using S0`, level: "silly" }); endpoint.addCC(ccId, { secure: false }); } } else { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `is included using Security S0, but endpoint ${endpoint.index} does not list the CC. Found no way to test if accepts secure commands anyways.`, level: "silly" }); } } if (this.supportsCC(import_core.CommandClasses.Version)) { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: `Endpoint ${endpoint.index} interview: ${(0, import_core.getCCName)(import_core.CommandClasses.Version)}`, level: "silly" }); const action = await interviewEndpoint(endpoint, import_core.CommandClasses.Version, true); if (typeof action === "boolean") return action; } else { this.driver.controllerLog.logNode(this.id, { endpoint: endpoint.index, message: "Version CC is not supported. Using the highest implemented version for each CC", level: "debug" }); for (const [ccId, info] of endpoint.getCCs()) { if (info.isSupported || ccId === import_core.CommandClasses.Basic) { endpoint.addCC(ccId, { version: (0, import_cc.getImplementedVersion)(ccId) }); } } } this.applyCommandClassesCompatFlag(endpoint.index); this.modifySupportedCCBeforeInterview(endpoint); const endpointInterviewGraph = endpoint.buildCCInterviewGraph([ import_core.CommandClasses.Security, import_core.CommandClasses["Security 2"], import_core.CommandClasses.Version, import_core.CommandClasses.Basic ]); let endpointInterviewOrder; try { endpointInterviewOrder = (0, import_core.topologicalSort)(endpointInterviewGraph); } catch { throw new import_core.ZWaveError("The CC interview cannot be completed because there are circular dependencies between CCs!", import_core.ZWaveErrorCodes.CC_Invalid); } this.driv