zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
925 lines • 360 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 { AssociationCC, ECDHProfiles, FLiRS2WakeUpTime, InclusionControllerCCComplete, InclusionControllerStatus, InclusionControllerStep, KEXFailType, KEXSchemes, ManufacturerSpecificCCValues, MultiChannelAssociationCC, Powerlevel, Security2CCKEXFail, Security2CCKEXGet, Security2CCKEXSet, Security2CCNetworkKeyGet, Security2CCNetworkKeyReport, Security2CCNetworkKeyVerify, Security2CCPublicKeyReport, Security2CCTransferEnd, Security2Command, SecurityCCNetworkKeySet, SecurityCCNonceGet, SecurityCCSchemeGet, SecurityCCSchemeInherit, VersionCCValues, VersionCommand, ZWaveProtocolCCAssignReturnRoute, ZWaveProtocolCCAssignReturnRoutePriority, ZWaveProtocolCCAssignSUCReturnRoute, ZWaveProtocolCCAssignSUCReturnRoutePriority, inclusionTimeouts, utils as ccUtils, } from "@zwave-js/cc";
import { BasicDeviceClass, CommandClasses, ControllerRole, ControllerStatus, EMPTY_ROUTE, LongRangeChannel, MAX_NODES, NODE_ID_BROADCAST, NODE_ID_BROADCAST_LR, NOT_KNOWN, NodeIDType, NodeType, ProtocolType, Protocols, RFRegion, RouteKind, SecurityClass, SecurityManager, SecurityManager2, TransmitStatus, UNKNOWN_STATE, ValueDB, ZWaveError, ZWaveErrorCodes, ZWaveLibraryTypes, authHomeIdFromDSK, averageRSSI, computePRK, deriveSharedECDHSecret, deriveTempKeys, dskFromString, dskToString, generateECDHKeyPair, getChipTypeAndVersion, getHighestSecurityClass, getLegalPowerlevelLR, getLegalPowerlevelMesh, indexDBsByNode, isEmptyRoute, isLongRangeNodeId, isValidDSK, isZWaveError, nwiHomeIdFromDSK, parseBitMask, sdkVersionGt, sdkVersionGte, sdkVersionLt, sdkVersionLte, securityClassIsS2, securityClassOrder, } from "@zwave-js/core";
import { BufferedNVMReader, NVM3, NVM3Adapter, NVM500, NVM500Adapter, migrateNVM, } from "@zwave-js/nvmedit";
import { FunctionType, } from "@zwave-js/serial";
import { AddNodeDSKToNetworkRequest, AddNodeStatus, AddNodeToNetworkRequest, AddNodeToNetworkRequestStatusReport, AddNodeType, ApplicationUpdateRequestNodeAdded, ApplicationUpdateRequestNodeInfoReceived, ApplicationUpdateRequestNodeRemoved, ApplicationUpdateRequestSUCIdChanged, ApplicationUpdateRequestSmartStartHomeIDReceived, ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived, AssignPriorityReturnRouteRequest, AssignPrioritySUCReturnRouteRequest, AssignReturnRouteRequest, AssignSUCReturnRouteRequest, AssignSUCReturnRouteRequestTransmitReport, DeleteReturnRouteRequest, DeleteSUCReturnRouteRequest, DeleteSUCReturnRouteRequestTransmitReport, EnableSmartStartListenRequest, ExtNVMReadLongBufferRequest, ExtNVMReadLongByteRequest, ExtNVMWriteLongBufferRequest, ExtNVMWriteLongByteRequest, ExtendedNVMOperationStatus, ExtendedNVMOperationsCloseRequest, ExtendedNVMOperationsCommand, ExtendedNVMOperationsOpenRequest, ExtendedNVMOperationsReadRequest, ExtendedNVMOperationsWriteRequest, FirmwareUpdateNVM_GetNewImageRequest, FirmwareUpdateNVM_InitRequest, FirmwareUpdateNVM_IsValidCRC16Request, FirmwareUpdateNVM_SetNewImageRequest, FirmwareUpdateNVM_UpdateCRC16Request, FirmwareUpdateNVM_WriteRequest, GetBackgroundRSSIRequest, GetControllerCapabilitiesRequest, GetControllerIdRequest, GetControllerVersionRequest, GetLongRangeChannelRequest, GetLongRangeNodesRequest, GetNVMIdRequest, GetPriorityRouteRequest, GetProtocolVersionRequest, GetRoutingInfoRequest, GetSUCNodeIdRequest, GetSerialApiCapabilitiesRequest, GetSerialApiInitDataRequest, HardResetRequest, IsFailedNodeRequest, LearnModeIntent, LearnModeStatus, NVMOperationStatus, NVMOperationsCloseRequest, NVMOperationsOpenRequest, NVMOperationsReadRequest, NVMOperationsWriteRequest, NodeNeighborUpdateStatus, RemoveFailedNodeRequest, RemoveFailedNodeResponse, RemoveFailedNodeStartFlags, RemoveFailedNodeStatus, RemoveNodeFromNetworkRequest, RemoveNodeFromNetworkRequestStatusReport, RemoveNodeStatus, RemoveNodeType, ReplaceFailedNodeRequest, ReplaceFailedNodeRequestStatusReport, ReplaceFailedNodeStartFlags, ReplaceFailedNodeStatus, RequestNodeNeighborUpdateRequest, SerialAPISetupCommand, SerialAPISetup_CommandUnsupportedResponse, SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest, SerialAPISetup_GetLongRangeMaximumTxPowerRequest, SerialAPISetup_GetMaximumPayloadSizeRequest, SerialAPISetup_GetPowerlevel16BitRequest, SerialAPISetup_GetPowerlevelRequest, SerialAPISetup_GetRFRegionRequest, SerialAPISetup_GetRegionInfoRequest, SerialAPISetup_GetSupportedCommandsRequest, SerialAPISetup_GetSupportedRegionsRequest, SerialAPISetup_SetLongRangeMaximumTxPowerRequest, SerialAPISetup_SetNodeIDTypeRequest, SerialAPISetup_SetPowerlevel16BitRequest, SerialAPISetup_SetPowerlevelRequest, SerialAPISetup_SetRFRegionRequest, SerialAPISetup_SetTXStatusReportRequest, SetApplicationNodeInformationRequest, SetLearnModeRequest, SetLongRangeChannelRequest, SetPriorityRouteRequest, SetRFReceiveModeRequest, SetSUCNodeIdRequest, SetSerialApiTimeoutsRequest, ShutdownRequest, StartWatchdogRequest, StopWatchdogRequest, computeNeighborDiscoveryTimeout, nvmSizeToBufferSize, } from "@zwave-js/serial/serialapi";
import { Bytes, Mixin, ObjectKeyMap, TypedEventTarget, areUint8ArraysEqual, cloneDeep, createThrowingMap, getEnumMemberName, getErrorMessage, noop, num2hex, pick, } from "@zwave-js/shared";
import { waitFor } from "@zwave-js/waddle";
import { distinct } from "alcalzone-shared/arrays";
import { wait } from "alcalzone-shared/async";
import { createDeferredPromise, } from "alcalzone-shared/deferred-promise";
import { isObject } from "alcalzone-shared/typeguards";
import { cacheKeyUtils, cacheKeys } from "../driver/NetworkCache.js";
import { TaskPriority } from "../driver/Task.js";
import { DeviceClass } from "../node/DeviceClass.js";
import { ZWaveNode } from "../node/Node.js";
import { VirtualNode } from "../node/VirtualNode.js";
import { InterviewStage, NodeStatus, } from "../node/_Types.js";
import { ControllerStatisticsHost, } from "./ControllerStatistics.js";
import { ZWaveFeature, minFeatureVersions } from "./Features.js";
import { downloadFirmwareUpdate, getAvailableFirmwareUpdatesBulk, } from "./FirmwareUpdateService.js";
import { ExclusionStrategy, InclusionState, InclusionStrategy, JoinNetworkResult, JoinNetworkStrategy, LeaveNetworkResult, ProvisioningEntryStatus, RemoveNodeReason, SecurityBootstrapFailure, } from "./Inclusion.js";
import { SerialNVMIO500, SerialNVMIO700 } from "./NVMIO.js";
import { determineNIF } from "./NodeInformationFrame.js";
import { getControllerProprietary, } from "./Proprietary.js";
import { createProxyInclusionMachine, } from "./ProxyInclusionMachine.js";
import { protocolVersionToSDKVersion } from "./ZWaveSDKVersions.js";
import { assertProvisioningEntry, isRebuildRoutesTask } from "./utils.js";
let ZWaveController = (() => {
let _classDecorators = [Mixin([ControllerStatisticsHost])];
let _classDescriptor;
let _classExtraInitializers = [];
let _classThis;
let _classSuper = TypedEventTarget;
var ZWaveController = 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);
ZWaveController = _classThis = _classDescriptor.value;
if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
__runInitializers(_classThis, _classExtraInitializers);
}
/** @internal */
constructor(driver) {
super();
this.driver = driver;
this._nodes = createThrowingMap((nodeId) => {
throw new ZWaveError(`Node ${nodeId} was not found!`, ZWaveErrorCodes.Controller_NodeNotFound, nodeId);
});
// register message handlers
driver.registerRequestHandler(FunctionType.SetLearnMode, this.handleLearnModeCallback.bind(this));
}
driver;
_type;
get type() {
return this._type;
}
_protocolVersion;
get protocolVersion() {
return this._protocolVersion;
}
_sdkVersion;
get sdkVersion() {
return this._sdkVersion;
}
_zwaveApiVersion;
get zwaveApiVersion() {
return this._zwaveApiVersion;
}
_zwaveChipType;
get zwaveChipType() {
return this._zwaveChipType;
}
_homeId;
/** A 32bit number identifying the current network */
get homeId() {
return this._homeId;
}
_ownNodeId;
/** The ID of the controller in the current network */
get ownNodeId() {
return this._ownNodeId;
}
_dsk;
/**
* The device specific key (DSK) of the controller in binary format.
*/
async getDSK() {
if (this._dsk == undefined) {
const { publicKey } = await this.driver
.getLearnModeAuthenticatedKeyPair();
this._dsk = publicKey.subarray(0, 16);
}
return this._dsk;
}
/** @deprecated Use {@link role} instead */
get isPrimary() {
switch (this.role) {
case NOT_KNOWN:
return NOT_KNOWN;
case ControllerRole.Primary:
return true;
default:
return false;
}
}
// This seems odd, but isPrimary comes from the Serial API init data command,
// while isSecondary comes from the GetControllerCapabilities command. They don't really do what their name implies
// and sometimes contradict each other...
_isPrimary;
_isSecondary;
_isUsingHomeIdFromOtherNetwork;
/** @deprecated Use {@link role} instead */
get isUsingHomeIdFromOtherNetwork() {
return this._isUsingHomeIdFromOtherNetwork;
}
_isSISPresent;
get isSISPresent() {
return this._isSISPresent;
}
_wasRealPrimary;
/** @deprecated Use {@link role} instead */
get wasRealPrimary() {
return this._wasRealPrimary;
}
_isSIS;
get isSIS() {
return this._isSIS;
}
_isSUC;
get isSUC() {
return this._isSUC;
}
_noNodesIncluded;
_nodeType;
get nodeType() {
return this._nodeType;
}
/** Checks if the SDK version is greater than the given one */
sdkVersionGt(version) {
return sdkVersionGt(this._sdkVersion, version);
}
/** Checks if the SDK version is greater than or equal to the given one */
sdkVersionGte(version) {
return sdkVersionGte(this._sdkVersion, version);
}
/** Checks if the SDK version is lower than the given one */
sdkVersionLt(version) {
return sdkVersionLt(this._sdkVersion, version);
}
/** Checks if the SDK version is lower than or equal to the given one */
sdkVersionLte(version) {
return sdkVersionLte(this._sdkVersion, version);
}
_manufacturerId;
get manufacturerId() {
return this._manufacturerId;
}
_productType;
get productType() {
return this._productType;
}
_productId;
get productId() {
return this._productId;
}
_firmwareVersion;
get firmwareVersion() {
return this._firmwareVersion;
}
_supportedFunctionTypes;
get supportedFunctionTypes() {
return this._supportedFunctionTypes;
}
_status = ControllerStatus.Ready;
/**
* Which status the controller is believed to be in
*/
get status() {
return this._status;
}
/**
* @internal
*/
setStatus(newStatus) {
// Ignore duplicate events
if (newStatus === this._status)
return;
const oldStatus = this._status;
this._status = newStatus;
if (newStatus === ControllerStatus.Jammed) {
this.driver.controllerLog.print("The controller is jammed", "warn");
}
else if (newStatus === ControllerStatus.Unresponsive) {
this.driver.controllerLog.print("The controller is unresponsive", "warn");
}
else if (newStatus === ControllerStatus.Ready) {
if (oldStatus === ControllerStatus.Jammed) {
this.driver.controllerLog.print("The controller is no longer jammed", "warn");
}
else if (oldStatus === ControllerStatus.Unresponsive) {
this.driver.controllerLog.print("The controller is no longer unresponsive", "warn");
}
else {
this.driver.controllerLog.print("The controller is ready");
}
}
this.emit("status changed", newStatus);
}
/**
* Checks if a given Z-Wave function type is supported by this controller.
* Returns `NOT_KNOWN`/`undefined` if this information isn't known yet.
*/
isFunctionSupported(functionType) {
if (!this._supportedFunctionTypes)
return NOT_KNOWN;
return this._supportedFunctionTypes.includes(functionType);
}
_supportedSerialAPISetupCommands;
get supportedSerialAPISetupCommands() {
return this._supportedSerialAPISetupCommands;
}
/**
* Checks if a given Serial API setup command is supported by this controller.
* Returns `NOT_KNOWN`/`undefined` if this information isn't known yet.
*/
isSerialAPISetupCommandSupported(command) {
if (!this._supportedSerialAPISetupCommands)
return NOT_KNOWN;
return this._supportedSerialAPISetupCommands.includes(command);
}
/**
* Tests if the controller supports a certain feature.
* Returns `undefined` if this information isn't known yet.
*/
supportsFeature(feature) {
switch (feature) {
case ZWaveFeature.SmartStart:
return this.sdkVersionGte(minFeatureVersions[feature]);
}
}
/** Throws if the controller does not support a certain feature */
assertFeature(feature) {
if (!this.supportsFeature(feature)) {
throw new ZWaveError(`The controller does not support the ${getEnumMemberName(ZWaveFeature, feature)} feature`, ZWaveErrorCodes.Controller_NotSupported);
}
}
_sucNodeId;
get sucNodeId() {
return this._sucNodeId;
}
_supportsTimers;
get supportsTimers() {
return this._supportsTimers;
}
_supportedRegions;
/** Which RF regions are supported by the controller, including information about them */
get supportedRegions() {
return this._supportedRegions;
}
_rfRegion;
/** Which RF region the controller is currently set to, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setRFRegion}. */
get rfRegion() {
return this._rfRegion;
}
_txPower;
/** The transmit power used for Z-Wave mesh, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setPowerlevel}. */
get txPower() {
return this._txPower;
}
_powerlevelCalibration;
/** The calibration value for the transmit power, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setPowerlevel}. */
get powerlevelCalibration() {
return this._powerlevelCalibration;
}
_supportsLongRange;
/** Whether the controller supports the Z-Wave Long Range protocol */
get supportsLongRange() {
return this._supportsLongRange;
}
_maxLongRangePowerlevel;
/** The maximum powerlevel to use for Z-Wave Long Range, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setMaxLongRangePowerlevel}. */
get maxLongRangePowerlevel() {
return this._maxLongRangePowerlevel;
}
_longRangeChannel;
/** The channel to use for Z-Wave Long Range, or `undefined` if it could not be determined (yet). This value is cached and can be changed through {@link setLongRangeChannel}. */
get longRangeChannel() {
return this._longRangeChannel;
}
_supportsLongRangeAutoChannelSelection;
/** Whether automatic LR channel selection is supported, or `undefined` if it could not be determined (yet). */
get supportsLongRangeAutoChannelSelection() {
return this._supportsLongRangeAutoChannelSelection;
}
_maxPayloadSize;
/** The maximum payload size that can be transmitted with a Z-Wave explorer frame */
get maxPayloadSize() {
return this._maxPayloadSize;
}
_maxPayloadSizeLR;
/** The maximum payload size that can be transmitted with a Z-Wave Long Range frame */
get maxPayloadSizeLR() {
return this._maxPayloadSizeLR;
}
_nodes;
/** A dictionary of the nodes connected to this controller */
get nodes() {
return this._nodes;
}
_nodeIdType = NodeIDType.Short;
/** Whether the controller is configured to use 8 or 16 bit node IDs */
get nodeIdType() {
return this._nodeIdType;
}
/** @internal */
set nodeIdType(value) {
this._nodeIdType = value;
}
/** Returns the node with the given DSK */
getNodeByDSK(dsk) {
try {
if (typeof dsk === "string")
dsk = dskFromString(dsk);
}
catch (e) {
// Return undefined if the DSK is invalid
if (isZWaveError(e) && e.code === ZWaveErrorCodes.Argument_Invalid) {
return undefined;
}
throw e;
}
for (const node of this._nodes.values()) {
if (node.dsk && Bytes.view(node.dsk).equals(dsk))
return node;
}
}
/** Returns the controller node's value DB */
get valueDB() {
return this._nodes.get(this._ownNodeId).valueDB;
}
/** @internal Which associations are currently configured */
get associations() {
return (this.driver.cacheGet(cacheKeys.controller.associations(1)) ?? []);
}
/** @internal */
set associations(value) {
this.driver.cacheSet(cacheKeys.controller.associations(1), value);
}
_powerlevel;
/**
* @internal
* Remembers which powerlevel was set by another node.
*/
get powerlevel() {
return this._powerlevel ?? {
powerlevel: Powerlevel["Normal Power"],
until: new Date(),
};
}
/** @internal */
set powerlevel(value) {
this._powerlevel = value;
}
/** The role of the controller on the network */
get role() {
if (this._wasRealPrimary)
return ControllerRole.Primary;
// Ideally we'd rely on wasRealPrimary, but there are some older controllers out there where this flag isn't set.
if (this._isPrimary && this._isSIS && this._isSecondary === false) {
return ControllerRole.Primary;
}
switch (this._isSecondary) {
case true:
return ControllerRole.Secondary;
case false:
return ControllerRole.Inclusion;
default:
return NOT_KNOWN;
}
}
/** Returns whether learn mode may be enabled on this controller */
get isLearnModePermitted() {
// The primary controller may only enter learn mode, if hasn't included nodes yet
if (this.role === ControllerRole.Primary) {
return !!this._noNodesIncluded;
}
else {
// Secondary controllers may only enter learn mode if they are not the SUC
return this._isSUC === false;
}
}
/**
* @internal
* Remembers the indicator values set by another node
*/
indicatorValues = new Map();
/** Returns whether the routes are currently being rebuilt for one or more nodes. */
get isRebuildingRoutes() {
return !!this.driver.scheduler.findTask(isRebuildRoutesTask);
}
/**
* Returns a reference to the (virtual) broadcast node, which allows sending commands to all nodes.
* This automatically groups nodes by security class, ignores nodes that cannot be controlled via multicast/broadcast, and will fall back to multicast(s) if necessary.
*/
getBroadcastNode() {
return new VirtualNode(NODE_ID_BROADCAST, this.driver, this.nodes.values());
}
/**
* Returns a reference to the (virtual) broadcast node for Z-Wave Long Range, which allows sending commands to all LR nodes.
* This automatically groups nodes by security class, ignores nodes that cannot be controlled via multicast/broadcast, and will fall back to multicast(s) if necessary.
*/
getBroadcastNodeLR() {
return new VirtualNode(NODE_ID_BROADCAST_LR, this.driver, this.nodes.values());
}
/**
* Creates a virtual node that can be used to send one or more multicast commands to several nodes.
* This automatically groups nodes by security class and ignores nodes that cannot be controlled via multicast.
*/
getMulticastGroup(nodeIDs) {
if (nodeIDs.length === 0) {
throw new ZWaveError("Cannot create an empty multicast group", ZWaveErrorCodes.Argument_Invalid);
}
const nodes = nodeIDs.map((id) => this._nodes.getOrThrow(id));
return new VirtualNode(undefined, this.driver, nodes);
}
/** @internal */
get provisioningList() {
return (this.driver.cacheGet(cacheKeys.controller.provisioningList) ?? []);
}
set provisioningList(value) {
this.driver.cacheSet(cacheKeys.controller.provisioningList, value);
}
/** @internal Tracks the number of failed SmartStart inclusion attempts per DSK */
_smartStartFailedAttempts = new Map();
/** @internal Resets the SmartStart inclusion failure counter for the given DSK */
resetSmartStartFailureCount(dsk) {
this._smartStartFailedAttempts.delete(dsk);
}
/** Adds the given entry (DSK and security classes) to the controller's SmartStart provisioning list or replaces an existing entry */
provisionSmartStartNode(entry) {
// Make sure the controller supports SmartStart
this.assertFeature(ZWaveFeature.SmartStart);
// And that the entry contains valid data
assertProvisioningEntry(entry);
const provisioningList = [...this.provisioningList];
const index = provisioningList.findIndex((e) => e.dsk === entry.dsk);
if (index === -1) {
provisioningList.push(entry);
}
else {
provisioningList[index] = entry;
}
this.provisioningList = provisioningList;
// Reset the failure counter when a provisioning entry is modified
this.resetSmartStartFailureCount(entry.dsk);
this.autoProvisionSmartStart();
}
/**
* Removes the given DSK or node ID from the controller's SmartStart provisioning list.
*
* **Note:** If this entry corresponds to an included node, it will **NOT** be excluded
*/
unprovisionSmartStartNode(dskOrNodeId) {
const provisioningList = [...this.provisioningList];
const entry = this.getProvisioningEntryInternal(dskOrNodeId);
if (!entry)
return;
const index = provisioningList.indexOf(entry);
if (index >= 0) {
provisioningList.splice(index, 1);
this.provisioningList = provisioningList;
// Reset the failure counter when a provisioning entry is removed
this.resetSmartStartFailureCount(entry.dsk);
this.autoProvisionSmartStart();
}
}
getProvisioningEntryInternal(dskOrNodeId) {
if (typeof dskOrNodeId === "string") {
return this.provisioningList.find((e) => e.dsk === dskOrNodeId);
}
else {
// The provisioning list may or may not contain the node ID for an entry, even if the node is already included.
let ret = this.provisioningList.find((e) => "nodeId" in e && e.nodeId === dskOrNodeId);
if (!ret) {
// Try to get the DSK from the node instance
const dsk = this.nodes.get(dskOrNodeId)?.dsk;
if (dsk) {
ret = this.provisioningList.find((e) => e.dsk === dskToString(dsk));
}
}
return ret;
}
}
/**
* Returns the entry for the given DSK or node ID from the controller's SmartStart provisioning list.
*/
getProvisioningEntry(dskOrNodeId) {
const entry = this.getProvisioningEntryInternal(dskOrNodeId);
// Try to look up the node ID for this entry
if (entry) {
const ret = {
...entry,
};
const node = typeof dskOrNodeId === "string"
? this.getNodeByDSK(dskOrNodeId)
: this.nodes.get(dskOrNodeId);
if (node)
ret.nodeId = node.id;
return ret;
}
}
/**
* Returns all entries from the controller's SmartStart provisioning list.
*/
getProvisioningEntries() {
// Determine which DSKs belong to which node IDs
const dskNodeMap = new Map();
for (const node of this.nodes.values()) {
if (node.dsk)
dskNodeMap.set(dskToString(node.dsk), node.id);
}
// Make copies so no one can modify the internal list (except for user info)
return this.provisioningList.map((e) => {
const { dsk, securityClasses, nodeId, ...rest } = e;
return {
dsk,
securityClasses: [...securityClasses],
...(dskNodeMap.has(dsk)
? { nodeId: dskNodeMap.get(dsk) }
: {}),
...rest,
};
});
}
/** Returns whether the SmartStart provisioning list contains active entries that have not been included yet */
hasPlannedProvisioningEntries() {
return this.provisioningList.some((e) => (e.status == undefined
|| e.status === ProvisioningEntryStatus.Active)
&& !this.getNodeByDSK(e.dsk));
}
/**
* @internal
* Automatically starts smart start inclusion if there are nodes pending inclusion.
*/
autoProvisionSmartStart() {
// Make sure the controller supports SmartStart
if (!this.supportsFeature(ZWaveFeature.SmartStart))
return;
if (this.hasPlannedProvisioningEntries()) {
// SmartStart should be enabled
void this.enableSmartStart().catch(noop);
}
else {
// SmartStart should be disabled
void this.disableSmartStart().catch(noop);
}
}
/**
* @internal
* Queries the controller / serial API capabilities.
* Returns a list of Z-Wave classic node IDs that are currently in the network.
*/
async queryCapabilities() {
// Figure out what the serial API can do
this.driver.controllerLog.print(`querying Serial API capabilities...`);
const apiCaps = await this.driver.sendMessage(new GetSerialApiCapabilitiesRequest(), {
supportCheck: false,
});
this._firmwareVersion = apiCaps.firmwareVersion;
this._manufacturerId = apiCaps.manufacturerId;
this._productType = apiCaps.productType;
this._productId = apiCaps.productId;
this._supportedFunctionTypes = apiCaps.supportedFunctionTypes;
this.driver.controllerLog.print(`received API capabilities:
firmware version: ${this._firmwareVersion}
manufacturer ID: ${num2hex(this._manufacturerId)}
product type: ${num2hex(this._productType)}
product ID: ${num2hex(this._productId)}
supported functions: ${this._supportedFunctionTypes
.map((fn) => `\n · ${FunctionType[fn]} (${num2hex(fn)})`)
.join("")}`);
// Request additional information about the controller/Z-Wave chip
const initData = await this.getSerialApiInitData();
// Get basic controller version info
this.driver.controllerLog.print(`querying version info...`);
const version = await this.driver.sendMessage(new GetControllerVersionRequest(), {
supportCheck: false,
});
this._protocolVersion = version.libraryVersion;
this._type = version.controllerType;
this.driver.controllerLog.print(`received version info:
controller type: ${getEnumMemberName(ZWaveLibraryTypes, this._type)}
library version: ${this._protocolVersion}`);
// If supported, get more fine-grained version info
if (this.isFunctionSupported(FunctionType.GetProtocolVersion)) {
this.driver.controllerLog.print(`querying protocol version info...`);
const protocol = await this.driver.sendMessage(new GetProtocolVersionRequest());
this._protocolVersion = protocol.protocolVersion;
let message = `received protocol version info:
protocol type: ${getEnumMemberName(ProtocolType, protocol.protocolType)}
protocol version: ${protocol.protocolVersion}`;
if (protocol.applicationFrameworkBuildNumber) {
message += `
appl. framework build no.: ${protocol.applicationFrameworkBuildNumber}`;
}
if (protocol.gitCommitHash) {
message += `
git commit hash: ${protocol.gitCommitHash}`;
}
this.driver.controllerLog.print(message);
}
// The SDK version cannot be queried directly, but we can deduce it from the protocol version
this._sdkVersion = protocolVersionToSDKVersion(this._protocolVersion);
// find out what the controller can do
await this.getControllerCapabilities();
// If the serial API can be configured, figure out which sub commands are supported
// This MUST be done after querying the SDK version due to a bug in some 7.xx firmwares, which incorrectly encode the bitmask
if (this.isFunctionSupported(FunctionType.SerialAPISetup)) {
this.driver.controllerLog.print(`querying serial API setup capabilities...`);
const setupCaps = await this.driver.sendMessage(new SerialAPISetup_GetSupportedCommandsRequest());
this._supportedSerialAPISetupCommands = setupCaps.supportedCommands;
this.driver.controllerLog.print(`supported serial API setup commands:${this._supportedSerialAPISetupCommands
.map((cmd) => `\n· ${getEnumMemberName(SerialAPISetupCommand, cmd)}`)
.join("")}`);
}
else {
this._supportedSerialAPISetupCommands = [];
}
// Figure out the maximum payload size for outgoing commands
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.GetMaximumPayloadSize)) {
this.driver.controllerLog.print(`querying max. payload size...`);
this._maxPayloadSize = await this.getMaxPayloadSize();
this.driver.controllerLog.print(`maximum payload size: ${this._maxPayloadSize} bytes`);
}
// On older controllers with soft-reset disabled, supportsLongRange is not automatically reported by the controller
// so we should set it manually
if (!this.isLongRangeCapable()) {
this._supportsLongRange = false;
this._supportsLongRangeAutoChannelSelection = false;
}
this.driver.controllerLog.print(`supported Z-Wave features: ${Object.keys(ZWaveFeature)
.filter((k) => /^\d+$/.test(k))
.map((k) => parseInt(k))
.filter((feat) => this.supportsFeature(feat))
.map((feat) => `\n · ${getEnumMemberName(ZWaveFeature, feat)}`)
.join("")}`);
return {
nodeIds: initData.nodeIds,
};
}
/**
* @internal
* Queries the controller's capabilities in regards to Z-Wave Long Range.
* Returns the list of Long Range node IDs
*/
async queryLongRangeCapabilities() {
this.driver.controllerLog.print(`querying Z-Wave Long Range capabilities...`);
// Fetch the list of Long Range nodes
const lrNodeIds = await this.getLongRangeNodes();
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.GetLongRangeMaximumPayloadSize)) {
this._maxPayloadSizeLR = await this.getMaxPayloadSizeLongRange();
}
this.driver.controllerLog.print(`received Z-Wave Long Range capabilities:
max. payload size: ${this._maxPayloadSizeLR} bytes
nodes: ${lrNodeIds.join(", ")}`);
return {
lrNodeIds,
};
}
isLongRangeCapable() {
// Z-Wave Long Range is supported if the controller supports changing the node ID type to 16 bit
// FIXME: Consider using the ZWaveFeature enum for this instead
return this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.SetNodeIDType);
}
/**
* Helper function to determine whether the controller is capable of EU Long Range,
* possibly without advertising it
*/
isEULongRangeCapable() {
// EU Long Range was added in SDK 7.22 for 800 series chips
// 7.22.1 adds support for querying the supported regions, so the following
// is really only necessary for 7.22.0.
//
// However, there seem to be cases where the new command is missing from
// later SDK versions too:
// https://github.com/zwave-js/zwave-js/issues/8174
return (this.isLongRangeCapable()
&& typeof this._zwaveChipType === "string"
&& getChipTypeAndVersion(this._zwaveChipType)?.type === 8
&& this.sdkVersionGte("7.22"));
}
/** Tries to determine the LR capable replacement of the given region. If none is found, the given region is returned. */
tryGetLRCapableRegion(region) {
if (this._supportedRegions) {
// If the region supports LR, use it
if (this._supportedRegions.get(region)?.supportsLongRange) {
return region;
}
// Find a possible LR capable superset for this region
for (const info of this._supportedRegions.values()) {
if (info.supportsLongRange && info.includesRegion === region) {
return info.region;
}
}
}
// US_LR is the first supported LR region, so if the controller supports LR, US_LR is supported
if (region === RFRegion.USA && this.isLongRangeCapable()) {
return RFRegion["USA (Long Range)"];
}
if (region === RFRegion.Europe && this.isEULongRangeCapable()) {
return RFRegion["Europe (Long Range)"];
}
return region;
}
/**
* @internal
* Queries the region and powerlevel settings and configures them if necessary
*/
async queryAndConfigureRF() {
// Figure out which regions are supported
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.GetSupportedRegions)) {
this.driver.controllerLog.print(`Querying supported RF regions and their information...`);
const supportedRegions = await this.querySupportedRFRegions().catch(() => []);
this._supportedRegions = new Map();
for (const region of supportedRegions) {
try {
const info = await this.queryRFRegionInfo(region);
if (info.region === RFRegion.Unknown)
continue;
this._supportedRegions.set(region, info);
}
catch {
continue;
}
}
this.driver.controllerLog.print(`supported regions:${[...this._supportedRegions.values()]
.map((info) => {
let ret = `\n· ${getEnumMemberName(RFRegion, info.region)}`;
if (info.includesRegion != undefined) {
ret += ` · superset of ${getEnumMemberName(RFRegion, info.includesRegion)}`;
}
if (info.supportsLongRange) {
ret += " · ZWLR";
if (!info.supportsZWave) {
ret += " only";
}
}
return ret;
})
.join("")}`);
}
// Check the current RF settings
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.GetRFRegion)) {
this.driver.controllerLog.print(`Querying configured RF region...`);
const resp = await this.getRFRegion().catch(() => undefined);
if (resp != undefined) {
this.driver.controllerLog.print(`The controller is using RF region ${getEnumMemberName(RFRegion, resp)}`);
}
else {
this.driver.controllerLog.print(`Querying the RF region failed!`, "warn");
}
}
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.GetPowerlevel)) {
this.driver.controllerLog.print(`Querying configured powerlevel...`);
const resp = await this.getPowerlevel().catch(() => undefined);
if (resp != undefined) {
this.driver.controllerLog.print(`The powerlevel is ${resp.powerlevel} dBm (${resp.measured0dBm} dBm calibration)`);
}
else {
this.driver.controllerLog.print(`Querying the powerlevel failed!`, "warn");
}
}
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.GetLongRangeMaximumTxPower)) {
this.driver.controllerLog.print(`Querying configured max. Long Range powerlevel...`);
const resp = await this.getMaxLongRangePowerlevel().catch(() => undefined);
if (resp != undefined) {
this.driver.controllerLog.print(`The max. LR powerlevel is ${resp.toFixed(1)} dBm`);
}
else {
this.driver.controllerLog.print(`Querying the max. Long Range powerlevel failed!`, "warn");
}
}
// Now make any changes that are necessary
let desiredRFRegion;
// If the user has set a region in the options, use that
if (this.driver.options.rf?.region != undefined) {
desiredRFRegion = this.driver.options.rf.region;
}
// Unless preferring LR regions is disabled, try to find a suitable replacement region
if (this.driver.options.rf?.preferLRRegion !== false) {
desiredRFRegion ??= this.rfRegion;
if (desiredRFRegion != undefined) {
desiredRFRegion = this.tryGetLRCapableRegion(desiredRFRegion);
}
}
if (this.isSerialAPISetupCommandSupported(SerialAPISetupCommand.SetRFRegion)
&& desiredRFRegion != undefined
&& this.rfRegion != desiredRFRegion) {
this.driver.controllerLog.print(`Current RF region (${getEnumMemberName(RFRegion, this.rfRegion ?? RFRegion.Unknown)}) differs from desired region (${getEnumMemberName(RFRegion, desiredRFRegion)}), configuring it...`);
// To know if the region is actually different, also consider the LR capable version of it
const isRegionActuallyDifferent = this.tryGetLRCapableRegion(this.rfRegion ?? RFRegion.Unknown)
!== this.tryGetLRCapableRegion(desiredRFRegion);
const resp = await this.setRFRegionInternal(desiredRFRegion,
// Do not soft reset here, we'll do it later
false).catch((e) => e.message);
if (resp === true) {
this.driver.controllerLog.print(`Changed RF region to ${getEnumMemberName(RFRegion, desiredRFRegion)}`);
}
else {
this.driver.controllerLog.print(`Changing the RF region failed!${resp ? ` Reason: ${resp}` : ""}`, "warn");
}
// Automatically configure powerlevels if desired
// and the region did actually change.
if (resp === true && isRegionActuallyDifferent) {
await this.applyLegalPowerlevelLimits(desiredRFRegion, this.driver.options.rf?.txPower?.powerlevel === "auto", this.driver.options.rf?.maxLongRangePowerlevel === "auto");
}
}
// When NOT in auto-powerlevel mode, set the desired powerlevels
let desiredTXPowerlevelMesh;
let desiredTXPowerlevelLR;
if (typeof this.driver.options.rf?.txPower?.powerlevel === "number") {
desiredTXPowerlevelMesh = this.driver.options.rf.txPower.powerlevel;
}
if (typeof this.driver.options.rf?.maxLongRangePowerlevel === "number") {
desiredTXPowerlevelLR =
this.driver.options.rf.maxLongRangePowerlevel;
}
if (desiredTXPowerlevelMesh != undefined) {
await this.applyDesiredPowerlevelMesh(desiredTXPowerlevelMesh);
}
if (desiredTXPowerlevelLR != undefined) {
await this.applyDesiredPowerlevelLR(desiredTXPowerlevelLR);
}
// In auto-powerlevel, we check for a possible misconfiguration of the powerlevels, e.g. because of wrong defaults in the firmware.
// The values being known implies that setting the powerlevel is supported.
if (this.driver.options.rf?.txPower?.powerlevel === "auto"
&& this._rfRegion != undefined
&& this._txPower === 20
&& this._maxLongRangePowerlevel === 20) {
// This is the default for 7.23.x firmwares, but it is not legal in the EU or the US
const legalPowerlevelMesh = getLegalPowerlevelMesh(this._rfRegion);
const legalPowerlevelLR = getLegalPowerlevelLR(this._rfRegion);
if (legalPowerlevelMesh != undefined
|| legalPowerlevelLR != undefined) {
this.driver.controllerLog.print(`The controller is set to incorrect powerlevel defaults, correcting them...`);
}
if (legalPowerlevelMesh != undefined) {
await this.setPowerlevel(legalPowerlevelMesh, this._powerlevelCalibration ?? 0).catch(noop);
}
if (legalPowerlevelLR != undefined) {
await this.setMaxLongRangePowerlevel(legalPowerlevelLR)
.catch(noop);
}
}
// Check and possibly update the Long Range channel settings
if (this.isFunctionSupported(FunctionType.GetLongRangeChannel)) {
this.driver.controllerLog.print(`Querying configured Long Range channel information...`);
const resp = await this.getLongRangeChannel().catch(() => undefined);
if (resp != undefined) {
this.driver.controllerLog.print(`received Z-Wave Long Range channel information:
channel: ${resp.channel != undefined
? getEnumMemberName(LongRangeChannel, resp.channel)
: "(unknown)"}
supports auto channel selection: ${resp.supportsAutoChannelSelection}
`);
}
else {
this.driver.controllerLog.print(`Querying the Long Range channel information failed!`, "warn");
}
}
else {
this._supportsLongRangeAutoChannelSelection = false;
}
if (this.isFunctionSupported(FunctionType.SetLongRangeChannel)
&& this.driver.options.rf?.longRangeChannel != undefined
&& this.longRangeChannel
!== this.driver.options.rf.longRangeChannel) {
const desired = this.driver.options.rf.longRangeChannel;
if (desired === LongRangeChannel.Auto
&& !this._supportsLongRangeAutoChannelSelection) {
this.driver.controllerLog.print(`Cannot set desired LR channel to Auto because the controller does not support it!`, "warn");
}
else {
this.driver.controllerLog.print(`Current LR channel ${this.longRangeChannel != undefined
? getEnumMemberName(LongRangeChannel, this.longRangeChannel)
: "(unknown)"} differs from desired channel ${getEnumMemberName(LongRangeChannel, desired)}, configuring it...`);
const resp = await this.setLongRangeChannel(desired)
.catch((e) => e.message);
if (resp === true) {
this.drive