UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

925 lines 360 kB
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; import { 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