From 3fd27bc2cc7ac229a049288545061be1d5730e25 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Fri, 19 Jan 2024 06:26:27 -0600 Subject: [PATCH] feat: support Z-Wave Long Range (#6401) Co-authored-by: Dominic Griesel --- docs/_sidebar.md | 1 + docs/api/CCs/_sidebar.md | 1 + docs/api/controller.md | 10 + docs/getting-started/long-range.md | 11 + packages/cc/src/cc/Security2CC.ts | 24 +- .../core/src/capabilities/CommandClasses.ts | 3 +- packages/core/src/capabilities/NodeInfo.ts | 110 +++- packages/core/src/capabilities/Protocols.ts | 11 + packages/core/src/consts/index.ts | 6 + packages/core/src/values/Primitive.ts | 40 +- packages/serial/src/message/Constants.ts | 8 +- packages/serial/src/message/Message.ts | 13 + packages/zwave-js/src/Utils.ts | 10 +- .../zwave-js/src/lib/controller/Controller.ts | 525 +++++++++++++----- .../zwave-js/src/lib/controller/Inclusion.ts | 14 +- packages/zwave-js/src/lib/controller/utils.ts | 20 + packages/zwave-js/src/lib/driver/Driver.ts | 104 ++-- .../zwave-js/src/lib/driver/NetworkCache.ts | 43 ++ packages/zwave-js/src/lib/node/Node.ts | 17 +- .../application/ApplicationUpdateRequest.ts | 16 +- .../capability/GetLongRangeNodesMessages.ts | 119 ++++ .../capability/LongRangeSetupMessages.ts | 150 +++++ .../capability/SerialAPISetupMessages.ts | 41 +- .../network-mgmt/AddNodeToNetworkRequest.ts | 16 +- .../GetNodeProtocolInfoMessages.ts | 18 +- 25 files changed, 1067 insertions(+), 264 deletions(-) create mode 100644 docs/getting-started/long-range.md create mode 100644 packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts create mode 100644 packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts diff --git a/docs/_sidebar.md b/docs/_sidebar.md index f3a95341ad92..3cd4a25d9fb9 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -6,6 +6,7 @@ - [Our Philosophy](getting-started/philosophy.md) - [Frequently Asked Questions](getting-started/faq.md) - [Security S2](getting-started/security-s2.md) + - [Z-Wave Long Range](getting-started/long-range.md) - [Migrating to v12](getting-started/migrating-to-v12.md) - [Migrating to v11](getting-started/migrating-to-v11.md) - [Migrating to v10](getting-started/migrating-to-v10.md) diff --git a/docs/api/CCs/_sidebar.md b/docs/api/CCs/_sidebar.md index de3678aa8967..32ed533ce0a6 100644 --- a/docs/api/CCs/_sidebar.md +++ b/docs/api/CCs/_sidebar.md @@ -6,6 +6,7 @@ - [Our Philosophy](getting-started/philosophy.md) - [Frequently Asked Questions](getting-started/faq.md) - [Security S2](getting-started/security-s2.md) + - [Z-Wave Long Range](getting-started/long-range.md) - [Migrating to v12](getting-started/migrating-to-v12.md) - [Migrating to v11](getting-started/migrating-to-v11.md) - [Migrating to v10](getting-started/migrating-to-v10.md) diff --git a/docs/api/controller.md b/docs/api/controller.md index 185c6f24ff29..ff76c7f0b138 100644 --- a/docs/api/controller.md +++ b/docs/api/controller.md @@ -170,6 +170,7 @@ interface PlannedProvisioningEntry { /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ dsk: string; securityClasses: SecurityClass[]; + // ...other fields are irrelevant for this inclusion procedure } ``` @@ -238,6 +239,7 @@ provisionSmartStartNode(entry: PlannedProvisioningEntry): void ``` Adds the given entry (DSK and security classes) to the controller's SmartStart provisioning list or replaces an existing entry. The node will be included out of band when it powers up. +If the `protocol` field is set to `Protocols.ZWaveLongRange`, the node will be included using Z-Wave Long Range instead of Z-Wave Classic. > [!ATTENTION] This method will throw when SmartStart is not supported by the controller! @@ -256,6 +258,14 @@ interface PlannedProvisioningEntry { /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ dsk: string; + /** Which protocol to use for inclusion. Default: Z-Wave Classic */ + protocol?: Protocols; + /** + * The protocols that are **supported** by the device. + * When this is not set, applications should default to Z-Wave classic. + */ + supportedProtocols?: readonly Protocols[]; + /** The security classes that have been **granted** by the user */ securityClasses: SecurityClass[]; /** diff --git a/docs/getting-started/long-range.md b/docs/getting-started/long-range.md new file mode 100644 index 000000000000..519b3b0b991a --- /dev/null +++ b/docs/getting-started/long-range.md @@ -0,0 +1,11 @@ +# Supporting Z-Wave Long Range in Applications + +Z-Wave Long Range (ZWLR) is an addition to Z-Wave, that allows for a massively increased transmission range and up to 4000 nodes in a single network. Z-Wave Long Range uses a star topology, where all nodes communicate directly with the controller. This means that ZWLR nodes cannot be used to route messages for non-ZWLR nodes. + +There are a few things applications need to be aware of to support Long Range using Z-Wave JS. + +1. ZWLR node IDs start at 256. This can be used to distinguish between ZWLR and classic Z-Wave nodes. +2. ZWLR inclusion works exclusively through [Smart Start](getting-started/security-s2#smartstart). + \ + ZWLR nodes advertise support for Long Range in the `supportedProtocols` field of the `QRProvisioningInformation` object (see [here](api/utils#other-qr-codes)). When this field is present, the user **MUST** have the choice between the advertised protocols. Currently this means deciding between including the node via Z-Wave Classic (mesh) or Z-Wave Long Range (no mesh).\ + To include a node via ZWLR, set the `protocol` field of the `PlannedProvisioningEntry` to `Protocols.ZWaveLongRange` when [provisioning the node](api/controller#provisionsmartstartnode). diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index a4caeb6536cc..afde5e81acd3 100644 --- a/packages/cc/src/cc/Security2CC.ts +++ b/packages/cc/src/cc/Security2CC.ts @@ -18,6 +18,7 @@ import { encryptAES128CCM, getCCName, highResTimestamp, + isLongRangeNodeId, isTransmissionError, isZWaveError, parseBitMask, @@ -94,13 +95,24 @@ function getAuthenticationData( commandLength: number, unencryptedPayload: Buffer, ): Buffer { - const ret = Buffer.allocUnsafe(8 + unencryptedPayload.length); - ret[0] = sendingNodeId; - ret[1] = destination; - ret.writeUInt32BE(homeId, 2); - ret.writeUInt16BE(commandLength, 6); + const nodeIdSize = + isLongRangeNodeId(sendingNodeId) || isLongRangeNodeId(destination) + ? 2 + : 1; + const ret = Buffer.allocUnsafe( + 2 * nodeIdSize + 6 + unencryptedPayload.length, + ); + let offset = 0; + ret.writeUIntBE(sendingNodeId, offset, nodeIdSize); + offset += nodeIdSize; + ret.writeUIntBE(destination, offset, nodeIdSize); + offset += nodeIdSize; + ret.writeUInt32BE(homeId, offset); + offset += 4; + ret.writeUInt16BE(commandLength, offset); + offset += 2; // This includes the sequence number and all unencrypted extensions - unencryptedPayload.copy(ret, 8, 0); + unencryptedPayload.copy(ret, offset, 0); return ret; } diff --git a/packages/core/src/capabilities/CommandClasses.ts b/packages/core/src/capabilities/CommandClasses.ts index 6fae5ee579dd..fd2bde0cb184 100644 --- a/packages/core/src/capabilities/CommandClasses.ts +++ b/packages/core/src/capabilities/CommandClasses.ts @@ -128,8 +128,9 @@ export enum CommandClasses { "Z/IP ND" = 0x58, "Z/IP Portal" = 0x61, "Z-Wave Plus Info" = 0x5e, - // Internal CC which is not used directly by applications + // Internal CCs which are not used directly by applications "Z-Wave Protocol" = 0x01, + "Z-Wave Long Range" = 0x04, } export function getCCName(cc: number): string { diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index 67a7f265e55b..eee547cf239a 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -215,6 +215,7 @@ export type NodeInformationFrame = export function parseNodeProtocolInfo( buffer: Buffer, offset: number, + isLongRange: boolean = false, ): NodeProtocolInfo { validatePayload(buffer.length >= offset + 3); @@ -222,19 +223,28 @@ export function parseNodeProtocolInfo( const isRouting = !!(buffer[offset] & 0b01_000_000); const supportedDataRates: DataRate[] = []; - const maxSpeed = buffer[offset] & 0b00_011_000; - const speedExtension = buffer[offset + 2] & 0b111; - if (maxSpeed & 0b00_010_000) { - supportedDataRates.push(40000); - } - if (maxSpeed & 0b00_001_000) { - supportedDataRates.push(9600); - } - if (speedExtension & 0b001) { - supportedDataRates.push(100000); - } - if (supportedDataRates.length === 0) { - supportedDataRates.push(9600); + const speed = buffer[offset] & 0b00_011_000; + const speedExt = buffer[offset + 2] & 0b111; + if (isLongRange) { + // In the LR NIF, the speed bitmask is reserved and contains no information + // The speedExt bitmask is used instead, but for some reason the bitmask + // is different from a classic NIF... + if (speedExt & 0b010) { + supportedDataRates.push(100000); + } + } else { + if (speed & 0b00_010_000) { + supportedDataRates.push(40000); + } + if (speed & 0b00_001_000) { + supportedDataRates.push(9600); + } + if (speedExt & 0b001) { + supportedDataRates.push(100000); + } + if (supportedDataRates.length === 0) { + supportedDataRates.push(9600); + } } const protocolVersion = buffer[offset] & 0b111; @@ -282,14 +292,23 @@ export function parseNodeProtocolInfo( }; } -export function encodeNodeProtocolInfo(info: NodeProtocolInfo): Buffer { +export function encodeNodeProtocolInfo( + info: NodeProtocolInfo, + isLongRange: boolean = false, +): Buffer { + // Technically a lot of these fields are reserved/unused in Z-Wave Long Range, + // but the only thing where it really matters is the speed bitmask. const ret = Buffer.alloc(3, 0); // Byte 0 and 2 if (info.isListening) ret[0] |= 0b10_000_000; if (info.isRouting) ret[0] |= 0b01_000_000; - if (info.supportedDataRates.includes(40000)) ret[0] |= 0b00_010_000; - if (info.supportedDataRates.includes(9600)) ret[0] |= 0b00_001_000; - if (info.supportedDataRates.includes(100000)) ret[2] |= 0b001; + if (isLongRange) { + if (info.supportedDataRates.includes(100000)) ret[2] |= 0b010; + } else { + if (info.supportedDataRates.includes(40000)) ret[0] |= 0b00_010_000; + if (info.supportedDataRates.includes(9600)) ret[0] |= 0b00_001_000; + if (info.supportedDataRates.includes(100000)) ret[2] |= 0b001; + } ret[0] |= info.protocolVersion & 0b111; // Byte 1 @@ -307,12 +326,20 @@ export function encodeNodeProtocolInfo(info: NodeProtocolInfo): Buffer { return ret; } -export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer): { +export function parseNodeProtocolInfoAndDeviceClass( + buffer: Buffer, + isLongRange: boolean = false, +): { info: NodeProtocolInfoAndDeviceClass; bytesRead: number; } { validatePayload(buffer.length >= 5); - const protocolInfo = parseNodeProtocolInfo(buffer, 0); + // The specs are a bit confusing here. We parse the response to GetNodeProtocolInfo, + // which always includes the basic device class, unlike the NIF that was received by + // the end device. However, the meaning of the flags in the first 3 bytes may change + // depending on the protocol in use. + const protocolInfo = parseNodeProtocolInfo(buffer, 0, isLongRange); + let offset = 3; const basic = buffer[offset++]; const generic = buffer[offset++]; @@ -334,9 +361,13 @@ export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer): { export function encodeNodeProtocolInfoAndDeviceClass( info: NodeProtocolInfoAndDeviceClass, + isLongRange: boolean = false, ): Buffer { return Buffer.concat([ - encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), + encodeNodeProtocolInfo( + { ...info, hasSpecificDeviceClass: true }, + isLongRange, + ), Buffer.from([ info.basicDeviceClass, info.genericDeviceClass, @@ -347,11 +378,26 @@ export function encodeNodeProtocolInfoAndDeviceClass( export function parseNodeInformationFrame( buffer: Buffer, + isLongRange: boolean = false, ): NodeInformationFrame { - const { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( + const result = parseNodeProtocolInfoAndDeviceClass( buffer, + isLongRange, ); - const supportedCCs = parseCCList(buffer.subarray(offset)).supportedCCs; + const info = result.info; + let offset = result.bytesRead; + + let ccList: Buffer; + if (isLongRange) { + const ccListLength = buffer[offset]; + offset += 1; + validatePayload(buffer.length >= offset + ccListLength); + ccList = buffer.subarray(offset, offset + ccListLength); + } else { + ccList = buffer.subarray(offset); + } + + const supportedCCs = parseCCList(ccList).supportedCCs; return { ...info, @@ -359,11 +405,21 @@ export function parseNodeInformationFrame( }; } -export function encodeNodeInformationFrame(info: NodeInformationFrame): Buffer { - return Buffer.concat([ - encodeNodeProtocolInfoAndDeviceClass(info), - encodeCCList(info.supportedCCs, []), - ]); +export function encodeNodeInformationFrame( + info: NodeInformationFrame, + isLongRange: boolean = false, +): Buffer { + const protocolInfo = encodeNodeProtocolInfoAndDeviceClass( + info, + isLongRange, + ); + + let ccList = encodeCCList(info.supportedCCs, []); + if (isLongRange) { + ccList = Buffer.concat([Buffer.from([ccList.length]), ccList]); + } + + return Buffer.concat([protocolInfo, ccList]); } export function parseNodeID( diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index 937078f098bf..4494dae91dfe 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -87,3 +87,14 @@ export function isEmptyRoute(route: Route): boolean { && route.routeSpeed === ZWaveDataRate["9k6"] ); } + +export enum LongRangeChannel { + Unknown = 0x00, // Reserved + A = 0x01, + B = 0x02, + // 0x03..0xFF are reserved and must not be used +} + +export function isLongRangeNodeId(nodeId: number): boolean { + return nodeId > 255; +} diff --git a/packages/core/src/consts/index.ts b/packages/core/src/consts/index.ts index 056a87828398..ac9d11985779 100644 --- a/packages/core/src/consts/index.ts +++ b/packages/core/src/consts/index.ts @@ -10,6 +10,12 @@ export const NODE_ID_MAX = MAX_NODES; /** The number of bytes in a node bit mask */ export const NUM_NODEMASK_BYTES = MAX_NODES / 8; +/** The number of node ids in a long range "segment" (GetLongRangeNodes response) */ +export const NUM_LR_NODES_PER_SEGMENT = 128; + +/** The number of bytes in a long range node bit mask segment */ +export const NUM_LR_NODEMASK_SEGMENT_BYTES = NUM_LR_NODES_PER_SEGMENT / 8; + export enum NodeIDType { Short = 0x01, Long = 0x02, diff --git a/packages/core/src/values/Primitive.ts b/packages/core/src/values/Primitive.ts index f7464a7a17fc..91fdc10358ed 100644 --- a/packages/core/src/values/Primitive.ts +++ b/packages/core/src/values/Primitive.ts @@ -1,4 +1,8 @@ -import { MAX_NODES, NUM_NODEMASK_BYTES } from "../consts"; +import { + MAX_NODES, + NUM_LR_NODES_PER_SEGMENT, + NUM_NODEMASK_BYTES, +} from "../consts"; import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError"; import { getBitMaskWidth, @@ -231,15 +235,17 @@ export function encodeFloatWithScale( } /** Parses a bit mask into a numeric array */ -export function parseBitMask(mask: Buffer, startValue: number = 1): number[] { - const numBits = mask.length * 8; - +export function parseBitMask( + mask: Buffer, + startValue: number = 1, + numBits: number = mask.length * 8, +): number[] { const ret: number[] = []; - for (let index = 1; index <= numBits; index++) { - const byteNum = (index - 1) >>> 3; // id / 8 - const bitNum = (index - 1) % 8; + for (let index = 0; index < numBits; index++) { + const byteNum = index >>> 3; // id / 8 + const bitNum = index % 8; if ((mask[byteNum] & (2 ** bitNum)) !== 0) { - ret.push(index + startValue - 1); + ret.push(index + startValue); } } return ret; @@ -266,10 +272,28 @@ export function parseNodeBitMask(mask: Buffer): number[] { return parseBitMask(mask.subarray(0, NUM_NODEMASK_BYTES)); } +export function parseLongRangeNodeBitMask( + mask: Buffer, + startValue: number, +): number[] { + return parseBitMask(mask, startValue); +} + export function encodeNodeBitMask(nodeIDs: readonly number[]): Buffer { return encodeBitMask(nodeIDs, MAX_NODES); } +export function encodeLongRangeNodeBitMask( + nodeIDs: readonly number[], + startValue: number, +): Buffer { + return encodeBitMask( + nodeIDs, + startValue + NUM_LR_NODES_PER_SEGMENT - 1, + startValue, + ); +} + /** * Parses a partial value from a "full" value. Example: * ```txt diff --git a/packages/serial/src/message/Constants.ts b/packages/serial/src/message/Constants.ts index 336a3a92254f..a87f3499956f 100644 --- a/packages/serial/src/message/Constants.ts +++ b/packages/serial/src/message/Constants.ts @@ -52,7 +52,7 @@ export enum FunctionType { UNKNOWN_FUNC_MEMORY_PUT_BUFFER = 0x24, EnterBootloader = 0x27, // Leave Serial API and enter bootloader (700+ series only). Enter Auto-Programming mode (500 series only). - UNKNOWN_FUNC_UNKNOWN_0x28 = 0x28, // ?? + UNKNOWN_FUNC_UNKNOWN_0x28 = 0x28, // ZW_NVRGetValue(offset, length) => NVRdata[], see INS13954-13 GetNVMId = 0x29, // Returns information about the external NVM ExtNVMReadLongBuffer = 0x2a, // Reads a buffer from the external NVM @@ -174,6 +174,12 @@ export enum FunctionType { Shutdown = 0xd9, // Instruct the Z-Wave API to shut down in order to safely remove the power + // Long range controller support + GetLongRangeNodes = 0xda, // Used after GetSerialApiInitData to get the nodes with IDs > 0xFF + GetLongRangeChannel = 0xdb, + SetLongRangeChannel = 0xdc, + SetLongRangeShadowNodeIDs = 0xdd, + UNKNOWN_FUNC_UNKNOWN_0xEF = 0xef, // ?? // Special commands for Z-Wave.me sticks diff --git a/packages/serial/src/message/Message.ts b/packages/serial/src/message/Message.ts index 4ef2e7d83240..3cffa084ab82 100644 --- a/packages/serial/src/message/Message.ts +++ b/packages/serial/src/message/Message.ts @@ -38,6 +38,8 @@ export interface MessageDeserializationOptions { parseCCs?: boolean; /** If known already, this contains the SDK version of the stick which can be used to interpret payloads differently */ sdkVersion?: string; + /** Optional context used during deserialization */ + context?: unknown; } /** @@ -251,8 +253,19 @@ export class Message { public static from( host: ZWaveHost, options: MessageDeserializationOptions, + contextStore?: Map>, ): Message { const Constructor = Message.getConstructor(options.data); + + // Take the context out of the context store if it exists + if (contextStore) { + const functionType = getFunctionTypeStatic(Constructor)!; + if (contextStore.has(functionType)) { + options.context = contextStore.get(functionType)!; + contextStore.delete(functionType); + } + } + const ret = new Constructor(host, options); return ret; } diff --git a/packages/zwave-js/src/Utils.ts b/packages/zwave-js/src/Utils.ts index 0c4a2d530896..ebbb5a1b2afa 100644 --- a/packages/zwave-js/src/Utils.ts +++ b/packages/zwave-js/src/Utils.ts @@ -1,5 +1,10 @@ export { + ProtocolDataRate, + ProtocolType, + ProtocolVersion, + Protocols, QRCodeVersion, + RouteProtocolDataRate, extractFirmware, guessFirmwareFileFormat, parseQRCodeString, @@ -8,12 +13,7 @@ export { export type { Firmware, FirmwareFileFormat, - ProtocolDataRate, - ProtocolType, - ProtocolVersion, - Protocols, QRProvisioningInformation, - RouteProtocolDataRate, protocolDataRateToString, } from "@zwave-js/core"; export { diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 4428f4170567..41ea6ac2edfe 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -34,6 +34,7 @@ import { ControllerStatus, EMPTY_ROUTE, type Firmware, + LongRangeChannel, MAX_NODES, type MaybeNotKnown, type MaybeUnknown, @@ -43,6 +44,7 @@ import { NodeType, type ProtocolDataRate, ProtocolType, + Protocols, RFRegion, type RSSI, type Route, @@ -65,6 +67,7 @@ import { encodeX25519KeyDERSPKI, indexDBsByNode, isEmptyRoute, + isLongRangeNodeId, isValidDSK, isZWaveError, nwiHomeIdFromDSK, @@ -122,6 +125,7 @@ import { ApplicationUpdateRequestNodeInfoReceived, ApplicationUpdateRequestNodeRemoved, ApplicationUpdateRequestSmartStartHomeIDReceived, + ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived, } from "../serialapi/application/ApplicationUpdateRequest"; import { type SerialAPIStartedRequest, @@ -139,6 +143,10 @@ import { GetControllerVersionRequest, type GetControllerVersionResponse, } from "../serialapi/capability/GetControllerVersionMessages"; +import { + GetLongRangeNodesRequest, + type GetLongRangeNodesResponse, +} from "../serialapi/capability/GetLongRangeNodesMessages"; import { GetProtocolVersionRequest, type GetProtocolVersionResponse, @@ -152,11 +160,15 @@ import { type GetSerialApiInitDataResponse, } from "../serialapi/capability/GetSerialApiInitDataMessages"; import { HardResetRequest } from "../serialapi/capability/HardResetRequest"; +import { + GetLongRangeChannelRequest, + type GetLongRangeChannelResponse, +} from "../serialapi/capability/LongRangeSetupMessages"; import { SerialAPISetupCommand, SerialAPISetup_CommandUnsupportedResponse, - SerialAPISetup_GetLRMaximumPayloadSizeRequest, - type SerialAPISetup_GetLRMaximumPayloadSizeResponse, + SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest, + type SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse, SerialAPISetup_GetMaximumPayloadSizeRequest, type SerialAPISetup_GetMaximumPayloadSizeResponse, SerialAPISetup_GetPowerlevel16BitRequest, @@ -672,6 +684,12 @@ export class ZWaveController return this._rfRegion; } + private _supportsLongRange: MaybeNotKnown; + /** Whether the controller supports the Z-Wave Long Range protocol */ + public get supportsLongRange(): MaybeNotKnown { + return this._supportsLongRange; + } + private _nodes: ThrowingMap; /** A dictionary of the nodes connected to this controller */ public get nodes(): ReadonlyThrowingMap { @@ -889,9 +907,10 @@ export class ZWaveController /** * @internal - * Queries the controller IDs and its Serial API capabilities + * Queries the controller / serial API capabilities. + * Returns a list of Z-Wave classic node IDs that are currently in the network. */ - public async identify(): Promise { + public async queryCapabilities(): Promise<{ nodeIds: readonly number[] }> { // Figure out what the serial API can do this.driver.controllerLog.print(`querying Serial API capabilities...`); const apiCaps = await this.driver.sendMessage< @@ -920,6 +939,48 @@ export class ZWaveController }`, ); + // 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} + Z-Wave Classic nodes: ${initData.nodeIds.join(", ")}`, + ); + // Get basic controller version info this.driver.controllerLog.print(`querying version info...`); const version = await this.driver.sendMessage< @@ -974,6 +1035,31 @@ export class ZWaveController // 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 + 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}`, + ); + // 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)) { @@ -1005,6 +1091,97 @@ export class ZWaveController this._supportedSerialAPISetupCommands = []; } + // Figure out the maximum payload size for outgoing commands + let maxPayloadSize: number | undefined; + if ( + this.isSerialAPISetupCommandSupported( + SerialAPISetupCommand.GetMaximumPayloadSize, + ) + ) { + this.driver.controllerLog.print(`querying max. payload size...`); + maxPayloadSize = await this.getMaxPayloadSize(); + this.driver.controllerLog.print( + `maximum payload size: ${maxPayloadSize} bytes`, + ); + // TODO: cache this information + } + + 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("") + }`, + ); + + 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 + */ + public async queryLongRangeCapabilities(): Promise<{ + lrNodeIds: readonly number[]; + }> { + this.driver.controllerLog.print( + `querying Z-Wave Long Range capabilities...`, + ); + + // Fetch the list of Long Range nodes + const lrNodeIds = await this.getLongRangeNodes(); + + let maxPayloadSizeLR: number | undefined; + if ( + this.isSerialAPISetupCommandSupported( + SerialAPISetupCommand.GetLongRangeMaximumPayloadSize, + ) + ) { + maxPayloadSizeLR = await this.getMaxPayloadSizeLongRange(); + + // TODO: cache this information + } + + let lrChannel: LongRangeChannel | undefined; + if ( + this.isFunctionSupported(FunctionType.GetLongRangeChannel) + ) { + // TODO: restore/set the channel + const lrChannelResp = await this.driver.sendMessage< + GetLongRangeChannelResponse + >(new GetLongRangeChannelRequest(this.driver)); + lrChannel = lrChannelResp.longRangeChannel; + } + + this.driver.controllerLog.print( + `received Z-Wave Long Range capabilities: + max. payload size: ${maxPayloadSizeLR} bytes + channel: ${ + lrChannel + ? getEnumMemberName(LongRangeChannel, lrChannel) + : "(unknown)" + } + nodes: ${lrNodeIds.join(", ")}`, + ); + + return { + lrNodeIds, + }; + } + + /** + * @internal + * Queries the region and powerlevel settings and configures them if necessary + */ + public async queryAndConfigureRF(): Promise { // Check and possibly update the RF region to the desired value if ( this.isSerialAPISetupCommandSupported( @@ -1029,6 +1206,7 @@ export class ZWaveController ); } } + if ( this.isSerialAPISetupCommandSupported( SerialAPISetupCommand.SetRFRegion, @@ -1120,12 +1298,13 @@ export class ZWaveController ); } } + } - // Switch to 16 bit node IDs if supported. We need to do this here, as a controller may still be - // in 16 bit mode when Z-Wave starts up. This would lead to an invalid node ID being reported. - await this.trySetNodeIDType(NodeIDType.Long); - - // get the home and node id of the controller + /** + * @internal + * Queries the home and node id of the controller + */ + public async identify(): Promise { this.driver.controllerLog.print(`querying controller IDs...`); const ids = await this.driver.sendMessage( new GetControllerIdRequest(this.driver), @@ -1142,50 +1321,9 @@ export class ZWaveController /** * @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 + * Performs additional controller configuration */ - public async interview( - restoreFromCache: () => Promise, - ): Promise { - 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}`, - ); - + public async configure(): Promise { // Enable TX status report if supported if ( this.isSerialAPISetupCommandSupported( @@ -1266,55 +1404,45 @@ export class ZWaveController // 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(", ")}`, - ); + 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`, + ); + } + } + /** + * @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 initNodes( + classicNodeIds: readonly number[], + lrNodeIds: readonly number[], + restoreFromCache: () => Promise, + ): Promise { // 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 - const nodeIds = [...initData.nodeIds]; + + const nodeIds = [...classicNodeIds]; if (nodeIds.length === 0) { this.driver.controllerLog.print( `Controller reports no nodes in its network. This could be an indication of a corrupted controller memory.`, @@ -1322,7 +1450,9 @@ export class ZWaveController ); nodeIds.unshift(this._ownNodeId!); } + nodeIds.push(...lrNodeIds); + // For each node, create an empty entry in the nodes map so we can initialize them afterwards for (const nodeId of nodeIds) { this._nodes.set( nodeId, @@ -1396,27 +1526,6 @@ export class ZWaveController 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"); } @@ -1429,6 +1538,31 @@ export class ZWaveController ); } + /** + * Gets the list of long range nodes from the controller. + * Warning: This only works when followed up by a hard-reset, so don't call this directly + * @internal + */ + public async getLongRangeNodes(): Promise { + const nodeIds: number[] = []; + + if (this.supportsLongRange) { + for (let segment = 0;; segment++) { + const nodesResponse = await this.driver.sendMessage< + GetLongRangeNodesResponse + >( + new GetLongRangeNodesRequest(this.driver, { + segmentNumber: segment, + }), + ); + nodeIds.push(...nodesResponse.nodeIds); + + if (!nodesResponse.moreNodes) break; + } + } + return nodeIds; + } + /** * 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 @@ -1651,16 +1785,26 @@ export class ZWaveController }; try { + // Kick off the inclusion process using either the + // specified protocol or the first supported one + const dskBuffer = dskFromString(provisioningEntry.dsk); + const protocol = provisioningEntry.protocol + ?? provisioningEntry.supportedProtocols?.[0] + ?? Protocols.ZWave; + this.driver.controllerLog.print( - `Including SmartStart node with DSK ${provisioningEntry.dsk}`, + `Including SmartStart node with DSK ${provisioningEntry.dsk}${ + protocol == Protocols.ZWaveLongRange + ? " using Z-Wave Long Range" + : "" + }`, ); - // kick off the inclusion process - const dskBuffer = dskFromString(provisioningEntry.dsk); await this.driver.sendMessage( new AddNodeDSKToNetworkRequest(this.driver, { nwiHomeId: nwiHomeIdFromDSK(dskBuffer), authHomeId: authHomeIdFromDSK(dskBuffer), + protocol, highPower: true, networkWide: true, }), @@ -2065,10 +2209,16 @@ export class ZWaveController } } else if ( msg instanceof ApplicationUpdateRequestSmartStartHomeIDReceived + || msg + instanceof ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived ) { - // the controller is in Smart Start learn mode and a node requests inclusion via Smart Start + const isLongRange = msg + instanceof ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived; + // The controller is in Smart Start learn mode and a node requests inclusion via Smart Start this.driver.controllerLog.print( - "Received Smart Start inclusion request", + `Received Smart Start inclusion request${ + isLongRange ? " (Z-Wave Long Range)" : "" + }`, ); if ( @@ -2082,11 +2232,21 @@ export class ZWaveController } // Check if the node is on the provisioning list - const provisioningEntry = this.provisioningList.find((entry) => - nwiHomeIdFromDSK(dskFromString(entry.dsk)).equals( - msg.nwiHomeId, - ) - ); + const provisioningEntry = this.provisioningList.find((entry) => { + if ( + !nwiHomeIdFromDSK(dskFromString(entry.dsk)).equals( + msg.nwiHomeId, + ) + ) { + return false; + } + // TODO: This is duplicated with the logic in beginInclusionSmartStart + const entryProtocol = entry.protocol + ?? entry.supportedProtocols?.[0] + ?? Protocols.ZWave; + return (entryProtocol === Protocols.ZWaveLongRange) + === isLongRange; + }); if (!provisioningEntry) { this.driver.controllerLog.print( "NWI Home ID not found in provisioning list, ignoring request...", @@ -2257,9 +2417,11 @@ supported CCs: ${ "no initiate command received, bootstrapping node...", ); - // Assign SUC return route to make sure the node knows where to get its routes from - newNode.hasSUCReturnRoute = await this - .assignSUCReturnRoutes(newNode.id); + if (newNode.protocol == Protocols.ZWave) { + // Assign SUC return route to make sure the node knows where to get its routes from + newNode.hasSUCReturnRoute = await this + .assignSUCReturnRoutes(newNode.id); + } // Include using the default inclusion strategy: // * Use S2 if possible, @@ -3412,10 +3574,13 @@ supported CCs: ${ // If it is actually a sleeping device, it will be marked as such later newNode.markAsAlive(); - // Assign SUC return route to make sure the node knows where to get its routes from - newNode.hasSUCReturnRoute = await this.assignSUCReturnRoutes( - newNode.id, - ); + if (newNode.protocol == Protocols.ZWave) { + // Assign SUC return route to make sure the node knows where to get its routes from + newNode.hasSUCReturnRoute = await this + .assignSUCReturnRoutes( + newNode.id, + ); + } const opts = this._inclusionOptions; @@ -3683,9 +3848,11 @@ supported CCs: ${ // If it is actually a sleeping device, it will be marked as such later newNode.markAsAlive(); - // Assign SUC return route to make sure the node knows where to get its routes from - newNode.hasSUCReturnRoute = await this - .assignSUCReturnRoutes(newNode.id); + if (newNode.protocol == Protocols.ZWave) { + // Assign SUC return route to make sure the node knows where to get its routes from + newNode.hasSUCReturnRoute = await this + .assignSUCReturnRoutes(newNode.id); + } // Try perform the security bootstrap process. When replacing a node, we don't know any supported CCs // yet, so we need to trust the chosen inclusion strategy. @@ -3985,6 +4152,9 @@ supported CCs: ${ const todoSleeping: number[] = []; const addTodo = (nodeId: number) => { + // Z-Wave Long Range does not route + if (isLongRangeNodeId(nodeId)) return; + if (pendingNodes.has(nodeId)) { pendingNodes.delete(nodeId); const node = this.nodes.getOrThrow(nodeId); @@ -4119,6 +4289,13 @@ supported CCs: ${ } const node = this.nodes.getOrThrow(nodeId); + // Z-Wave Long Range does not route + if (node.protocol == Protocols.ZWaveLongRange) { + throw new ZWaveError( + `Cannot rebuild routes for nodes using Z-Wave Long Range!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } // Don't start the process twice if (this._isRebuildingRoutes) { @@ -4355,6 +4532,15 @@ ${associatedNodes.join(", ")}`, * This will assign up to 4 routes, depending on the network topology (that the controller knows about). */ public async assignSUCReturnRoutes(nodeId: number): Promise { + if (isLongRangeNodeId(nodeId)) { + this.driver.controllerLog.logNode( + nodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } + this.driver.controllerLog.logNode(nodeId, { message: `Assigning SUC return route...`, direction: "outbound", @@ -4442,6 +4628,15 @@ ${associatedNodes.join(", ")}`, routes: Route[], priorityRoute?: Route, ): Promise { + if (isLongRangeNodeId(nodeId)) { + this.driver.controllerLog.logNode( + nodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } + this.driver.controllerLog.logNode(nodeId, { message: `Assigning custom SUC return routes...`, direction: "outbound", @@ -4541,6 +4736,15 @@ ${associatedNodes.join(", ")}`, * This will assign up to 4 routes, depending on the network topology (that the controller knows about). */ public async deleteSUCReturnRoutes(nodeId: number): Promise { + if (isLongRangeNodeId(nodeId)) { + this.driver.controllerLog.logNode( + nodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } + this.driver.controllerLog.logNode(nodeId, { message: `Deleting SUC return route...`, direction: "outbound", @@ -4627,6 +4831,22 @@ ${associatedNodes.join(", ")}`, nodeId: number, destinationNodeId: number, ): Promise { + if (isLongRangeNodeId(nodeId)) { + this.driver.controllerLog.logNode( + nodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } else if (isLongRangeNodeId(destinationNodeId)) { + this.driver.controllerLog.logNode( + destinationNodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } + // Make sure this is not misused by passing the controller's node ID if (destinationNodeId === this.ownNodeId) { throw new ZWaveError( @@ -4699,6 +4919,22 @@ ${associatedNodes.join(", ")}`, routes: Route[], priorityRoute?: Route, ): Promise { + if (isLongRangeNodeId(nodeId)) { + this.driver.controllerLog.logNode( + nodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } else if (isLongRangeNodeId(destinationNodeId)) { + this.driver.controllerLog.logNode( + destinationNodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } + // Make sure this is not misused by passing the controller's node ID if (destinationNodeId === this.ownNodeId) { throw new ZWaveError( @@ -4823,6 +5059,15 @@ ${associatedNodes.join(", ")}`, * other end nodes, including the priority return routes. */ public async deleteReturnRoutes(nodeId: number): Promise { + if (isLongRangeNodeId(nodeId)) { + this.driver.controllerLog.logNode( + nodeId, + `Cannot manage routes for nodes using Z-Wave Long Range!`, + "error", + ); + return false; + } + this.driver.controllerLog.logNode(nodeId, { message: `Deleting all return routes...`, direction: "outbound", @@ -5804,9 +6049,13 @@ ${associatedNodes.join(", ")}`, */ public async getMaxPayloadSizeLongRange(): Promise { const result = await this.driver.sendMessage< - | SerialAPISetup_GetLRMaximumPayloadSizeResponse + | SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse | SerialAPISetup_CommandUnsupportedResponse - >(new SerialAPISetup_GetLRMaximumPayloadSizeRequest(this.driver)); + >( + new SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest( + this.driver, + ), + ); if (result instanceof SerialAPISetup_CommandUnsupportedResponse) { throw new ZWaveError( `Your hardware does not support getting the max. long range payload size!`, diff --git a/packages/zwave-js/src/lib/controller/Inclusion.ts b/packages/zwave-js/src/lib/controller/Inclusion.ts index e85b47d88d90..ba5313f45e52 100644 --- a/packages/zwave-js/src/lib/controller/Inclusion.ts +++ b/packages/zwave-js/src/lib/controller/Inclusion.ts @@ -1,4 +1,8 @@ -import type { CommandClasses, SecurityClass } from "@zwave-js/core/safe"; +import type { + CommandClasses, + Protocols, + SecurityClass, +} from "@zwave-js/core/safe"; import type { DeviceClass } from "../node/DeviceClass"; /** Additional information about the outcome of a node inclusion */ @@ -219,6 +223,14 @@ export interface PlannedProvisioningEntry { /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ dsk: string; + /** Which protocol to use for inclusion. Default: Z-Wave Classic */ + protocol?: Protocols; + /** + * The protocols that are **supported** by the device. + * When this is not set, applications should default to Z-Wave classic. + */ + supportedProtocols?: readonly Protocols[]; + /** The security classes that have been **granted** by the user */ securityClasses: SecurityClass[]; /** diff --git a/packages/zwave-js/src/lib/controller/utils.ts b/packages/zwave-js/src/lib/controller/utils.ts index 2b2dff68a3f1..3d8837ba5fc9 100644 --- a/packages/zwave-js/src/lib/controller/utils.ts +++ b/packages/zwave-js/src/lib/controller/utils.ts @@ -1,5 +1,6 @@ import { type MaybeNotKnown, + Protocols, SecurityClass, ZWaveError, ZWaveErrorCodes, @@ -61,6 +62,25 @@ export function assertProvisioningEntry( } } } + + if ( + arg.protocol != undefined + && (typeof arg.protocol !== "number" || !(arg.protocol in Protocols)) + ) { + throw fail("protocol is not a valid"); + } + + if (arg.supportedProtocols != undefined) { + if (!isArray(arg.supportedProtocols)) { + throw fail("supportedProtocols must be an array"); + } else if ( + !arg.supportedProtocols.every( + (p: any) => typeof p === "number" && p in Protocols, + ) + ) { + throw fail("supportedProtocols contains invalid entries"); + } + } } /** Checks if the SDK version is greater than the given one */ diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 8ab149181974..ddc73c38baf4 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -695,6 +695,16 @@ export class Driver extends TypedEventEmitter return this.nodeSessions.get(nodeId)!; } + private _requestContext: Map> = + new Map(); + /** + * @internal + * Stores data from Serial API command requests to be used by their responses + */ + public get requestContext(): Map> { + return this._requestContext; + } + public readonly cacheDir: string; private _valueDB: JsonlDB | undefined; @@ -1421,17 +1431,15 @@ export class Driver extends TypedEventEmitter } if (!this._options.testingHooks?.skipControllerIdentification) { - // Determine controller IDs to open the Value DBs - // We need to do this first because some older controllers, especially the UZB1 and - // some 500-series sticks in virtualized environments don't respond after a soft reset + // Determine what the controller can do + const { nodeIds } = await this.controller.queryCapabilities(); - // No need to initialize databases if skipInterview is true, because it is only used in some - // Driver unit tests that don't need access to them - - // Identify the controller and determine if it supports soft reset - await this.controller.identify(); - await this.initNetworkCache(this.controller.homeId!); + // Configure the radio + await this.controller.queryAndConfigureRF(); + // Soft-reset the stick if possible. + // On 700+ series, we'll also learn about whether the stick supports + // Z-Wave Long Range in the current region. const maySoftReset = this.maySoftReset(); if (this._options.features.softReset && !maySoftReset) { this.driverLog.print( @@ -1445,40 +1453,44 @@ export class Driver extends TypedEventEmitter await this.softResetInternal(false); } - // There are situations where a controller claims it has the ID 0, - // which isn't valid. In this case try again after having soft-reset the stick - // TODO: Check if this is still necessary now that we support 16-bit node IDs - if (this.controller.ownNodeId === 0 && maySoftReset) { - this.driverLog.print( - `Controller identification returned invalid node ID 0 - trying again...`, - "warn", - ); - // We might end up with a different home ID, so close the cache before re-identifying the controller - await this._networkCache?.close(); - await this.controller.identify(); - await this.initNetworkCache(this.controller.homeId!); + let lrNodeIds: readonly number[] | undefined; + if (this.controller.supportsLongRange) { + // If the controller supports ZWLR, we need to query the node IDs again + // to get the full list of nodes + lrNodeIds = (await this.controller.queryLongRangeCapabilities()) + .lrNodeIds; } - if (this.controller.ownNodeId === 0) { - this.driverLog.print( - `Controller identification returned invalid node ID 0`, - "error", - ); - await this.destroy(); - return; - } + // If the controller supports ZWLR in the current region, switch to + // 16-bit node IDs. Otherwise, make sure that it is actually using 8-bit node IDs. + await this.controller.trySetNodeIDType( + this.controller.supportsLongRange + ? NodeIDType.Long + : NodeIDType.Short, + ); + + // Now that we know the node ID type, we can identify the controller + await this.controller.identify(); + + // Perform additional configuration + await this.controller.configure(); // now that we know the home ID, we can open the databases + await this.initNetworkCache(this.controller.homeId!); await this.initValueDBs(this.controller.homeId!); await this.performCacheMigration(); - // Interview the controller. - await this._controller.interview(async () => { - // Try to restore the network information from the cache - if (process.env.NO_CACHE !== "true") { - await this.restoreNetworkStructureFromCache(); - } - }); + // Initialize all nodes and restore the data from cache + await this._controller.initNodes( + nodeIds, + lrNodeIds ?? [], + async () => { + // Try to restore the network information from the cache + if (process.env.NO_CACHE !== "true") { + await this.restoreNetworkStructureFromCache(); + } + }, + ); // Auto-enable smart start inclusion this._controller.autoProvisionSmartStart(); @@ -1604,18 +1616,6 @@ export class Driver extends TypedEventEmitter // Ping non-sleeping nodes to determine their status await node.ping(); } - - // Previous versions of zwave-js didn't configure the SUC return route. Make sure each node has one - // and remember that we did. If the node is not responsive - tough luck, try again next time - if ( - !node.hasSUCReturnRoute - && node.status !== NodeStatus.Dead - ) { - node.hasSUCReturnRoute = await this.controller - .assignSUCReturnRoutes( - node.id, - ); - } })(); } @@ -2659,8 +2659,10 @@ export class Driver extends TypedEventEmitter ).catch(() => false as const); if (waitResult) { - // Serial API did start, maybe do something with the information? + // Serial API did start this.controllerLog.print("reconnected and restarted"); + this.controller["_supportsLongRange"] = + waitResult.supportsLongRange; return true; } @@ -2686,6 +2688,8 @@ export class Driver extends TypedEventEmitter if (waitResult) { // Serial API did start, maybe do something with the information? this.controllerLog.print("Serial API started"); + this.controller["_supportsLongRange"] = + waitResult.supportsLongRange; return true; } @@ -2987,7 +2991,7 @@ export class Driver extends TypedEventEmitter msg = Message.from(this, { data, sdkVersion: this._controller?.sdkVersion, - }); + }, this._requestContext); if (isCommandClassContainer(msg)) { // Whether successful or not, a message from a node should update last seen const node = this.getNodeUnsafe(msg); diff --git a/packages/zwave-js/src/lib/driver/NetworkCache.ts b/packages/zwave-js/src/lib/driver/NetworkCache.ts index 06e18b2b96d2..90bf7c9b51ca 100644 --- a/packages/zwave-js/src/lib/driver/NetworkCache.ts +++ b/packages/zwave-js/src/lib/driver/NetworkCache.ts @@ -2,6 +2,7 @@ import type { JsonlDB } from "@alcalzone/jsonl-db"; import { type CommandClasses, NodeType, + Protocols, SecurityClass, ZWaveError, ZWaveErrorCodes, @@ -177,6 +178,15 @@ function tryParseProvisioningList( && entry.requestedSecurityClasses.every((s) => isSerializedSecurityClass(s) ))) + // protocol and supportedProtocols are stored as strings, not the enum values + && (entry.protocol == undefined + || isSerializedProtocol(entry.protocol)) + && (entry.supportedProtocols == undefined || ( + isArray(entry.supportedProtocols) + && entry.supportedProtocols.every((s) => + isSerializedProtocol(s) + ) + )) && (entry.status == undefined || isSerializedProvisioningEntryStatus(entry.status)) ) { @@ -203,6 +213,17 @@ function tryParseProvisioningList( entry.status as any ] as any as ProvisioningEntryStatus; } + if (entry.protocol != undefined) { + parsed.protocol = + Protocols[entry.protocol as any] as any as Protocols; + } + if (entry.supportedProtocols) { + parsed.supportedProtocols = ( + entry.supportedProtocols as any[] + ) + .map((s) => Protocols[s] as any as Protocols) + .filter((s): s is Protocols => s !== undefined); + } ret.push(parsed); } else { return; @@ -265,6 +286,16 @@ function isSerializedProvisioningEntryStatus( ); } +function isSerializedProtocol( + s: unknown, +): s is keyof typeof Protocols { + return ( + typeof s === "string" + && s in Protocols + && typeof Protocols[s as any] === "number" + ); +} + function tryParseDate(value: unknown): Date | undefined { // Dates are stored as timestamps if (typeof value === "number") { @@ -452,6 +483,18 @@ export function serializeNetworkCacheValue( entry.status, ); } + if (entry.protocol != undefined) { + serialized.protocol = getEnumMemberName( + Protocols, + entry.protocol, + ); + } + if (entry.supportedProtocols != undefined) { + serialized.supportedProtocols = entry.supportedProtocols + .map( + (p) => getEnumMemberName(Protocols, p), + ); + } ret.push(serialized); } return ret; diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index c292764561d0..5752ac76b6fe 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -153,6 +153,7 @@ import { NodeType, type NodeUpdatePayload, type ProtocolVersion, + Protocols, type RSSI, RssiError, SecurityClass, @@ -176,6 +177,7 @@ import { applicationCCs, getCCName, getDSTInfo, + isLongRangeNodeId, isRssiError, isSupervisionResult, isTransmissionError, @@ -193,7 +195,7 @@ import { valueIdToString, } from "@zwave-js/core"; import type { NodeSchedulePollOptions } from "@zwave-js/host"; -import type { Message } from "@zwave-js/serial"; +import { FunctionType, type Message } from "@zwave-js/serial"; import { Mixin, ObjectKeyMap, @@ -884,6 +886,13 @@ export class ZWaveNode extends Endpoint } } + /** Which protocol is used to communicate with this node */ + public get protocol(): Protocols { + return isLongRangeNodeId(this.id) + ? Protocols.ZWaveLongRange + : Protocols.ZWave; + } + /** Whether a SUC return route was configured for this node */ public get hasSUCReturnRoute(): boolean { return !!this.driver.cacheGet( @@ -1869,6 +1878,12 @@ export class ZWaveNode extends Endpoint message: "querying protocol info...", direction: "outbound", }); + // The GetNodeProtocolInfoRequest needs to know the node ID to distinguish + // between ZWLR and ZW classic. We store it on the driver's context, so it + // can be retrieved when needed. + this.driver.requestContext.set(FunctionType.GetNodeProtocolInfo, { + nodeId: this.id, + }); const resp = await this.driver.sendMessage( new GetNodeProtocolInfoRequest(this.driver, { requestedNodeId: this.id, diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 1c6c433c8f89..46b900c6108c 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts @@ -28,6 +28,7 @@ import { buffer2hex, getEnumMemberName } from "@zwave-js/shared"; export enum ApplicationUpdateTypes { SmartStart_NodeInfo_Received = 0x86, // An included smart start node has been powered up SmartStart_HomeId_Received = 0x85, // A smart start node requests inclusion + SmartStart_LongRange_HomeId_Received = 0x87, // A smart start long range note requests inclusion NodeInfo_Received = 0x84, NodeInfo_RequestDone = 0x82, NodeInfo_RequestFailed = 0x81, @@ -164,8 +165,7 @@ export class ApplicationUpdateRequestNodeRemoved public nodeId: number; } -@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_Received) -export class ApplicationUpdateRequestSmartStartHomeIDReceived +class ApplicationUpdateRequestSmartStartHomeIDReceivedBase extends ApplicationUpdateRequest { public constructor( @@ -223,3 +223,15 @@ export class ApplicationUpdateRequestSmartStartHomeIDReceived }; } } + +@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_Received) +export class ApplicationUpdateRequestSmartStartHomeIDReceived + extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase +{} + +@applicationUpdateType( + ApplicationUpdateTypes.SmartStart_LongRange_HomeId_Received, +) +export class ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived + extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase +{} diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts new file mode 100644 index 000000000000..dbbe9b26d0c1 --- /dev/null +++ b/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts @@ -0,0 +1,119 @@ +import { + MessagePriority, + NUM_LR_NODEMASK_SEGMENT_BYTES, + NUM_LR_NODES_PER_SEGMENT, + encodeLongRangeNodeBitMask, + parseLongRangeNodeBitMask, +} from "@zwave-js/core"; +import type { ZWaveHost } from "@zwave-js/host"; +import { + FunctionType, + Message, + type MessageBaseOptions, + type MessageDeserializationOptions, + MessageType, + expectedResponse, + gotDeserializationOptions, + messageTypes, + priority, +} from "@zwave-js/serial"; + +export interface GetLongRangeNodesRequestOptions extends MessageBaseOptions { + segmentNumber: number; +} + +@messageTypes(MessageType.Request, FunctionType.GetLongRangeNodes) +@expectedResponse(FunctionType.GetLongRangeNodes) +@priority(MessagePriority.Controller) +export class GetLongRangeNodesRequest extends Message { + public constructor( + host: ZWaveHost, + options: + | MessageDeserializationOptions + | GetLongRangeNodesRequestOptions, + ) { + super(host, options); + + if (gotDeserializationOptions(options)) { + this.segmentNumber = this.payload[0]; + } else { + this.segmentNumber = options.segmentNumber; + } + } + + public segmentNumber: number; + + public serialize(): Buffer { + this.payload = Buffer.from([this.segmentNumber]); + return super.serialize(); + } +} + +export interface GetLongRangeNodesResponseOptions extends MessageBaseOptions { + moreNodes: boolean; + segmentNumber: number; + nodeIds: number[]; +} + +@messageTypes(MessageType.Response, FunctionType.GetLongRangeNodes) +export class GetLongRangeNodesResponse extends Message { + public constructor( + host: ZWaveHost, + options: + | MessageDeserializationOptions + | GetLongRangeNodesResponseOptions, + ) { + super(host, options); + + if (gotDeserializationOptions(options)) { + this.moreNodes = this.payload[0] != 0; + this.segmentNumber = this.payload[1]; + const listLength = this.payload[2]; + + const listStart = 3; + const listEnd = listStart + listLength; + if (listEnd <= this.payload.length) { + const nodeBitMask = this.payload.subarray( + listStart, + listEnd, + ); + this.nodeIds = parseLongRangeNodeBitMask( + nodeBitMask, + this.listStartNode(), + ); + } else { + this.nodeIds = []; + } + } else { + this.moreNodes = options.moreNodes; + this.segmentNumber = options.segmentNumber; + this.nodeIds = options.nodeIds; + } + } + + public moreNodes: boolean; + public segmentNumber: number; + public nodeIds: readonly number[]; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe( + 3 + NUM_LR_NODEMASK_SEGMENT_BYTES, + ); + + this.payload[0] = this.moreNodes ? 1 : 0; + this.payload[1] = this.segmentNumber; + this.payload[2] = NUM_LR_NODEMASK_SEGMENT_BYTES; + + const nodeBitMask = encodeLongRangeNodeBitMask( + this.nodeIds, + this.listStartNode(), + ); + nodeBitMask.copy(this.payload, 3); + + return super.serialize(); + } + + private listStartNode(): number { + return 256 + NUM_LR_NODES_PER_SEGMENT * this.segmentNumber; + } +} diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts new file mode 100644 index 000000000000..30d30ce81ae0 --- /dev/null +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -0,0 +1,150 @@ +import { MessagePriority, encodeBitMask, parseBitMask } from "@zwave-js/core"; +import type { ZWaveHost } from "@zwave-js/host"; +import type { SuccessIndicator } from "@zwave-js/serial"; +import { + FunctionType, + Message, + type MessageBaseOptions, + type MessageDeserializationOptions, + MessageType, + expectedResponse, + gotDeserializationOptions, + messageTypes, + priority, +} from "@zwave-js/serial"; + +import { LongRangeChannel } from "@zwave-js/core"; + +@messageTypes(MessageType.Request, FunctionType.GetLongRangeChannel) +@expectedResponse(FunctionType.GetLongRangeChannel) +@priority(MessagePriority.Controller) +export class GetLongRangeChannelRequest extends Message {} + +export interface LongRangeChannelResponseOptions extends MessageBaseOptions { + longRangeChannel: LongRangeChannel; +} + +export class LongRangeChannelMessageBase extends Message { + public constructor( + host: ZWaveHost, + options: + | MessageDeserializationOptions + | LongRangeChannelResponseOptions, + ) { + super(host, options); + + if (gotDeserializationOptions(options)) { + switch (this.payload[0]) { + case 0x01: + this.longRangeChannel = LongRangeChannel.A; + break; + case 0x02: + this.longRangeChannel = LongRangeChannel.B; + break; + default: + this.longRangeChannel = LongRangeChannel.Unknown; + break; + } + } else { + this.longRangeChannel = options.longRangeChannel; + } + } + + public longRangeChannel: LongRangeChannel; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe(1); + + switch (this.longRangeChannel) { + default: // Don't use reserved values, default back to A + case LongRangeChannel.A: + this.payload[0] = LongRangeChannel.A; + break; + + case LongRangeChannel.B: + this.payload[0] = LongRangeChannel.B; + } + + return super.serialize(); + } +} + +@messageTypes(MessageType.Response, FunctionType.GetLongRangeChannel) +@priority(MessagePriority.Controller) +export class GetLongRangeChannelResponse extends LongRangeChannelMessageBase {} + +@messageTypes(MessageType.Request, FunctionType.SetLongRangeChannel) +@expectedResponse(FunctionType.SetLongRangeChannel) +@priority(MessagePriority.Controller) +export class SetLongRangeChannelRequest extends LongRangeChannelMessageBase { +} + +export interface SetLongRangeChannelResponseOptions extends MessageBaseOptions { + responseStatus: number; +} + +@messageTypes(MessageType.Response, FunctionType.SetLongRangeChannel) +export class SetLongRangeChannelResponse extends Message + implements SuccessIndicator +{ + public constructor( + host: ZWaveHost, + options: MessageDeserializationOptions, + ) { + super(host, options); + this.success = this.payload[0] !== 0; + } + + public readonly success: boolean; + + public isOK(): boolean { + return this.success; + } +} + +export interface LongRangeShadowNodeIDsRequestOptions + extends MessageBaseOptions +{ + shadowNodeIds: number[]; +} + +const LONG_RANGE_SHADOW_NODE_IDS_START = 2002; +const NUM_LONG_RANGE_SHADOW_NODE_IDS = 4; + +@messageTypes(MessageType.Request, FunctionType.SetLongRangeShadowNodeIDs) +@priority(MessagePriority.Controller) +export class SetLongRangeShadowNodeIDsRequest extends Message { + public constructor( + host: ZWaveHost, + options: + | MessageDeserializationOptions + | LongRangeShadowNodeIDsRequestOptions, + ) { + super(host, options); + + if (gotDeserializationOptions(options)) { + this.shadowNodeIds = parseBitMask( + this.payload.subarray(0, 1), + LONG_RANGE_SHADOW_NODE_IDS_START, + NUM_LONG_RANGE_SHADOW_NODE_IDS, + ); + } else { + this.shadowNodeIds = options.shadowNodeIds; + } + } + + public shadowNodeIds: number[]; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe(1); + this.payload = encodeBitMask( + this.shadowNodeIds, + LONG_RANGE_SHADOW_NODE_IDS_START + + NUM_LONG_RANGE_SHADOW_NODE_IDS + - 1, + LONG_RANGE_SHADOW_NODE_IDS_START, + ); + + return super.serialize(); + } +} diff --git a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts index fc07c29be60e..a75eef6dc45d 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts @@ -34,17 +34,16 @@ export enum SerialAPISetupCommand { Unsupported = 0x00, GetSupportedCommands = 0x01, SetTxStatusReport = 0x02, + SetLongRangeMaximumTxPower = 0x03, SetPowerlevel = 0x04, + GetLongRangeMaximumTxPower = 0x05, GetPowerlevel = 0x08, GetMaximumPayloadSize = 0x10, GetRFRegion = 0x20, SetRFRegion = 0x40, SetNodeIDType = 0x80, - // These are added "inbetween" the existing commands - SetLRMaximumTxPower = 0x03, - GetLRMaximumTxPower = 0x05, - GetLRMaximumPayloadSize = 0x11, + GetLongRangeMaximumPayloadSize = 0x11, SetPowerlevel16Bit = 0x12, GetPowerlevel16Bit = 0x13, } @@ -787,18 +786,18 @@ export class SerialAPISetup_SetPowerlevel16BitResponse // ============================================================================= -@subCommandRequest(SerialAPISetupCommand.GetLRMaximumTxPower) -export class SerialAPISetup_GetLRMaximumTxPowerRequest +@subCommandRequest(SerialAPISetupCommand.GetLongRangeMaximumTxPower) +export class SerialAPISetup_GetLongRangeMaximumTxPowerRequest extends SerialAPISetupRequest { public constructor(host: ZWaveHost, options?: MessageOptions) { super(host, options); - this.command = SerialAPISetupCommand.GetLRMaximumTxPower; + this.command = SerialAPISetupCommand.GetLongRangeMaximumTxPower; } } -@subCommandResponse(SerialAPISetupCommand.GetLRMaximumTxPower) -export class SerialAPISetup_GetLRMaximumTxPowerResponse +@subCommandResponse(SerialAPISetupCommand.GetLongRangeMaximumTxPower) +export class SerialAPISetup_GetLongRangeMaximumTxPowerResponse extends SerialAPISetupResponse { public constructor( @@ -828,24 +827,24 @@ export class SerialAPISetup_GetLRMaximumTxPowerResponse // ============================================================================= -export interface SerialAPISetup_SetLRMaximumTxPowerOptions +export interface SerialAPISetup_SetLongRangeMaximumTxPowerOptions extends MessageBaseOptions { limit: number; } -@subCommandRequest(SerialAPISetupCommand.SetLRMaximumTxPower) -export class SerialAPISetup_SetLRMaximumTxPowerRequest +@subCommandRequest(SerialAPISetupCommand.SetLongRangeMaximumTxPower) +export class SerialAPISetup_SetLongRangeMaximumTxPowerRequest extends SerialAPISetupRequest { public constructor( host: ZWaveHost, options: | MessageDeserializationOptions - | SerialAPISetup_SetLRMaximumTxPowerOptions, + | SerialAPISetup_SetLongRangeMaximumTxPowerOptions, ) { super(host, options); - this.command = SerialAPISetupCommand.SetLRMaximumTxPower; + this.command = SerialAPISetupCommand.SetLongRangeMaximumTxPower; if (gotDeserializationOptions(options)) { throw new ZWaveError( @@ -886,8 +885,8 @@ export class SerialAPISetup_SetLRMaximumTxPowerRequest } } -@subCommandResponse(SerialAPISetupCommand.SetLRMaximumTxPower) -export class SerialAPISetup_SetLRMaximumTxPowerResponse +@subCommandResponse(SerialAPISetupCommand.SetLongRangeMaximumTxPower) +export class SerialAPISetup_SetLongRangeMaximumTxPowerResponse extends SerialAPISetupResponse implements SuccessIndicator { @@ -951,18 +950,18 @@ export class SerialAPISetup_GetMaximumPayloadSizeResponse // ============================================================================= -@subCommandRequest(SerialAPISetupCommand.GetLRMaximumPayloadSize) -export class SerialAPISetup_GetLRMaximumPayloadSizeRequest +@subCommandRequest(SerialAPISetupCommand.GetLongRangeMaximumPayloadSize) +export class SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest extends SerialAPISetupRequest { public constructor(host: ZWaveHost, options?: MessageOptions) { super(host, options); - this.command = SerialAPISetupCommand.GetLRMaximumPayloadSize; + this.command = SerialAPISetupCommand.GetLongRangeMaximumPayloadSize; } } -@subCommandResponse(SerialAPISetupCommand.GetLRMaximumPayloadSize) -export class SerialAPISetup_GetLRMaximumPayloadSizeResponse +@subCommandResponse(SerialAPISetupCommand.GetLongRangeMaximumPayloadSize) +export class SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse extends SerialAPISetupResponse { public constructor( diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts index 52b54348e29d..2e9cbca15f72 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts @@ -4,6 +4,7 @@ import { MessagePriority, type MessageRecord, NodeType, + Protocols, parseNodeID, parseNodeUpdatePayload, } from "@zwave-js/core"; @@ -48,6 +49,7 @@ export enum AddNodeStatus { enum AddNodeFlags { HighPower = 0x80, NetworkWide = 0x40, + ProtocolLongRange = 0x20, } interface AddNodeToNetworkRequestOptions extends MessageBaseOptions { @@ -61,6 +63,7 @@ interface AddNodeDSKToNetworkRequestOptions extends MessageBaseOptions { authHomeId: Buffer; highPower?: boolean; networkWide?: boolean; + protocol?: Protocols; } export function computeNeighborDiscoveryTimeout( @@ -210,20 +213,26 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { this.authHomeId = options.authHomeId; this.highPower = !!options.highPower; this.networkWide = !!options.networkWide; + this.protocol = options.protocol ?? Protocols.ZWave; } /** The home IDs of node to add */ public nwiHomeId: Buffer; public authHomeId: Buffer; /** Whether to use high power */ - public highPower: boolean = false; + public highPower: boolean; /** Whether to include network wide */ - public networkWide: boolean = false; + public networkWide: boolean; + /** Whether to include as long-range or not */ + public protocol: Protocols; public serialize(): Buffer { let control: number = AddNodeType.SmartStartDSK; if (this.highPower) control |= AddNodeFlags.HighPower; if (this.networkWide) control |= AddNodeFlags.NetworkWide; + if (this.protocol === Protocols.ZWaveLongRange) { + control |= AddNodeFlags.ProtocolLongRange; + } this.payload = Buffer.concat([ Buffer.from([control, this.callbackId]), @@ -240,6 +249,9 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { "NWI Home ID": buffer2hex(this.nwiHomeId), "high power": this.highPower, "network wide": this.networkWide, + protocol: this.protocol === Protocols.ZWaveLongRange + ? "Z-Wave Long Range" + : "Z-Wave Classic", }; if (this.hasCallbackId()) { message["callback id"] = this.callbackId; diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts index 63883ab4e46c..2596f6713fb7 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts @@ -7,6 +7,8 @@ import { type ProtocolVersion, encodeNodeID, encodeNodeProtocolInfo, + isLongRangeNodeId, + parseNodeID, parseNodeProtocolInfo, } from "@zwave-js/core"; import type { ZWaveHost } from "@zwave-js/host"; @@ -21,6 +23,7 @@ import { messageTypes, priority, } from "@zwave-js/serial"; +import { isObject } from "alcalzone-shared/typeguards"; interface GetNodeProtocolInfoRequestOptions extends MessageBaseOptions { requestedNodeId: number; @@ -38,7 +41,8 @@ export class GetNodeProtocolInfoRequest extends Message { ) { super(host, options); if (gotDeserializationOptions(options)) { - this.requestedNodeId = this.payload[0]; + this.requestedNodeId = + parseNodeID(this.payload, this.host.nodeIdType, 0).nodeId; } else { this.requestedNodeId = options.requestedNodeId; } @@ -69,9 +73,21 @@ export class GetNodeProtocolInfoResponse extends Message { super(host, options); if (gotDeserializationOptions(options)) { + // The context should contain the node ID the protocol info was requested for. + // We use it here to determine whether the node is long range. + let isLongRange = false; + if ( + isObject(options.context) + && "nodeId" in options.context + && typeof options.context.nodeId === "number" + ) { + isLongRange = isLongRangeNodeId(options.context.nodeId); + } + const { hasSpecificDeviceClass, ...rest } = parseNodeProtocolInfo( this.payload, 0, + isLongRange, ); this.isListening = rest.isListening; this.isFrequentListening = rest.isFrequentListening;