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