inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
1,655 lines (1,525 loc) • 143 kB
text/typescript
import {
ECDHProfiles,
inclusionTimeouts,
KEXFailType,
KEXSchemes,
ManufacturerSpecificCCValues,
Security2CCKEXFail,
Security2CCKEXSet,
Security2CCNetworkKeyGet,
Security2CCNetworkKeyVerify,
Security2CCPublicKeyReport,
Security2CCTransferEnd,
utils as ccUtils,
VersionCCValues,
type AssociationAddress,
type AssociationGroup,
} from "@zwave-js/cc";
import {
authHomeIdFromDSK,
CommandClasses,
computePRK,
decodeX25519KeyDER,
deriveTempKeys,
dskFromString,
dskToString,
encodeX25519KeyDERSPKI,
Firmware,
indexDBsByNode,
isRecoverableZWaveError,
isTransmissionError,
isZWaveError,
NodeType,
NODE_ID_BROADCAST,
nwiHomeIdFromDSK,
ProtocolType,
RFRegion,
RSSI,
SecurityClass,
securityClassIsS2,
securityClassOrder,
ValueDB,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import { migrateNVM } from "@zwave-js/nvmedit";
import type { Message, SuccessIndicator } from "@zwave-js/serial";
import { FunctionType } from "@zwave-js/serial";
import {
createThrowingMap,
flatMap,
getEnumMemberName,
getErrorMessage,
Mixin,
num2hex,
padVersion,
pick,
ReadonlyObjectKeyMap,
ReadonlyThrowingMap,
ThrowingMap,
TypedEventEmitter,
} from "@zwave-js/shared";
import { distinct } from "alcalzone-shared/arrays";
import { wait } from "alcalzone-shared/async";
import {
createDeferredPromise,
DeferredPromise,
} from "alcalzone-shared/deferred-promise";
import { isObject } from "alcalzone-shared/typeguards";
import crypto from "crypto";
import semver from "semver";
import util from "util";
import type { Driver } from "../driver/Driver";
import { cacheKeys, cacheKeyUtils } from "../driver/NetworkCache";
import type { StatisticsEventCallbacks } from "../driver/Statistics";
import { DeviceClass } from "../node/DeviceClass";
import { ZWaveNode } from "../node/Node";
import { VirtualNode } from "../node/VirtualNode";
import { InterviewStage, LifelineRoutes, NodeStatus } from "../node/_Types";
import {
GetControllerCapabilitiesRequest,
GetControllerCapabilitiesResponse,
} from "../serialapi/capability/GetControllerCapabilitiesMessages";
import {
GetControllerVersionRequest,
GetControllerVersionResponse,
} from "../serialapi/capability/GetControllerVersionMessages";
import {
GetProtocolVersionRequest,
GetProtocolVersionResponse,
} from "../serialapi/capability/GetProtocolVersionMessages";
import {
GetSerialApiCapabilitiesRequest,
GetSerialApiCapabilitiesResponse,
} from "../serialapi/capability/GetSerialApiCapabilitiesMessages";
import {
GetSerialApiInitDataRequest,
GetSerialApiInitDataResponse,
} from "../serialapi/capability/GetSerialApiInitDataMessages";
import { HardResetRequest } from "../serialapi/capability/HardResetRequest";
import {
SerialAPISetupCommand,
SerialAPISetup_CommandUnsupportedResponse,
SerialAPISetup_GetLRMaximumPayloadSizeRequest,
SerialAPISetup_GetLRMaximumPayloadSizeResponse,
SerialAPISetup_GetMaximumPayloadSizeRequest,
SerialAPISetup_GetMaximumPayloadSizeResponse,
SerialAPISetup_GetPowerlevelRequest,
SerialAPISetup_GetPowerlevelResponse,
SerialAPISetup_GetRFRegionRequest,
SerialAPISetup_GetRFRegionResponse,
SerialAPISetup_GetSupportedCommandsRequest,
SerialAPISetup_GetSupportedCommandsResponse,
SerialAPISetup_SetNodeIDTypeRequest,
SerialAPISetup_SetNodeIDTypeResponse,
SerialAPISetup_SetPowerlevelRequest,
SerialAPISetup_SetPowerlevelResponse,
SerialAPISetup_SetRFRegionRequest,
SerialAPISetup_SetRFRegionResponse,
SerialAPISetup_SetTXStatusReportRequest,
SerialAPISetup_SetTXStatusReportResponse,
} from "../serialapi/capability/SerialAPISetupMessages";
import { SetApplicationNodeInformationRequest } from "../serialapi/capability/SetApplicationNodeInformationRequest";
import {
GetControllerIdRequest,
GetControllerIdResponse,
} from "../serialapi/memory/GetControllerIdMessages";
import {
GetBackgroundRSSIRequest,
GetBackgroundRSSIResponse,
} from "../serialapi/misc/GetBackgroundRSSIMessages";
import {
SetRFReceiveModeRequest,
SetRFReceiveModeResponse,
} from "../serialapi/misc/SetRFReceiveModeMessages";
import {
SetSerialApiTimeoutsRequest,
SetSerialApiTimeoutsResponse,
} from "../serialapi/misc/SetSerialApiTimeoutsMessages";
import {
AddNodeDSKToNetworkRequest,
AddNodeStatus,
AddNodeToNetworkRequest,
AddNodeToNetworkRequestStatusReport,
AddNodeType,
computeNeighborDiscoveryTimeout,
EnableSmartStartListenRequest,
} from "../serialapi/network-mgmt/AddNodeToNetworkRequest";
import { AssignReturnRouteRequest } from "../serialapi/network-mgmt/AssignReturnRouteMessages";
import { AssignSUCReturnRouteRequest } from "../serialapi/network-mgmt/AssignSUCReturnRouteMessages";
import { DeleteReturnRouteRequest } from "../serialapi/network-mgmt/DeleteReturnRouteMessages";
import { DeleteSUCReturnRouteRequest } from "../serialapi/network-mgmt/DeleteSUCReturnRouteMessages";
import {
GetRoutingInfoRequest,
GetRoutingInfoResponse,
} from "../serialapi/network-mgmt/GetRoutingInfoMessages";
import {
GetSUCNodeIdRequest,
GetSUCNodeIdResponse,
} from "../serialapi/network-mgmt/GetSUCNodeIdMessages";
import {
IsFailedNodeRequest,
IsFailedNodeResponse,
} from "../serialapi/network-mgmt/IsFailedNodeMessages";
import {
RemoveFailedNodeRequest,
RemoveFailedNodeResponse,
RemoveFailedNodeStartFlags,
RemoveFailedNodeStatus,
type RemoveFailedNodeRequestStatusReport,
} from "../serialapi/network-mgmt/RemoveFailedNodeMessages";
import {
RemoveNodeFromNetworkRequest,
RemoveNodeFromNetworkRequestStatusReport,
RemoveNodeStatus,
RemoveNodeType,
} from "../serialapi/network-mgmt/RemoveNodeFromNetworkRequest";
import {
ReplaceFailedNodeRequest,
ReplaceFailedNodeRequestStatusReport,
ReplaceFailedNodeResponse,
ReplaceFailedNodeStartFlags,
ReplaceFailedNodeStatus,
} from "../serialapi/network-mgmt/ReplaceFailedNodeRequest";
import {
NodeNeighborUpdateStatus,
RequestNodeNeighborUpdateReport,
RequestNodeNeighborUpdateRequest,
} from "../serialapi/network-mgmt/RequestNodeNeighborUpdateMessages";
import { SetSUCNodeIdRequest } from "../serialapi/network-mgmt/SetSUCNodeIDMessages";
import {
ExtNVMReadLongBufferRequest,
ExtNVMReadLongBufferResponse,
} from "../serialapi/nvm/ExtNVMReadLongBufferMessages";
import {
ExtNVMReadLongByteRequest,
ExtNVMReadLongByteResponse,
} from "../serialapi/nvm/ExtNVMReadLongByteMessages";
import {
ExtNVMWriteLongBufferRequest,
ExtNVMWriteLongBufferResponse,
} from "../serialapi/nvm/ExtNVMWriteLongBufferMessages";
import {
ExtNVMWriteLongByteRequest,
ExtNVMWriteLongByteResponse,
} from "../serialapi/nvm/ExtNVMWriteLongByteMessages";
import {
GetNVMIdRequest,
GetNVMIdResponse,
NVMId,
nvmSizeToBufferSize,
} from "../serialapi/nvm/GetNVMIdMessages";
import {
NVMOperationsCloseRequest,
NVMOperationsOpenRequest,
NVMOperationsReadRequest,
NVMOperationsResponse,
NVMOperationStatus,
NVMOperationsWriteRequest,
} from "../serialapi/nvm/NVMOperationsMessages";
import {
NodeIDType,
ZWaveApiVersion,
ZWaveLibraryTypes,
} from "../serialapi/_Types";
import {
ControllerStatistics,
ControllerStatisticsHost,
} from "./ControllerStatistics";
import { minFeatureVersions, ZWaveFeature } from "./Features";
import {
downloadFirmwareUpdate,
getAvailableFirmwareUpdates,
} from "./FirmwareUpdateService";
import {
ExclusionOptions,
ExclusionStrategy,
FoundNode,
InclusionOptions,
InclusionOptionsInternal,
InclusionResult,
InclusionState,
InclusionStrategy,
InclusionUserCallbacks,
PlannedProvisioningEntry,
ProvisioningEntryStatus,
ReplaceNodeOptions,
SmartStartProvisioningEntry,
} from "./Inclusion";
import { determineNIF } from "./NodeInformationFrame";
import { assertProvisioningEntry } from "./utils";
import type { UnknownZWaveChipType } from "./ZWaveChipTypes";
import { protocolVersionToSDKVersion } from "./ZWaveSDKVersions";
import type {
FirmwareUpdateFileInfo,
FirmwareUpdateInfo,
GetFirmwareUpdatesOptions,
HealNodeStatus,
SDKVersion,
} from "./_Types";
// Strongly type the event emitter events
interface ControllerEventCallbacks
extends StatisticsEventCallbacks<ControllerStatistics> {
"inclusion failed": () => void;
"exclusion failed": () => void;
"inclusion started": (secure: boolean, strategy: InclusionStrategy) => void;
"exclusion started": () => void;
"inclusion stopped": () => void;
"exclusion stopped": () => void;
"node found": (node: FoundNode) => void;
"node added": (node: ZWaveNode, result: InclusionResult) => void;
"node removed": (node: ZWaveNode, replaced: boolean) => void;
"heal network progress": (
progress: ReadonlyMap<number, HealNodeStatus>,
) => void;
"heal network done": (result: ReadonlyMap<number, HealNodeStatus>) => void;
}
export type ControllerEvents = Extract<keyof ControllerEventCallbacks, string>;
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface ZWaveController extends ControllerStatisticsHost {}
@Mixin([ControllerStatisticsHost])
export class ZWaveController extends TypedEventEmitter<ControllerEventCallbacks> {
/** @internal */
public constructor(private readonly driver: Driver) {
super();
this._nodes = createThrowingMap((nodeId) => {
throw new ZWaveError(
`Node ${nodeId} was not found!`,
ZWaveErrorCodes.Controller_NodeNotFound,
);
});
// register message handlers
driver.registerRequestHandler(
FunctionType.AddNodeToNetwork,
this.handleAddNodeStatusReport.bind(this),
);
driver.registerRequestHandler(
FunctionType.RemoveNodeFromNetwork,
this.handleRemoveNodeStatusReport.bind(this),
);
driver.registerRequestHandler(
FunctionType.ReplaceFailedNode,
this.handleReplaceNodeStatusReport.bind(this),
);
}
private _type: ZWaveLibraryTypes | undefined;
public get type(): ZWaveLibraryTypes | undefined {
return this._type;
}
private _sdkVersion: string | undefined;
public get sdkVersion(): string | undefined {
return this._sdkVersion;
}
private _zwaveApiVersion: ZWaveApiVersion | undefined;
public get zwaveApiVersion(): ZWaveApiVersion | undefined {
return this._zwaveApiVersion;
}
private _zwaveChipType: string | UnknownZWaveChipType | undefined;
public get zwaveChipType(): string | UnknownZWaveChipType | undefined {
return this._zwaveChipType;
}
private _homeId: number | undefined;
/** A 32bit number identifying the current network */
public get homeId(): number | undefined {
return this._homeId;
}
private _ownNodeId: number | undefined;
/** The ID of the controller in the current network */
public get ownNodeId(): number | undefined {
return this._ownNodeId;
}
private _isPrimary: boolean | undefined;
public get isPrimary(): boolean | undefined {
return this._isPrimary;
}
private _isUsingHomeIdFromOtherNetwork: boolean | undefined;
public get isUsingHomeIdFromOtherNetwork(): boolean | undefined {
return this._isUsingHomeIdFromOtherNetwork;
}
private _isSISPresent: boolean | undefined;
public get isSISPresent(): boolean | undefined {
return this._isSISPresent;
}
private _wasRealPrimary: boolean | undefined;
public get wasRealPrimary(): boolean | undefined {
return this._wasRealPrimary;
}
private _isSIS: boolean | undefined;
public get isSIS(): boolean | undefined {
return this._isSIS;
}
private _isSUC: boolean | undefined;
public get isSUC(): boolean | undefined {
return this._isSUC;
}
private _nodeType: NodeType | undefined;
public get nodeType(): NodeType | undefined {
return this._nodeType;
}
/** Checks if the SDK version is greater than the given one */
public sdkVersionGt(version: SDKVersion): boolean | undefined {
if (this._sdkVersion === undefined) {
return undefined;
}
const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion);
return semver.gt(padVersion(sdkVersion), padVersion(version));
}
/** Checks if the SDK version is greater than or equal to the given one */
public sdkVersionGte(version: SDKVersion): boolean | undefined {
if (this._sdkVersion === undefined) {
return undefined;
}
const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion);
return semver.gte(padVersion(sdkVersion), padVersion(version));
}
/** Checks if the SDK version is lower than the given one */
public sdkVersionLt(version: SDKVersion): boolean | undefined {
if (this._sdkVersion === undefined) {
return undefined;
}
const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion);
return semver.lt(padVersion(sdkVersion), padVersion(version));
}
/** Checks if the SDK version is lower than or equal to the given one */
public sdkVersionLte(version: SDKVersion): boolean | undefined {
if (this._sdkVersion === undefined) {
return undefined;
}
const sdkVersion = protocolVersionToSDKVersion(this._sdkVersion);
return semver.lte(padVersion(sdkVersion), padVersion(version));
}
private _manufacturerId: number | undefined;
public get manufacturerId(): number | undefined {
return this._manufacturerId;
}
private _productType: number | undefined;
public get productType(): number | undefined {
return this._productType;
}
private _productId: number | undefined;
public get productId(): number | undefined {
return this._productId;
}
private _firmwareVersion: string | undefined;
public get firmwareVersion(): string | undefined {
return this._firmwareVersion;
}
private _supportedFunctionTypes: FunctionType[] | undefined;
public get supportedFunctionTypes(): readonly FunctionType[] | undefined {
return this._supportedFunctionTypes;
}
/** Checks if a given Z-Wave function type is supported by this controller */
public isFunctionSupported(functionType: FunctionType): boolean {
if (this._supportedFunctionTypes == null) {
throw new ZWaveError(
"Cannot check yet if a function is supported by the controller. The interview process has not been completed.",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._supportedFunctionTypes.indexOf(functionType) > -1;
}
private _supportedSerialAPISetupCommands:
| SerialAPISetupCommand[]
| undefined;
public get supportedSerialAPISetupCommands():
| readonly SerialAPISetupCommand[]
| undefined {
return this._supportedSerialAPISetupCommands;
}
/** Checks if a given Serial API setup command is supported by this controller */
public isSerialAPISetupCommandSupported(
command: SerialAPISetupCommand,
): boolean {
if (!this._supportedSerialAPISetupCommands) {
throw new ZWaveError(
"Cannot check yet if a Serial API setup command is supported by the controller. The interview process has not been completed.",
ZWaveErrorCodes.Driver_NotReady,
);
}
return this._supportedSerialAPISetupCommands.indexOf(command) > -1;
}
/**
* Tests if the controller supports a certain feature.
* Returns `undefined` if this information isn't known yet.
*/
public supportsFeature(feature: ZWaveFeature): boolean | undefined {
switch (feature) {
case ZWaveFeature.SmartStart:
return this.sdkVersionGte(minFeatureVersions[feature]);
}
}
/** Throws if the controller does not support a certain feature */
private assertFeature(feature: ZWaveFeature): void {
if (!this.supportsFeature(feature)) {
throw new ZWaveError(
`The controller does not support the ${getEnumMemberName(
ZWaveFeature,
feature,
)} feature`,
ZWaveErrorCodes.Controller_NotSupported,
);
}
}
private _sucNodeId: number | undefined;
public get sucNodeId(): number | undefined {
return this._sucNodeId;
}
private _supportsTimers: boolean | undefined;
public get supportsTimers(): boolean | undefined {
return this._supportsTimers;
}
/** Whether the controller is known to support soft reset */
public get supportsSoftReset(): boolean | undefined {
return this.driver.cacheGet(cacheKeys.controller.supportsSoftReset);
}
/** @internal */
public set supportsSoftReset(value: boolean | undefined) {
this.driver.cacheSet(cacheKeys.controller.supportsSoftReset, value);
}
private _nodes: ThrowingMap<number, ZWaveNode>;
/** A dictionary of the nodes connected to this controller */
public get nodes(): ReadonlyThrowingMap<number, ZWaveNode> {
return this._nodes;
}
/** Returns the node with the given DSK */
public getNodeByDSK(dsk: Buffer | string): ZWaveNode | undefined {
if (typeof dsk === "string") dsk = dskFromString(dsk);
for (const node of this._nodes.values()) {
if (node.dsk?.equals(dsk)) return node;
}
}
/** Returns the controller node's value DB */
public get valueDB(): ValueDB {
return this._nodes.get(this._ownNodeId!)!.valueDB;
}
private _healNetworkActive: boolean = false;
/** Returns whether the network or a node is currently being healed. */
public get isHealNetworkActive(): boolean {
return this._healNetworkActive;
}
/** Returns a reference to the (virtual) broadcast node, which allows sending commands to all nodes */
public getBroadcastNode(): VirtualNode {
return new VirtualNode(
NODE_ID_BROADCAST,
this.driver,
this.nodes.values(),
);
}
/** Creates a virtual node that can be used to send multicast commands to several nodes */
public getMulticastGroup(nodeIDs: number[]): VirtualNode {
const nodes = nodeIDs.map((id) => this._nodes.getOrThrow(id));
return new VirtualNode(undefined, this.driver, nodes);
}
/** @internal */
public get provisioningList(): readonly SmartStartProvisioningEntry[] {
return (
this.driver.cacheGet(cacheKeys.controller.provisioningList) ?? []
);
}
private set provisioningList(
value: readonly SmartStartProvisioningEntry[],
) {
this.driver.cacheSet(cacheKeys.controller.provisioningList, value);
}
/** Adds the given entry (DSK and security classes) to the controller's SmartStart provisioning list or replaces an existing entry */
public provisionSmartStartNode(entry: PlannedProvisioningEntry): void {
// 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;
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
*/
public unprovisionSmartStartNode(dskOrNodeId: string | number): void {
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.autoProvisionSmartStart();
this.provisioningList = provisioningList;
}
}
private getProvisioningEntryInternal(
dskOrNodeId: string | number,
): SmartStartProvisioningEntry | undefined {
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.
*/
public getProvisioningEntry(
dskOrNodeId: string | number,
): Readonly<SmartStartProvisioningEntry> | undefined {
const entry = this.getProvisioningEntryInternal(dskOrNodeId);
// Try to look up the node ID for this entry
if (entry) {
const ret: SmartStartProvisioningEntry = {
...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.
*/
public getProvisioningEntries(): SmartStartProvisioningEntry[] {
// Determine which DSKs belong to which node IDs
const dskNodeMap = new Map<string, number>();
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 */
public hasPlannedProvisioningEntries(): boolean {
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.
*/
public autoProvisionSmartStart(): void {
// Make sure the controller supports SmartStart
if (!this.supportsFeature(ZWaveFeature.SmartStart)) return;
if (this.hasPlannedProvisioningEntries()) {
// SmartStart should be enabled
// eslint-disable-next-line @typescript-eslint/no-empty-function
void this.enableSmartStart().catch(() => {});
} else {
// SmartStart should be disabled
// eslint-disable-next-line @typescript-eslint/no-empty-function
void this.disableSmartStart().catch(() => {});
}
}
/**
* @internal
* Queries the controller IDs and its Serial API capabilities
*/
public async identify(): Promise<void> {
// get the home and node id of the controller
this.driver.controllerLog.print(`querying controller IDs...`);
const ids = await this.driver.sendMessage<GetControllerIdResponse>(
new GetControllerIdRequest(this.driver),
{ supportCheck: false },
);
this._homeId = ids.homeId;
this._ownNodeId = ids.ownNodeId;
this.driver.controllerLog.print(
`received controller IDs:
home ID: ${num2hex(this._homeId)}
own node ID: ${this._ownNodeId}`,
);
// Figure out what the serial API can do
this.driver.controllerLog.print(`querying API capabilities...`);
const apiCaps =
await this.driver.sendMessage<GetSerialApiCapabilitiesResponse>(
new GetSerialApiCapabilitiesRequest(this.driver),
{
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("")}`,
);
}
/**
* @internal
* Interviews the controller for the necessary information.
* @param restoreFromCache Asynchronous callback for the driver to restore the network from cache after nodes are created
*/
public async interview(
restoreFromCache: () => Promise<void>,
): Promise<void> {
// get basic controller version info
this.driver.controllerLog.print(`querying version info...`);
const version =
await this.driver.sendMessage<GetControllerVersionResponse>(
new GetControllerVersionRequest(this.driver),
{
supportCheck: false,
},
);
this._sdkVersion = version.libraryVersion;
this._type = version.controllerType;
this.driver.controllerLog.print(
`received version info:
controller type: ${getEnumMemberName(ZWaveLibraryTypes, this._type)}
library version: ${this._sdkVersion}`,
);
// 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<GetProtocolVersionResponse>(
new GetProtocolVersionRequest(this.driver),
);
// Overwrite the SDK version with the more fine grained protocol version. We can assume this to be
// valid for 7.x firmwares, where SDK and protocol version are the same.
this._sdkVersion = 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);
}
this.driver.controllerLog.print(
`supported Z-Wave features: ${Object.keys(ZWaveFeature)
.filter((k) => /^\d+$/.test(k))
.map((k) => parseInt(k) as ZWaveFeature)
.filter((feat) => this.supportsFeature(feat))
.map((feat) => `\n · ${getEnumMemberName(ZWaveFeature, feat)}`)
.join("")}`,
);
// find out what the controller can do
this.driver.controllerLog.print(`querying controller capabilities...`);
const ctrlCaps =
await this.driver.sendMessage<GetControllerCapabilitiesResponse>(
new GetControllerCapabilitiesRequest(this.driver),
{
supportCheck: false,
},
);
this._isPrimary = !ctrlCaps.isSecondary;
this._isUsingHomeIdFromOtherNetwork =
ctrlCaps.isUsingHomeIdFromOtherNetwork;
this._isSISPresent = ctrlCaps.isSISPresent;
this._wasRealPrimary = ctrlCaps.wasRealPrimary;
this._isSUC = ctrlCaps.isStaticUpdateController;
this.driver.controllerLog.print(
`received controller capabilities:
controller role: ${this._isPrimary ? "primary" : "secondary"}
is the SUC: ${this._isSUC}
started this network: ${!this._isUsingHomeIdFromOtherNetwork}
SIS is present: ${this._isSISPresent}
was real primary: ${this._wasRealPrimary}`,
);
// Figure out which sub commands of SerialAPISetup are supported
if (this.isFunctionSupported(FunctionType.SerialAPISetup)) {
this.driver.controllerLog.print(
`querying serial API setup capabilities...`,
);
const setupCaps =
await this.driver.sendMessage<SerialAPISetup_GetSupportedCommandsResponse>(
new SerialAPISetup_GetSupportedCommandsRequest(this.driver),
);
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 = [];
}
// Enable TX status report if supported
if (
this.isSerialAPISetupCommandSupported(
SerialAPISetupCommand.SetTxStatusReport,
)
) {
this.driver.controllerLog.print(`Enabling TX status report...`);
const resp =
await this.driver.sendMessage<SerialAPISetup_SetTXStatusReportResponse>(
new SerialAPISetup_SetTXStatusReportRequest(this.driver, {
enabled: true,
}),
);
this.driver.controllerLog.print(
`Enabling TX status report ${
resp.success ? "successful" : "failed"
}...`,
);
}
// find the SUC
this.driver.controllerLog.print(`finding SUC...`);
const suc = await this.driver.sendMessage<GetSUCNodeIdResponse>(
new GetSUCNodeIdRequest(this.driver),
{ supportCheck: false },
);
this._sucNodeId = suc.sucNodeId;
if (this._sucNodeId === 0) {
this.driver.controllerLog.print(`No SUC present in the network`);
} else if (this._sucNodeId === this._ownNodeId) {
this.driver.controllerLog.print(`This is the SUC`);
} else {
this.driver.controllerLog.print(
`SUC has node ID ${this.sucNodeId}`,
);
}
// There needs to be a SUC/SIS in the network. If not, we promote ourselves to one if the following conditions are met:
// We are the primary controller, but we are not SUC, there is no SUC and there is no SIS
if (
this._isPrimary &&
this._sucNodeId === 0 &&
!this._isSUC &&
!this._isSISPresent
) {
this.driver.controllerLog.print(
`There is no SUC/SIS in the network - promoting ourselves...`,
);
try {
const result = await this.configureSUC(
this._ownNodeId!,
true,
true,
);
if (result) {
this._sucNodeId = this._ownNodeId;
}
this.driver.controllerLog.print(
`Promotion to SUC/SIS ${result ? "succeeded" : "failed"}.`,
result ? undefined : "warn",
);
} catch (e) {
this.driver.controllerLog.print(
`Error while promoting to SUC/SIS: ${getErrorMessage(e)}`,
"error",
);
}
}
// if it's a bridge controller, request the virtual nodes
if (
this.type === ZWaveLibraryTypes["Bridge Controller"] &&
this.isFunctionSupported(FunctionType.FUNC_ID_ZW_GET_VIRTUAL_NODES)
) {
// TODO: send FUNC_ID_ZW_GET_VIRTUAL_NODES message
}
// Request additional information about the controller/Z-Wave chip
this.driver.controllerLog.print(
`querying additional controller information...`,
);
const initData =
await this.driver.sendMessage<GetSerialApiInitDataResponse>(
new GetSerialApiInitDataRequest(this.driver),
);
// and remember the new info
this._zwaveApiVersion = initData.zwaveApiVersion;
this._zwaveChipType = initData.zwaveChipType;
this._isPrimary = initData.isPrimary;
this._isSIS = initData.isSIS;
this._nodeType = initData.nodeType;
this._supportsTimers = initData.supportsTimers;
// ignore the initVersion, no clue what to do with it
this.driver.controllerLog.print(
`received additional controller information:
Z-Wave API version: ${this._zwaveApiVersion.version} (${
this._zwaveApiVersion.kind
})${
this._zwaveChipType
? `
Z-Wave chip type: ${
typeof this._zwaveChipType === "string"
? this._zwaveChipType
: `unknown (type: ${num2hex(
this._zwaveChipType.type,
)}, version: ${num2hex(this._zwaveChipType.version)})`
}`
: ""
}
node type ${getEnumMemberName(NodeType, this._nodeType)}
controller role: ${this._isPrimary ? "primary" : "secondary"}
controller is the SIS: ${this._isSIS}
controller supports timers: ${this._supportsTimers}
nodes in the network: ${initData.nodeIds.join(", ")}`,
);
// Index the value DB for optimal performance
const valueDBIndexes = indexDBsByNode([
this.driver.valueDB!,
this.driver.metadataDB!,
]);
// create an empty entry in the nodes map so we can initialize them afterwards
for (const nodeId of initData.nodeIds) {
this._nodes.set(
nodeId,
new ZWaveNode(
nodeId,
this.driver,
undefined,
undefined,
undefined,
// Use the previously created index to avoid doing extra work when creating the value DB
this.createValueDBForNode(
nodeId,
valueDBIndexes.get(nodeId),
),
),
);
}
// Now try to deserialize all nodes from the cache
await restoreFromCache();
// Set manufacturer information for the controller node
const controllerValueDB = this.valueDB;
controllerValueDB.setMetadata(
ManufacturerSpecificCCValues.manufacturerId.id,
ManufacturerSpecificCCValues.manufacturerId.meta,
);
controllerValueDB.setMetadata(
ManufacturerSpecificCCValues.productType.id,
ManufacturerSpecificCCValues.productType.meta,
);
controllerValueDB.setMetadata(
ManufacturerSpecificCCValues.productId.id,
ManufacturerSpecificCCValues.productId.meta,
);
controllerValueDB.setValue(
ManufacturerSpecificCCValues.manufacturerId.id,
this._manufacturerId,
);
controllerValueDB.setValue(
ManufacturerSpecificCCValues.productType.id,
this._productType,
);
controllerValueDB.setValue(
ManufacturerSpecificCCValues.productId.id,
this._productId,
);
// Set firmware version information for the controller node
controllerValueDB.setMetadata(
VersionCCValues.firmwareVersions.id,
VersionCCValues.firmwareVersions.meta,
);
controllerValueDB.setValue(VersionCCValues.firmwareVersions.id, [
this._firmwareVersion,
]);
controllerValueDB.setMetadata(
VersionCCValues.sdkVersion.id,
VersionCCValues.sdkVersion.meta,
);
controllerValueDB.setValue(
VersionCCValues.sdkVersion.id,
this._sdkVersion,
);
if (
this.type !== ZWaveLibraryTypes["Bridge Controller"] &&
this.isFunctionSupported(FunctionType.SetSerialApiTimeouts)
) {
const { ack, byte } = this.driver.options.timeouts;
this.driver.controllerLog.print(
`setting serial API timeouts: ack = ${ack} ms, byte = ${byte} ms`,
);
const resp =
await this.driver.sendMessage<SetSerialApiTimeoutsResponse>(
new SetSerialApiTimeoutsRequest(this.driver, {
ackTimeout: ack,
byteTimeout: byte,
}),
);
this.driver.controllerLog.print(
`serial API timeouts overwritten. The old values were: ack = ${resp.oldAckTimeout} ms, byte = ${resp.oldByteTimeout} ms`,
);
}
this.driver.controllerLog.print("Interview completed");
}
private createValueDBForNode(nodeId: number, ownKeys?: Set<string>) {
return new ValueDB(
nodeId,
this.driver.valueDB!,
this.driver.metadataDB!,
ownKeys,
);
}
/**
* Sets the NIF of the controller to the Gateway device type and to include the CCs supported by Z-Wave JS.
* Warning: This only works when followed up by a hard-reset, so don't call this directly
* @internal
*/
public async setControllerNIF(): Promise<void> {
this.driver.controllerLog.print("Updating the controller NIF...");
await this.driver.sendMessage(
new SetApplicationNodeInformationRequest(this.driver, {
isListening: true,
...determineNIF(),
}),
);
}
/**
* Performs a hard reset on the controller. This wipes out all configuration!
* Warning: The driver needs to re-interview the controller, so don't call this directly
* @internal
*/
public async hardReset(): Promise<void> {
// begin the reset process
try {
this.driver.controllerLog.print("performing hard reset...");
await this.driver.sendMessage(new HardResetRequest(this.driver), {
supportCheck: false,
});
this.driver.controllerLog.print(`hard reset succeeded`);
// Clean up
this._nodes.forEach((node) => node.removeAllListeners());
this._nodes.clear();
} catch (e) {
this.driver.controllerLog.print(
`hard reset failed: ${getErrorMessage(e)}`,
"error",
);
throw e;
}
}
private _inclusionState: InclusionState = InclusionState.Idle;
public get inclusionState(): InclusionState {
return this._inclusionState;
}
/** @internal */
public setInclusionState(state: InclusionState): void {
if (this._inclusionState === state) return;
this._inclusionState = state;
if (
state === InclusionState.Idle &&
this._smartStartEnabled &&
this.supportsFeature(ZWaveFeature.SmartStart)
) {
// If Smart Start was enabled before the inclusion/exclusion,
// enable it again and ignore errors
// eslint-disable-next-line @typescript-eslint/no-empty-function
this.enableSmartStart().catch(() => {});
}
}
private _smartStartEnabled: boolean = false;
private _includeController: boolean = false;
private _exclusionOptions: ExclusionOptions | undefined;
private _inclusionOptions: InclusionOptionsInternal | undefined;
private _nodePendingInclusion: ZWaveNode | undefined;
private _nodePendingExclusion: ZWaveNode | undefined;
private _nodePendingReplace: ZWaveNode | undefined;
private _replaceFailedPromise: DeferredPromise<boolean> | undefined;
/**
* Starts the inclusion process of new nodes.
* Resolves to true when the process was started, and false if the inclusion was already active.
*
* @param options Defines the inclusion strategy to use.
*/
public async beginInclusion(
options: InclusionOptions = {
strategy: InclusionStrategy.Insecure,
},
): Promise<boolean> {
if (
this._inclusionState === InclusionState.Including ||
this._inclusionState === InclusionState.Excluding ||
this._inclusionState === InclusionState.Busy
) {
return false;
}
// Protect against invalid inclusion options
if (
!(options.strategy in InclusionStrategy) ||
// @ts-expect-error We're checking for user errors
options.strategy === InclusionStrategy.SmartStart
) {
throw new ZWaveError(
`Invalid inclusion strategy: ${options.strategy}`,
ZWaveErrorCodes.Argument_Invalid,
);
}
// Leave SmartStart listening mode so we can switch to exclusion mode
await this.pauseSmartStart();
this.setInclusionState(InclusionState.Including);
this._inclusionOptions = options;
try {
this.driver.controllerLog.print(
`Starting inclusion process with strategy ${getEnumMemberName(
InclusionStrategy,
options.strategy,
)}...`,
);
// kick off the inclusion process
await this.driver.sendMessage(
new AddNodeToNetworkRequest(this.driver, {
addNodeType: AddNodeType.Any,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(
`The controller is now ready to add nodes`,
);
this.emit(
"inclusion started",
// TODO: Remove first parameter in next major version
options.strategy !== InclusionStrategy.Insecure,
options.strategy,
);
} catch (e) {
this.setInclusionState(InclusionState.Idle);
if (
isZWaveError(e) &&
e.code === ZWaveErrorCodes.Controller_CallbackNOK
) {
this.driver.controllerLog.print(
`Starting the inclusion failed`,
"error",
);
throw new ZWaveError(
"The inclusion could not be started.",
ZWaveErrorCodes.Controller_InclusionFailed,
);
}
throw e;
}
return true;
}
/** @internal */
public async beginInclusionSmartStart(
provisioningEntry: PlannedProvisioningEntry,
): Promise<boolean> {
if (
this._inclusionState === InclusionState.Including ||
this._inclusionState === InclusionState.Excluding ||
this._inclusionState === InclusionState.Busy
) {
return false;
}
// Disable listening mode so we can switch to inclusion mode
await this.stopInclusion();
this.setInclusionState(InclusionState.Including);
this._inclusionOptions = {
strategy: InclusionStrategy.SmartStart,
provisioning: provisioningEntry,
};
try {
this.driver.controllerLog.print(
`Including SmartStart node with DSK ${provisioningEntry.dsk}`,
);
// kick off the inclusion process
const dskBuffer = dskFromString(provisioningEntry.dsk);
await this.driver.sendMessage(
new AddNodeDSKToNetworkRequest(this.driver, {
nwiHomeId: nwiHomeIdFromDSK(dskBuffer),
authHomeId: authHomeIdFromDSK(dskBuffer),
highPower: true,
networkWide: true,
}),
);
this.emit(
"inclusion started",
// TODO: Remove first parameter in next major version
true,
InclusionStrategy.SmartStart,
);
} catch (e) {
this.setInclusionState(InclusionState.Idle);
// Error handling for this happens at the call site
throw e;
}
return true;
}
/**
* Is used internally to stop an active inclusion process without waiting for a confirmation
* @internal
*/
public async stopInclusionNoCallback(): Promise<void> {
await this.driver.sendMessage(
new AddNodeToNetworkRequest(this.driver, {
callbackId: 0, // disable callbacks
addNodeType: AddNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(`The inclusion process was stopped`);
this.emit("inclusion stopped");
}
/**
* Finishes an inclusion process. This must only be called after the ProtocolDone status is received.
* Returns the ID of the newly added node.
*/
private async finishInclusion(): Promise<number> {
this.driver.controllerLog.print(`finishing inclusion process...`);
const response =
await this.driver.sendMessage<AddNodeToNetworkRequestStatusReport>(
new AddNodeToNetworkRequest(this.driver, {
addNodeType: AddNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
if (response.status === AddNodeStatus.Done) {
return response.statusContext!.nodeId;
}
this.driver.controllerLog.print(
`Finishing the inclusion failed`,
"error",
);
throw new ZWaveError(
"Finishing the inclusion failed",
ZWaveErrorCodes.Controller_InclusionFailed,
);
}
/**
* Stops an active inclusion process. Resolves to true when the controller leaves inclusion mode,
* and false if the inclusion was not active.
*/
public async stopInclusion(): Promise<boolean> {
if (this._inclusionState !== InclusionState.Including) {
return false;
}
this.driver.controllerLog.print(`stopping inclusion process...`);
try {
// stop the inclusion process
await this.driver.sendMessage(
new AddNodeToNetworkRequest(this.driver, {
addNodeType: AddNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(
`The inclusion process was stopped`,
);
this.emit("inclusion stopped");
this.setInclusionState(InclusionState.Idle);
return true;
} catch (e) {
if (
isZWaveError(e) &&
e.code === ZWaveErrorCodes.Controller_CallbackNOK
) {
this.driver.controllerLog.print(
`Stopping the inclusion failed`,
"error",
);
throw new ZWaveError(
"The inclusion could not be stopped.",
ZWaveErrorCodes.Controller_InclusionFailed,
);
}
throw e;
}
}
/**
* Puts the controller into listening mode for Smart Start inclusion.
* Whenever a node on the provisioning list announces itself, it will automatically be added.
*
* Resolves to `true` when the listening mode is started or was active, and `false` if it is scheduled for later activation.
*/
private async enableSmartStart(): Promise<boolean> {
if (!this.supportsFeature(ZWaveFeature.SmartStart)) {
this.driver.controllerLog.print(
`Smart Start is not supported by this controller, NOT enabling listening mode...`,
"warn",
);
}
this._smartStartEnabled = true;
if (this._inclusionState === InclusionState.Idle) {
this.setInclusionState(InclusionState.SmartStart);
this.driver.controllerLog.print(
`Enabling Smart Start listening mode...`,
);
try {
await this.driver.sendMessage(
new EnableSmartStartListenRequest(this.driver, {}),
);
this.driver.controllerLog.print(
`Smart Start listening mode enabled`,
);
return true;
} catch (e) {
this.setInclusionState(InclusionState.Idle);
this.driver.controllerLog.print(
`Smart Start listening mode could not be enabled: ${getErrorMessage(
e,
)}`,
"error",
);
throw e;
}
} else if (this._inclusionState === InclusionState.SmartStart) {
return true;
} else {
this.driver.controllerLog.print(
`Smart Start listening mode scheduled for later activation...`,
);
return false;
}
}
/**
* Disables the listening mode for Smart Start inclusion.
*
* Resolves to `true` when the listening mode is stopped, and `false` if was not active.
*/
private async disableSmartStart(): Promise<boolean> {
if (!this.supportsFeature(ZWaveFeature.SmartStart)) return true;
this._smartStartEnabled = false;
if (this._inclusionState === InclusionState.SmartStart) {
this.setInclusionState(InclusionState.Idle);
this.driver.controllerLog.print(
`disabling Smart Start listening mode...`,
);
try {
await this.driver.sendMessage(
new AddNodeToNetworkRequest(this.driver, {
callbackId: 0, // disable callbacks
addNodeType: AddNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(
`Smart Start listening mode disabled`,
);
return true;
} catch (e) {
this.setInclusionState(InclusionState.SmartStart);
this.driver.controllerLog.print(
`Smart Start listening mode could not be disabled: ${getErrorMessage(
e,
)}`,
"error",
);
throw e;
}
} else if (this._inclusionState === InclusionState.Idle) {
return true;
} else {
this.driver.controllerLog.print(
`Smart Start listening mode disabled`,
);
return true;
}
}
private async pauseSmartStart(): Promise<boolean> {
if (!this.supportsFeature(ZWaveFeature.SmartStart)) return true;
if (this._inclusionState === InclusionState.SmartStart) {
this.driver.controllerLog.print(
`Leaving Smart Start listening mode...`,
);
try {
await this.driver.sendMessage(
new AddNodeToNetworkRequest(this.driver, {
callbackId: 0, // disable callbacks
addNodeType: AddNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(
`Left Smart Start listening mode`,
);
return true;
} catch (e) {
this.driver.controllerLog.print(
`Smart Start listening mode could not be left: ${getErrorMessage(
e,
)}`,
"error",
);
throw e;
}
} else {
return true;
}
}
/**
* Starts the exclusion process of new nodes.
* Resolves to true when the process was started, and false if an inclusion or exclusion process was already active.
*
* @param options Influences the exclusion process and what happens with the Smart Start provisioning list.
*/
public async beginExclusion(options?: ExclusionOptions): Promise<boolean>;
/**
* Starts the exclusion process of new nodes.
* Resolves to true when the process was started, and false if an inclusion or exclusion process was already active.
*
* @param unprovision Whether the removed node should also be removed from the Smart Start provisioning list.
* A value of `"inactive"` will keep the provisioning entry, but disable it.
*
* @deprecated Use the overload with {@link ExclusionOptions} instead.
*/
public async beginExclusion(
unprovision: boolean | "inactive",
): Promise<boolean>;
public async beginExclusion(
options: ExclusionOptions | boolean | "inactive" = {
strategy: ExclusionStrategy.DisableProvisioningEntry,
},
): Promise<boolean> {
if (
this._inclusionState === InclusionState.Including ||
this._inclusionState === InclusionState.Excluding ||
this._inclusionState === InclusionState.Busy
) {
return false;
}
if (typeof options === "boolean") {
options = {
strategy: options
? ExclusionStrategy.Unprovision
: ExclusionStrategy.ExcludeOnly,
};
} else if (options === "inactive") {
options = {
strategy: ExclusionStrategy.DisableProvisioningEntry,
};
}
// Leave SmartStart listening mode so we can switch to exclusion mode
await this.pauseSmartStart();
this.setInclusionState(InclusionState.Excluding);
this.driver.controllerLog.print(`starting exclusion process...`);
try {
// kick off the inclusion process
await this.driver.sendMessage(
new RemoveNodeFromNetworkRequest(this.driver, {
removeNodeType: RemoveNodeType.Any,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(
`The controller is now ready to remove nodes`,
);
this._exclusionOptions = options;
this.emit("exclusion started");
return true;
} catch (e) {
this.setInclusionState(InclusionState.Idle);
if (
isZWaveError(e) &&
e.code === ZWaveErrorCodes.Controller_CallbackNOK
) {
this.driver.controllerLog.print(
`Starting the exclusion failed`,
"error",
);
throw new ZWaveError(
"The exclusion could not be started.",
ZWaveErrorCodes.Controller_ExclusionFailed,
);
}
throw e;
}
}
/**
* Is used internally to stop an active exclusion process without waiting for confirmation
* @internal
*/
public async stopExclusionNoCallback(): Promise<void> {
await this.driver.sendMessage(
new RemoveNodeFromNetworkRequest(this.driver, {
callbackId: 0, // disable callbacks
removeNodeType: RemoveNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(`the exclusion process was stopped`);
this.emit("exclusion stopped");
}
/**
* Stops an active exclusion process. Resolves to true when the controller leaves exclusion mode,
* and false if the inclusion was not active.
*/
public async stopExclusion(): Promise<boolean> {
if (this._inclusionState !== InclusionState.Excluding) {
return false;
}
this.driver.controllerLog.print(`stopping exclusion process...`);
try {
// kick off the inclusion process
await this.driver.sendMessage(
new RemoveNodeFromNetworkRequest(this.driver, {
removeNodeType: RemoveNodeType.Stop,
highPower: true,
networkWide: true,
}),
);
this.driver.controllerLog.print(
`the exclusion process was stopped`,
);
this.emit("exclusion stopped");
this.setInclusionState(InclusionState.Idle);
return true;
} catch (e) {
if (
isZWaveError(e) &&
e.code === ZWaveErrorCodes.Controller_CallbackNOK
) {
this.driver.controllerLog.print(
`Stopping the exclusion failed`,
"error",
);
throw new ZWaveError(
"The exclusion could not be stopped.",
ZWaveErrorCodes.Controller_ExclusionFailed,