UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

920 lines (919 loc) 157 kB
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; import { BatteryCCReport, ClockCommand, CommandClass, DeviceResetLocallyCCNotification, IndicatorCCDescriptionGet, IndicatorCCGet, IndicatorCCSet, IndicatorCCSupportedGet, MultiChannelAssociationCCGet, MultiChannelAssociationCCRemove, MultiChannelAssociationCCSet, MultiChannelAssociationCCSupportedGroupingsGet, MultiCommandCCCommandEncapsulation, Powerlevel, PowerlevelTestStatus, ScheduleEntryLockCommand, SetValueStatus, TimeCCDateGet, TimeCCTimeGet, TimeCCTimeOffsetGet, TimeCommand, TimeParametersCommand, UserCodeCCValues, getImplementedVersion, supervisionResultToSetValueResult, utils as ccUtils, } from "@zwave-js/cc"; import { ApplicationStatusCCBusy } from "@zwave-js/cc/ApplicationStatusCC"; import { AssociationCCGet, AssociationCCRemove, AssociationCCSet, AssociationCCSpecificGroupGet, AssociationCCSupportedGroupingsGet, AssociationCCValues, } from "@zwave-js/cc/AssociationCC"; import { AssociationGroupInfoCCCommandListGet, AssociationGroupInfoCCInfoGet, AssociationGroupInfoCCNameGet, } from "@zwave-js/cc/AssociationGroupInfoCC"; import { BasicCC } from "@zwave-js/cc/BasicCC"; import { BinarySwitchCCSet } from "@zwave-js/cc/BinarySwitchCC"; import { CentralSceneCCNotification } from "@zwave-js/cc/CentralSceneCC"; import { ClockCCReport } from "@zwave-js/cc/ClockCC"; import { EntryControlCCNotification } from "@zwave-js/cc/EntryControlCC"; import { FirmwareUpdateMetaDataCCGet, FirmwareUpdateMetaDataCCMetaDataGet, FirmwareUpdateMetaDataCCPrepareGet, FirmwareUpdateMetaDataCCRequestGet, } from "@zwave-js/cc/FirmwareUpdateMetaDataCC"; import { HailCC } from "@zwave-js/cc/HailCC"; import { ManufacturerSpecificCCGet } from "@zwave-js/cc/ManufacturerSpecificCC"; import { MultilevelSwitchCC } from "@zwave-js/cc/MultilevelSwitchCC"; import { NodeNamingAndLocationCCValues } from "@zwave-js/cc/NodeNamingCC"; import { NotificationCCReport } from "@zwave-js/cc/NotificationCC"; import { PowerlevelCCGet, PowerlevelCCSet, PowerlevelCCTestNodeGet, PowerlevelCCTestNodeReport, PowerlevelCCTestNodeSet, } from "@zwave-js/cc/PowerlevelCC"; import { SceneActivationCCSet } from "@zwave-js/cc/SceneActivationCC"; import { Security2CCNonceGet } from "@zwave-js/cc/Security2CC"; import { SecurityCCNonceGet } from "@zwave-js/cc/SecurityCC"; import { ThermostatModeCCSet } from "@zwave-js/cc/ThermostatModeCC"; import { VersionCCCapabilitiesGet, VersionCCCommandClassGet, VersionCCGet, } from "@zwave-js/cc/VersionCC"; import { WakeUpCCWakeUpNotification } from "@zwave-js/cc/WakeUpCC"; import { ZWavePlusCCGet } from "@zwave-js/cc/ZWavePlusCC"; import { embeddedDevicesDir } from "@zwave-js/config"; import { BasicDeviceClass, CommandClasses, Duration, EncapsulationFlags, MessagePriority, NOT_KNOWN, NodeType, ProtocolVersion, Protocols, RssiError, SecurityClass, SupervisionStatus, TransmitOptions, ZWaveError, ZWaveErrorCodes, actuatorCCs, applicationCCs, dskToString, getCCName, getDSTInfo, getNotification, isRssiError, isSupervisionResult, isTransmissionError, isUnsupervisedOrSucceeded, isZWaveError, nonApplicationCCs, normalizeValueID, securityClassIsLongRange, securityClassIsS2, securityClassOrder, sensorCCs, serializeCacheValue, supervisedCommandFailed, supervisedCommandSucceeded, topologicalSort, } from "@zwave-js/core"; import { FunctionType } from "@zwave-js/serial"; import { ApplicationUpdateRequestNodeInfoReceived, ApplicationUpdateRequestNodeInfoRequestFailed, GetNodeProtocolInfoRequest, RequestNodeInfoRequest, RequestNodeInfoResponse, } from "@zwave-js/serial/serialapi"; import { Mixin, TypedEventTarget, cloneDeep, discreteLinearSearch, formatId, getEnumMemberName, getErrorMessage, pick, } from "@zwave-js/shared"; import { wait } from "alcalzone-shared/async"; import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; import { roundTo } from "alcalzone-shared/math"; import path from "pathe"; import { cacheKeys } from "../driver/NetworkCache.js"; import { handleApplicationBusy } from "./CCHandlers/ApplicationStatusCC.js"; import { handleAssociationGet, handleAssociationRemove, handleAssociationSet, handleAssociationSpecificGroupGet, } from "./CCHandlers/AssociationCC.js"; import { handleAGICommandListGet, handleAGIInfoGet, handleAGINameGet, handleAssociationSupportedGroupingsGet, } from "./CCHandlers/AssociationGroupInformationCC.js"; import { handleBasicCommand } from "./CCHandlers/BasicCC.js"; import { handleBatteryReport } from "./CCHandlers/BatteryCC.js"; import { handleBinarySwitchCommand } from "./CCHandlers/BinarySwitchCC.js"; import { getDefaultCentralSceneHandlerStore, handleCentralSceneNotification, } from "./CCHandlers/CentralSceneCC.js"; import { getDefaultClockHandlerStore, handleClockReport, } from "./CCHandlers/ClockCC.js"; import { handleDeviceResetLocallyNotification } from "./CCHandlers/DeviceResetLocallyCC.js"; import { getDefaultEntryControlHandlerStore, handleEntryControlNotification, } from "./CCHandlers/EntryControlCC.js"; import { getDefaultHailHandlerStore, handleHail } from "./CCHandlers/HailCC.js"; import { handleIndicatorDescriptionGet, handleIndicatorGet, handleIndicatorSet, handleIndicatorSupportedGet, } from "./CCHandlers/IndicatorCC.js"; import { handleManufacturerSpecificGet } from "./CCHandlers/ManufacturerSpecificCC.js"; import { handleMultiChannelAssociationGet, handleMultiChannelAssociationRemove, handleMultiChannelAssociationSet, handleMultiChannelAssociationSupportedGroupingsGet, } from "./CCHandlers/MultiChannelAssociationCC.js"; import { handleMultilevelSwitchCommand } from "./CCHandlers/MultilevelSwitchCC.js"; import { getDefaultNotificationHandlerStore, handleNotificationReport, manuallyIdleNotificationValueInternal, } from "./CCHandlers/NotificationCC.js"; import { handlePowerlevelGet, handlePowerlevelSet, handlePowerlevelTestNodeGet, handlePowerlevelTestNodeReport, handlePowerlevelTestNodeSet, } from "./CCHandlers/PowerlevelCC.js"; import { handleThermostatModeCommand } from "./CCHandlers/ThermostatModeCC.js"; import { handleDateGet, handleTimeGet, handleTimeOffsetGet, } from "./CCHandlers/TimeCC.js"; import { handleVersionCapabilitiesGet, handleVersionCommandClassGet, handleVersionGet, } from "./CCHandlers/VersionCC.js"; import { getDefaultWakeUpHandlerStore, handleWakeUpNotification, } from "./CCHandlers/WakeUpCC.js"; import { handleZWavePlusGet } from "./CCHandlers/ZWavePlusCC.js"; import { DeviceClass } from "./DeviceClass.js"; import { formatLifelineHealthCheckSummary, formatRouteHealthCheckSummary, healthCheckTestFrameCount, } from "./HealthCheck.js"; import { NodeStatisticsHost, routeStatisticsEquals, } from "./NodeStatistics.js"; import { InterviewStage, LinkReliabilityCheckMode, NodeStatus, } from "./_Types.js"; import { ZWaveNodeMixins } from "./mixins/index.js"; import * as nodeUtils from "./utils.js"; /** * A ZWaveNode represents a node in a Z-Wave network. It is also an instance * of its root endpoint (index 0) */ let ZWaveNode = (() => { let _classDecorators = [Mixin([TypedEventTarget, NodeStatisticsHost])]; let _classDescriptor; let _classExtraInitializers = []; let _classThis; let _classSuper = ZWaveNodeMixins; var ZWaveNode = class extends _classSuper { static { _classThis = this; } static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); ZWaveNode = _classThis = _classDescriptor.value; if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); __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); // Add optional controlled CCs - endpoints don't have this for (const cc of controlledCCs) this.addCC(cc, { isControlled: true }); } /** * Cleans up all resources used by this node */ destroy() { // Remove all timeouts for (const timeout of [ this.centralSceneHandlerStore.keyHeldDownContext?.timeout, ...this.notificationHandlerStore.idleTimeouts.values(), ]) { timeout?.clear(); } // Remove all event handlers this.removeAllListeners(); // Clear all scheduled polls that would interfere with the interview 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(cacheKeys.node(this.id).dsk); } /** @internal */ set dsk(value) { const cacheKey = 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(NodeNamingAndLocationCCValues.name.id); } set name(value) { if (value != undefined) { this._valueDB.setValue(NodeNamingAndLocationCCValues.name.id, value); } else { this._valueDB.removeValue(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(NodeNamingAndLocationCCValues.location.id); } set location(value) { if (value != undefined) { this._valueDB.setValue(NodeNamingAndLocationCCValues.location.id, value); } else { this._valueDB.removeValue(NodeNamingAndLocationCCValues.location.id); } } /** Whether a SUC return route was configured for this node */ get hasSUCReturnRoute() { return !!this.driver.cacheGet(cacheKeys.node(this.id).hasSUCReturnRoute); } set hasSUCReturnRoute(value) { this.driver.cacheSet(cacheKeys.node(this.id).hasSUCReturnRoute, value); } /** The last time a message was received from this node */ get lastSeen() { return this.driver.cacheGet(cacheKeys.node(this.id).lastSeen); } /** @internal */ set lastSeen(value) { this.driver.cacheSet(cacheKeys.node(this.id).lastSeen, value); // Also update statistics 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(cacheKeys.node(this.id).defaultVolume); } set defaultVolume(value) { if (value != undefined && (value < 0 || value > 100)) { throw new ZWaveError(`The default volume must be a number between 0 and 100!`, ZWaveErrorCodes.Argument_Invalid); } this.driver.cacheSet(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(cacheKeys.node(this.id).defaultTransitionDuration); } set defaultTransitionDuration(value) { // Normalize to strings value = Duration.from(value)?.toString(); this.driver.cacheSet(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) { // For the controller, look at proprietary implementations to get the value IDs 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) { // Ensure we're dealing with a valid value ID, with no extra properties valueId = normalizeValueID(valueId); // For the controller, look at proprietary implementations to set the value 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 to retrieve the corresponding CC API try { // Access the CC API by name const endpointInstance = this.getEndpoint(valueId.endpoint || 0); if (!endpointInstance) { return { status: SetValueStatus.EndpointNotFound, message: `Endpoint ${valueId.endpoint} does not exist on Node ${this.id}`, }; } let api = endpointInstance.commandClasses[valueId.commandClass]; // Check if the setValue method is implemented if (!api.setValue) { return { status: SetValueStatus.NotImplemented, message: `The ${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", }); } // Merge the provided value change options with the defaults 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: async (update) => { try { if (update.status === SupervisionStatus.Success) { await hooks.supervisionOnSuccess(); } else if (update.status === SupervisionStatus.Fail) { await hooks.supervisionOnFailure(); } } catch { // TODO: Log error? } }, }); } // If the caller wants progress updates, they shall have them if (typeof options.onProgress === "function") { api = api.withOptions({ onProgress: options.onProgress, }); } // And call it 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 (isSupervisionResult(result)) { message += ` (SupervisionResult) status: ${getEnumMemberName(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", }); } // Remember the new value for the value we just set, if... // ... the call did not throw (assume that the call was successful) // ... the call was supervised and successful if (api.isSetValueOptimistic(valueId) && isUnsupervisedOrSucceeded(result)) { 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 options = {}; // We need to emit an event if applications opted in, or if this was a supervised call // because in this case there won't be a verification query which would result in an update if (emitEvent) { options.source = "driver"; } else { options.noEvent = true; } // Only update the timestamp of the value for successful supervised commands. Otherwise we don't know // if the command was actually executed. If it wasn't, we'd have a wrong timestamp. options.updateTimestamp = supervisedCommandSucceeded(result); this._valueDB.setValue(valueId, value, options); } else if (loglevel === "silly") { this.driver.controllerLog.logNode(this.id, { endpoint: valueId.endpoint, message: `[setValue] not updating value`, level: "silly", }); } // Depending on the settings of the SET_VALUE implementation, we may have to // optimistically update a different value and/or verify the changes if (hooks) { const supervisedAndSuccessful = isSupervisionResult(result) && result.status === SupervisionStatus.Success; const shouldUpdateOptimistically = api.isSetValueOptimistic(valueId) // Check if the device class supports optimistic value updates && (endpointInstance.deviceClass?.specific .supportsOptimisticValueUpdate ?? true) // For successful supervised commands, we know that an optimistic update is ok && (supervisedAndSuccessful // For unsupervised commands that did not fail, we let the application decide whether // to update related value optimistically || (!this.driver.options.disableOptimisticValueUpdate && result == undefined)); // The actual API implementation handles additional optimistic updates if (shouldUpdateOptimistically) { hooks.optimisticallyUpdateRelatedValues?.(supervisedAndSuccessful); } const isSlowDeviceClass = endpointInstance.deviceClass?.specific .supportsOptimisticValueUpdate === false; // Verify the current value after a delay, unless... // ...the command was supervised and successful // ... and this is not a slow device class // ...and the CC API decides not to verify anyways if (!supervisedCommandSucceeded(result) || isSlowDeviceClass || hooks.forceVerifyChanges?.()) { // Let the CC API implementation handle the verification. // It may still decide not to do it. await hooks.verifyChanges?.(result); } } return supervisionResultToSetValueResult(result); } catch (e) { // Define which errors during setValue are expected and won't throw an error if (isZWaveError(e)) { let result; switch (e.code) { // This CC or API is not implemented case ZWaveErrorCodes.CC_NotImplemented: case ZWaveErrorCodes.CC_NoAPI: result = { status: SetValueStatus.NotImplemented, message: e.message, }; break; // A user tried to set an invalid value case ZWaveErrorCodes.Argument_Invalid: result = { status: 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 ${getEnumMemberName(ZWaveErrorCodes, e.code)}): ${e.message}`, level: "silly", }); } if (result) return result; } throw e; } } /** * Requests a value for a given property of a given CommandClass by polling the node. * **Warning:** Some value IDs share a command, so make sure not to blindly call this for every property */ pollValue(valueId, sendCommandOptions = {}) { // Ensure we're dealing with a valid value ID, with no extra properties valueId = normalizeValueID(valueId); // For the controller, look at proprietary implementations to poll the value 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); } } } // Try to retrieve the corresponding CC API const endpointInstance = this.getEndpoint(valueId.endpoint || 0); if (!endpointInstance) { throw new ZWaveError(`Endpoint ${valueId.endpoint} does not exist on Node ${this.id}`, ZWaveErrorCodes.Argument_Invalid); } const api = endpointInstance.commandClasses[valueId.commandClass].withOptions({ // We do not want to delay more important communication by polling, so give it // the lowest priority and don't retry unless overwritten by the options maxSendAttempts: 1, priority: MessagePriority.Poll, ...sendCommandOptions, }); // Check if the pollValue method is implemented if (!api.pollValue) { throw new ZWaveError(`The pollValue API is not implemented for CC ${getCCName(valueId.commandClass)}!`, ZWaveErrorCodes.CC_NoAPI); } // And call it 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() { // The initial interview of the controller node is always done // and cannot be deferred. if (this.isControllerNode) return; if (!this.driver.options.interview?.disableOnNodeAdded) { throw new 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.`, 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 = {}) { // It does not make sense to re-interview the controller. All important information is queried // directly via the serial API if (this.isControllerNode) return; // The driver does deduplicate re-interview requests, but only at the end of this method. // Without blocking here, many re-interview tasks for sleeping nodes may be queued, leading to parallel interviews if (this._refreshInfoPending) return; this._refreshInfoPending = true; const { resetSecurityClasses = false, waitForWakeup = true } = options; // Unless desired, don't forget the information about sleeping nodes immediately, so they continue to function let didWakeUp = false; const wasAwake = this.status === NodeStatus.Awake; if (waitForWakeup && this.canSleep && !wasAwake && this.supportsCC(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); } // preserve the node name and location, since they might not be stored on the node const name = this.name; const location = this.location; // Preserve user codes if they aren't queried during the interview const preservedValues = []; const preservedMetadata = []; if (this.supportsCC(CommandClasses["User Code"]) && !this.driver.options.interview.queryAllUserCodes) { const mustBackup = (v) => UserCodeCCValues.userCode.is(v) || UserCodeCCValues.userIdStatus.is(v) || UserCodeCCValues.userCodeChecksum.is(v); const values = this.valueDB .getValues(CommandClasses["User Code"]) .filter(mustBackup); preservedValues.push(...values); const meta = this.valueDB .getAllMetadata(CommandClasses["User Code"]) .filter(mustBackup); preservedMetadata.push(...meta); } // Force a new detection of security classes if desired if (resetSecurityClasses) this.securityClasses.clear(); this._interviewAttempts = 0; this.interviewStage = InterviewStage.None; this.ready = false; this.deviceClass = undefined; this.isListening = undefined; this.isFrequentListening = undefined; this.isRouting = undefined; this.supportedDataRates = undefined; this.protocolVersion = undefined; this.nodeType = undefined; this.supportsSecurity = undefined; this.supportsBeaming = undefined; this.deviceConfig = undefined; this.currentDeviceConfigHash = undefined; this.cachedDeviceConfigHash = undefined; this._hasEmittedNoS0NetworkKeyError = false; this._hasEmittedNoS2NetworkKeyError = false; for (const ep of this.getAllEndpoints()) { ep["reset"](); } this._valueDB.clear({ noEvent: true }); this._endpointInstances.clear(); super.reset(); // Restart all state machines this.restartReadyMachine(); this.restartStatusMachine(); // Remove queued polls that would interfere with the interview this.cancelAllScheduledPolls(); // Restore the previously saved name/location if (name != undefined) this.name = name; if (location != undefined) this.location = location; // And preserved values/metadata 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 }); } // Don't keep the node awake after the interview this.keepAwake = false; // If we did wait for the wakeup, mark the node as awake again so it does not // get considered asleep after querying protocol info. if (didWakeUp || wasAwake) { // Re-interviewing forgets the node's capabilities. To be able to mark it // as awake, we need to set those again. 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 === InterviewStage.Complete) { this.driver.controllerLog.logNode(this.id, `skipping interview because it is already completed`); return true; } else { this.driver.controllerLog.interviewStart(this); } // Remember that we tried to interview this node this._interviewAttempts++; // Wrapper around interview methods to return false in case of a communication error // This way the single methods don't all need to have the same error handler const tryInterviewStage = async (method) => { try { await method(); return true; } catch (e) { if (isTransmissionError(e)) { return false; } throw e; } }; // The interview is done in several stages. At each point, the interview process might be aborted // due to a stage failing. The reached stage is saved, so we can continue it later without // repeating stages unnecessarily if (this.interviewStage === InterviewStage.None) { // do a full interview starting with the protocol info this.driver.controllerLog.logNode(this.id, `new node, doing a full interview...`); this.emit("interview started", this); await this.queryProtocolInfo(); } if (!this.isControllerNode) { if ((this.isListening || this.isFrequentListening) && this.status !== NodeStatus.Alive) { // Ping non-sleeping nodes to determine their status if (!await this.ping()) { // Not alive, abort the interview return false; } } if (this.interviewStage === InterviewStage.ProtocolInfo) { if (!(await tryInterviewStage(() => this.interviewNodeInfo()))) { return false; } } // At this point the basic interview of new nodes is done. Start here when re-interviewing known nodes // to get updated information about command classes if (this.interviewStage === InterviewStage.NodeInfo) { // Only advance the interview if it was completed, otherwise abort if (await this.interviewCCs()) { this.setInterviewStage(InterviewStage.CommandClasses); } else { return false; } } } if ((this.isControllerNode && this.interviewStage === InterviewStage.ProtocolInfo) || (!this.isControllerNode && this.interviewStage === InterviewStage.CommandClasses)) { // Load a config file for this node if it exists and overwrite the previously reported information await this.overwriteConfig(); } // Remember the state of the device config that is used for this node this.cachedDeviceConfigHash = await this.deviceConfig?.getHash(); this.setInterviewStage(InterviewStage.Complete); this.updateReadyMachine({ value: "INTERVIEW_DONE" }); // Tell listeners that the interview is completed // The driver will then send this node to sleep 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, getEnumMemberName(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", }); // The GetNodeProtocolInfoRequest needs to know the node ID to distinguish // between ZWLR and ZW classic. We store it on the driver's context, so it // can be retrieved when needed. this.driver.requestStorage.set(FunctionType.GetNodeProtocolInfo, { nodeId: this.id, }); const resp = await this.driver.sendMessage(new 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 DeviceClass(resp.basicDeviceClass, resp.genericDeviceClass, resp.specificDeviceClass); const logMessage = `received response for protocol info: basic device class: ${getEnumMemberName(BasicDeviceClass, this.deviceClass.basic)} generic device class: ${this.deviceClass.generic.label} specific device class: ${this.deviceClass.specific.label} node type: ${getEnumMemberName(NodeType, this.nodeType)} is always listening: ${this.isListening} is frequent listening: ${this.isFrequentListening} can route messages: ${this.isRouting} supports security: ${this.supportsSecurity} supports beaming: ${this.supportsBeaming} maximum data rate: ${this.maxDataRate} kbps protocol version: ${this.protocolVersion}`; this.driver.controllerLog.logNode(this.id, { message: logMessage, direction: "inbound", }); // Assume that sleeping nodes start asleep (unless we know it is awake) if (this.canSleep) { if (this.status === NodeStatus.Alive) { // If it was just included and is currently communicating with us, // then we didn't know yet that it can sleep. So we need to switch from alive/dead to awake/asleep this.markAsAwake(); } else if (this.status !== NodeStatus.Awake) { this.markAsAsleep(); } } this.setInterviewStage(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"]; // Enable route resolution and explorer frames if desired if (tryReallyHard) { api = api.withOptions({ transmitOptions: 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: ${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; } // If we incorrectly assumed a sleeping node to be awake, this step will fail. // In order to fail the interview, we retry here 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(`· ${getCCName(cc)}`); } this.driver.controllerLog.logNode(this.id, { message: logLines.join("\n"), direction: "inbound", }); this.updateNodeInfo(nodeInfo); break; } catch (e) { if (isZWaveError(e)) { if (attempts === 1 && this.canSleep && this.status !== NodeStatus.Asleep && e.code === ZWaveErrorCodes.Controller_CallbackNOK) { this.driver.controllerLog.logNode(this.id, `Querying the node info failed, the node is probably asleep. Retrying after wakeup...`, "error"); // We assumed the node to be awake, but it is not. this.markAsAsleep(); // Retry the query when the node wakes up continue; } if (e.code === ZWaveErrorCodes.Controller_ResponseNOK || e.code === ZWaveErrorCodes.Controller_CallbackNOK) { this.driver.controllerLog.logNode(this.id, `Querying the node info failed`, "error"); } throw e; } } } this.setInterviewStage(InterviewStage.NodeInfo); } async requestNodeInfo() { const resp = await this.driver.sendMessage(new RequestNodeInfoRequest({ nodeId: this.id })); if (resp instanceof RequestNodeInfoResponse && !resp.wasSent) { // TODO: handle this in SendThreadMachine throw new ZWaveError(`Querying the node info failed`, ZWaveErrorCodes.Controller_ResponseNOK); } else if (resp instanceof ApplicationUpdateRequestNodeInfoRequestFailed) { // TODO: handle this in SendThreadMachine throw new ZWaveError(`Querying the node info failed`, ZWaveErrorCodes.Controller_CallbackNOK); } else if (resp instanceof ApplicationUpdateRequestNodeInfoReceived) { const logLines = ["node info received", "supported CCs:"]; for (const cc of resp.nodeInformation.supportedCCs) { logLines.push(`· ${getCCName(cc)}`); } this.driver.controllerLog.logNode(this.id, { message: logLines.join("\n"), direction: "inbound", }); return resp.nodeInformation; } throw new ZWaveError(`Received unexpected response to RequestNodeInfoRequest`, 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); /** * @param force When this is `true`, the interview will be attempted even when the CC is not supported by the endpoint. */ const interviewEndpoint = async (endpoint, cc, force = false) => { let instance; try { if (force) { instance = CommandClass.createInstanceUnchecked(endpoint, cc); } else { instance = endpoint.createCCInstance(cc); } } catch (e) { if (isZWaveError(e) && e.code === ZWaveErrorCodes.CC_NotSupported) { // The CC is no longer supported. This can happen if the node tells us // something different in the Version interview than it did in its NIF return "continue"; } // we want to pass all other errors through throw e; } if (endpoint.isCCSecure(cc) && !this.driver.securityManager && !securityManager2) {