zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
920 lines (919 loc) • 157 kB
JavaScript
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) {