From 666257e4962007bf144ae56887958d1b426883b4 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 09:11:16 -0500 Subject: [PATCH 01/43] feat(workaround): zst39 workaround for corrupted soft reset ack --- packages/serial/src/parsers/SerialAPIParser.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/serial/src/parsers/SerialAPIParser.ts b/packages/serial/src/parsers/SerialAPIParser.ts index 5a0f927e6c74..d8093514fc90 100644 --- a/packages/serial/src/parsers/SerialAPIParser.ts +++ b/packages/serial/src/parsers/SerialAPIParser.ts @@ -44,6 +44,12 @@ export class SerialAPIParser extends Transform { this.push(MessageHeaders.ACK); break; } + case 0x86: { + // This is _maybe_ a corrupted ACK byte from a ZST39 after a soft reset, transform it and keep it + this.logger?.ACK("inbound"); + this.push(MessageHeaders.ACK); + break; + } case MessageHeaders.NAK: { this.logger?.NAK("inbound"); this.push(MessageHeaders.NAK); From acfb9ead8b2beb335873ecd32cea4dcc1a62f922 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Fri, 13 Oct 2023 18:43:22 -0500 Subject: [PATCH 02/43] feat(longrange): add FunctionType values --- packages/serial/src/message/Constants.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/serial/src/message/Constants.ts b/packages/serial/src/message/Constants.ts index 336a3a92254f..caa44417b288 100644 --- a/packages/serial/src/message/Constants.ts +++ b/packages/serial/src/message/Constants.ts @@ -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, + GetLongRangeChannel = 0xdb, + SetLongRangeChannel = 0xdc, + SetLongRangeShadowNodeIDs = 0xdd, + UNKNOWN_FUNC_UNKNOWN_0xEF = 0xef, // ?? // Special commands for Z-Wave.me sticks From 5f81ca90d02dc527230bec9175d3882159d37d70 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:16:57 -0500 Subject: [PATCH 03/43] fix(functype): add comment about source of FunctionType=0x28 --- packages/serial/src/message/Constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/serial/src/message/Constants.ts b/packages/serial/src/message/Constants.ts index caa44417b288..4f2edeb4c7b0 100644 --- a/packages/serial/src/message/Constants.ts +++ b/packages/serial/src/message/Constants.ts @@ -52,8 +52,8 @@ 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 ExtNVMWriteLongBuffer = 0x2b, // Writes a buffer to the external NVM From b4ddd897e5e11a18652588724d786cce7b8478fa Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sat, 14 Oct 2023 06:38:42 -0500 Subject: [PATCH 04/43] feat(longrange): support encoding/parsing node information These are the new function types for the long range controllers --- packages/core/src/capabilities/NodeInfo.ts | 159 ++++++++++++++------- 1 file changed, 107 insertions(+), 52 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index 67a7f265e55b..15574c4d375f 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -114,7 +114,7 @@ export function encodeCCId( } } -export function parseCCList(payload: Buffer): { +export function parseCCList(payload: Buffer, isLongRange: boolean = false): { supportedCCs: CommandClasses[]; controlledCCs: CommandClasses[]; } { @@ -124,32 +124,48 @@ export function parseCCList(payload: Buffer): { }; let offset = 0; let isAfterMark = false; - while (offset < payload.length) { + let listEnd = payload.length; + if (isLongRange) { + validatePayload(payload.length >= offset + 1); + const listLength = payload[offset++]; + listEnd = offset + listLength; + validatePayload(payload.length >= listEnd); + } + while (offset < listEnd) { // Read either the normal or extended ccId const { ccId: cc, bytesRead } = parseCCId(payload, offset); offset += bytesRead; // CCs before the support/control mark are supported // CCs after the support/control mark are controlled + // BUGBUG: does the "mark" and support/control convention apply to isLongRange? if (cc === CommandClasses["Support/Control Mark"]) { isAfterMark = true; continue; } (isAfterMark ? ret.controlledCCs : ret.supportedCCs).push(cc); } + // BUGBUG: isLongRange prohibits CC from 0x00..0x20 from being advertised here, as does 4.3.2.1.1.17 + // BUGBUG: how do >0xFF CC get advertised? I don't immediately see a mechanism for indicating a multi-byte CC return ret; } export function encodeCCList( supportedCCs: readonly CommandClasses[], controlledCCs: readonly CommandClasses[], + isLongRange: boolean = false, ): Buffer { const bufferLength = - sum(supportedCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))) + (isLongRange ? 1 : 0) + + sum(supportedCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))) + (controlledCCs.length > 0 ? 1 : 0) // support/control mark + sum(controlledCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))); const ret = Buffer.allocUnsafe(bufferLength); let offset = 0; + if (isLongRange) { + // BUGBUG: validate bufferLength - 1 is <= 0xFF + ret[offset++] = bufferLength - 1; + } for (const cc of supportedCCs) { offset += encodeCCId(cc, ret, offset); } @@ -215,34 +231,47 @@ export type NodeInformationFrame = export function parseNodeProtocolInfo( buffer: Buffer, offset: number, + isLongRange: boolean, ): NodeProtocolInfo { validatePayload(buffer.length >= offset + 3); const isListening = !!(buffer[offset] & 0b10_000_000); - const isRouting = !!(buffer[offset] & 0b01_000_000); + let isRouting = false; + if (!isLongRange) { + 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); + if (isLongRange) { + const speedExtension = buffer[offset + 2] & 0b111; + if (speedExtension & 0b010) { + supportedDataRates.push(100000); + } + } else { + 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 protocolVersion = buffer[offset] & 0b111; + // BUGBUG: what's the correct protocol version here for long range? + const protocolVersion = isLongRange ? 0 + : buffer[offset] & 0b111; const capability = buffer[offset + 1]; - const optionalFunctionality = !!(capability & 0b1000_0000); + const optionalFunctionality = (!isLongRange) && !!(capability & 0b1000_0000); let isFrequentListening: FLiRS; - switch (capability & 0b0110_0000) { + switch (capability & (isLongRange ? 0b0100_0000 : 0b0110_0000)) { case 0b0100_0000: isFrequentListening = "1000ms"; break; @@ -252,21 +281,26 @@ export function parseNodeProtocolInfo( default: isFrequentListening = false; } - const supportsBeaming = !!(capability & 0b0001_0000); + const supportsBeaming = (!isLongRange) && !!(capability & 0b0001_0000); let nodeType: NodeType; - switch (capability & 0b1010) { - case 0b1000: + + switch (isLongRange ? (0b1_0000_0000 | (capability & 0b0010)) : (capability && 0b1010)) { + case 0b0_0000_1000: + case 0b1_0000_0000: nodeType = NodeType["End Node"]; break; - case 0b0010: + case 0b0_0000_0010: + case 0b1_0000_0010: default: + // BUGBUG: is Controller correct for default && isLongRange? nodeType = NodeType.Controller; break; } - const hasSpecificDeviceClass = !!(capability & 0b100); - const supportsSecurity = !!(capability & 0b1); + const hasSpecificDeviceClass = isLongRange || !!(capability & 0b100); + // BUGBUG: can we assume security is true? + const supportsSecurity = isLongRange || !!(capability & 0b1); return { isListening, @@ -282,39 +316,53 @@ export function parseNodeProtocolInfo( }; } -export function encodeNodeProtocolInfo(info: NodeProtocolInfo): Buffer { +export function encodeNodeProtocolInfo(info: NodeProtocolInfo, isLongRange: boolean = false): Buffer { 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; - ret[0] |= info.protocolVersion & 0b111; + if (isLongRange) { + if (info.supportedDataRates.includes(100000)) ret[2] |= 0b010; + } else { + 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; + ret[0] |= info.protocolVersion & 0b111; + } // Byte 1 - if (info.optionalFunctionality) ret[1] |= 0b1000_0000; + if (!isLongRange) { + if (info.optionalFunctionality) ret[1] |= 0b1000_0000; + } if (info.isFrequentListening === "1000ms") ret[1] |= 0b0100_0000; - else if (info.isFrequentListening === "250ms") ret[1] |= 0b0010_0000; + else if (!isLongRange && info.isFrequentListening === "250ms") ret[1] |= 0b0010_0000; - if (info.supportsBeaming) ret[1] |= 0b0001_0000; - if (info.supportsSecurity) ret[1] |= 0b1; - if (info.nodeType === NodeType["End Node"]) ret[1] |= 0b1000; - else ret[1] |= 0b0010; // Controller + if (!isLongRange) { + if (info.supportsBeaming) ret[1] |= 0b0001_0000; + if (info.supportsSecurity) ret[1] |= 0b1; + } - if (info.hasSpecificDeviceClass) ret[1] |= 0b100; + if (info.nodeType === NodeType["End Node"]) { + if (!isLongRange) ret[1] |= 0b1000; + } else ret[1] |= 0b0010; // Controller + + if (!isLongRange && info.hasSpecificDeviceClass) ret[1] |= 0b100; 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); + const protocolInfo = parseNodeProtocolInfo(buffer, 0, isLongRange); let offset = 3; - const basic = buffer[offset++]; + // BUGBUG: 4.3.2.1.1.14 says this is omitted if the Controller field is set to 0, yet we always parse it? + let basic = 0x100; // BUGBUG: is there an assume one here, or...? + if (!isLongRange) { + basic = buffer[offset++]; + } const generic = buffer[offset++]; let specific = 0; if (protocolInfo.hasSpecificDeviceClass) { @@ -334,24 +382,31 @@ export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer): { export function encodeNodeProtocolInfoAndDeviceClass( info: NodeProtocolInfoAndDeviceClass, + isLongRange: boolean = false ): Buffer { + const deviceClasses = isLongRange ? + Buffer.from([ + info.genericDeviceClass, + info.specificDeviceClass, + ]) : Buffer.from([ + info.basicDeviceClass, + info.genericDeviceClass, + info.specificDeviceClass, + ]); return Buffer.concat([ encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), - Buffer.from([ - info.basicDeviceClass, - info.genericDeviceClass, - info.specificDeviceClass, - ]), + deviceClasses, ]); } export function parseNodeInformationFrame( buffer: Buffer, + isLongRange: boolean = false, ): NodeInformationFrame { const { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( - buffer, + buffer, isLongRange ); - const supportedCCs = parseCCList(buffer.subarray(offset)).supportedCCs; + const supportedCCs = parseCCList(buffer.subarray(offset), isLongRange).supportedCCs; return { ...info, @@ -359,10 +414,10 @@ export function parseNodeInformationFrame( }; } -export function encodeNodeInformationFrame(info: NodeInformationFrame): Buffer { +export function encodeNodeInformationFrame(info: NodeInformationFrame, isLongRange: boolean = false): Buffer { return Buffer.concat([ - encodeNodeProtocolInfoAndDeviceClass(info), - encodeCCList(info.supportedCCs, []), + encodeNodeProtocolInfoAndDeviceClass(info, isLongRange), + encodeCCList(info.supportedCCs, [], isLongRange), ]); } From 5eb0762c1e8ca96ab12aaad5fa0b0d55d494f066 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:15:39 -0500 Subject: [PATCH 05/43] feat(longrange): handle node ids > 256 in S2 auth data --- packages/cc/src/cc/Security2CC.ts | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index 6042efa698c1..b54ebbfdd582 100644 --- a/packages/cc/src/cc/Security2CC.ts +++ b/packages/cc/src/cc/Security2CC.ts @@ -94,13 +94,26 @@ 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 = (sendingNodeId < 256 && destination < 256) ? 1 : 2; + const ret = Buffer.allocUnsafe(2*nodeIdSize + 6 + unencryptedPayload.length); + let offset = 0; + if (nodeIdSize == 1) { + ret[offset++] = sendingNodeId; + ret[offset++] = destination; + + } else { + ret.writeUint16BE(sendingNodeId, offset); + offset += 2; + ret.writeUint16BE(destination, offset); + offset += 2; + } + + 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; } From fcaacb32251c7ee2d071ad262d8ad043a7acdac0 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sat, 14 Oct 2023 06:39:42 -0500 Subject: [PATCH 06/43] feat(longrange): add long range command class --- packages/cc/src/cc/ZWaveLRProtocolCC.ts | 236 ++++++++++++++++++ packages/cc/src/cc/index.ts | 13 + packages/cc/src/lib/_Types.ts | 16 ++ .../core/src/capabilities/CommandClasses.ts | 1 + packages/zwave-js/src/lib/driver/Driver.ts | 1 + 5 files changed, 267 insertions(+) create mode 100644 packages/cc/src/cc/ZWaveLRProtocolCC.ts diff --git a/packages/cc/src/cc/ZWaveLRProtocolCC.ts b/packages/cc/src/cc/ZWaveLRProtocolCC.ts new file mode 100644 index 000000000000..5b83c7164295 --- /dev/null +++ b/packages/cc/src/cc/ZWaveLRProtocolCC.ts @@ -0,0 +1,236 @@ +import { + CommandClasses, + type DataRate, + type FLiRS, + type NodeInformationFrame, + type NodeType, + type ProtocolVersion, + ZWaveError, + ZWaveErrorCodes, + //parseBitMask, + encodeNodeInformationFrame, + parseNodeInformationFrame, + validatePayload, +} from "@zwave-js/core"; +import type { ZWaveHost } from "@zwave-js/host"; +import { + type CCCommandOptions, + CommandClass, + type CommandClassDeserializationOptions, + gotDeserializationOptions, +} from "../lib/CommandClass"; +import { + CCCommand, + commandClass, + expectedCCResponse, + implementedVersion, +} from "../lib/CommandClassDecorators"; +import { + //type NetworkTransferStatus, + //WakeUpTime, + ZWaveLRProtocolCommand, + //parseWakeUpTime, +} from "../lib/_Types"; + +@commandClass(CommandClasses["Z-Wave Long Range Protocol"]) +@implementedVersion(1) +export class ZWaveLRProtocolCC extends CommandClass { + declare ccCommand: ZWaveLRProtocolCommand; +} + +@CCCommand(ZWaveLRProtocolCommand.NOP) +export class ZWaveLRProtocolCCNOP extends ZWaveLRProtocolCC {} + +interface ZWaveLRProtocolCCNodeInformationFrameOptions + extends CCCommandOptions, NodeInformationFrame +{} +// BUGBUG: how much of this can we share with existing stuff? Can we use a ZWaveProtocolCCNodeInformationFrameOptions field to do the `isLongRange` stuff? +// BUGBUG: how much can we share also with the Smart Start things below that are VERY close to this stuff? +@CCCommand(ZWaveLRProtocolCommand.NodeInformationFrame) +export class ZWaveLRProtocolCCNodeInformationFrame extends ZWaveLRProtocolCC + implements NodeInformationFrame +{ + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveLRProtocolCCNodeInformationFrameOptions, + ) { + super(host, options); + + let nif: NodeInformationFrame; + if (gotDeserializationOptions(options)) { + nif = parseNodeInformationFrame(this.payload, true); + } else { + nif = options; + } + + this.basicDeviceClass = 0x100; // BUGBUG: what fake value can we safely use here? + this.genericDeviceClass = nif.genericDeviceClass; + this.specificDeviceClass = nif.specificDeviceClass; + this.isListening = nif.isListening; + this.isFrequentListening = nif.isFrequentListening; + this.isRouting = false; + this.supportedDataRates = nif.supportedDataRates; + this.protocolVersion = 0; // "unknown"; + this.optionalFunctionality = false; + this.nodeType = nif.nodeType; + this.supportsSecurity = nif.supportsSecurity; + this.supportsBeaming = false; + this.supportedCCs = nif.supportedCCs; + } + + public basicDeviceClass: number; + public genericDeviceClass: number; + public specificDeviceClass: number; + public isListening: boolean; + public isFrequentListening: FLiRS; + public isRouting: boolean; + public supportedDataRates: DataRate[]; + public protocolVersion: ProtocolVersion; + public optionalFunctionality: boolean; + public nodeType: NodeType; + public supportsSecurity: boolean; + public supportsBeaming: boolean; + public supportedCCs: CommandClasses[]; + + public serialize(): Buffer { + this.payload = encodeNodeInformationFrame(this, true); + return super.serialize(); + } +} + +@CCCommand(ZWaveLRProtocolCommand.RequestNodeInformationFrame) +@expectedCCResponse(ZWaveLRProtocolCCNodeInformationFrame) +export class ZWaveLRProtocolCCRequestNodeInformationFrame + extends ZWaveLRProtocolCC +{} + +interface ZWaveLRProtocolCCAssignIDsOptions extends CCCommandOptions { + assignedNodeId: number; + homeId: number; +} + +@CCCommand(ZWaveLRProtocolCommand.AssignIDs) +export class ZWaveLRProtocolCCAssignIDs extends ZWaveLRProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveLRProtocolCCAssignIDsOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 6); + this.assignedNodeId = this.payload.readUInt16BE(0) & 0xFFF; + this.homeId = this.payload.readUInt32BE(2); + } else { + this.assignedNodeId = options.assignedNodeId; + this.homeId = options.homeId; + } + } + + public assignedNodeId: number; + public homeId: number; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe(6); + this.payload.writeUInt16BE(this.assignedNodeId, 0); + this.payload.writeUInt32BE(this.homeId, 2); + return super.serialize(); + } +} + +@CCCommand(ZWaveLRProtocolCommand.ExcludeRequest) +export class ZWaveLRProtocolCCExcludeRequest + extends ZWaveLRProtocolCCNodeInformationFrame +{} + +interface ZWaveLRProtocolCCSmartStartIncludedNodeInformationOptions + extends CCCommandOptions +{ + nwiHomeId: Buffer; +} + +// BUGBUG: this is exactly equal to the ZWaveProtocolCommand.SmartStartIncludedNodeInformation, can we reuse/inherit that somehow? +@CCCommand(ZWaveLRProtocolCommand.SmartStartIncludedNodeInformation) +export class ZWaveLRProtocolCCSmartStartIncludedNodeInformation + extends ZWaveLRProtocolCC +{ + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveLRProtocolCCSmartStartIncludedNodeInformationOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 4); + this.nwiHomeId = this.payload.subarray(0, 4); + } else { + if (options.nwiHomeId.length !== 4) { + throw new ZWaveError( + `nwiHomeId must have length 4`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.nwiHomeId = options.nwiHomeId; + } + } + + public nwiHomeId: Buffer; + + public serialize(): Buffer { + this.payload = Buffer.from(this.nwiHomeId); + return super.serialize(); + } +} + + +// BUGBUG this needs to include support for Sensor256ms and BeamCapability fields, yet the GetNodeInfo reserves those +@CCCommand(ZWaveLRProtocolCommand.SmartStartPrime) +export class ZWaveLRProtocolCCSmartStartPrime + extends ZWaveLRProtocolCCNodeInformationFrame +{} + +// BUGBUG this needs to include support for Sensor256ms and BeamCapability fields, yet the GetNodeInfo reserves those +@CCCommand(ZWaveLRProtocolCommand.SmartStartInclusionRequest) +export class ZWaveLRProtocolCCSmartStartInclusionRequest + extends ZWaveLRProtocolCCNodeInformationFrame +{} + + +// BUGBUG: this is identical to the AssignNodeID message, except for the field names +@CCCommand(ZWaveLRProtocolCommand.ExcludeRequestConfirimation) +export class ZWaveLRProtocolCCExcludeRequestConfirimation extends ZWaveLRProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveLRProtocolCCAssignIDsOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 6); + this.requestingNodeId = this.payload.readUInt16BE(0) & 0xFFF; + this.homeId = this.payload.readUInt32BE(2); + } else { + this.requestingNodeId = options.assignedNodeId; + this.homeId = options.homeId; + } + } + + public requestingNodeId: number; + public homeId: number; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe(6); + this.payload.writeUInt16BE(this.requestingNodeId, 0); + this.payload.writeUInt32BE(this.homeId, 2); + return super.serialize(); + } +} + +@CCCommand(ZWaveLRProtocolCommand.NonSecureIncusionStepComplete) +export class ZWaveLRProtocolCCNonSecureIncusionStepComplete extends ZWaveLRProtocolCC {} + diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index 740266bb7c26..f050002380d3 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -651,6 +651,19 @@ export { ZWaveProtocolCCTransferPresentation, ZWaveProtocolCCTransferRangeInformation, } from "./ZWaveProtocolCC"; +export { + ZWaveLRProtocolCC, + ZWaveLRProtocolCCNOP, + ZWaveLRProtocolCCAssignIDs, + ZWaveLRProtocolCCExcludeRequest, + ZWaveLRProtocolCCNodeInformationFrame, + ZWaveLRProtocolCCRequestNodeInformationFrame, + ZWaveLRProtocolCCSmartStartIncludedNodeInformation, + ZWaveLRProtocolCCSmartStartInclusionRequest, + ZWaveLRProtocolCCSmartStartPrime, + ZWaveLRProtocolCCExcludeRequestConfirimation, + ZWaveLRProtocolCCNonSecureIncusionStepComplete, +} from "./ZWaveLRProtocolCC"; export { fibaroCC, fibaroCCCommand, diff --git a/packages/cc/src/lib/_Types.ts b/packages/cc/src/lib/_Types.ts index 3d7b4bab0d3b..4e89a491bc53 100644 --- a/packages/cc/src/lib/_Types.ts +++ b/packages/cc/src/lib/_Types.ts @@ -1670,6 +1670,22 @@ export enum ZWaveProtocolCommand { SmartStartInclusionRequest = 0x28, } +export enum ZWaveLRProtocolCommand { + NOP = 0x00, + + // BUGBUG: all defined above, can they be shared? + NodeInformationFrame = 0x01, + RequestNodeInformationFrame = 0x02, + AssignIDs = 0x03, + ExcludeRequest = 0x23, + SmartStartIncludedNodeInformation = 0x26, + SmartStartPrime = 0x27, + SmartStartInclusionRequest = 0x28, + + ExcludeRequestConfirimation = 0x29, + NonSecureIncusionStepComplete = 0x2A, +} + export enum WakeUpTime { None, "1000ms", diff --git a/packages/core/src/capabilities/CommandClasses.ts b/packages/core/src/capabilities/CommandClasses.ts index 6fae5ee579dd..0afccf52b350 100644 --- a/packages/core/src/capabilities/CommandClasses.ts +++ b/packages/core/src/capabilities/CommandClasses.ts @@ -130,6 +130,7 @@ export enum CommandClasses { "Z-Wave Plus Info" = 0x5e, // Internal CC which is not used directly by applications "Z-Wave Protocol" = 0x01, + "Z-Wave Long Range Protocol" = 0x04, } export function getCCName(cc: number): string { diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 022af36393c8..9e22c3dbcd23 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -5345,6 +5345,7 @@ ${handlers.length} left`, } /** @internal */ + // BUGBUG: do we need to replicate this for Long Range? public async sendZWaveProtocolCC( command: ZWaveProtocolCC, options: Pick< From 29ef9afb3118ef5c5624a9d588f0fc7aafbab75b Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:49:17 -0500 Subject: [PATCH 07/43] feat(squash): with other ZWaveLRProtocolCC.ts changes --- packages/cc/src/cc/ZWaveLRProtocolCC.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/cc/src/cc/ZWaveLRProtocolCC.ts b/packages/cc/src/cc/ZWaveLRProtocolCC.ts index 5b83c7164295..0cc70c4e23ac 100644 --- a/packages/cc/src/cc/ZWaveLRProtocolCC.ts +++ b/packages/cc/src/cc/ZWaveLRProtocolCC.ts @@ -7,7 +7,7 @@ import { type ProtocolVersion, ZWaveError, ZWaveErrorCodes, - //parseBitMask, + // parseBitMask, encodeNodeInformationFrame, parseNodeInformationFrame, validatePayload, @@ -26,10 +26,10 @@ import { implementedVersion, } from "../lib/CommandClassDecorators"; import { - //type NetworkTransferStatus, - //WakeUpTime, + // type NetworkTransferStatus, + // WakeUpTime, ZWaveLRProtocolCommand, - //parseWakeUpTime, + // parseWakeUpTime, } from "../lib/_Types"; @commandClass(CommandClasses["Z-Wave Long Range Protocol"]) @@ -44,6 +44,7 @@ export class ZWaveLRProtocolCCNOP extends ZWaveLRProtocolCC {} interface ZWaveLRProtocolCCNodeInformationFrameOptions extends CCCommandOptions, NodeInformationFrame {} + // BUGBUG: how much of this can we share with existing stuff? Can we use a ZWaveProtocolCCNodeInformationFrameOptions field to do the `isLongRange` stuff? // BUGBUG: how much can we share also with the Smart Start things below that are VERY close to this stuff? @CCCommand(ZWaveLRProtocolCommand.NodeInformationFrame) @@ -186,7 +187,6 @@ export class ZWaveLRProtocolCCSmartStartIncludedNodeInformation } } - // BUGBUG this needs to include support for Sensor256ms and BeamCapability fields, yet the GetNodeInfo reserves those @CCCommand(ZWaveLRProtocolCommand.SmartStartPrime) export class ZWaveLRProtocolCCSmartStartPrime @@ -199,10 +199,11 @@ export class ZWaveLRProtocolCCSmartStartInclusionRequest extends ZWaveLRProtocolCCNodeInformationFrame {} - // BUGBUG: this is identical to the AssignNodeID message, except for the field names @CCCommand(ZWaveLRProtocolCommand.ExcludeRequestConfirimation) -export class ZWaveLRProtocolCCExcludeRequestConfirimation extends ZWaveLRProtocolCC { +export class ZWaveLRProtocolCCExcludeRequestConfirimation + extends ZWaveLRProtocolCC +{ public constructor( host: ZWaveHost, options: @@ -232,5 +233,6 @@ export class ZWaveLRProtocolCCExcludeRequestConfirimation extends ZWaveLRProtoco } @CCCommand(ZWaveLRProtocolCommand.NonSecureIncusionStepComplete) -export class ZWaveLRProtocolCCNonSecureIncusionStepComplete extends ZWaveLRProtocolCC {} - +export class ZWaveLRProtocolCCNonSecureIncusionStepComplete + extends ZWaveLRProtocolCC +{} From 1b56690611328bc5cf1c3f89ff1057d81eefdd02 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:50:23 -0500 Subject: [PATCH 08/43] feat(squash): with other index.ts changes --- packages/cc/src/cc/index.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index f050002380d3..e724101524cf 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -607,6 +607,19 @@ export { WindowCoveringCCSupportedReport, WindowCoveringCCValues, } from "./WindowCoveringCC"; +export { + ZWaveLRProtocolCC, + ZWaveLRProtocolCCAssignIDs, + ZWaveLRProtocolCCExcludeRequest, + ZWaveLRProtocolCCExcludeRequestConfirimation, + ZWaveLRProtocolCCNOP, + ZWaveLRProtocolCCNodeInformationFrame, + ZWaveLRProtocolCCNonSecureIncusionStepComplete, + ZWaveLRProtocolCCRequestNodeInformationFrame, + ZWaveLRProtocolCCSmartStartIncludedNodeInformation, + ZWaveLRProtocolCCSmartStartInclusionRequest, + ZWaveLRProtocolCCSmartStartPrime, +} from "./ZWaveLRProtocolCC"; export { ZWavePlusCC, ZWavePlusCCGet, @@ -651,19 +664,6 @@ export { ZWaveProtocolCCTransferPresentation, ZWaveProtocolCCTransferRangeInformation, } from "./ZWaveProtocolCC"; -export { - ZWaveLRProtocolCC, - ZWaveLRProtocolCCNOP, - ZWaveLRProtocolCCAssignIDs, - ZWaveLRProtocolCCExcludeRequest, - ZWaveLRProtocolCCNodeInformationFrame, - ZWaveLRProtocolCCRequestNodeInformationFrame, - ZWaveLRProtocolCCSmartStartIncludedNodeInformation, - ZWaveLRProtocolCCSmartStartInclusionRequest, - ZWaveLRProtocolCCSmartStartPrime, - ZWaveLRProtocolCCExcludeRequestConfirimation, - ZWaveLRProtocolCCNonSecureIncusionStepComplete, -} from "./ZWaveLRProtocolCC"; export { fibaroCC, fibaroCCCommand, From 63d142b2cb222ef930e21710a4ef838e52b8ea9e Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:51:08 -0500 Subject: [PATCH 09/43] feat(squash): with other Constants.ts --- packages/serial/src/message/Constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/serial/src/message/Constants.ts b/packages/serial/src/message/Constants.ts index 4f2edeb4c7b0..2714dd63e839 100644 --- a/packages/serial/src/message/Constants.ts +++ b/packages/serial/src/message/Constants.ts @@ -175,7 +175,7 @@ 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, + GetLongRangeNodes = 0xda, // Used after GetSerialApiInitData to get the nodes with IDs > 0xFF GetLongRangeChannel = 0xdb, SetLongRangeChannel = 0xdc, SetLongRangeShadowNodeIDs = 0xdd, From 6005dfa987219a55fcf09c9834182ae7ff6192d8 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:51:46 -0500 Subject: [PATCH 10/43] feat(squash): with other Protocols.ts --- packages/core/src/capabilities/Protocols.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index 937078f098bf..b03cbdfac809 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -19,7 +19,7 @@ export function zwaveDataRateToString(rate: ZWaveDataRate): string { return "40 kbit/s"; case ZWaveDataRate["100k"]: return "100 kbit/s"; - } + } return `Unknown (${num2hex(rate)})`; } From cd27d02c63772905a701cac6ad58c7e6e07bba7a Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 20:53:14 -0500 Subject: [PATCH 11/43] feat(longrange): the bulk of long range support --- packages/core/src/capabilities/NodeInfo.ts | 67 ++++--- packages/core/src/consts/index.ts | 6 + packages/core/src/values/Primitive.ts | 40 ++++- .../zwave-js/src/lib/controller/Controller.ts | 140 ++++++++++++--- .../zwave-js/src/lib/controller/Inclusion.ts | 22 +++ .../application/ApplicationUpdateRequest.ts | 14 +- .../GetSerialApiCapabilitiesMessages.ts | 40 ++++- .../GetSerialApiInitDataMessages.ts | 119 +++++++++++- .../capability/LongRangeSetupMessages.ts | 169 ++++++++++++++++++ .../network-mgmt/AddNodeToNetworkRequest.ts | 13 ++ 10 files changed, 571 insertions(+), 59 deletions(-) create mode 100644 packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index 15574c4d375f..a4c911118684 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -154,8 +154,7 @@ export function encodeCCList( controlledCCs: readonly CommandClasses[], isLongRange: boolean = false, ): Buffer { - const bufferLength = - (isLongRange ? 1 : 0) + const bufferLength = (isLongRange ? 1 : 0) + sum(supportedCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))) + (controlledCCs.length > 0 ? 1 : 0) // support/control mark + sum(controlledCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))); @@ -231,7 +230,7 @@ export type NodeInformationFrame = export function parseNodeProtocolInfo( buffer: Buffer, offset: number, - isLongRange: boolean, + isLongRange: boolean = false, ): NodeProtocolInfo { validatePayload(buffer.length >= offset + 3); @@ -265,11 +264,13 @@ export function parseNodeProtocolInfo( } // BUGBUG: what's the correct protocol version here for long range? - const protocolVersion = isLongRange ? 0 - : buffer[offset] & 0b111; + const protocolVersion = isLongRange + ? 0 + : buffer[offset] & 0b111; const capability = buffer[offset + 1]; - const optionalFunctionality = (!isLongRange) && !!(capability & 0b1000_0000); + const optionalFunctionality = (!isLongRange) + && !!(capability & 0b1000_0000); let isFrequentListening: FLiRS; switch (capability & (isLongRange ? 0b0100_0000 : 0b0110_0000)) { case 0b0100_0000: @@ -285,9 +286,13 @@ export function parseNodeProtocolInfo( let nodeType: NodeType; - switch (isLongRange ? (0b1_0000_0000 | (capability & 0b0010)) : (capability && 0b1010)) { + switch ( + isLongRange + ? (0b1_0000_0000 | (capability & 0b0010)) + : (capability && 0b1010) + ) { case 0b0_0000_1000: - case 0b1_0000_0000: + case 0b1_0000_0000: nodeType = NodeType["End Node"]; break; case 0b0_0000_0010: @@ -316,7 +321,10 @@ export function parseNodeProtocolInfo( }; } -export function encodeNodeProtocolInfo(info: NodeProtocolInfo, isLongRange: boolean = false): Buffer { +export function encodeNodeProtocolInfo( + info: NodeProtocolInfo, + isLongRange: boolean = false, +): Buffer { const ret = Buffer.alloc(3, 0); // Byte 0 and 2 if (info.isListening) ret[0] |= 0b10_000_000; @@ -335,7 +343,9 @@ export function encodeNodeProtocolInfo(info: NodeProtocolInfo, isLongRange: bool if (info.optionalFunctionality) ret[1] |= 0b1000_0000; } if (info.isFrequentListening === "1000ms") ret[1] |= 0b0100_0000; - else if (!isLongRange && info.isFrequentListening === "250ms") ret[1] |= 0b0010_0000; + else if (!isLongRange && info.isFrequentListening === "250ms") { + ret[1] |= 0b0010_0000; + } if (!isLongRange) { if (info.supportsBeaming) ret[1] |= 0b0001_0000; @@ -351,7 +361,10 @@ export function encodeNodeProtocolInfo(info: NodeProtocolInfo, isLongRange: bool return ret; } -export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer, isLongRange: boolean = false): { +export function parseNodeProtocolInfoAndDeviceClass( + buffer: Buffer, + isLongRange: boolean = false, +): { info: NodeProtocolInfoAndDeviceClass; bytesRead: number; } { @@ -382,17 +395,18 @@ export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer, isLongRange: export function encodeNodeProtocolInfoAndDeviceClass( info: NodeProtocolInfoAndDeviceClass, - isLongRange: boolean = false + isLongRange: boolean = false, ): Buffer { - const deviceClasses = isLongRange ? - Buffer.from([ - info.genericDeviceClass, - info.specificDeviceClass, - ]) : Buffer.from([ - info.basicDeviceClass, - info.genericDeviceClass, - info.specificDeviceClass, - ]); + const deviceClasses = isLongRange + ? Buffer.from([ + info.genericDeviceClass, + info.specificDeviceClass, + ]) + : Buffer.from([ + info.basicDeviceClass, + info.genericDeviceClass, + info.specificDeviceClass, + ]); return Buffer.concat([ encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), deviceClasses, @@ -404,9 +418,11 @@ export function parseNodeInformationFrame( isLongRange: boolean = false, ): NodeInformationFrame { const { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( - buffer, isLongRange + buffer, + isLongRange, ); - const supportedCCs = parseCCList(buffer.subarray(offset), isLongRange).supportedCCs; + const supportedCCs = + parseCCList(buffer.subarray(offset), isLongRange).supportedCCs; return { ...info, @@ -414,7 +430,10 @@ export function parseNodeInformationFrame( }; } -export function encodeNodeInformationFrame(info: NodeInformationFrame, isLongRange: boolean = false): Buffer { +export function encodeNodeInformationFrame( + info: NodeInformationFrame, + isLongRange: boolean = false, +): Buffer { return Buffer.concat([ encodeNodeProtocolInfoAndDeviceClass(info, isLongRange), encodeCCList(info.supportedCCs, [], isLongRange), 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/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 571dfa70f4c8..2184597b2264 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -121,6 +121,7 @@ import { ApplicationUpdateRequestNodeAdded, ApplicationUpdateRequestNodeInfoReceived, ApplicationUpdateRequestNodeRemoved, + ApplicationUpdateRequestSmartStartHomeIDLRReceived, ApplicationUpdateRequestSmartStartHomeIDReceived, } from "../serialapi/application/ApplicationUpdateRequest"; import { @@ -148,6 +149,8 @@ import { type GetSerialApiCapabilitiesResponse, } from "../serialapi/capability/GetSerialApiCapabilitiesMessages"; import { + GetLongRangeNodesRequest, + type GetLongRangeNodesResponse, GetSerialApiInitDataRequest, type GetSerialApiInitDataResponse, } from "../serialapi/capability/GetSerialApiInitDataMessages"; @@ -178,6 +181,10 @@ import { SerialAPISetup_SetTXStatusReportRequest, type SerialAPISetup_SetTXStatusReportResponse, } from "../serialapi/capability/SerialAPISetupMessages"; +import { + GetLongRangeChannelRequest, LongRangeChannel, + type GetLongRangeChannelResponse, +} from "../serialapi/capability/LongRangeSetupMessages"; import { SetApplicationNodeInformationRequest } from "../serialapi/capability/SetApplicationNodeInformationRequest"; import { GetControllerIdRequest, @@ -326,6 +333,7 @@ import { type ExclusionOptions, ExclusionStrategy, type FoundNode, + type InclusionFlagsInternal, type InclusionOptions, type InclusionOptionsInternal, type InclusionResult, @@ -897,7 +905,7 @@ export class ZWaveController const apiCaps = await this.driver.sendMessage< GetSerialApiCapabilitiesResponse >( - new GetSerialApiCapabilitiesRequest(this.driver), + new GetSerialApiCapabilitiesRequest(this.driver, {mysteryValue: 3}), { supportCheck: false, }, @@ -1022,11 +1030,13 @@ export class ZWaveController ) }`, ); + this._supportsLongRange = resp == RFRegion["USA (Long Range)"]; } else { this.driver.controllerLog.print( `Querying the RF region failed!`, "warn", ); + this._supportsLongRange = false; } } if ( @@ -1283,6 +1293,46 @@ export class ZWaveController this._nodeType = initData.nodeType; this._supportsTimers = initData.supportsTimers; // ignore the initVersion, no clue what to do with it + + // BUGBUG: Dummy check? SimplicityStudio adds this extra 3 argument to this command. I suspect that's not the secret sauce for LR inclusion, but S2 encryption is. + const apiCaps = await this.driver.sendMessage< + GetSerialApiCapabilitiesResponse + >( + new GetSerialApiCapabilitiesRequest(this.driver, {mysteryValue: 3}), + { + supportCheck: false, + }, + ); + + // fetch the list of long range nodes until the controller reports no more + const lrNodeIds: Array = []; + let lrChannel : LongRangeChannel | undefined; + const maxPayloadSize = await this.getMaxPayloadSize(); + let maxPayloadSizeLR : number | undefined; + if (this.isLongRange()) { + const nodePage = 0; + while (true) { + const nodesResponse = await this.driver.sendMessage< + GetLongRangeNodesResponse + >( + new GetLongRangeNodesRequest(this.driver, { + listStartOffset128: nodePage, + }), + ); + lrNodeIds.push(...nodesResponse.nodeIds); + if (!nodesResponse.moreNodes) { + break; + } + } + + // TODO: restore/set the channel + const lrChannelResp = await this.driver.sendMessage(new GetLongRangeChannelRequest(this.driver)); + lrChannel = lrChannelResp.longRangeChannel; + + // TODO: fetch the long range max payload size and cache it + maxPayloadSizeLR = await this.getMaxPayloadSizeLongRange(); + } + this.driver.controllerLog.print( `received additional controller information: Z-Wave API version: ${this._zwaveApiVersion.version} (${this._zwaveApiVersion.kind})${ @@ -1305,7 +1355,11 @@ export class ZWaveController controller role: ${this._isPrimary ? "primary" : "secondary"} controller is the SIS: ${this._isSIS} controller supports timers: ${this._supportsTimers} - nodes in the network: ${initData.nodeIds.join(", ")}`, + zwave nodes in the network: ${initData.nodeIds.join(", ")} + max payload size: ${maxPayloadSize} + LR nodes in the network: ${lrNodeIds.join(", ")} + LR channel: ${lrChannel ? getEnumMemberName(LongRangeChannel, lrChannel) : ""} + LR max payload size: ${maxPayloadSizeLR}`, ); // Index the value DB for optimal performance @@ -1323,6 +1377,9 @@ export class ZWaveController nodeIds.unshift(this._ownNodeId!); } + // BUGBUG: do nodes need to implicitly know that they were a long range node? Or are long range nodes determined 100% by their nodeID values being >= 256? + // The controller is the odd-man out, as it's both. Let's assume that apart from explicit exclusion we don't need to know for now. + nodeIds.push(...lrNodeIds); for (const nodeId of nodeIds) { this._nodes.set( nodeId, @@ -1420,6 +1477,10 @@ export class ZWaveController this.driver.controllerLog.print("Interview completed"); } + private isLongRange(): boolean { + return !!this._supportsLongRange; + } + private createValueDBForNode(nodeId: number, ownKeys?: Set) { return new ValueDB( nodeId, @@ -1537,10 +1598,12 @@ export class ZWaveController private _includeController: boolean = false; private _exclusionOptions: ExclusionOptions | undefined; private _inclusionOptions: InclusionOptionsInternal | undefined; + private _inclusionFlags: InclusionFlagsInternal | undefined; private _nodePendingInclusion: ZWaveNode | undefined; private _nodePendingExclusion: ZWaveNode | undefined; private _nodePendingReplace: ZWaveNode | undefined; private _replaceFailedPromise: DeferredPromise | undefined; + private _supportsLongRange: boolean | undefined; /** * Starts the inclusion process of new nodes. @@ -1573,11 +1636,26 @@ export class ZWaveController ); } + // BUGBUG: fix me + // if (options.isLongRange && !this.isLongRange()) { + // throw new ZWaveError( + // `Invalid long range inclusion on a non-LR host`, + // ZWaveErrorCodes.Argument_Invalid, + // ) + // } + const isLongRange = this.isLongRange(); + // Leave SmartStart listening mode so we can switch to exclusion mode await this.pauseSmartStart(); this.setInclusionState(InclusionState.Including); this._inclusionOptions = options; + // BUGBUG: todo: cache the options used to start inclusion so they can be re-used for stopping, etc + this._inclusionFlags = { + highPower: true, + networkWide: true, + protocolLongRange: isLongRange, + }; try { this.driver.controllerLog.print( @@ -1593,8 +1671,7 @@ export class ZWaveController await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Any, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); @@ -1649,6 +1726,11 @@ export class ZWaveController strategy: InclusionStrategy.SmartStart, provisioning: provisioningEntry, }; + this._inclusionFlags = { + highPower: true, + networkWide: true, + protocolLongRange: provisioningEntry.isLongRange, + }; try { this.driver.controllerLog.print( @@ -1661,8 +1743,7 @@ export class ZWaveController new AddNodeDSKToNetworkRequest(this.driver, { nwiHomeId: nwiHomeIdFromDSK(dskBuffer), authHomeId: authHomeIdFromDSK(dskBuffer), - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); @@ -1690,8 +1771,7 @@ export class ZWaveController new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print(`The inclusion process was stopped`); @@ -1710,8 +1790,7 @@ export class ZWaveController >( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); if (response.status === AddNodeStatus.Done) { @@ -1744,8 +1823,7 @@ export class ZWaveController await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print( @@ -1843,8 +1921,7 @@ export class ZWaveController new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print( @@ -1885,8 +1962,7 @@ export class ZWaveController new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print( @@ -1928,19 +2004,32 @@ export class ZWaveController return false; } + // BUGBUG: fix me, same logic as beginInclusion, probably an incoming option... + // if (options.isLongRange && !this.isLongRange()) { + // throw new ZWaveError( + // `Invalid long range inclusion on a non-LR host`, + // ZWaveErrorCodes.Argument_Invalid, + // ) + // } + const isLongRange = this.isLongRange(); + // 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...`); + this._inclusionFlags = { + highPower: true, + networkWide: true, + protocolLongRange: isLongRange, + }; try { // kick off the inclusion process await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { removeNodeType: RemoveNodeType.Any, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print( @@ -1977,8 +2066,7 @@ export class ZWaveController new RemoveNodeFromNetworkRequest(this.driver, { callbackId: 0, // disable callbacks removeNodeType: RemoveNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print(`the exclusion process was stopped`); @@ -2001,8 +2089,7 @@ export class ZWaveController await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { removeNodeType: RemoveNodeType.Stop, - highPower: true, - networkWide: true, + ...this._inclusionFlags, }), ); this.driver.controllerLog.print( @@ -2065,6 +2152,7 @@ export class ZWaveController } } else if ( msg instanceof ApplicationUpdateRequestSmartStartHomeIDReceived + || msg instanceof ApplicationUpdateRequestSmartStartHomeIDLRReceived ) { // the controller is in Smart Start learn mode and a node requests inclusion via Smart Start this.driver.controllerLog.print( @@ -4309,6 +4397,14 @@ ${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 (nodeId >= 0x100) { + this.driver.controllerLog.logNode(nodeId, { + message: `Skipping SUC return route because isLR...`, + direction: "outbound", + }); + return true; + } + this.driver.controllerLog.logNode(nodeId, { message: `Assigning SUC return route...`, direction: "outbound", diff --git a/packages/zwave-js/src/lib/controller/Inclusion.ts b/packages/zwave-js/src/lib/controller/Inclusion.ts index e85b47d88d90..184500214544 100644 --- a/packages/zwave-js/src/lib/controller/Inclusion.ts +++ b/packages/zwave-js/src/lib/controller/Inclusion.ts @@ -119,6 +119,7 @@ export interface InclusionUserCallbacks { } /** Options for inclusion of a new node */ +// TODO: how should "preferLongRange" fit in here? We probably want the user to be able to control that? export type InclusionOptions = | { strategy: InclusionStrategy.Default; @@ -132,6 +133,11 @@ export type InclusionOptions = * This is not recommended due to the overhead caused by S0. */ forceSecurity?: boolean; + + /** + * Force long range. If not provided, will default to long range iff the controller supports it, and not otherwise. + */ + isLongRange?: boolean; } | { strategy: InclusionStrategy.Security_S2; @@ -158,6 +164,11 @@ export type InclusionOptions = strategy: | InclusionStrategy.Insecure | InclusionStrategy.Security_S0; + + /** + * Force long range. If not provided, will default to long range iff the controller supports it, and not otherwise. + */ + isLongRange?: boolean; }; /** @@ -171,6 +182,13 @@ export type InclusionOptionsInternal = provisioning: PlannedProvisioningEntry; }; +// BUGBUG: better way to do this? +export type InclusionFlagsInternal = { + highPower: boolean; + networkWide: boolean; + protocolLongRange: boolean; +}; + export type ExclusionOptions = { strategy: | ExclusionStrategy.ExcludeOnly @@ -219,6 +237,10 @@ export interface PlannedProvisioningEntry { /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ dsk: string; + /** The device must be included via ZWave Long Range */ + // BUGBUG: by adding this here we probably broke the API used by JSUI, etc... + isLongRange: boolean; + /** The security classes that have been **granted** by the user */ securityClasses: SecurityClass[]; /** diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 1c6c433c8f89..7c4f11a4d034 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_HomeId_LR_Received = 0x87, // A start 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,13 @@ export class ApplicationUpdateRequestSmartStartHomeIDReceived }; } } + +@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_Received) +export class ApplicationUpdateRequestSmartStartHomeIDReceived + extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase +{} + +@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_LR_Received) +export class ApplicationUpdateRequestSmartStartHomeIDLRReceived + extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase +{} diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts index dbf295ca63d6..09cb5ed03b53 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts @@ -15,10 +15,48 @@ import { const NUM_FUNCTIONS = 256; const NUM_FUNCTION_BYTES = NUM_FUNCTIONS / 8; +export interface GetSerialApiCapabilitiesRequestOptions + extends MessageBaseOptions +{ + mysteryValue: number | undefined, +} + @messageTypes(MessageType.Request, FunctionType.GetSerialApiCapabilities) @expectedResponse(FunctionType.GetSerialApiCapabilities) @priority(MessagePriority.Controller) -export class GetSerialApiCapabilitiesRequest extends Message {} +export class GetSerialApiCapabilitiesRequest extends Message { + public constructor( + host: ZWaveHost, + options: + | MessageDeserializationOptions + | GetSerialApiCapabilitiesRequestOptions, + ) { + super(host, options); + + if (gotDeserializationOptions(options)) { + if (this.payload.length >= 1) { + this.mysteryValue = this.payload[0]; + } else { + this.mysteryValue = undefined; + } + } else { + this.mysteryValue = options.mysteryValue; + } + } + + public mysteryValue: number | undefined; + + public serialize(): Buffer { + if (this.mysteryValue !== undefined) { + this.payload = Buffer.allocUnsafe(1); + this.payload[0] = this.mysteryValue; + } else { + this.payload = Buffer.allocUnsafe(0); + } + + return super.serialize(); + } +} export interface GetSerialApiCapabilitiesResponseOptions extends MessageBaseOptions diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts index b0613533f78e..ac5c84338704 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts @@ -1,9 +1,12 @@ import { MAX_NODES, MessagePriority, + NUM_LR_NODEMASK_SEGMENT_BYTES, + NUM_LR_NODES_PER_SEGMENT, NUM_NODEMASK_BYTES, NodeType, - encodeBitMask, + encodeLongRangeNodeBitMask, + parseLongRangeNodeBitMask, parseNodeBitMask, } from "@zwave-js/core"; import type { ZWaveHost } from "@zwave-js/host"; @@ -151,7 +154,7 @@ export class GetSerialApiInitDataResponse extends Message { this.payload[1] = capabilities; this.payload[2] = NUM_NODEMASK_BYTES; - const nodeBitMask = encodeBitMask(this.nodeIds, MAX_NODES); + const nodeBitMask = encodeLongRangeNodeBitMask(this.nodeIds, MAX_NODES); nodeBitMask.copy(this.payload, 3); if (chipType) { @@ -215,3 +218,115 @@ export class GetSerialApiInitDataResponse extends Message { // is SUC: true // chip type: 7 // chip version: 0 + +export interface GetLongRangeNodesRequestOptions extends MessageBaseOptions { + listStartOffset128: 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)) { + // BUGBUG: validate length at least + this.listStartOffset128 = this.payload[0]; + } else { + this.listStartOffset128 = options.listStartOffset128; + } + } + + public listStartOffset128: number; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe( + 1, + ); + + this.payload[0] = this.listStartOffset128; + return super.serialize(); + } +} + +export interface GetLongRangeNodesResponseOptions extends MessageBaseOptions { + moreNodes: boolean; + listStartOffset128: 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)) { + // BUGBUG: validate length at least + this.moreNodes = this.payload[0] != 0; + this.listStartOffset128 = this.payload[1]; + const listLength = this.payload[2]; + + const listStart = 3; + const listEnd = listStart + listLength; + // BUGGUG validate listEnd <= this.payload.length + 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.listStartOffset128 = options.listStartOffset128; + this.nodeIds = options.nodeIds; + } + } + + public moreNodes: boolean; + public listStartOffset128: 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.listStartOffset128; + 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 * 8 * this.listStartOffset128; + } + + private listEndNode(): number { + return 256 + + NUM_LR_NODES_PER_SEGMENT * 8 * (1 + this.listStartOffset128); + } +} 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..b8f86aa6285e --- /dev/null +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -0,0 +1,169 @@ +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"; + +// TODO: move to header? +export enum LongRangeChannel { + Unknown = 0x00, // Reserved + A = 0x01, + B = 0x02, + // 0x03..0xFF are reserved and must not be used +} + +@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)) { + // BUGBUG: verify length? + 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; +} + +// BUGBUG: move someplace common, see Spec 4.2.5 +// +// Although, that section refers to a "callback message", and SetZWaveLongRangeChannel doesn't define a callback message... +export enum ResponseStatus { + FAILED = 0x00, +} + +export class ResponseStatusMessageBase extends Message + implements SuccessIndicator +{ + public constructor( + host: ZWaveHost, + options: MessageDeserializationOptions, + ) { + super(host, options); + this._status = this.payload[0]; + } + + private _status: ResponseStatus; + public get getStatus(): ResponseStatus { + return this._status; + } + + public isOK(): boolean { + return this._status != ResponseStatus.FAILED; + } +} + +@messageTypes(MessageType.Response, FunctionType.SetLongRangeChannel) +export class SetLongRangeChannelResponse extends ResponseStatusMessageBase {} + +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)) { + // BUGBUG: verify length? + 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/network-mgmt/AddNodeToNetworkRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts index 52b54348e29d..7df76c027208 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts @@ -48,12 +48,14 @@ export enum AddNodeStatus { enum AddNodeFlags { HighPower = 0x80, NetworkWide = 0x40, + ProtocolLongRange = 0x20, } interface AddNodeToNetworkRequestOptions extends MessageBaseOptions { addNodeType?: AddNodeType; highPower?: boolean; networkWide?: boolean; + protocolLongRange?: boolean; } interface AddNodeDSKToNetworkRequestOptions extends MessageBaseOptions { @@ -61,6 +63,7 @@ interface AddNodeDSKToNetworkRequestOptions extends MessageBaseOptions { authHomeId: Buffer; highPower?: boolean; networkWide?: boolean; + protocolLongRange?: boolean; } export function computeNeighborDiscoveryTimeout( @@ -134,6 +137,7 @@ export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { this.addNodeType = options.addNodeType; this.highPower = !!options.highPower; this.networkWide = !!options.networkWide; + this.protocolLongRange = !!options.protocolLongRange; } /** The type of node to add */ @@ -142,11 +146,14 @@ export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { public highPower: boolean = false; /** Whether to include network wide */ public networkWide: boolean = false; + /** Whether to include as long-range or not */ + public protocolLongRange: boolean = false; public serialize(): Buffer { let data: number = this.addNodeType || AddNodeType.Any; if (this.highPower) data |= AddNodeFlags.HighPower; if (this.networkWide) data |= AddNodeFlags.NetworkWide; + if (this.protocolLongRange) data |= AddNodeFlags.ProtocolLongRange; this.payload = Buffer.from([data, this.callbackId]); @@ -166,6 +173,7 @@ export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { ...message, "high power": this.highPower, "network wide": this.networkWide, + "long range": this.protocolLongRange, }; if (this.hasCallbackId()) { @@ -210,6 +218,7 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { this.authHomeId = options.authHomeId; this.highPower = !!options.highPower; this.networkWide = !!options.networkWide; + this.protocolLongRange = !!options.protocolLongRange; } /** The home IDs of node to add */ @@ -219,11 +228,14 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { public highPower: boolean = false; /** Whether to include network wide */ public networkWide: boolean = false; + /** Whether to include as long-range or not */ + public protocolLongRange: boolean = false; public serialize(): Buffer { let control: number = AddNodeType.SmartStartDSK; if (this.highPower) control |= AddNodeFlags.HighPower; if (this.networkWide) control |= AddNodeFlags.NetworkWide; + if (this.protocolLongRange) control |= AddNodeFlags.ProtocolLongRange; this.payload = Buffer.concat([ Buffer.from([control, this.callbackId]), @@ -240,6 +252,7 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { "NWI Home ID": buffer2hex(this.nwiHomeId), "high power": this.highPower, "network wide": this.networkWide, + "long range": this.protocolLongRange, }; if (this.hasCallbackId()) { message["callback id"] = this.callbackId; From 8f15e3a7682d4bd1c8dfcde312bd11e4a76589d0 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 21:02:32 -0500 Subject: [PATCH 12/43] fix(lint): lint errors --- packages/zwave-js/src/lib/controller/Controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 2184597b2264..5bf343e1c605 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1295,7 +1295,7 @@ export class ZWaveController // ignore the initVersion, no clue what to do with it // BUGBUG: Dummy check? SimplicityStudio adds this extra 3 argument to this command. I suspect that's not the secret sauce for LR inclusion, but S2 encryption is. - const apiCaps = await this.driver.sendMessage< + await this.driver.sendMessage< GetSerialApiCapabilitiesResponse >( new GetSerialApiCapabilitiesRequest(this.driver, {mysteryValue: 3}), From d1e23fc6a2e58c15eff86d31cf78951d8b62476b Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Sun, 15 Oct 2023 21:02:32 -0500 Subject: [PATCH 13/43] fix(lint): lint errors --- packages/cc/src/cc/ZWaveLRProtocolCC.ts | 4 ---- packages/zwave-js/src/lib/controller/Controller.ts | 1 - 2 files changed, 5 deletions(-) diff --git a/packages/cc/src/cc/ZWaveLRProtocolCC.ts b/packages/cc/src/cc/ZWaveLRProtocolCC.ts index 0cc70c4e23ac..f8975704c3db 100644 --- a/packages/cc/src/cc/ZWaveLRProtocolCC.ts +++ b/packages/cc/src/cc/ZWaveLRProtocolCC.ts @@ -7,7 +7,6 @@ import { type ProtocolVersion, ZWaveError, ZWaveErrorCodes, - // parseBitMask, encodeNodeInformationFrame, parseNodeInformationFrame, validatePayload, @@ -26,10 +25,7 @@ import { implementedVersion, } from "../lib/CommandClassDecorators"; import { - // type NetworkTransferStatus, - // WakeUpTime, ZWaveLRProtocolCommand, - // parseWakeUpTime, } from "../lib/_Types"; @commandClass(CommandClasses["Z-Wave Long Range Protocol"]) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 5bf343e1c605..39b183099cee 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1650,7 +1650,6 @@ export class ZWaveController this.setInclusionState(InclusionState.Including); this._inclusionOptions = options; - // BUGBUG: todo: cache the options used to start inclusion so they can be re-used for stopping, etc this._inclusionFlags = { highPower: true, networkWide: true, From 9b679c3b104bc82490e682c077242a57d4b2e133 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:02:42 -0800 Subject: [PATCH 14/43] feat(longrange): remove corrupted ack workaround --- packages/serial/src/parsers/SerialAPIParser.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/serial/src/parsers/SerialAPIParser.ts b/packages/serial/src/parsers/SerialAPIParser.ts index bb6c658fa35e..80fbbbf31f9a 100644 --- a/packages/serial/src/parsers/SerialAPIParser.ts +++ b/packages/serial/src/parsers/SerialAPIParser.ts @@ -49,12 +49,6 @@ export class SerialAPIParser extends Transform { this.ignoreAckHighNibble = false; break; } - case 0x86: { - // This is _maybe_ a corrupted ACK byte from a ZST39 after a soft reset, transform it and keep it - this.logger?.ACK("inbound"); - this.push(MessageHeaders.ACK); - break; - } case MessageHeaders.NAK: { this.logger?.NAK("inbound"); this.push(MessageHeaders.NAK); From 9637127e28d17e031b43bfb9b16bd8fff3a79175 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:04:12 -0800 Subject: [PATCH 15/43] feat(longrange): remove ZWaveLRProtocolCC --- packages/cc/src/cc/Security2CC.ts | 15 +- packages/cc/src/cc/ZWaveLRProtocolCC.ts | 234 ------------------------ packages/cc/src/cc/index.ts | 13 -- packages/cc/src/lib/_Types.ts | 16 -- 4 files changed, 4 insertions(+), 274 deletions(-) delete mode 100644 packages/cc/src/cc/ZWaveLRProtocolCC.ts diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index d6b6a5aafbd9..dea4221d0f25 100644 --- a/packages/cc/src/cc/Security2CC.ts +++ b/packages/cc/src/cc/Security2CC.ts @@ -97,17 +97,10 @@ function getAuthenticationData( const nodeIdSize = (sendingNodeId < 256 && destination < 256) ? 1 : 2; const ret = Buffer.allocUnsafe(2*nodeIdSize + 6 + unencryptedPayload.length); let offset = 0; - if (nodeIdSize == 1) { - ret[offset++] = sendingNodeId; - ret[offset++] = destination; - - } else { - ret.writeUint16BE(sendingNodeId, offset); - offset += 2; - ret.writeUint16BE(destination, offset); - offset += 2; - } - + ret.writeUIntBE(sendingNodeId, offset, nodeIdSize); + offset += nodeIdSize; + ret.writeUIntBE(destination, offset, nodeIdSize); + offset += nodeIdSize; ret.writeUInt32BE(homeId, offset); offset += 4; ret.writeUInt16BE(commandLength, offset); diff --git a/packages/cc/src/cc/ZWaveLRProtocolCC.ts b/packages/cc/src/cc/ZWaveLRProtocolCC.ts deleted file mode 100644 index f8975704c3db..000000000000 --- a/packages/cc/src/cc/ZWaveLRProtocolCC.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { - CommandClasses, - type DataRate, - type FLiRS, - type NodeInformationFrame, - type NodeType, - type ProtocolVersion, - ZWaveError, - ZWaveErrorCodes, - encodeNodeInformationFrame, - parseNodeInformationFrame, - validatePayload, -} from "@zwave-js/core"; -import type { ZWaveHost } from "@zwave-js/host"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; -import { - CCCommand, - commandClass, - expectedCCResponse, - implementedVersion, -} from "../lib/CommandClassDecorators"; -import { - ZWaveLRProtocolCommand, -} from "../lib/_Types"; - -@commandClass(CommandClasses["Z-Wave Long Range Protocol"]) -@implementedVersion(1) -export class ZWaveLRProtocolCC extends CommandClass { - declare ccCommand: ZWaveLRProtocolCommand; -} - -@CCCommand(ZWaveLRProtocolCommand.NOP) -export class ZWaveLRProtocolCCNOP extends ZWaveLRProtocolCC {} - -interface ZWaveLRProtocolCCNodeInformationFrameOptions - extends CCCommandOptions, NodeInformationFrame -{} - -// BUGBUG: how much of this can we share with existing stuff? Can we use a ZWaveProtocolCCNodeInformationFrameOptions field to do the `isLongRange` stuff? -// BUGBUG: how much can we share also with the Smart Start things below that are VERY close to this stuff? -@CCCommand(ZWaveLRProtocolCommand.NodeInformationFrame) -export class ZWaveLRProtocolCCNodeInformationFrame extends ZWaveLRProtocolCC - implements NodeInformationFrame -{ - public constructor( - host: ZWaveHost, - options: - | CommandClassDeserializationOptions - | ZWaveLRProtocolCCNodeInformationFrameOptions, - ) { - super(host, options); - - let nif: NodeInformationFrame; - if (gotDeserializationOptions(options)) { - nif = parseNodeInformationFrame(this.payload, true); - } else { - nif = options; - } - - this.basicDeviceClass = 0x100; // BUGBUG: what fake value can we safely use here? - this.genericDeviceClass = nif.genericDeviceClass; - this.specificDeviceClass = nif.specificDeviceClass; - this.isListening = nif.isListening; - this.isFrequentListening = nif.isFrequentListening; - this.isRouting = false; - this.supportedDataRates = nif.supportedDataRates; - this.protocolVersion = 0; // "unknown"; - this.optionalFunctionality = false; - this.nodeType = nif.nodeType; - this.supportsSecurity = nif.supportsSecurity; - this.supportsBeaming = false; - this.supportedCCs = nif.supportedCCs; - } - - public basicDeviceClass: number; - public genericDeviceClass: number; - public specificDeviceClass: number; - public isListening: boolean; - public isFrequentListening: FLiRS; - public isRouting: boolean; - public supportedDataRates: DataRate[]; - public protocolVersion: ProtocolVersion; - public optionalFunctionality: boolean; - public nodeType: NodeType; - public supportsSecurity: boolean; - public supportsBeaming: boolean; - public supportedCCs: CommandClasses[]; - - public serialize(): Buffer { - this.payload = encodeNodeInformationFrame(this, true); - return super.serialize(); - } -} - -@CCCommand(ZWaveLRProtocolCommand.RequestNodeInformationFrame) -@expectedCCResponse(ZWaveLRProtocolCCNodeInformationFrame) -export class ZWaveLRProtocolCCRequestNodeInformationFrame - extends ZWaveLRProtocolCC -{} - -interface ZWaveLRProtocolCCAssignIDsOptions extends CCCommandOptions { - assignedNodeId: number; - homeId: number; -} - -@CCCommand(ZWaveLRProtocolCommand.AssignIDs) -export class ZWaveLRProtocolCCAssignIDs extends ZWaveLRProtocolCC { - public constructor( - host: ZWaveHost, - options: - | CommandClassDeserializationOptions - | ZWaveLRProtocolCCAssignIDsOptions, - ) { - super(host, options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 6); - this.assignedNodeId = this.payload.readUInt16BE(0) & 0xFFF; - this.homeId = this.payload.readUInt32BE(2); - } else { - this.assignedNodeId = options.assignedNodeId; - this.homeId = options.homeId; - } - } - - public assignedNodeId: number; - public homeId: number; - - public serialize(): Buffer { - this.payload = Buffer.allocUnsafe(6); - this.payload.writeUInt16BE(this.assignedNodeId, 0); - this.payload.writeUInt32BE(this.homeId, 2); - return super.serialize(); - } -} - -@CCCommand(ZWaveLRProtocolCommand.ExcludeRequest) -export class ZWaveLRProtocolCCExcludeRequest - extends ZWaveLRProtocolCCNodeInformationFrame -{} - -interface ZWaveLRProtocolCCSmartStartIncludedNodeInformationOptions - extends CCCommandOptions -{ - nwiHomeId: Buffer; -} - -// BUGBUG: this is exactly equal to the ZWaveProtocolCommand.SmartStartIncludedNodeInformation, can we reuse/inherit that somehow? -@CCCommand(ZWaveLRProtocolCommand.SmartStartIncludedNodeInformation) -export class ZWaveLRProtocolCCSmartStartIncludedNodeInformation - extends ZWaveLRProtocolCC -{ - public constructor( - host: ZWaveHost, - options: - | CommandClassDeserializationOptions - | ZWaveLRProtocolCCSmartStartIncludedNodeInformationOptions, - ) { - super(host, options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.nwiHomeId = this.payload.subarray(0, 4); - } else { - if (options.nwiHomeId.length !== 4) { - throw new ZWaveError( - `nwiHomeId must have length 4`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.nwiHomeId = options.nwiHomeId; - } - } - - public nwiHomeId: Buffer; - - public serialize(): Buffer { - this.payload = Buffer.from(this.nwiHomeId); - return super.serialize(); - } -} - -// BUGBUG this needs to include support for Sensor256ms and BeamCapability fields, yet the GetNodeInfo reserves those -@CCCommand(ZWaveLRProtocolCommand.SmartStartPrime) -export class ZWaveLRProtocolCCSmartStartPrime - extends ZWaveLRProtocolCCNodeInformationFrame -{} - -// BUGBUG this needs to include support for Sensor256ms and BeamCapability fields, yet the GetNodeInfo reserves those -@CCCommand(ZWaveLRProtocolCommand.SmartStartInclusionRequest) -export class ZWaveLRProtocolCCSmartStartInclusionRequest - extends ZWaveLRProtocolCCNodeInformationFrame -{} - -// BUGBUG: this is identical to the AssignNodeID message, except for the field names -@CCCommand(ZWaveLRProtocolCommand.ExcludeRequestConfirimation) -export class ZWaveLRProtocolCCExcludeRequestConfirimation - extends ZWaveLRProtocolCC -{ - public constructor( - host: ZWaveHost, - options: - | CommandClassDeserializationOptions - | ZWaveLRProtocolCCAssignIDsOptions, - ) { - super(host, options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 6); - this.requestingNodeId = this.payload.readUInt16BE(0) & 0xFFF; - this.homeId = this.payload.readUInt32BE(2); - } else { - this.requestingNodeId = options.assignedNodeId; - this.homeId = options.homeId; - } - } - - public requestingNodeId: number; - public homeId: number; - - public serialize(): Buffer { - this.payload = Buffer.allocUnsafe(6); - this.payload.writeUInt16BE(this.requestingNodeId, 0); - this.payload.writeUInt32BE(this.homeId, 2); - return super.serialize(); - } -} - -@CCCommand(ZWaveLRProtocolCommand.NonSecureIncusionStepComplete) -export class ZWaveLRProtocolCCNonSecureIncusionStepComplete - extends ZWaveLRProtocolCC -{} diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index 59709e22f64c..709b3f9fce7c 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -872,19 +872,6 @@ export { WindowCoveringCCValues, } from "./WindowCoveringCC"; export type { ZWavePlusCCReportOptions } from "./ZWavePlusCC"; -export { - ZWaveLRProtocolCC, - ZWaveLRProtocolCCAssignIDs, - ZWaveLRProtocolCCExcludeRequest, - ZWaveLRProtocolCCExcludeRequestConfirimation, - ZWaveLRProtocolCCNOP, - ZWaveLRProtocolCCNodeInformationFrame, - ZWaveLRProtocolCCNonSecureIncusionStepComplete, - ZWaveLRProtocolCCRequestNodeInformationFrame, - ZWaveLRProtocolCCSmartStartIncludedNodeInformation, - ZWaveLRProtocolCCSmartStartInclusionRequest, - ZWaveLRProtocolCCSmartStartPrime, -} from "./ZWaveLRProtocolCC"; export { ZWavePlusCC, ZWavePlusCCGet, diff --git a/packages/cc/src/lib/_Types.ts b/packages/cc/src/lib/_Types.ts index 4e89a491bc53..3d7b4bab0d3b 100644 --- a/packages/cc/src/lib/_Types.ts +++ b/packages/cc/src/lib/_Types.ts @@ -1670,22 +1670,6 @@ export enum ZWaveProtocolCommand { SmartStartInclusionRequest = 0x28, } -export enum ZWaveLRProtocolCommand { - NOP = 0x00, - - // BUGBUG: all defined above, can they be shared? - NodeInformationFrame = 0x01, - RequestNodeInformationFrame = 0x02, - AssignIDs = 0x03, - ExcludeRequest = 0x23, - SmartStartIncludedNodeInformation = 0x26, - SmartStartPrime = 0x27, - SmartStartInclusionRequest = 0x28, - - ExcludeRequestConfirimation = 0x29, - NonSecureIncusionStepComplete = 0x2A, -} - export enum WakeUpTime { None, "1000ms", From 8223ee9dff2dc0495186238b40b6688562f40cbb Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:04:53 -0800 Subject: [PATCH 16/43] feat(longrange): remove "Protocol" from the LR CC name --- packages/core/src/capabilities/CommandClasses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/capabilities/CommandClasses.ts b/packages/core/src/capabilities/CommandClasses.ts index 0afccf52b350..b309f36e6bdb 100644 --- a/packages/core/src/capabilities/CommandClasses.ts +++ b/packages/core/src/capabilities/CommandClasses.ts @@ -130,7 +130,7 @@ export enum CommandClasses { "Z-Wave Plus Info" = 0x5e, // Internal CC which is not used directly by applications "Z-Wave Protocol" = 0x01, - "Z-Wave Long Range Protocol" = 0x04, + "Z-Wave Long Range" = 0x04, } export function getCCName(cc: number): string { From e8f90236a2cdaffe84748e3459ae71e6198b0ced Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:05:39 -0800 Subject: [PATCH 17/43] feat(longrange): move NodeInfo parsing/encoding changes one function call up --- packages/core/src/capabilities/NodeInfo.ts | 51 +++++++++++----------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index a4c911118684..c37d0730dd64 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -114,7 +114,7 @@ export function encodeCCId( } } -export function parseCCList(payload: Buffer, isLongRange: boolean = false): { +export function parseCCList(payload: Buffer): { supportedCCs: CommandClasses[]; controlledCCs: CommandClasses[]; } { @@ -124,47 +124,32 @@ export function parseCCList(payload: Buffer, isLongRange: boolean = false): { }; let offset = 0; let isAfterMark = false; - let listEnd = payload.length; - if (isLongRange) { - validatePayload(payload.length >= offset + 1); - const listLength = payload[offset++]; - listEnd = offset + listLength; - validatePayload(payload.length >= listEnd); - } - while (offset < listEnd) { + while (offset < payload.length) { // Read either the normal or extended ccId const { ccId: cc, bytesRead } = parseCCId(payload, offset); offset += bytesRead; // CCs before the support/control mark are supported // CCs after the support/control mark are controlled - // BUGBUG: does the "mark" and support/control convention apply to isLongRange? if (cc === CommandClasses["Support/Control Mark"]) { isAfterMark = true; continue; } (isAfterMark ? ret.controlledCCs : ret.supportedCCs).push(cc); } - // BUGBUG: isLongRange prohibits CC from 0x00..0x20 from being advertised here, as does 4.3.2.1.1.17 - // BUGBUG: how do >0xFF CC get advertised? I don't immediately see a mechanism for indicating a multi-byte CC return ret; } export function encodeCCList( supportedCCs: readonly CommandClasses[], controlledCCs: readonly CommandClasses[], - isLongRange: boolean = false, ): Buffer { - const bufferLength = (isLongRange ? 1 : 0) - + sum(supportedCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))) + const bufferLength = + sum(supportedCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))) + (controlledCCs.length > 0 ? 1 : 0) // support/control mark + sum(controlledCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))); const ret = Buffer.allocUnsafe(bufferLength); let offset = 0; - if (isLongRange) { - // BUGBUG: validate bufferLength - 1 is <= 0xFF - ret[offset++] = bufferLength - 1; - } for (const cc of supportedCCs) { offset += encodeCCId(cc, ret, offset); } @@ -304,7 +289,6 @@ export function parseNodeProtocolInfo( } const hasSpecificDeviceClass = isLongRange || !!(capability & 0b100); - // BUGBUG: can we assume security is true? const supportsSecurity = isLongRange || !!(capability & 0b1); return { @@ -417,12 +401,19 @@ export function parseNodeInformationFrame( buffer: Buffer, isLongRange: boolean = false, ): NodeInformationFrame { - const { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( + let { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( buffer, isLongRange, ); + var ccListLength; + if (isLongRange) { + ccListLength = buffer[offset]; + offset += 1; + } else { + ccListLength = buffer.length - offset; + } const supportedCCs = - parseCCList(buffer.subarray(offset), isLongRange).supportedCCs; + parseCCList(buffer.subarray(offset, ccListLength)).supportedCCs; return { ...info, @@ -434,10 +425,18 @@ export function encodeNodeInformationFrame( info: NodeInformationFrame, isLongRange: boolean = false, ): Buffer { - return Buffer.concat([ - encodeNodeProtocolInfoAndDeviceClass(info, isLongRange), - encodeCCList(info.supportedCCs, [], isLongRange), - ]); + const protocolInfo = encodeNodeProtocolInfoAndDeviceClass(info, isLongRange); + const ccList = encodeCCList(info.supportedCCs, []); + + var buffers = [protocolInfo] + if (isLongRange) { + const ccListLength = Buffer.allocUnsafe(1); + ccListLength[0] = ccList.length; + buffers.concat(ccListLength); + } + buffers.concat(ccList); + + return Buffer.concat(buffers); } export function parseNodeID( From 02052efe8144acd0c4279c080373665aa2c1d43d Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:14:22 -0800 Subject: [PATCH 18/43] feat(longrange): expand LR to LongRange in smart start messages --- packages/zwave-js/src/lib/controller/Controller.ts | 4 ++-- .../lib/serialapi/application/ApplicationUpdateRequest.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index a9e6b5732f3e..e82cc8fd7c51 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -121,7 +121,7 @@ import { ApplicationUpdateRequestNodeAdded, ApplicationUpdateRequestNodeInfoReceived, ApplicationUpdateRequestNodeRemoved, - ApplicationUpdateRequestSmartStartHomeIDLRReceived, + ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived, ApplicationUpdateRequestSmartStartHomeIDReceived, } from "../serialapi/application/ApplicationUpdateRequest"; import { @@ -2151,7 +2151,7 @@ export class ZWaveController } } else if ( msg instanceof ApplicationUpdateRequestSmartStartHomeIDReceived - || msg instanceof ApplicationUpdateRequestSmartStartHomeIDLRReceived + || msg instanceof ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived ) { // the controller is in Smart Start learn mode and a node requests inclusion via Smart Start this.driver.controllerLog.print( diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 7c4f11a4d034..3598965e24cc 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts @@ -28,7 +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_HomeId_LR_Received = 0x87, // A start start long range note requests inclusion + SmartStart_HomeId_LongRange_Received = 0x87, // A start start long range note requests inclusion NodeInfo_Received = 0x84, NodeInfo_RequestDone = 0x82, NodeInfo_RequestFailed = 0x81, @@ -229,7 +229,7 @@ export class ApplicationUpdateRequestSmartStartHomeIDReceived extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase {} -@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_LR_Received) -export class ApplicationUpdateRequestSmartStartHomeIDLRReceived +@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_LongRange_Received) +export class ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase {} From c9184ef1936bf7116ce34892615b21b2987e2d01 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:15:32 -0800 Subject: [PATCH 19/43] feat(longrange): remove comment about long range CC --- packages/zwave-js/src/lib/driver/Driver.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index b1bd57a81da5..4b86274a557f 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -5557,7 +5557,6 @@ ${handlers.length} left`, } /** @internal */ - // BUGBUG: do we need to replicate this for Long Range? public async sendZWaveProtocolCC( command: ZWaveProtocolCC, options: Pick< From d00b0846c67fea699790983d921b7f06f7584f2c Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:27:54 -0800 Subject: [PATCH 20/43] feat(longrange): undo changes to GetSerialApiCapabilitiesRequest These were speculative, and I believe are not required. Long range inclusion request smart-start, not any sort of magic init here. --- .../zwave-js/src/lib/controller/Controller.ts | 12 +----- .../GetSerialApiCapabilitiesMessages.ts | 40 +------------------ 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index e82cc8fd7c51..f5de8c7f6893 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -905,7 +905,7 @@ export class ZWaveController const apiCaps = await this.driver.sendMessage< GetSerialApiCapabilitiesResponse >( - new GetSerialApiCapabilitiesRequest(this.driver, {mysteryValue: 3}), + new GetSerialApiCapabilitiesRequest(this.driver), { supportCheck: false, }, @@ -1294,16 +1294,6 @@ export class ZWaveController this._supportsTimers = initData.supportsTimers; // ignore the initVersion, no clue what to do with it - // BUGBUG: Dummy check? SimplicityStudio adds this extra 3 argument to this command. I suspect that's not the secret sauce for LR inclusion, but S2 encryption is. - await this.driver.sendMessage< - GetSerialApiCapabilitiesResponse - >( - new GetSerialApiCapabilitiesRequest(this.driver, {mysteryValue: 3}), - { - supportCheck: false, - }, - ); - // fetch the list of long range nodes until the controller reports no more const lrNodeIds: Array = []; let lrChannel : LongRangeChannel | undefined; diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts index 09cb5ed03b53..dbf295ca63d6 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts @@ -15,48 +15,10 @@ import { const NUM_FUNCTIONS = 256; const NUM_FUNCTION_BYTES = NUM_FUNCTIONS / 8; -export interface GetSerialApiCapabilitiesRequestOptions - extends MessageBaseOptions -{ - mysteryValue: number | undefined, -} - @messageTypes(MessageType.Request, FunctionType.GetSerialApiCapabilities) @expectedResponse(FunctionType.GetSerialApiCapabilities) @priority(MessagePriority.Controller) -export class GetSerialApiCapabilitiesRequest extends Message { - public constructor( - host: ZWaveHost, - options: - | MessageDeserializationOptions - | GetSerialApiCapabilitiesRequestOptions, - ) { - super(host, options); - - if (gotDeserializationOptions(options)) { - if (this.payload.length >= 1) { - this.mysteryValue = this.payload[0]; - } else { - this.mysteryValue = undefined; - } - } else { - this.mysteryValue = options.mysteryValue; - } - } - - public mysteryValue: number | undefined; - - public serialize(): Buffer { - if (this.mysteryValue !== undefined) { - this.payload = Buffer.allocUnsafe(1); - this.payload[0] = this.mysteryValue; - } else { - this.payload = Buffer.allocUnsafe(0); - } - - return super.serialize(); - } -} +export class GetSerialApiCapabilitiesRequest extends Message {} export interface GetSerialApiCapabilitiesResponseOptions extends MessageBaseOptions From 15bc80867bb5203f7dc3e98c105d8a985861b7b2 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:48:24 -0800 Subject: [PATCH 21/43] feat(longrange): use segment for GetLongRangeNodes* --- .../zwave-js/src/lib/controller/Controller.ts | 4 +-- .../GetSerialApiInitDataMessages.ts | 34 ++++++------------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index f5de8c7f6893..45b1cf1cfceb 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1300,13 +1300,13 @@ export class ZWaveController const maxPayloadSize = await this.getMaxPayloadSize(); let maxPayloadSizeLR : number | undefined; if (this.isLongRange()) { - const nodePage = 0; + const segment = 0; while (true) { const nodesResponse = await this.driver.sendMessage< GetLongRangeNodesResponse >( new GetLongRangeNodesRequest(this.driver, { - listStartOffset128: nodePage, + segmentNumber: segment, }), ); lrNodeIds.push(...nodesResponse.nodeIds); diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts index ac5c84338704..a36982be3e97 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts @@ -220,7 +220,7 @@ export class GetSerialApiInitDataResponse extends Message { // chip version: 0 export interface GetLongRangeNodesRequestOptions extends MessageBaseOptions { - listStartOffset128: number; + segmentNumber: number; } @messageTypes(MessageType.Request, FunctionType.GetLongRangeNodes) @@ -236,28 +236,23 @@ export class GetLongRangeNodesRequest extends Message { super(host, options); if (gotDeserializationOptions(options)) { - // BUGBUG: validate length at least - this.listStartOffset128 = this.payload[0]; + this.segmentNumber = this.payload[0]; } else { - this.listStartOffset128 = options.listStartOffset128; + this.segmentNumber = options.segmentNumber; } } - public listStartOffset128: number; + public segmentNumber: number; public serialize(): Buffer { - this.payload = Buffer.allocUnsafe( - 1, - ); - - this.payload[0] = this.listStartOffset128; + this.payload = Buffer.from([this.segmentNumber]); return super.serialize(); } } export interface GetLongRangeNodesResponseOptions extends MessageBaseOptions { moreNodes: boolean; - listStartOffset128: number; + segmentNumber: number; nodeIds: number[]; } @@ -272,14 +267,12 @@ export class GetLongRangeNodesResponse extends Message { super(host, options); if (gotDeserializationOptions(options)) { - // BUGBUG: validate length at least this.moreNodes = this.payload[0] != 0; - this.listStartOffset128 = this.payload[1]; + this.segmentNumber = this.payload[1]; const listLength = this.payload[2]; const listStart = 3; const listEnd = listStart + listLength; - // BUGGUG validate listEnd <= this.payload.length if (listEnd <= this.payload.length) { const nodeBitMask = this.payload.subarray( listStart, @@ -294,13 +287,13 @@ export class GetLongRangeNodesResponse extends Message { } } else { this.moreNodes = options.moreNodes; - this.listStartOffset128 = options.listStartOffset128; + this.segmentNumber = options.segmentNumber; this.nodeIds = options.nodeIds; } } public moreNodes: boolean; - public listStartOffset128: number; + public segmentNumber: number; public nodeIds: readonly number[]; public serialize(): Buffer { @@ -309,7 +302,7 @@ export class GetLongRangeNodesResponse extends Message { ); this.payload[0] = this.moreNodes ? 1 : 0; - this.payload[1] = this.listStartOffset128; + this.payload[1] = this.segmentNumber; this.payload[2] = NUM_LR_NODEMASK_SEGMENT_BYTES; const nodeBitMask = encodeLongRangeNodeBitMask( @@ -322,11 +315,6 @@ export class GetLongRangeNodesResponse extends Message { } private listStartNode(): number { - return 256 + NUM_LR_NODES_PER_SEGMENT * 8 * this.listStartOffset128; - } - - private listEndNode(): number { - return 256 - + NUM_LR_NODES_PER_SEGMENT * 8 * (1 + this.listStartOffset128); + return 256 + NUM_LR_NODES_PER_SEGMENT * this.segmentNumber; } } From 7b268b4619b098d520853601dcd558a0747865bb Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 05:52:01 -0800 Subject: [PATCH 22/43] feat(longrange): array.push, not array.concat to append --- packages/core/src/capabilities/NodeInfo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index c37d0730dd64..9c8d34ad6755 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -432,9 +432,9 @@ export function encodeNodeInformationFrame( if (isLongRange) { const ccListLength = Buffer.allocUnsafe(1); ccListLength[0] = ccList.length; - buffers.concat(ccListLength); + buffers.push(ccListLength); } - buffers.concat(ccList); + buffers.push(ccList); return Buffer.concat(buffers); } From 1bb54b2138b63c68449efaa5beaa62bb82129c04 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 06:02:59 -0800 Subject: [PATCH 23/43] feat(longrange): move fetching LR nodes to a helper method --- .../zwave-js/src/lib/controller/Controller.ts | 44 ++++++++++++------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 45b1cf1cfceb..cf7cec802a01 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1295,26 +1295,11 @@ export class ZWaveController // ignore the initVersion, no clue what to do with it // fetch the list of long range nodes until the controller reports no more - const lrNodeIds: Array = []; + const lrNodeIds = await this.getLongRangeNodes(); let lrChannel : LongRangeChannel | undefined; const maxPayloadSize = await this.getMaxPayloadSize(); let maxPayloadSizeLR : number | undefined; if (this.isLongRange()) { - const segment = 0; - while (true) { - const nodesResponse = await this.driver.sendMessage< - GetLongRangeNodesResponse - >( - new GetLongRangeNodesRequest(this.driver, { - segmentNumber: segment, - }), - ); - lrNodeIds.push(...nodesResponse.nodeIds); - if (!nodesResponse.moreNodes) { - break; - } - } - // TODO: restore/set the channel const lrChannelResp = await this.driver.sendMessage(new GetLongRangeChannelRequest(this.driver)); lrChannel = lrChannelResp.longRangeChannel; @@ -1480,6 +1465,33 @@ 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: Array = []; + + if (this.isLongRange()) { + const segment = 0; + while (true) { + 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 From 9b5c7bac17e25b92c0988db619ea073aeb4874d2 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 06:18:13 -0800 Subject: [PATCH 24/43] feat(longrange): move LongRangeChannel to core..Protocols.ts --- packages/core/src/capabilities/Protocols.ts | 19 +++++++++++++++++++ .../zwave-js/src/lib/controller/Controller.ts | 3 ++- .../capability/LongRangeSetupMessages.ts | 10 +++------- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index b03cbdfac809..a1b267614593 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -87,3 +87,22 @@ 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 longRangeChannelToString(channel: LongRangeChannel): string { + switch (channel) { + case LongRangeChannel.Unknown: + return "Unknown"; + case LongRangeChannel.A: + return "Channel A (912MHz)"; + case LongRangeChannel.B: + return "Channel B (920MHz)"; + } + return `Unknown (${num2hex(channel)})`; +} \ No newline at end of file diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index cf7cec802a01..6374f7e26ff5 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -70,6 +70,7 @@ import { nwiHomeIdFromDSK, securityClassIsS2, securityClassOrder, + LongRangeChannel, } from "@zwave-js/core"; import { migrateNVM } from "@zwave-js/nvmedit"; import { @@ -182,7 +183,7 @@ import { type SerialAPISetup_SetTXStatusReportResponse, } from "../serialapi/capability/SerialAPISetupMessages"; import { - GetLongRangeChannelRequest, LongRangeChannel, + GetLongRangeChannelRequest, type GetLongRangeChannelResponse, } from "../serialapi/capability/LongRangeSetupMessages"; import { SetApplicationNodeInformationRequest } from "../serialapi/capability/SetApplicationNodeInformationRequest"; diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts index b8f86aa6285e..2bc2a333357f 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -13,13 +13,9 @@ import { priority, } from "@zwave-js/serial"; -// TODO: move to header? -export enum LongRangeChannel { - Unknown = 0x00, // Reserved - A = 0x01, - B = 0x02, - // 0x03..0xFF are reserved and must not be used -} +import { + LongRangeChannel +} from "@zwave-js/core"; @messageTypes(MessageType.Request, FunctionType.GetLongRangeChannel) @expectedResponse(FunctionType.GetLongRangeChannel) From 32c0c34084e1c998b12b4698a420cb43afbfdc72 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 06:19:18 -0800 Subject: [PATCH 25/43] feat(longrange): remove validate length comments --- .../src/lib/serialapi/capability/LongRangeSetupMessages.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts index 2bc2a333357f..8459eef7cc15 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -36,7 +36,6 @@ export class LongRangeChannelMessageBase extends Message { super(host, options); if (gotDeserializationOptions(options)) { - // BUGBUG: verify length? switch (this.payload[0]) { case 0x01: this.longRangeChannel = LongRangeChannel.A; @@ -137,7 +136,6 @@ export class SetLongRangeShadowNodeIDsRequest extends Message { super(host, options); if (gotDeserializationOptions(options)) { - // BUGBUG: verify length? this.shadowNodeIds = parseBitMask( this.payload.subarray(0, 1), LONG_RANGE_SHADOW_NODE_IDS_START, From 25908de2fcd229995669e2e3fc4b4730cb7715f8 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 07:13:13 -0800 Subject: [PATCH 26/43] feat(longrange): remove ResponseStatus --- .../capability/LongRangeSetupMessages.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts index 8459eef7cc15..f246874aadca 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -85,13 +85,6 @@ export interface SetLongRangeChannelResponseOptions extends MessageBaseOptions { responseStatus: number; } -// BUGBUG: move someplace common, see Spec 4.2.5 -// -// Although, that section refers to a "callback message", and SetZWaveLongRangeChannel doesn't define a callback message... -export enum ResponseStatus { - FAILED = 0x00, -} - export class ResponseStatusMessageBase extends Message implements SuccessIndicator { @@ -100,16 +93,13 @@ export class ResponseStatusMessageBase extends Message options: MessageDeserializationOptions, ) { super(host, options); - this._status = this.payload[0]; + this.success = this.payload[0] !== 0; } - private _status: ResponseStatus; - public get getStatus(): ResponseStatus { - return this._status; - } + public readonly success: boolean; public isOK(): boolean { - return this._status != ResponseStatus.FAILED; + return this.success; } } From e8c31345abe7eb50a230563589c9e2940d8aa690 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 07:29:25 -0800 Subject: [PATCH 27/43] feat(longrange): fix erronous boolean instead of bitwise and --- packages/core/src/capabilities/NodeInfo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index 9c8d34ad6755..fe7f5b79f0b4 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -274,7 +274,7 @@ export function parseNodeProtocolInfo( switch ( isLongRange ? (0b1_0000_0000 | (capability & 0b0010)) - : (capability && 0b1010) + : (capability & 0b1010) ) { case 0b0_0000_1000: case 0b1_0000_0000: From 6e52505634e19609de90fb764bc9d76e54a5472f Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 07:44:40 -0800 Subject: [PATCH 28/43] feat(longrange): fix lint errors --- packages/core/src/capabilities/NodeInfo.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index fe7f5b79f0b4..cc0e453b0910 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -401,11 +401,13 @@ export function parseNodeInformationFrame( buffer: Buffer, isLongRange: boolean = false, ): NodeInformationFrame { - let { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass( + const result = parseNodeProtocolInfoAndDeviceClass( buffer, isLongRange, ); - var ccListLength; + const info = result.info; + let offset = result.bytesRead; + let ccListLength; if (isLongRange) { ccListLength = buffer[offset]; offset += 1; @@ -428,7 +430,7 @@ export function encodeNodeInformationFrame( const protocolInfo = encodeNodeProtocolInfoAndDeviceClass(info, isLongRange); const ccList = encodeCCList(info.supportedCCs, []); - var buffers = [protocolInfo] + const buffers = [protocolInfo] if (isLongRange) { const ccListLength = Buffer.allocUnsafe(1); ccListLength[0] = ccList.length; From e55e4793cd66df9515efe545958f81f8e8bb2eda Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 10:23:10 -0800 Subject: [PATCH 29/43] feat(longrange): lint fixes --- packages/cc/src/cc/Security2CC.ts | 4 ++- packages/core/src/capabilities/NodeInfo.ts | 9 ++++-- packages/core/src/capabilities/Protocols.ts | 4 +-- packages/serial/src/message/Constants.ts | 2 +- .../zwave-js/src/lib/controller/Controller.ts | 31 ++++++++++++------- .../application/ApplicationUpdateRequest.ts | 4 ++- .../capability/LongRangeSetupMessages.ts | 4 +-- 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index dea4221d0f25..0e70e3d42ef5 100644 --- a/packages/cc/src/cc/Security2CC.ts +++ b/packages/cc/src/cc/Security2CC.ts @@ -95,7 +95,9 @@ function getAuthenticationData( unencryptedPayload: Buffer, ): Buffer { const nodeIdSize = (sendingNodeId < 256 && destination < 256) ? 1 : 2; - const ret = Buffer.allocUnsafe(2*nodeIdSize + 6 + unencryptedPayload.length); + const ret = Buffer.allocUnsafe( + 2 * nodeIdSize + 6 + unencryptedPayload.length, + ); let offset = 0; ret.writeUIntBE(sendingNodeId, offset, nodeIdSize); offset += nodeIdSize; diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index cc0e453b0910..d322fd9e8907 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -427,10 +427,13 @@ export function encodeNodeInformationFrame( info: NodeInformationFrame, isLongRange: boolean = false, ): Buffer { - const protocolInfo = encodeNodeProtocolInfoAndDeviceClass(info, isLongRange); - const ccList = encodeCCList(info.supportedCCs, []); + const protocolInfo = encodeNodeProtocolInfoAndDeviceClass( + info, + isLongRange, + ); + const ccList = encodeCCList(info.supportedCCs, []); - const buffers = [protocolInfo] + const buffers = [protocolInfo]; if (isLongRange) { const ccListLength = Buffer.allocUnsafe(1); ccListLength[0] = ccList.length; diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index a1b267614593..9b2e1a44ec49 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -19,7 +19,7 @@ export function zwaveDataRateToString(rate: ZWaveDataRate): string { return "40 kbit/s"; case ZWaveDataRate["100k"]: return "100 kbit/s"; - } + } return `Unknown (${num2hex(rate)})`; } @@ -105,4 +105,4 @@ export function longRangeChannelToString(channel: LongRangeChannel): string { return "Channel B (920MHz)"; } return `Unknown (${num2hex(channel)})`; -} \ No newline at end of file +} diff --git a/packages/serial/src/message/Constants.ts b/packages/serial/src/message/Constants.ts index 2714dd63e839..a87f3499956f 100644 --- a/packages/serial/src/message/Constants.ts +++ b/packages/serial/src/message/Constants.ts @@ -53,7 +53,7 @@ export enum FunctionType { EnterBootloader = 0x27, // Leave Serial API and enter bootloader (700+ series only). Enter Auto-Programming mode (500 series only). 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 ExtNVMWriteLongBuffer = 0x2b, // Writes a buffer to the external NVM diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 6374f7e26ff5..71076bd5bd69 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, @@ -70,7 +71,6 @@ import { nwiHomeIdFromDSK, securityClassIsS2, securityClassOrder, - LongRangeChannel, } from "@zwave-js/core"; import { migrateNVM } from "@zwave-js/nvmedit"; import { @@ -122,8 +122,8 @@ import { ApplicationUpdateRequestNodeAdded, ApplicationUpdateRequestNodeInfoReceived, ApplicationUpdateRequestNodeRemoved, - ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived, ApplicationUpdateRequestSmartStartHomeIDReceived, + ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived, } from "../serialapi/application/ApplicationUpdateRequest"; import { type SerialAPIStartedRequest, @@ -156,6 +156,10 @@ 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, @@ -182,10 +186,6 @@ import { SerialAPISetup_SetTXStatusReportRequest, type SerialAPISetup_SetTXStatusReportResponse, } from "../serialapi/capability/SerialAPISetupMessages"; -import { - GetLongRangeChannelRequest, - type GetLongRangeChannelResponse, -} from "../serialapi/capability/LongRangeSetupMessages"; import { SetApplicationNodeInformationRequest } from "../serialapi/capability/SetApplicationNodeInformationRequest"; import { GetControllerIdRequest, @@ -1297,12 +1297,14 @@ export class ZWaveController // fetch the list of long range nodes until the controller reports no more const lrNodeIds = await this.getLongRangeNodes(); - let lrChannel : LongRangeChannel | undefined; + let lrChannel: LongRangeChannel | undefined; const maxPayloadSize = await this.getMaxPayloadSize(); - let maxPayloadSizeLR : number | undefined; + let maxPayloadSizeLR: number | undefined; if (this.isLongRange()) { // TODO: restore/set the channel - const lrChannelResp = await this.driver.sendMessage(new GetLongRangeChannelRequest(this.driver)); + const lrChannelResp = await this.driver.sendMessage< + GetLongRangeChannelResponse + >(new GetLongRangeChannelRequest(this.driver)); lrChannel = lrChannelResp.longRangeChannel; // TODO: fetch the long range max payload size and cache it @@ -1334,7 +1336,11 @@ export class ZWaveController zwave nodes in the network: ${initData.nodeIds.join(", ")} max payload size: ${maxPayloadSize} LR nodes in the network: ${lrNodeIds.join(", ")} - LR channel: ${lrChannel ? getEnumMemberName(LongRangeChannel, lrChannel) : ""} + LR channel: ${ + lrChannel + ? getEnumMemberName(LongRangeChannel, lrChannel) + : "" + } LR max payload size: ${maxPayloadSizeLR}`, ); @@ -2154,7 +2160,8 @@ export class ZWaveController } } else if ( msg instanceof ApplicationUpdateRequestSmartStartHomeIDReceived - || msg instanceof ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived + || msg + instanceof ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived ) { // the controller is in Smart Start learn mode and a node requests inclusion via Smart Start this.driver.controllerLog.print( @@ -4423,7 +4430,7 @@ ${associatedNodes.join(", ")}`, this.driver.controllerLog.logNode(nodeId, { message: `Skipping SUC return route because isLR...`, direction: "outbound", - }); + }); return true; } diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 3598965e24cc..54b8473a9a55 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts @@ -229,7 +229,9 @@ export class ApplicationUpdateRequestSmartStartHomeIDReceived extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase {} -@applicationUpdateType(ApplicationUpdateTypes.SmartStart_HomeId_LongRange_Received) +@applicationUpdateType( + ApplicationUpdateTypes.SmartStart_HomeId_LongRange_Received, +) export class ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase {} diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts index f246874aadca..b0e438d8c595 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -13,9 +13,7 @@ import { priority, } from "@zwave-js/serial"; -import { - LongRangeChannel -} from "@zwave-js/core"; +import { LongRangeChannel } from "@zwave-js/core"; @messageTypes(MessageType.Request, FunctionType.GetLongRangeChannel) @expectedResponse(FunctionType.GetLongRangeChannel) From f0d0dde1c590e216711c7b1b4ba6f434f685a0f0 Mon Sep 17 00:00:00 2001 From: "Jeremy T. Braun" Date: Wed, 15 Nov 2023 11:52:28 -0800 Subject: [PATCH 30/43] feat(longrange): change LR=>LongRange, condition some serial api setup commands on supported state --- .../zwave-js/src/lib/controller/Controller.ts | 22 +++++++--- .../capability/SerialAPISetupMessages.ts | 41 +++++++++---------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 71076bd5bd69..6a7732f73a1d 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -163,8 +163,8 @@ import { import { SerialAPISetupCommand, SerialAPISetup_CommandUnsupportedResponse, - SerialAPISetup_GetLRMaximumPayloadSizeRequest, - type SerialAPISetup_GetLRMaximumPayloadSizeResponse, + SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest, + type SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse, SerialAPISetup_GetMaximumPayloadSizeRequest, type SerialAPISetup_GetMaximumPayloadSizeResponse, SerialAPISetup_GetPowerlevel16BitRequest, @@ -1298,9 +1298,19 @@ export class ZWaveController // fetch the list of long range nodes until the controller reports no more const lrNodeIds = await this.getLongRangeNodes(); let lrChannel: LongRangeChannel | undefined; - const maxPayloadSize = await this.getMaxPayloadSize(); + let maxPayloadSize : number | undefined; + if ( + this.isSerialAPISetupCommandSupported( + SerialAPISetupCommand.GetMaximumPayloadSize + ) + ) { + maxPayloadSize = await this.getMaxPayloadSize(); + } let maxPayloadSizeLR: number | undefined; - if (this.isLongRange()) { + if (this.isLongRange() && this.isSerialAPISetupCommandSupported( + SerialAPISetupCommand.GetLongRangeMaximumPayloadSize + ) +) { // TODO: restore/set the channel const lrChannelResp = await this.driver.sendMessage< GetLongRangeChannelResponse @@ -5883,9 +5893,9 @@ ${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/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( From 2e7971db330d36781d661b19f0ef0055d57eda7f Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 16 Jan 2024 14:30:25 +0100 Subject: [PATCH 31/43] fix: only pass Z-Wave LR protocol flag to SmartStart inclusion --- packages/zwave-js/src/Utils.ts | 10 +-- .../zwave-js/src/lib/controller/Controller.ts | 87 ++++++++----------- .../zwave-js/src/lib/controller/Inclusion.ts | 18 ++-- .../zwave-js/src/lib/driver/NetworkCache.ts | 11 +++ .../network-mgmt/AddNodeToNetworkRequest.ts | 25 +++--- 5 files changed, 69 insertions(+), 82 deletions(-) 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 6a7732f73a1d..9d2e6cefc9cc 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -334,7 +334,6 @@ import { type ExclusionOptions, ExclusionStrategy, type FoundNode, - type InclusionFlagsInternal, type InclusionOptions, type InclusionOptionsInternal, type InclusionResult, @@ -1298,19 +1297,20 @@ export class ZWaveController // fetch the list of long range nodes until the controller reports no more const lrNodeIds = await this.getLongRangeNodes(); let lrChannel: LongRangeChannel | undefined; - let maxPayloadSize : number | undefined; + let maxPayloadSize: number | undefined; if ( this.isSerialAPISetupCommandSupported( - SerialAPISetupCommand.GetMaximumPayloadSize + SerialAPISetupCommand.GetMaximumPayloadSize, ) ) { - maxPayloadSize = await this.getMaxPayloadSize(); + maxPayloadSize = await this.getMaxPayloadSize(); } let maxPayloadSizeLR: number | undefined; - if (this.isLongRange() && this.isSerialAPISetupCommandSupported( - SerialAPISetupCommand.GetLongRangeMaximumPayloadSize - ) -) { + if ( + this.isLongRange() && this.isSerialAPISetupCommandSupported( + SerialAPISetupCommand.GetLongRangeMaximumPayloadSize, + ) + ) { // TODO: restore/set the channel const lrChannelResp = await this.driver.sendMessage< GetLongRangeChannelResponse @@ -1617,7 +1617,6 @@ export class ZWaveController private _includeController: boolean = false; private _exclusionOptions: ExclusionOptions | undefined; private _inclusionOptions: InclusionOptionsInternal | undefined; - private _inclusionFlags: InclusionFlagsInternal | undefined; private _nodePendingInclusion: ZWaveNode | undefined; private _nodePendingExclusion: ZWaveNode | undefined; private _nodePendingReplace: ZWaveNode | undefined; @@ -1655,25 +1654,11 @@ export class ZWaveController ); } - // BUGBUG: fix me - // if (options.isLongRange && !this.isLongRange()) { - // throw new ZWaveError( - // `Invalid long range inclusion on a non-LR host`, - // ZWaveErrorCodes.Argument_Invalid, - // ) - // } - const isLongRange = this.isLongRange(); - // Leave SmartStart listening mode so we can switch to exclusion mode await this.pauseSmartStart(); this.setInclusionState(InclusionState.Including); this._inclusionOptions = options; - this._inclusionFlags = { - highPower: true, - networkWide: true, - protocolLongRange: isLongRange, - }; try { this.driver.controllerLog.print( @@ -1689,7 +1674,8 @@ export class ZWaveController await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Any, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); @@ -1744,11 +1730,6 @@ export class ZWaveController strategy: InclusionStrategy.SmartStart, provisioning: provisioningEntry, }; - this._inclusionFlags = { - highPower: true, - networkWide: true, - protocolLongRange: provisioningEntry.isLongRange, - }; try { this.driver.controllerLog.print( @@ -1761,7 +1742,9 @@ export class ZWaveController new AddNodeDSKToNetworkRequest(this.driver, { nwiHomeId: nwiHomeIdFromDSK(dskBuffer), authHomeId: authHomeIdFromDSK(dskBuffer), - ...this._inclusionFlags, + protocol: provisioningEntry.protocol, + highPower: true, + networkWide: true, }), ); @@ -1789,7 +1772,8 @@ export class ZWaveController new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print(`The inclusion process was stopped`); @@ -1808,7 +1792,8 @@ export class ZWaveController >( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); if (response.status === AddNodeStatus.Done) { @@ -1841,7 +1826,8 @@ export class ZWaveController await this.driver.sendMessage( new AddNodeToNetworkRequest(this.driver, { addNodeType: AddNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print( @@ -1939,7 +1925,8 @@ export class ZWaveController new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print( @@ -1980,7 +1967,8 @@ export class ZWaveController new AddNodeToNetworkRequest(this.driver, { callbackId: 0, // disable callbacks addNodeType: AddNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print( @@ -2022,32 +2010,19 @@ export class ZWaveController return false; } - // BUGBUG: fix me, same logic as beginInclusion, probably an incoming option... - // if (options.isLongRange && !this.isLongRange()) { - // throw new ZWaveError( - // `Invalid long range inclusion on a non-LR host`, - // ZWaveErrorCodes.Argument_Invalid, - // ) - // } - const isLongRange = this.isLongRange(); - // 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...`); - this._inclusionFlags = { - highPower: true, - networkWide: true, - protocolLongRange: isLongRange, - }; try { // kick off the inclusion process await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { removeNodeType: RemoveNodeType.Any, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print( @@ -2084,7 +2059,8 @@ export class ZWaveController new RemoveNodeFromNetworkRequest(this.driver, { callbackId: 0, // disable callbacks removeNodeType: RemoveNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print(`the exclusion process was stopped`); @@ -2107,7 +2083,8 @@ export class ZWaveController await this.driver.sendMessage( new RemoveNodeFromNetworkRequest(this.driver, { removeNodeType: RemoveNodeType.Stop, - ...this._inclusionFlags, + highPower: true, + networkWide: true, }), ); this.driver.controllerLog.print( @@ -5895,7 +5872,11 @@ ${associatedNodes.join(", ")}`, const result = await this.driver.sendMessage< | SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse | SerialAPISetup_CommandUnsupportedResponse - >(new SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest(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 184500214544..ee6d07ea283c 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 */ @@ -182,13 +186,6 @@ export type InclusionOptionsInternal = provisioning: PlannedProvisioningEntry; }; -// BUGBUG: better way to do this? -export type InclusionFlagsInternal = { - highPower: boolean; - networkWide: boolean; - protocolLongRange: boolean; -}; - export type ExclusionOptions = { strategy: | ExclusionStrategy.ExcludeOnly @@ -237,9 +234,8 @@ export interface PlannedProvisioningEntry { /** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */ dsk: string; - /** The device must be included via ZWave Long Range */ - // BUGBUG: by adding this here we probably broke the API used by JSUI, etc... - isLongRange: boolean; + /** Which protocol to use for inclusion. Default: Z-Wave Classic */ + protocol?: Protocols; /** The security classes that have been **granted** by the user */ securityClasses: SecurityClass[]; diff --git a/packages/zwave-js/src/lib/driver/NetworkCache.ts b/packages/zwave-js/src/lib/driver/NetworkCache.ts index 06e18b2b96d2..af467f1baa7d 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, @@ -203,6 +204,10 @@ 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; + } ret.push(parsed); } else { return; @@ -452,6 +457,12 @@ export function serializeNetworkCacheValue( entry.status, ); } + if (entry.protocol != undefined) { + serialized.protocol = getEnumMemberName( + Protocols, + entry.protocol, + ); + } ret.push(serialized); } return ret; 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 7df76c027208..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"; @@ -55,7 +56,6 @@ interface AddNodeToNetworkRequestOptions extends MessageBaseOptions { addNodeType?: AddNodeType; highPower?: boolean; networkWide?: boolean; - protocolLongRange?: boolean; } interface AddNodeDSKToNetworkRequestOptions extends MessageBaseOptions { @@ -63,7 +63,7 @@ interface AddNodeDSKToNetworkRequestOptions extends MessageBaseOptions { authHomeId: Buffer; highPower?: boolean; networkWide?: boolean; - protocolLongRange?: boolean; + protocol?: Protocols; } export function computeNeighborDiscoveryTimeout( @@ -137,7 +137,6 @@ export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { this.addNodeType = options.addNodeType; this.highPower = !!options.highPower; this.networkWide = !!options.networkWide; - this.protocolLongRange = !!options.protocolLongRange; } /** The type of node to add */ @@ -146,14 +145,11 @@ export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { public highPower: boolean = false; /** Whether to include network wide */ public networkWide: boolean = false; - /** Whether to include as long-range or not */ - public protocolLongRange: boolean = false; public serialize(): Buffer { let data: number = this.addNodeType || AddNodeType.Any; if (this.highPower) data |= AddNodeFlags.HighPower; if (this.networkWide) data |= AddNodeFlags.NetworkWide; - if (this.protocolLongRange) data |= AddNodeFlags.ProtocolLongRange; this.payload = Buffer.from([data, this.callbackId]); @@ -173,7 +169,6 @@ export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { ...message, "high power": this.highPower, "network wide": this.networkWide, - "long range": this.protocolLongRange, }; if (this.hasCallbackId()) { @@ -218,24 +213,26 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { this.authHomeId = options.authHomeId; this.highPower = !!options.highPower; this.networkWide = !!options.networkWide; - this.protocolLongRange = !!options.protocolLongRange; + 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 protocolLongRange: boolean = false; + 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.protocolLongRange) control |= AddNodeFlags.ProtocolLongRange; + if (this.protocol === Protocols.ZWaveLongRange) { + control |= AddNodeFlags.ProtocolLongRange; + } this.payload = Buffer.concat([ Buffer.from([control, this.callbackId]), @@ -252,7 +249,9 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { "NWI Home ID": buffer2hex(this.nwiHomeId), "high power": this.highPower, "network wide": this.networkWide, - "long range": this.protocolLongRange, + protocol: this.protocol === Protocols.ZWaveLongRange + ? "Z-Wave Long Range" + : "Z-Wave Classic", }; if (this.hasCallbackId()) { message["callback id"] = this.callbackId; From 8715fced2d75906a9926244b90663accf8f1d2e5 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 16 Jan 2024 21:44:37 +0100 Subject: [PATCH 32/43] refactor: cleanup, prevent managing routes for LR, highlight some FIXMEs --- packages/core/src/capabilities/NodeInfo.ts | 1 + packages/core/src/capabilities/Protocols.ts | 4 + .../zwave-js/src/lib/controller/Controller.ts | 115 +++++++++++++++--- .../zwave-js/src/lib/controller/Inclusion.ts | 15 +-- packages/zwave-js/src/lib/controller/utils.ts | 20 +++ packages/zwave-js/src/lib/driver/Driver.ts | 7 ++ .../zwave-js/src/lib/driver/NetworkCache.ts | 32 +++++ packages/zwave-js/src/lib/node/Node.ts | 9 ++ 8 files changed, 174 insertions(+), 29 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index d322fd9e8907..de09d4a349bf 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -212,6 +212,7 @@ export type NodeInformationFrame = & NodeProtocolInfoAndDeviceClass & ApplicationNodeInformation; +// FIXME: Split these methods into two, one each for long range and one each for classic Z-Wave export function parseNodeProtocolInfo( buffer: Buffer, offset: number, diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index 9b2e1a44ec49..38ad1aa1a32e 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -106,3 +106,7 @@ export function longRangeChannelToString(channel: LongRangeChannel): string { } return `Unknown (${num2hex(channel)})`; } + +export function isLongRangeNodeId(nodeId: number): boolean { + return nodeId > 255; +} diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 9d2e6cefc9cc..59080c0e11f8 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -44,6 +44,7 @@ import { NodeType, type ProtocolDataRate, ProtocolType, + Protocols, RFRegion, type RSSI, type Route, @@ -66,6 +67,7 @@ import { encodeX25519KeyDERSPKI, indexDBsByNode, isEmptyRoute, + isLongRangeNodeId, isValidDSK, isZWaveError, nwiHomeIdFromDSK, @@ -1368,10 +1370,8 @@ export class ZWaveController ); nodeIds.unshift(this._ownNodeId!); } - - // BUGBUG: do nodes need to implicitly know that they were a long range node? Or are long range nodes determined 100% by their nodeID values being >= 256? - // The controller is the odd-man out, as it's both. Let's assume that apart from explicit exclusion we don't need to know for now. nodeIds.push(...lrNodeIds); + for (const nodeId of nodeIds) { this._nodes.set( nodeId, @@ -1470,6 +1470,7 @@ export class ZWaveController } private isLongRange(): boolean { + // FIXME: Rely on the SerialAPIStarted command, make sure the controller is soft-reset before we need to know this return !!this._supportsLongRange; } @@ -2341,9 +2342,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, @@ -3496,10 +3499,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; @@ -3767,9 +3773,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. @@ -4046,6 +4054,8 @@ supported CCs: ${ const todoSleeping: number[] = []; const addTodo = (nodeId: number) => { + if (isLongRangeNodeId(nodeId)) return; + if (pendingNodes.has(nodeId)) { pendingNodes.delete(nodeId); const node = this.nodes.getOrThrow(nodeId); @@ -4177,6 +4187,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) { @@ -4413,12 +4430,13 @@ ${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 (nodeId >= 0x100) { - this.driver.controllerLog.logNode(nodeId, { - message: `Skipping SUC return route because isLR...`, - direction: "outbound", - }); - return true; + 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, { @@ -4508,6 +4526,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", @@ -4607,6 +4634,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", @@ -4693,6 +4729,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( @@ -4765,6 +4817,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( @@ -4889,6 +4957,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", diff --git a/packages/zwave-js/src/lib/controller/Inclusion.ts b/packages/zwave-js/src/lib/controller/Inclusion.ts index ee6d07ea283c..10be0236386a 100644 --- a/packages/zwave-js/src/lib/controller/Inclusion.ts +++ b/packages/zwave-js/src/lib/controller/Inclusion.ts @@ -137,11 +137,6 @@ export type InclusionOptions = * This is not recommended due to the overhead caused by S0. */ forceSecurity?: boolean; - - /** - * Force long range. If not provided, will default to long range iff the controller supports it, and not otherwise. - */ - isLongRange?: boolean; } | { strategy: InclusionStrategy.Security_S2; @@ -168,11 +163,6 @@ export type InclusionOptions = strategy: | InclusionStrategy.Insecure | InclusionStrategy.Security_S0; - - /** - * Force long range. If not provided, will default to long range iff the controller supports it, and not otherwise. - */ - isLongRange?: boolean; }; /** @@ -236,6 +226,11 @@ export interface PlannedProvisioningEntry { /** 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 4b86274a557f..644b50db2e4b 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -1428,6 +1428,8 @@ export class Driver extends TypedEventEmitter // 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 + // FIXME: Setting the node ID type, opening the cache and querying the controller ID should be done AFTER soft-resetting + // Identify the controller and determine if it supports soft reset await this.controller.identify(); await this.initNetworkCache(this.controller.homeId!); @@ -1445,6 +1447,11 @@ export class Driver extends TypedEventEmitter await this.softResetInternal(false); } + // FIXME: We should now know if the controller supports ZWLR or not + // Also, set the node ID type to 16-bit here only if ZWLR is supported. + + // FIXME: This block is unnecessary when setting the node ID type explicitly + // 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 diff --git a/packages/zwave-js/src/lib/driver/NetworkCache.ts b/packages/zwave-js/src/lib/driver/NetworkCache.ts index af467f1baa7d..90bf7c9b51ca 100644 --- a/packages/zwave-js/src/lib/driver/NetworkCache.ts +++ b/packages/zwave-js/src/lib/driver/NetworkCache.ts @@ -178,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)) ) { @@ -208,6 +217,13 @@ function tryParseProvisioningList( 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; @@ -270,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") { @@ -463,6 +489,12 @@ export function serializeNetworkCacheValue( 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 d9a12cf74ba8..6e246c94664a 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, @@ -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( From f9585f09bcf08b56fbf1de74054a9c442c726a06 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Tue, 16 Jan 2024 21:57:36 +0100 Subject: [PATCH 33/43] chore: more cleanup --- packages/cc/src/cc/Security2CC.ts | 6 +++++- packages/core/src/capabilities/CommandClasses.ts | 2 +- packages/core/src/capabilities/Protocols.ts | 4 ++-- packages/zwave-js/src/lib/controller/Inclusion.ts | 1 - .../lib/serialapi/application/ApplicationUpdateRequest.ts | 4 ++-- .../serialapi/capability/GetSerialApiInitDataMessages.ts | 1 + 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index 0e70e3d42ef5..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,7 +95,10 @@ function getAuthenticationData( commandLength: number, unencryptedPayload: Buffer, ): Buffer { - const nodeIdSize = (sendingNodeId < 256 && destination < 256) ? 1 : 2; + const nodeIdSize = + isLongRangeNodeId(sendingNodeId) || isLongRangeNodeId(destination) + ? 2 + : 1; const ret = Buffer.allocUnsafe( 2 * nodeIdSize + 6 + unencryptedPayload.length, ); diff --git a/packages/core/src/capabilities/CommandClasses.ts b/packages/core/src/capabilities/CommandClasses.ts index b309f36e6bdb..fd2bde0cb184 100644 --- a/packages/core/src/capabilities/CommandClasses.ts +++ b/packages/core/src/capabilities/CommandClasses.ts @@ -128,7 +128,7 @@ 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, } diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index 38ad1aa1a32e..d9720e45eaa2 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -100,9 +100,9 @@ export function longRangeChannelToString(channel: LongRangeChannel): string { case LongRangeChannel.Unknown: return "Unknown"; case LongRangeChannel.A: - return "Channel A (912MHz)"; + return "Channel A (912 MHz)"; case LongRangeChannel.B: - return "Channel B (920MHz)"; + return "Channel B (920 MHz)"; } return `Unknown (${num2hex(channel)})`; } diff --git a/packages/zwave-js/src/lib/controller/Inclusion.ts b/packages/zwave-js/src/lib/controller/Inclusion.ts index 10be0236386a..ba5313f45e52 100644 --- a/packages/zwave-js/src/lib/controller/Inclusion.ts +++ b/packages/zwave-js/src/lib/controller/Inclusion.ts @@ -123,7 +123,6 @@ export interface InclusionUserCallbacks { } /** Options for inclusion of a new node */ -// TODO: how should "preferLongRange" fit in here? We probably want the user to be able to control that? export type InclusionOptions = | { strategy: InclusionStrategy.Default; diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 54b8473a9a55..0e6d9462acfb 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts @@ -28,7 +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_HomeId_LongRange_Received = 0x87, // A start start long range note requests inclusion + SmartStart_LongRange_HomeId_Received = 0x87, // A start start long range note requests inclusion NodeInfo_Received = 0x84, NodeInfo_RequestDone = 0x82, NodeInfo_RequestFailed = 0x81, @@ -230,7 +230,7 @@ export class ApplicationUpdateRequestSmartStartHomeIDReceived {} @applicationUpdateType( - ApplicationUpdateTypes.SmartStart_HomeId_LongRange_Received, + ApplicationUpdateTypes.SmartStart_LongRange_HomeId_Received, ) export class ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts index a36982be3e97..14928374d0e2 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts @@ -219,6 +219,7 @@ export class GetSerialApiInitDataResponse extends Message { // chip type: 7 // chip version: 0 +// FIXME: Move these into their own file export interface GetLongRangeNodesRequestOptions extends MessageBaseOptions { segmentNumber: number; } From fe446ebc0e545c55365b8705481f60f01b2a39df Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 13:06:51 +0100 Subject: [PATCH 34/43] fix: default to the first supported protocol --- .../zwave-js/src/lib/controller/Controller.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 59080c0e11f8..a4b39d03dd2b 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1733,17 +1733,25 @@ export class ZWaveController }; try { + // kick off the inclusion process + 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: provisioningEntry.protocol, + protocol, highPower: true, networkWide: true, }), From 79b1a6ae0028e592bb3b6f48fbf03bba7d61c062 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 13:13:16 +0100 Subject: [PATCH 35/43] chore: rework controller info message --- .../zwave-js/src/lib/controller/Controller.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index a4b39d03dd2b..9227e0d60790 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1323,6 +1323,7 @@ export class ZWaveController maxPayloadSizeLR = await this.getMaxPayloadSizeLongRange(); } + // FIXME: refactor this message this.driver.controllerLog.print( `received additional controller information: Z-Wave API version: ${this._zwaveApiVersion.version} (${this._zwaveApiVersion.kind})${ @@ -1345,15 +1346,23 @@ export class ZWaveController controller role: ${this._isPrimary ? "primary" : "secondary"} controller is the SIS: ${this._isSIS} controller supports timers: ${this._supportsTimers} - zwave nodes in the network: ${initData.nodeIds.join(", ")} - max payload size: ${maxPayloadSize} - LR nodes in the network: ${lrNodeIds.join(", ")} + max. payload size: + Z-Wave: ${maxPayloadSize} + Long Range: ${maxPayloadSizeLR ?? "(unknown)"} + nodes in the network: + Z-Wave: ${ + initData.nodeIds.length > 0 + ? initData.nodeIds.join(", ") + : "(none)" + } + Long Range: ${ + lrNodeIds.length > 0 ? lrNodeIds.join(", ") : "(none)" + } LR channel: ${ lrChannel ? getEnumMemberName(LongRangeChannel, lrChannel) : "" - } - LR max payload size: ${maxPayloadSizeLR}`, + }`, ); // Index the value DB for optimal performance From 8f22fd19e4d7ce6fb8a145664f1bbc808c410970 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 14:05:11 +0100 Subject: [PATCH 36/43] refactor: rework initialization sequence --- packages/core/src/capabilities/Protocols.ts | 12 - .../zwave-js/src/lib/controller/Controller.ts | 375 ++++++++++-------- packages/zwave-js/src/lib/driver/Driver.ts | 85 ++-- 3 files changed, 252 insertions(+), 220 deletions(-) diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index d9720e45eaa2..4494dae91dfe 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -95,18 +95,6 @@ export enum LongRangeChannel { // 0x03..0xFF are reserved and must not be used } -export function longRangeChannelToString(channel: LongRangeChannel): string { - switch (channel) { - case LongRangeChannel.Unknown: - return "Unknown"; - case LongRangeChannel.A: - return "Channel A (912 MHz)"; - case LongRangeChannel.B: - return "Channel B (920 MHz)"; - } - return `Unknown (${num2hex(channel)})`; -} - export function isLongRangeNodeId(nodeId: number): boolean { return nodeId > 255; } diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 9227e0d60790..3a38222db524 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -682,6 +682,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 { @@ -899,9 +905,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< @@ -930,6 +937,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< @@ -984,6 +1033,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)) { @@ -1015,6 +1089,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( @@ -1041,6 +1206,7 @@ export class ZWaveController this._supportsLongRange = false; } } + if ( this.isSerialAPISetupCommandSupported( SerialAPISetupCommand.SetRFRegion, @@ -1132,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), @@ -1154,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( @@ -1278,100 +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 - - // fetch the list of long range nodes until the controller reports no more - const lrNodeIds = await this.getLongRangeNodes(); - let lrChannel: LongRangeChannel | undefined; - let maxPayloadSize: number | undefined; if ( - this.isSerialAPISetupCommandSupported( - SerialAPISetupCommand.GetMaximumPayloadSize, - ) - ) { - maxPayloadSize = await this.getMaxPayloadSize(); - } - let maxPayloadSizeLR: number | undefined; - if ( - this.isLongRange() && this.isSerialAPISetupCommandSupported( - SerialAPISetupCommand.GetLongRangeMaximumPayloadSize, - ) + this.type !== ZWaveLibraryTypes["Bridge Controller"] + && this.isFunctionSupported(FunctionType.SetSerialApiTimeouts) ) { - // TODO: restore/set the channel - const lrChannelResp = await this.driver.sendMessage< - GetLongRangeChannelResponse - >(new GetLongRangeChannelRequest(this.driver)); - lrChannel = lrChannelResp.longRangeChannel; - - // TODO: fetch the long range max payload size and cache it - maxPayloadSizeLR = await this.getMaxPayloadSizeLongRange(); + 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`, + ); } + } - // FIXME: refactor this message - 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} - max. payload size: - Z-Wave: ${maxPayloadSize} - Long Range: ${maxPayloadSizeLR ?? "(unknown)"} - nodes in the network: - Z-Wave: ${ - initData.nodeIds.length > 0 - ? initData.nodeIds.join(", ") - : "(none)" - } - Long Range: ${ - lrNodeIds.length > 0 ? lrNodeIds.join(", ") : "(none)" - } - LR channel: ${ - lrChannel - ? getEnumMemberName(LongRangeChannel, lrChannel) - : "" - }`, - ); - + /** + * @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.`, @@ -1381,6 +1452,7 @@ export class ZWaveController } 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, @@ -1454,35 +1526,9 @@ 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"); } - private isLongRange(): boolean { - // FIXME: Rely on the SerialAPIStarted command, make sure the controller is soft-reset before we need to know this - return !!this._supportsLongRange; - } - private createValueDBForNode(nodeId: number, ownKeys?: Set) { return new ValueDB( nodeId, @@ -1497,10 +1543,10 @@ export class ZWaveController * Warning: This only works when followed up by a hard-reset, so don't call this directly * @internal */ - public async getLongRangeNodes(): Promise> { - const nodeIds: Array = []; + public async getLongRangeNodes(): Promise { + const nodeIds: number[] = []; - if (this.isLongRange()) { + if (this.supportsLongRange) { const segment = 0; while (true) { const nodesResponse = await this.driver.sendMessage< @@ -1631,7 +1677,6 @@ export class ZWaveController private _nodePendingExclusion: ZWaveNode | undefined; private _nodePendingReplace: ZWaveNode | undefined; private _replaceFailedPromise: DeferredPromise | undefined; - private _supportsLongRange: boolean | undefined; /** * Starts the inclusion process of new nodes. diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 644b50db2e4b..edaf225ed7b3 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -1421,19 +1421,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 - - // FIXME: Setting the node ID type, opening the cache and querying the controller ID should be done AFTER soft-resetting - - // 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( @@ -1447,45 +1443,44 @@ export class Driver extends TypedEventEmitter await this.softResetInternal(false); } - // FIXME: We should now know if the controller supports ZWLR or not - // Also, set the node ID type to 16-bit here only if ZWLR is supported. + 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; + } - // FIXME: This block is unnecessary when setting the node ID type explicitly + // 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, + ); - // 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!); - } + // Now that we know the node ID type, we can identify the controller + await this.controller.identify(); - if (this.controller.ownNodeId === 0) { - this.driverLog.print( - `Controller identification returned invalid node ID 0`, - "error", - ); - await this.destroy(); - return; - } + // 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(); @@ -2665,8 +2660,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; } @@ -2692,6 +2689,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; } From a42243d781334491571672b1e440504139505a68 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 16:07:02 +0100 Subject: [PATCH 37/43] fix: restore original encoding of GetSerialApiInitDataResponse --- .../lib/serialapi/capability/GetSerialApiInitDataMessages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts index 14928374d0e2..e018978b0b63 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts @@ -5,6 +5,7 @@ import { NUM_LR_NODES_PER_SEGMENT, NUM_NODEMASK_BYTES, NodeType, + encodeBitMask, encodeLongRangeNodeBitMask, parseLongRangeNodeBitMask, parseNodeBitMask, @@ -154,7 +155,7 @@ export class GetSerialApiInitDataResponse extends Message { this.payload[1] = capabilities; this.payload[2] = NUM_NODEMASK_BYTES; - const nodeBitMask = encodeLongRangeNodeBitMask(this.nodeIds, MAX_NODES); + const nodeBitMask = encodeBitMask(this.nodeIds, MAX_NODES); nodeBitMask.copy(this.payload, 3); if (chipType) { From 683db90cc35f9aa0d1af071a43f745c7141b288e Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 21:57:21 +0100 Subject: [PATCH 38/43] fix: typo --- .../src/lib/serialapi/application/ApplicationUpdateRequest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 0e6d9462acfb..46b900c6108c 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts @@ -28,7 +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 start start long range note requests inclusion + SmartStart_LongRange_HomeId_Received = 0x87, // A smart start long range note requests inclusion NodeInfo_Received = 0x84, NodeInfo_RequestDone = 0x82, NodeInfo_RequestFailed = 0x81, From ea170ce34760779283f4b09c45c1870010ae52d9 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 22:04:10 +0100 Subject: [PATCH 39/43] fix: split GetLongRangeNodes into own file, fix query loop --- .../zwave-js/src/lib/controller/Controller.ts | 14 +-- .../capability/GetLongRangeNodesMessages.ts | 119 ++++++++++++++++++ .../GetSerialApiInitDataMessages.ts | 105 ---------------- 3 files changed, 126 insertions(+), 112 deletions(-) create mode 100644 packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 3a38222db524..18e3a3f155c8 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -143,6 +143,10 @@ import { GetControllerVersionRequest, type GetControllerVersionResponse, } from "../serialapi/capability/GetControllerVersionMessages"; +import { + GetLongRangeNodesRequest, + type GetLongRangeNodesResponse, +} from "../serialapi/capability/GetLongRangeNodesMessages"; import { GetProtocolVersionRequest, type GetProtocolVersionResponse, @@ -152,8 +156,6 @@ import { type GetSerialApiCapabilitiesResponse, } from "../serialapi/capability/GetSerialApiCapabilitiesMessages"; import { - GetLongRangeNodesRequest, - type GetLongRangeNodesResponse, GetSerialApiInitDataRequest, type GetSerialApiInitDataResponse, } from "../serialapi/capability/GetSerialApiInitDataMessages"; @@ -1547,8 +1549,7 @@ export class ZWaveController const nodeIds: number[] = []; if (this.supportsLongRange) { - const segment = 0; - while (true) { + for (let segment = 0;; segment++) { const nodesResponse = await this.driver.sendMessage< GetLongRangeNodesResponse >( @@ -1557,9 +1558,8 @@ export class ZWaveController }), ); nodeIds.push(...nodesResponse.nodeIds); - if (!nodesResponse.moreNodes) { - break; - } + + if (!nodesResponse.moreNodes) break; } } return nodeIds; 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/GetSerialApiInitDataMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts index e018978b0b63..b0613533f78e 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts @@ -1,13 +1,9 @@ import { MAX_NODES, MessagePriority, - NUM_LR_NODEMASK_SEGMENT_BYTES, - NUM_LR_NODES_PER_SEGMENT, NUM_NODEMASK_BYTES, NodeType, encodeBitMask, - encodeLongRangeNodeBitMask, - parseLongRangeNodeBitMask, parseNodeBitMask, } from "@zwave-js/core"; import type { ZWaveHost } from "@zwave-js/host"; @@ -219,104 +215,3 @@ export class GetSerialApiInitDataResponse extends Message { // is SUC: true // chip type: 7 // chip version: 0 - -// FIXME: Move these into their own file -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; - } -} From f226a822217fb4cba78f2fd72fb571b336e37719 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Wed, 17 Jan 2024 22:46:04 +0100 Subject: [PATCH 40/43] fix: node info parsing --- packages/core/src/capabilities/NodeInfo.ts | 131 ++++++++---------- packages/zwave-js/src/lib/driver/Driver.ts | 12 -- .../GetNodeProtocolInfoMessages.ts | 6 +- 3 files changed, 59 insertions(+), 90 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index de09d4a349bf..7c8c9725eb54 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -212,7 +212,6 @@ export type NodeInformationFrame = & NodeProtocolInfoAndDeviceClass & ApplicationNodeInformation; -// FIXME: Split these methods into two, one each for long range and one each for classic Z-Wave export function parseNodeProtocolInfo( buffer: Buffer, offset: number, @@ -221,27 +220,26 @@ export function parseNodeProtocolInfo( validatePayload(buffer.length >= offset + 3); const isListening = !!(buffer[offset] & 0b10_000_000); - let isRouting = false; - if (!isLongRange) { - isRouting = !!(buffer[offset] & 0b01_000_000); - } + const isRouting = !!(buffer[offset] & 0b01_000_000); const supportedDataRates: DataRate[] = []; + const speed = buffer[offset] & 0b00_011_000; + const speedExt = buffer[offset + 2] & 0b111; if (isLongRange) { - const speedExtension = buffer[offset + 2] & 0b111; - if (speedExtension & 0b010) { + // 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 { - const maxSpeed = buffer[offset] & 0b00_011_000; - const speedExtension = buffer[offset + 2] & 0b111; - if (maxSpeed & 0b00_010_000) { + if (speed & 0b00_010_000) { supportedDataRates.push(40000); } - if (maxSpeed & 0b00_001_000) { + if (speed & 0b00_001_000) { supportedDataRates.push(9600); } - if (speedExtension & 0b001) { + if (speedExt & 0b001) { supportedDataRates.push(100000); } if (supportedDataRates.length === 0) { @@ -249,16 +247,12 @@ export function parseNodeProtocolInfo( } } - // BUGBUG: what's the correct protocol version here for long range? - const protocolVersion = isLongRange - ? 0 - : buffer[offset] & 0b111; + const protocolVersion = buffer[offset] & 0b111; const capability = buffer[offset + 1]; - const optionalFunctionality = (!isLongRange) - && !!(capability & 0b1000_0000); + const optionalFunctionality = !!(capability & 0b1000_0000); let isFrequentListening: FLiRS; - switch (capability & (isLongRange ? 0b0100_0000 : 0b0110_0000)) { + switch (capability & 0b0110_0000) { case 0b0100_0000: isFrequentListening = "1000ms"; break; @@ -268,29 +262,21 @@ export function parseNodeProtocolInfo( default: isFrequentListening = false; } - const supportsBeaming = (!isLongRange) && !!(capability & 0b0001_0000); + const supportsBeaming = !!(capability & 0b0001_0000); let nodeType: NodeType; - - switch ( - isLongRange - ? (0b1_0000_0000 | (capability & 0b0010)) - : (capability & 0b1010) - ) { - case 0b0_0000_1000: - case 0b1_0000_0000: + switch (capability & 0b1010) { + case 0b1000: nodeType = NodeType["End Node"]; break; - case 0b0_0000_0010: - case 0b1_0000_0010: + case 0b0010: default: - // BUGBUG: is Controller correct for default && isLongRange? nodeType = NodeType.Controller; break; } - const hasSpecificDeviceClass = isLongRange || !!(capability & 0b100); - const supportsSecurity = isLongRange || !!(capability & 0b1); + const hasSpecificDeviceClass = !!(capability & 0b100); + const supportsSecurity = !!(capability & 0b1); return { isListening, @@ -310,38 +296,32 @@ export function encodeNodeProtocolInfo( info: NodeProtocolInfo, isLongRange: boolean = false, ): Buffer { + // Technically a lot of these fields are reserved 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 (isLongRange) { if (info.supportedDataRates.includes(100000)) ret[2] |= 0b010; } else { - 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; - ret[0] |= info.protocolVersion & 0b111; } + ret[0] |= info.protocolVersion & 0b111; // Byte 1 - if (!isLongRange) { - if (info.optionalFunctionality) ret[1] |= 0b1000_0000; - } + if (info.optionalFunctionality) ret[1] |= 0b1000_0000; if (info.isFrequentListening === "1000ms") ret[1] |= 0b0100_0000; - else if (!isLongRange && info.isFrequentListening === "250ms") { - ret[1] |= 0b0010_0000; - } + else if (info.isFrequentListening === "250ms") ret[1] |= 0b0010_0000; - if (!isLongRange) { - if (info.supportsBeaming) ret[1] |= 0b0001_0000; - if (info.supportsSecurity) ret[1] |= 0b1; - } + if (info.supportsBeaming) ret[1] |= 0b0001_0000; + if (info.supportsSecurity) ret[1] |= 0b1; + if (info.nodeType === NodeType["End Node"]) ret[1] |= 0b1000; + else ret[1] |= 0b0010; // Controller - if (info.nodeType === NodeType["End Node"]) { - if (!isLongRange) ret[1] |= 0b1000; - } else ret[1] |= 0b0010; // Controller - - if (!isLongRange && info.hasSpecificDeviceClass) ret[1] |= 0b100; + if (info.hasSpecificDeviceClass) ret[1] |= 0b100; return ret; } @@ -354,13 +334,14 @@ export function parseNodeProtocolInfoAndDeviceClass( bytesRead: number; } { validatePayload(buffer.length >= 5); + // 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; - // BUGBUG: 4.3.2.1.1.14 says this is omitted if the Controller field is set to 0, yet we always parse it? - let basic = 0x100; // BUGBUG: is there an assume one here, or...? - if (!isLongRange) { - basic = buffer[offset++]; - } + const basic = buffer[offset++]; const generic = buffer[offset++]; let specific = 0; if (protocolInfo.hasSpecificDeviceClass) { @@ -382,19 +363,16 @@ export function encodeNodeProtocolInfoAndDeviceClass( info: NodeProtocolInfoAndDeviceClass, isLongRange: boolean = false, ): Buffer { - const deviceClasses = isLongRange - ? Buffer.from([ - info.genericDeviceClass, - info.specificDeviceClass, - ]) - : Buffer.from([ + return Buffer.concat([ + encodeNodeProtocolInfo( + { ...info, hasSpecificDeviceClass: true }, + isLongRange, + ), + Buffer.from([ info.basicDeviceClass, info.genericDeviceClass, info.specificDeviceClass, - ]); - return Buffer.concat([ - encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), - deviceClasses, + ]), ]); } @@ -408,15 +386,18 @@ export function parseNodeInformationFrame( ); const info = result.info; let offset = result.bytesRead; - let ccListLength; + + let ccList: Buffer; if (isLongRange) { - ccListLength = buffer[offset]; + const ccListLength = buffer[offset]; offset += 1; + validatePayload(buffer.length >= offset + ccListLength); + ccList = buffer.subarray(offset, offset + ccListLength); } else { - ccListLength = buffer.length - offset; + ccList = buffer.subarray(offset); } - const supportedCCs = - parseCCList(buffer.subarray(offset, ccListLength)).supportedCCs; + + const supportedCCs = parseCCList(ccList).supportedCCs; return { ...info, @@ -432,17 +413,13 @@ export function encodeNodeInformationFrame( info, isLongRange, ); - const ccList = encodeCCList(info.supportedCCs, []); - const buffers = [protocolInfo]; + let ccList = encodeCCList(info.supportedCCs, []); if (isLongRange) { - const ccListLength = Buffer.allocUnsafe(1); - ccListLength[0] = ccList.length; - buffers.push(ccListLength); + ccList = Buffer.concat([Buffer.from([ccList.length]), ccList]); } - buffers.push(ccList); - return Buffer.concat(buffers); + return Buffer.concat([protocolInfo, ccList]); } export function parseNodeID( diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index edaf225ed7b3..ee99c4d084d6 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -1606,18 +1606,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, - ); - } })(); } 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..38e7a6917cf6 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"; @@ -38,7 +40,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; } @@ -72,6 +75,7 @@ export class GetNodeProtocolInfoResponse extends Message { const { hasSpecificDeviceClass, ...rest } = parseNodeProtocolInfo( this.payload, 0, + isLongRangeNodeId(this.getNodeId()!), ); this.isListening = rest.isListening; this.isFrequentListening = rest.isFrequentListening; From 1e098887762aef4ef5e85c07d227bee60d6cf26d Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Thu, 18 Jan 2024 14:19:49 +0100 Subject: [PATCH 41/43] fix: remember node ID when requesting protocol info --- packages/serial/src/message/Message.ts | 13 +++++++++++++ packages/zwave-js/src/lib/driver/Driver.ts | 12 +++++++++++- packages/zwave-js/src/lib/node/Node.ts | 8 +++++++- .../network-mgmt/GetNodeProtocolInfoMessages.ts | 14 +++++++++++++- 4 files changed, 44 insertions(+), 3 deletions(-) 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/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index ee99c4d084d6..17834ec482b1 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; @@ -2980,7 +2990,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/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 6e246c94664a..14c5a36e42bd 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -195,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, @@ -1871,6 +1871,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/network-mgmt/GetNodeProtocolInfoMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts index 38e7a6917cf6..76b392a1c07a 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts @@ -23,6 +23,7 @@ import { messageTypes, priority, } from "@zwave-js/serial"; +import { isObject } from "alcalzone-shared/typeguards"; interface GetNodeProtocolInfoRequestOptions extends MessageBaseOptions { requestedNodeId: number; @@ -72,10 +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. + // Use it 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, - isLongRangeNodeId(this.getNodeId()!), + isLongRange, ); this.isListening = rest.isListening; this.isFrequentListening = rest.isFrequentListening; From 098b8d15e2b083944f9ab2812ed4ed6762d1ce7c Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 19 Jan 2024 12:55:49 +0100 Subject: [PATCH 42/43] fix: cleanup --- packages/core/src/capabilities/NodeInfo.ts | 4 +-- .../zwave-js/src/lib/controller/Controller.ts | 34 +++++++++++++------ .../capability/LongRangeSetupMessages.ts | 7 ++-- .../GetNodeProtocolInfoMessages.ts | 2 +- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index 7c8c9725eb54..eee547cf239a 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -296,8 +296,8 @@ export function encodeNodeProtocolInfo( info: NodeProtocolInfo, isLongRange: boolean = false, ): Buffer { - // Technically a lot of these fields are reserved in Z-Wave Long Range, but the - // only thing where it really matters is the speed bitmask + // 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; diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index 18e3a3f155c8..a3390918d583 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -1199,13 +1199,11 @@ export class ZWaveController ) }`, ); - this._supportsLongRange = resp == RFRegion["USA (Long Range)"]; } else { this.driver.controllerLog.print( `Querying the RF region failed!`, "warn", ); - this._supportsLongRange = false; } } @@ -1787,7 +1785,8 @@ export class ZWaveController }; try { - // kick off the inclusion process + // 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] @@ -2213,9 +2212,13 @@ export class ZWaveController || 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 ( @@ -2229,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...", @@ -4116,6 +4129,7 @@ supported CCs: ${ const todoSleeping: number[] = []; const addTodo = (nodeId: number) => { + // Z-Wave Long Range does not route if (isLongRangeNodeId(nodeId)) return; if (pendingNodes.has(nodeId)) { diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts index b0e438d8c595..30d30ce81ae0 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeSetupMessages.ts @@ -83,7 +83,8 @@ export interface SetLongRangeChannelResponseOptions extends MessageBaseOptions { responseStatus: number; } -export class ResponseStatusMessageBase extends Message +@messageTypes(MessageType.Response, FunctionType.SetLongRangeChannel) +export class SetLongRangeChannelResponse extends Message implements SuccessIndicator { public constructor( @@ -101,14 +102,12 @@ export class ResponseStatusMessageBase extends Message } } -@messageTypes(MessageType.Response, FunctionType.SetLongRangeChannel) -export class SetLongRangeChannelResponse extends ResponseStatusMessageBase {} - export interface LongRangeShadowNodeIDsRequestOptions extends MessageBaseOptions { shadowNodeIds: number[]; } + const LONG_RANGE_SHADOW_NODE_IDS_START = 2002; const NUM_LONG_RANGE_SHADOW_NODE_IDS = 4; 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 76b392a1c07a..2596f6713fb7 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts @@ -74,7 +74,7 @@ export class GetNodeProtocolInfoResponse extends Message { if (gotDeserializationOptions(options)) { // The context should contain the node ID the protocol info was requested for. - // Use it to determine whether the node is long range + // We use it here to determine whether the node is long range. let isLongRange = false; if ( isObject(options.context) From c58c2fa0f01d26082f4f7e8594860634ce798854 Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 19 Jan 2024 13:14:51 +0100 Subject: [PATCH 43/43] docs: add documentation --- docs/_sidebar.md | 1 + docs/api/CCs/_sidebar.md | 1 + docs/api/controller.md | 10 ++++++++++ docs/getting-started/long-range.md | 11 +++++++++++ 4 files changed, 23 insertions(+) create mode 100644 docs/getting-started/long-range.md 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 b8782393cd2e..89d296afd848 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).