diff --git a/.vscode/typescript.code-snippets b/.vscode/typescript.code-snippets index f8605871000..f34c2d33207 100644 --- a/.vscode/typescript.code-snippets +++ b/.vscode/typescript.code-snippets @@ -5,23 +5,21 @@ "body": [ "import {", "\tMessageOrCCLogEntry,", + "\tMessagePriority,", "\tZWaveError,", "\tZWaveErrorCodes,", "} from \"@zwave-js/core\";", - "import type { ZWaveHost } from \"@zwave-js/host\";", + "import type { ZWaveApplicationHost, ZWaveHost } from \"@zwave-js/host\";", "import {", "\tFunctionType,", - "\tMessagePriority,", - "\tMessageType,", - "} from \"../../message/Constants\";", - "import {", "\tgotDeserializationOptions,", "\tMessage,", "\tMessageBaseOptions,", "\tMessageDeserializationOptions,", + "\tMessageType,", "\tmessageTypes,", "\tpriority,", - "} from \"../../message/Message\";", + "} from \"@zwave-js/serial\";", "", "export interface ${1}RequestOptions extends MessageBaseOptions {", "\t${0:someProperty: number;}", @@ -71,25 +69,23 @@ "body": [ "import {", "\tMessageOrCCLogEntry,", + "\tMessagePriority,", "\tZWaveError,", "\tZWaveErrorCodes,", "} from \"@zwave-js/core\";", - "import type { ZWaveHost } from \"@zwave-js/host\";", - "import {", - "\tFunctionType,", - "\tMessagePriority,", - "\tMessageType,", - "} from \"../../message/Constants\";", + "import type { ZWaveApplicationHost, ZWaveHost } from \"@zwave-js/host\";", "import {", "\texpectedResponse,", + "\tFunctionType,", "\tgotDeserializationOptions,", "\tMessage,", "\tMessageBaseOptions,", "\tMessageDeserializationOptions,", + "\tMessageType,", "\tmessageTypes,", "\tpriority,", - "} from \"../../message/Message\";", - "import type { SuccessIndicator } from \"../../message/SuccessIndicator\";", + "\tSuccessIndicator,", + "} from \"@zwave-js/serial\";", "", "export interface ${1}RequestOptions extends MessageBaseOptions {", "\t${0:someProperty: number;}", @@ -161,27 +157,25 @@ "body": [ "import {", "\tMessageOrCCLogEntry,", + "\tMessagePriority,", "\tZWaveError,", "\tZWaveErrorCodes,", "} from \"@zwave-js/core\";", - "import type { ZWaveHost } from \"@zwave-js/host\";", - "import {", - "\tFunctionType,", - "\tMessagePriority,", - "\tMessageType,", - "} from \"../../message/Constants\";", + "import type { ZWaveApplicationHost, ZWaveHost } from \"@zwave-js/host\";", "import {", "\texpectedCallback,", "\texpectedResponse,", + "\tFunctionType,", "\tgotDeserializationOptions,", "\tMessage,", "\tMessageBaseOptions,", "\tMessageDeserializationOptions,", "\tMessageOptions,", + "\tMessageType,", "\tmessageTypes,", "\tpriority,", - "} from \"../../message/Message\";", - "import type { SuccessIndicator } from \"../../message/SuccessIndicator\";", + "\tSuccessIndicator,", + "} from \"@zwave-js/serial\";", "", "@messageTypes(MessageType.Request, FunctionType.${1:Dummy})", "@priority(MessagePriority.${2:Normal})", @@ -290,16 +284,14 @@ "body": [ "import {", "\tMessageOrCCLogEntry,", + "\tMessagePriority,", "\tZWaveError,", "\tZWaveErrorCodes,", "} from \"@zwave-js/core\";", - "import type { ZWaveHost } from \"@zwave-js/host\";", + "import type { ZWaveApplicationHost, ZWaveHost } from \"@zwave-js/host\";", "import {", "\tFunctionType,", - "\tMessagePriority,", "\tMessageType,", - "} from \"../../message/Constants\";", - "import {", "\texpectedCallback,", "\tgotDeserializationOptions,", "\tMessage,", @@ -308,8 +300,8 @@ "\tMessageOptions,", "\tmessageTypes,", "\tpriority,", - "} from \"../../message/Message\";", - "import type { SuccessIndicator } from \"../../message/SuccessIndicator\";", + "\tSuccessIndicator,", + "} from \"@zwave-js/serial\";", "", "@messageTypes(MessageType.Request, FunctionType.${1:Dummy})", "@priority(MessagePriority.${2:Normal})", @@ -439,19 +431,25 @@ "body": [ "import {", "\tCCCommand,", - "\tCCCommandOptions,", "\tCommandClass,", "\tcommandClass,", "\texpectedCCResponse,", "\timplementedVersion,", - "\tCommandClassDeserializationOptions,", "\tgotDeserializationOptions,", - "} from \"./CommandClass\";", - "import { CommandClasses } from \"@zwave-js/core\";", - "import type { Driver } from \"../driver/Driver\";", + "\ttype CCCommandOptions,", + "\ttype CommandClassDeserializationOptions,", + "} from \"../lib/CommandClass\";", + "import {", + "\tCommandClasses,", + "\tMessageOrCCLogEntry,", + "\tMessagePriority,", + "\tZWaveError,", + "\tZWaveErrorCodes,", + "} from \"@zwave-js/core\";", "import type { ZWaveApplicationHost, ZWaveHost } from \"@zwave-js/host\";", + "import { CCAPI } from \"../lib/API\";", "", - "// TODO: Move this enumeration into the _Types.ts file", + "// TODO: Move this enumeration into the src/lib/_Types.ts file", "// All additional type definitions (except CC constructor options) must be defined there too", "export enum ${1:${TM_FILENAME_BASE/(.*)CC$/$1/}}Command {", "\t${3:// Get = 0x01}", @@ -528,14 +526,7 @@ "prefix": "zwccemptycmd", "body": [ "@CCCommand(${1:${TM_FILENAME_BASE/(.*)CC$/$1/}}Command.${2:Get})", - "export class ${1}CC${2} extends ${1}CC {", - "\tpublic constructor(", - "\t\thost: ZWaveHost,", - "\t\toptions: CommandClassDeserializationOptions | CCCommandOptions,", - "\t) {", - "\t\tsuper(host, options);", - "\t}", - "}" + "export class ${1}CC${2} extends ${1}CC {}" ], "description": "Specific implementation of CC command that is sent and expects no parameters" }, @@ -621,35 +612,39 @@ "scope": "typescript", "prefix": "zwccinterview", "body": [ - "public async interview(driver: Driver): Promise {", - "\tconst node = this.getNode()!;", - "\tconst endpoint = this.getEndpoint()!;", - "\tconst api = endpoint.commandClasses.${1:${TM_FILENAME_BASE/(.*)CC$/$1/}}.withOptions({", + "public async interview(applHost: ZWaveApplicationHost): Promise {", + "\tconst node = this.getNode(applHost)!;", + "\tconst endpoint = this.getEndpoint(applHost)!;", + "\tconst api = CCAPI.create(", + "\t\tCommandClasses.${1:${TM_FILENAME_BASE/(.*)CC$/$1/}},", + "\t\tapplHost,", + "\t\tendpoint", + "\t).withOptions({", "\t\tpriority: MessagePriority.NodeQuery,", "\t});", - "\tconst valueDB = this.getValueDB(driver);", + "\tconst valueDB = this.getValueDB(applHost);", "", - "\tdriver.controllerLog.logNode(node.id, {", + "\tapplHost.controllerLog.logNode(node.id, {", "\t\tendpoint: this.endpointIndex,", "\t\tmessage: `Interviewing ${this.ccName}...`,", "\t\tdirection: \"none\",", "\t});", "", - "\tdriver.controllerLog.logNode(node.id, {", + "\tapplHost.controllerLog.logNode(node.id, {", "\t\tendpoint: this.endpointIndex,", "\t\tmessage: \"doing something...\",", "\t\tdirection: \"outbound\",", "\t});", "\t${0:// TODO: Implementation}", "\tconst logMessage = `received response for something...`;", - "\tdriver.controllerLog.logNode(node.id, {", + "\tapplHost.controllerLog.logNode(node.id, {", "\t\tendpoint: this.endpointIndex,", "\t\tmessage: logMessage,", "\t\tdirection: \"inbound\",", "\t});", "", "\t// Remember that the interview is complete", - "\tthis.setInterviewComplete(driver, true);", + "\tthis.setInterviewComplete(applHost, true);", "}" ] }, @@ -669,13 +664,17 @@ "scope": "typescript", "prefix": "zwccrefval", "body": [ - "public async refreshValues(driver: Driver): Promise {", - "\tconst node = this.getNode()!;", - "\tconst endpoint = this.getEndpoint()!;", - "\tconst api = endpoint.commandClasses[\"${1:${TM_FILENAME_BASE/(.*)CC$/$1/}}\"].withOptions({", + "public async refreshValues(applHost: ZWaveApplicationHost): Promise {", + "\tconst node = this.getNode(applHost)!;", + "\tconst endpoint = this.getEndpoint(applHost)!;", + "\tconst api = CCAPI.create(", + "\t\tCommandClasses.${1:${TM_FILENAME_BASE/(.*)CC$/$1/}},", + "\t\tapplHost,", + "\t\tendpoint", + "\t).withOptions({", "\t\tpriority: MessagePriority.NodeQuery,", "\t});", - "\tconst valueDB = this.getValueDB(driver);", + "\tconst valueDB = this.getValueDB(applHost);", "", "\t${0:// TODO: Implementation}", "}" diff --git a/packages/cc/src/cc/MultiChannelCC.ts b/packages/cc/src/cc/MultiChannelCC.ts index 02d95dfdf94..c437a00a3e3 100644 --- a/packages/cc/src/cc/MultiChannelCC.ts +++ b/packages/cc/src/cc/MultiChannelCC.ts @@ -1,5 +1,6 @@ import type { GenericDeviceClass, SpecificDeviceClass } from "@zwave-js/config"; import { + ApplicationNodeInformation, CommandClasses, encodeBitMask, getCCName, @@ -8,8 +9,8 @@ import { MessageOrCCLogEntry, MessagePriority, MessageRecord, + parseApplicationNodeInformation, parseBitMask, - parseNodeInformationFrame, validatePayload, ValueID, ZWaveError, @@ -719,7 +720,10 @@ export class MultiChannelCCEndPointReport extends MultiChannelCC { export class MultiChannelCCEndPointGet extends MultiChannelCC {} @CCCommand(MultiChannelCommand.CapabilityReport) -export class MultiChannelCCCapabilityReport extends MultiChannelCC { +export class MultiChannelCCCapabilityReport + extends MultiChannelCC + implements ApplicationNodeInformation +{ public constructor( host: ZWaveHost, options: CommandClassDeserializationOptions, @@ -727,15 +731,15 @@ export class MultiChannelCCCapabilityReport extends MultiChannelCC { super(host, options); // Only validate the bytes we expect to see here - // parseNodeInformationFrame does its own validation + // parseApplicationNodeInformation does its own validation validatePayload(this.payload.length >= 1); this.endpointIndex = this.payload[0] & 0b01111111; - const NIF = parseNodeInformationFrame(this.payload.slice(1)); this.isDynamic = !!(this.payload[0] & 0b10000000); - this.genericDeviceClass = NIF.generic; - this.specificDeviceClass = NIF.specific; + + const NIF = parseApplicationNodeInformation(this.payload.slice(1)); + this.genericDeviceClass = NIF.genericDeviceClass; + this.specificDeviceClass = NIF.specificDeviceClass; this.supportedCCs = NIF.supportedCCs; - // TODO: does this include controlledCCs aswell? // Removal reports have very specific information this.wasRemoved = diff --git a/packages/cc/src/cc/ZWaveProtocolCC.ts b/packages/cc/src/cc/ZWaveProtocolCC.ts new file mode 100644 index 00000000000..38f92b4650a --- /dev/null +++ b/packages/cc/src/cc/ZWaveProtocolCC.ts @@ -0,0 +1,1214 @@ +import { + CommandClasses, + DataRate, + encodeBitMask, + encodeNodeInformationFrame, + encodeNodeProtocolInfoAndDeviceClass, + FLiRS, + MAX_NODES, + MAX_REPEATERS, + NodeInformationFrame, + NodeProtocolInfoAndDeviceClass, + NodeType, + parseBitMask, + parseNodeInformationFrame, + parseNodeProtocolInfoAndDeviceClass, + ProtocolVersion, + validatePayload, + ZWaveDataRate, + ZWaveError, + ZWaveErrorCodes, +} from "@zwave-js/core"; +import type { ZWaveHost } from "@zwave-js/host"; +import { + CCCommand, + CommandClass, + commandClass, + expectedCCResponse, + gotDeserializationOptions, + implementedVersion, + type CCCommandOptions, + type CommandClassDeserializationOptions, +} from "../lib/CommandClass"; +import { + NetworkTransferStatus, + parseWakeUpTime, + WakeUpTime, + ZWaveProtocolCommand, +} from "../lib/_Types"; + +// TODO: Move this enumeration into the _Types.ts file +// All additional type definitions (except CC constructor options) must be defined there too + +@commandClass(CommandClasses["Z-Wave Protocol"]) +@implementedVersion(1) +export class ZWaveProtocolCC extends CommandClass { + declare ccCommand: ZWaveProtocolCommand; +} + +interface ZWaveProtocolCCNodeInformationFrameOptions + extends CCCommandOptions, + NodeInformationFrame {} + +@CCCommand(ZWaveProtocolCommand.NodeInformationFrame) +export class ZWaveProtocolCCNodeInformationFrame + extends ZWaveProtocolCC + implements NodeInformationFrame +{ + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCNodeInformationFrameOptions, + ) { + super(host, options); + + let nif: NodeInformationFrame; + if (gotDeserializationOptions(options)) { + nif = parseNodeInformationFrame(this.payload); + } else { + nif = options; + } + + this.basicDeviceClass = nif.basicDeviceClass; + this.genericDeviceClass = nif.genericDeviceClass; + this.specificDeviceClass = nif.specificDeviceClass; + this.isListening = nif.isListening; + this.isFrequentListening = nif.isFrequentListening; + this.isRouting = nif.isRouting; + this.supportedDataRates = nif.supportedDataRates; + this.protocolVersion = nif.protocolVersion; + this.optionalFunctionality = nif.optionalFunctionality; + this.nodeType = nif.nodeType; + this.supportsSecurity = nif.supportsSecurity; + this.supportsBeaming = nif.supportsBeaming; + 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); + return super.serialize(); + } +} + +@CCCommand(ZWaveProtocolCommand.RequestNodeInformationFrame) +@expectedCCResponse(ZWaveProtocolCCNodeInformationFrame) +export class ZWaveProtocolCCRequestNodeInformationFrame extends ZWaveProtocolCC {} + +interface ZWaveProtocolCCAssignIDsOptions extends CCCommandOptions { + nodeId: number; + homeId: number; +} + +@CCCommand(ZWaveProtocolCommand.AssignIDs) +export class ZWaveProtocolCCAssignIDs extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCAssignIDsOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 5); + this.nodeId = this.payload[0]; + this.homeId = this.payload.readUInt32BE(1); + } else { + this.nodeId = options.nodeId; + this.homeId = options.homeId; + } + } + + public nodeId: number; + public homeId: number; + + public serialize(): Buffer { + this.payload = Buffer.allocUnsafe(5); + this.payload[0] = this.nodeId; + this.payload.writeUInt32BE(this.homeId, 1); + return super.serialize(); + } +} + +interface ZWaveProtocolCCFindNodesInRangeOptions extends CCCommandOptions { + candidateNodeIds: number[]; + wakeUpTime: WakeUpTime; + dataRate?: ZWaveDataRate; +} + +@CCCommand(ZWaveProtocolCommand.FindNodesInRange) +export class ZWaveProtocolCCFindNodesInRange extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCFindNodesInRangeOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + const speedPresent = this.payload[0] & 0b1000_0000; + const bitmaskLength = this.payload[0] & 0b0001_1111; + + validatePayload(this.payload.length >= 1 + bitmaskLength); + this.candidateNodeIds = parseBitMask( + this.payload.slice(1, 1 + bitmaskLength), + ); + + const rest = this.payload.slice(1 + bitmaskLength); + if (speedPresent) { + validatePayload(rest.length >= 1); + if (rest.length === 1) { + this.dataRate = rest[0] & 0b111; + this.wakeUpTime = WakeUpTime.None; + } else if (rest.length === 2) { + this.wakeUpTime = parseWakeUpTime(rest[0]); + this.dataRate = rest[1] & 0b111; + } else { + throw validatePayload.fail("Invalid payload length"); + } + } else if (rest.length >= 1) { + this.wakeUpTime = parseWakeUpTime(rest[0]); + this.dataRate = ZWaveDataRate["9k6"]; + } else { + this.wakeUpTime = WakeUpTime.None; + this.dataRate = ZWaveDataRate["9k6"]; + } + } else { + this.candidateNodeIds = options.candidateNodeIds; + this.wakeUpTime = options.wakeUpTime; + this.dataRate = options.dataRate ?? ZWaveDataRate["9k6"]; + } + } + + public candidateNodeIds: number[]; + public wakeUpTime: WakeUpTime; + public dataRate: ZWaveDataRate; + + public serialize(): Buffer { + const nodesBitmask = encodeBitMask(this.candidateNodeIds, MAX_NODES); + const speedAndLength = 0b1000_0000 | nodesBitmask.length; + this.payload = Buffer.concat([ + Buffer.from([speedAndLength]), + nodesBitmask, + Buffer.from([this.wakeUpTime, this.dataRate]), + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCRangeInfoOptions extends CCCommandOptions { + neighborNodeIds: number[]; + wakeUpTime?: WakeUpTime; +} + +@CCCommand(ZWaveProtocolCommand.RangeInfo) +export class ZWaveProtocolCCRangeInfo extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCRangeInfoOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + const bitmaskLength = this.payload[0] & 0b0001_1111; + + validatePayload(this.payload.length >= 1 + bitmaskLength); + this.neighborNodeIds = parseBitMask( + this.payload.slice(1, 1 + bitmaskLength), + ); + if (this.payload.length >= 2 + bitmaskLength) { + this.wakeUpTime = parseWakeUpTime( + this.payload[1 + bitmaskLength], + ); + } + } else { + this.neighborNodeIds = options.neighborNodeIds; + this.wakeUpTime = options.wakeUpTime; + } + } + + public neighborNodeIds: number[]; + public wakeUpTime?: WakeUpTime; + + public serialize(): Buffer { + const nodesBitmask = encodeBitMask(this.neighborNodeIds, MAX_NODES); + this.payload = Buffer.concat([ + Buffer.from([nodesBitmask.length]), + nodesBitmask, + this.wakeUpTime != undefined + ? Buffer.from([this.wakeUpTime]) + : Buffer.alloc(0), + ]); + return super.serialize(); + } +} + +@CCCommand(ZWaveProtocolCommand.GetNodesInRange) +@expectedCCResponse(ZWaveProtocolCCRangeInfo) +export class ZWaveProtocolCCGetNodesInRange extends ZWaveProtocolCC {} + +interface ZWaveProtocolCCCommandCompleteOptions extends CCCommandOptions { + sequenceNumber: number; +} + +@CCCommand(ZWaveProtocolCommand.CommandComplete) +export class ZWaveProtocolCCCommandComplete extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCCommandCompleteOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.sequenceNumber = this.payload[0]; + } else { + this.sequenceNumber = options.sequenceNumber; + } + } + + public sequenceNumber: number; + + public serialize(): Buffer { + this.payload = Buffer.from([this.sequenceNumber]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCTransferPresentationOptions extends CCCommandOptions { + supportsNWI: boolean; + includeNode: boolean; + excludeNode: boolean; +} + +@CCCommand(ZWaveProtocolCommand.TransferPresentation) +export class ZWaveProtocolCCTransferPresentation extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCTransferPresentationOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + const option = this.payload[0]; + this.supportsNWI = !!(option & 0b0001); + this.excludeNode = !!(option & 0b0010); + this.includeNode = !!(option & 0b0100); + } else { + if (options.includeNode && options.excludeNode) { + throw new ZWaveError( + `${this.constructor.name}: the includeNode and excludeNode options cannot both be true`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.supportsNWI = options.supportsNWI; + this.includeNode = options.includeNode; + this.excludeNode = options.excludeNode; + } + } + + public supportsNWI: boolean; + public includeNode: boolean; + public excludeNode: boolean; + + public serialize(): Buffer { + this.payload = Buffer.from([ + (this.supportsNWI ? 0b0001 : 0) | + (this.excludeNode ? 0b0010 : 0) | + (this.includeNode ? 0b0100 : 0), + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCTransferNodeInformationOptions + extends CCCommandOptions, + NodeProtocolInfoAndDeviceClass { + sequenceNumber: number; + nodeId: number; +} + +@CCCommand(ZWaveProtocolCommand.TransferNodeInformation) +export class ZWaveProtocolCCTransferNodeInformation + extends ZWaveProtocolCC + implements NodeProtocolInfoAndDeviceClass +{ + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCTransferNodeInformationOptions, + ) { + super(host, options); + + let info: NodeProtocolInfoAndDeviceClass; + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.sequenceNumber = this.payload[0]; + this.nodeId = this.payload[1]; + info = parseNodeProtocolInfoAndDeviceClass( + this.payload.slice(2), + ).info; + } else { + this.sequenceNumber = options.sequenceNumber; + this.nodeId = options.nodeId; + info = options; + } + + this.basicDeviceClass = info.basicDeviceClass; + this.genericDeviceClass = info.genericDeviceClass; + this.specificDeviceClass = info.specificDeviceClass; + this.isListening = info.isListening; + this.isFrequentListening = info.isFrequentListening; + this.isRouting = info.isRouting; + this.supportedDataRates = info.supportedDataRates; + this.protocolVersion = info.protocolVersion; + this.optionalFunctionality = info.optionalFunctionality; + this.nodeType = info.nodeType; + this.supportsSecurity = info.supportsSecurity; + this.supportsBeaming = info.supportsBeaming; + } + + public sequenceNumber: number; + public nodeId: number; + 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 serialize(): Buffer { + this.payload = Buffer.concat([ + Buffer.from([this.sequenceNumber, this.nodeId]), + encodeNodeProtocolInfoAndDeviceClass(this), + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCTransferRangeInformationOptions + extends CCCommandOptions { + sequenceNumber: number; + nodeId: number; + neighborNodeIds: number[]; +} + +@CCCommand(ZWaveProtocolCommand.TransferRangeInformation) +export class ZWaveProtocolCCTransferRangeInformation extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCTransferRangeInformationOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 3); + this.sequenceNumber = this.payload[0]; + this.nodeId = this.payload[1]; + const bitmaskLength = this.payload[2]; + validatePayload(this.payload.length >= 3 + bitmaskLength); + this.neighborNodeIds = parseBitMask( + this.payload.slice(3, 3 + bitmaskLength), + ); + } else { + this.sequenceNumber = options.sequenceNumber; + this.nodeId = options.nodeId; + this.neighborNodeIds = options.neighborNodeIds; + } + } + + public sequenceNumber: number; + public nodeId: number; + public neighborNodeIds: number[]; + + public serialize(): Buffer { + const nodesBitmask = encodeBitMask(this.neighborNodeIds, MAX_NODES); + this.payload = Buffer.concat([ + Buffer.from([ + this.sequenceNumber, + this.nodeId, + nodesBitmask.length, + ]), + nodesBitmask, + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCTransferEndOptions extends CCCommandOptions { + status: NetworkTransferStatus; +} + +@CCCommand(ZWaveProtocolCommand.TransferEnd) +export class ZWaveProtocolCCTransferEnd extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCTransferEndOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.status = this.payload[0]; + } else { + this.status = options.status; + } + } + + public status: NetworkTransferStatus; + + public serialize(): Buffer { + this.payload = Buffer.from([this.status]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCAssignReturnRouteOptions extends CCCommandOptions { + nodeId: number; + routeIndex: number; + repeaters: number[]; + destinationWakeUp: WakeUpTime; + destinationSpeed: ZWaveDataRate; +} + +@CCCommand(ZWaveProtocolCommand.AssignReturnRoute) +export class ZWaveProtocolCCAssignReturnRoute extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCAssignReturnRouteOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 7); + this.nodeId = this.payload[0]; + this.routeIndex = this.payload[1] >>> 4; + const numRepeaters = this.payload[1] & 0b1111; + this.repeaters = [...this.payload.slice(2, 2 + numRepeaters)]; + const speedAndWakeup = this.payload[2 + numRepeaters]; + this.destinationSpeed = (speedAndWakeup >>> 3) & 0b111; + this.destinationWakeUp = (speedAndWakeup >>> 1) & 0b11; + } else { + if (options.repeaters.length > MAX_REPEATERS) { + throw new ZWaveError( + `${this.constructor.name}: too many repeaters`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + + this.nodeId = options.nodeId; + this.routeIndex = options.routeIndex; + this.repeaters = options.repeaters; + this.destinationWakeUp = options.destinationWakeUp; + this.destinationSpeed = options.destinationSpeed; + } + } + + public nodeId: number; + public routeIndex: number; + public repeaters: number[]; + public destinationWakeUp: WakeUpTime; + public destinationSpeed: ZWaveDataRate; + + public serialize(): Buffer { + this.payload = Buffer.from([ + this.nodeId, + (this.routeIndex << 4) | this.repeaters.length, + this.repeaters[0] ?? 0, + this.repeaters[1] ?? 0, + this.repeaters[2] ?? 0, + this.repeaters[3] ?? 0, + (this.destinationSpeed << 3) | (this.destinationWakeUp << 1), + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCNewNodeRegisteredOptions + extends CCCommandOptions, + NodeInformationFrame { + nodeId: number; +} + +@CCCommand(ZWaveProtocolCommand.NewNodeRegistered) +export class ZWaveProtocolCCNewNodeRegistered + extends ZWaveProtocolCC + implements NodeInformationFrame +{ + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCNewNodeRegisteredOptions, + ) { + super(host, options); + + let nif: NodeInformationFrame; + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.nodeId = this.payload[0]; + nif = parseNodeInformationFrame(this.payload.slice(1)); + } else { + this.nodeId = options.nodeId; + nif = options; + } + + this.basicDeviceClass = nif.basicDeviceClass; + this.genericDeviceClass = nif.genericDeviceClass; + this.specificDeviceClass = nif.specificDeviceClass; + this.isListening = nif.isListening; + this.isFrequentListening = nif.isFrequentListening; + this.isRouting = nif.isRouting; + this.supportedDataRates = nif.supportedDataRates; + this.protocolVersion = nif.protocolVersion; + this.optionalFunctionality = nif.optionalFunctionality; + this.nodeType = nif.nodeType; + this.supportsSecurity = nif.supportsSecurity; + this.supportsBeaming = nif.supportsBeaming; + this.supportedCCs = nif.supportedCCs; + } + + public nodeId: number; + 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 = Buffer.concat([ + Buffer.from([this.nodeId]), + encodeNodeInformationFrame(this), + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCNewRangeRegisteredOptions extends CCCommandOptions { + nodeId: number; + neighborNodeIds: number[]; +} + +@CCCommand(ZWaveProtocolCommand.NewRangeRegistered) +export class ZWaveProtocolCCNewRangeRegistered extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCNewRangeRegisteredOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.nodeId = this.payload[0]; + const numNeighbors = this.payload[1]; + this.neighborNodeIds = [...this.payload.slice(2, 2 + numNeighbors)]; + } else { + this.nodeId = options.nodeId; + this.neighborNodeIds = options.neighborNodeIds; + } + } + + public nodeId: number; + public neighborNodeIds: number[]; + + public serialize(): Buffer { + const nodesBitmask = encodeBitMask(this.neighborNodeIds, MAX_NODES); + this.payload = Buffer.concat([ + Buffer.from([this.nodeId, nodesBitmask.length]), + nodesBitmask, + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCTransferNewPrimaryControllerCompleteOptions + extends CCCommandOptions { + genericDeviceClass: number; +} + +@CCCommand(ZWaveProtocolCommand.TransferNewPrimaryControllerComplete) +export class ZWaveProtocolCCTransferNewPrimaryControllerComplete extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCTransferNewPrimaryControllerCompleteOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.genericDeviceClass = this.payload[0]; + } else { + this.genericDeviceClass = options.genericDeviceClass; + } + } + + public genericDeviceClass: number; + + public serialize(): Buffer { + this.payload = Buffer.from([this.genericDeviceClass]); + return super.serialize(); + } +} + +@CCCommand(ZWaveProtocolCommand.AutomaticControllerUpdateStart) +export class ZWaveProtocolCCAutomaticControllerUpdateStart extends ZWaveProtocolCC {} + +interface ZWaveProtocolCCSUCNodeIDOptions extends CCCommandOptions { + sucNodeId: number; + isSIS: boolean; +} + +@CCCommand(ZWaveProtocolCommand.SUCNodeID) +export class ZWaveProtocolCCSUCNodeID extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCSUCNodeIDOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.sucNodeId = this.payload[0]; + const capabilities = this.payload[1] ?? 0; + this.isSIS = !!(capabilities & 0b1); + } else { + this.sucNodeId = options.sucNodeId; + this.isSIS = options.isSIS; + } + } + + public sucNodeId: number; + public isSIS: boolean; + + public serialize(): Buffer { + this.payload = Buffer.from([this.sucNodeId, this.isSIS ? 0b1 : 0]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCSetSUCOptions extends CCCommandOptions { + enableSIS: boolean; +} + +@CCCommand(ZWaveProtocolCommand.SetSUC) +export class ZWaveProtocolCCSetSUC extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCSetSUCOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + // Byte 0 must be 0x01 or ignored + const capabilities = this.payload[1] ?? 0; + this.enableSIS = !!(capabilities & 0b1); + } else { + this.enableSIS = options.enableSIS; + } + } + + public enableSIS: boolean; + + public serialize(): Buffer { + this.payload = Buffer.from([0x01, this.enableSIS ? 0b1 : 0]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCSetSUCAckOptions extends CCCommandOptions { + accepted: boolean; + isSIS: boolean; +} + +@CCCommand(ZWaveProtocolCommand.SetSUCAck) +export class ZWaveProtocolCCSetSUCAck extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCSetSUCAckOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.accepted = this.payload[0] === 0x01; + const capabilities = this.payload[1] ?? 0; + this.isSIS = !!(capabilities & 0b1); + } else { + this.accepted = options.accepted; + this.isSIS = options.isSIS; + } + } + + public accepted: boolean; + public isSIS: boolean; + + public serialize(): Buffer { + this.payload = Buffer.from([ + this.accepted ? 0x01 : 0x00, + this.isSIS ? 0b1 : 0, + ]); + return super.serialize(); + } +} + +@CCCommand(ZWaveProtocolCommand.AssignSUCReturnRoute) +export class ZWaveProtocolCCAssignSUCReturnRoute extends ZWaveProtocolCCAssignReturnRoute {} + +interface ZWaveProtocolCCStaticRouteRequestOptions extends CCCommandOptions { + nodeIds: number[]; +} + +@CCCommand(ZWaveProtocolCommand.StaticRouteRequest) +export class ZWaveProtocolCCStaticRouteRequest extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCStaticRouteRequestOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 5); + this.nodeIds = [...this.payload.slice(0, 5)].filter( + (id) => id > 0 && id <= MAX_NODES, + ); + } else { + if (options.nodeIds.some((n) => n < 1 || n > MAX_NODES)) { + throw new ZWaveError( + `All node IDs must be between 1 and ${MAX_NODES}!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.nodeIds = options.nodeIds; + } + } + + public nodeIds: number[]; + + public serialize(): Buffer { + this.payload = Buffer.alloc(5, 0); + for (let i = 0; i < this.nodeIds.length && i < 5; i++) { + this.payload[i] = this.nodeIds[i]; + } + return super.serialize(); + } +} + +interface ZWaveProtocolCCLostOptions extends CCCommandOptions { + nodeId: number; +} + +@CCCommand(ZWaveProtocolCommand.Lost) +export class ZWaveProtocolCCLost extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCLostOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.nodeId = this.payload[0]; + } else { + this.nodeId = options.nodeId; + } + } + + public nodeId: number; + + public serialize(): Buffer { + this.payload = Buffer.from([this.nodeId]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCAcceptLostOptions extends CCCommandOptions { + accepted: boolean; +} + +@CCCommand(ZWaveProtocolCommand.AcceptLost) +export class ZWaveProtocolCCAcceptLost extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCAcceptLostOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + validatePayload( + this.payload[0] === 0x04 || this.payload[0] === 0x05, + ); + this.accepted = this.payload[0] === 0x05; + } else { + this.accepted = options.accepted; + } + } + + public accepted: boolean; + + public serialize(): Buffer { + this.payload = Buffer.from([this.accepted ? 0x05 : 0x04]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCNOPPowerOptions extends CCCommandOptions { + powerDampening: number; +} + +@CCCommand(ZWaveProtocolCommand.NOPPower) +export class ZWaveProtocolCCNOPPower extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCNOPPowerOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + if (this.payload.length >= 2) { + // Ignore byte 0 + this.powerDampening = this.payload[1]; + } else if (this.payload.length === 1) { + this.powerDampening = [ + 0xf0, 0xc8, 0xa7, 0x91, 0x77, 0x67, 0x60, 0x46, 0x38, 0x35, + 0x32, 0x30, 0x24, 0x22, 0x20, + ].indexOf(this.payload[0]); + if (this.powerDampening === -1) this.powerDampening = 0; + } else { + throw validatePayload.fail("Invalid payload length!"); + } + } else { + if (options.powerDampening < 0 || options.powerDampening > 14) { + throw new ZWaveError( + `${this.constructor.name}: power dampening must be between 0 and 14 dBm!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.powerDampening = options.powerDampening; + } + } + + // Power dampening in (negative) dBm. A value of 2 means -2 dBm. + public powerDampening: number; + + public serialize(): Buffer { + this.payload = Buffer.from([0, this.powerDampening]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCReservedIDsOptions extends CCCommandOptions { + reservedNodeIDs: number[]; +} + +@CCCommand(ZWaveProtocolCommand.ReservedIDs) +export class ZWaveProtocolCCReservedIDs extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCReservedIDsOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + const numNodeIDs = this.payload[0]; + validatePayload(this.payload.length >= 1 + numNodeIDs); + this.reservedNodeIDs = [...this.payload.slice(1, 1 + numNodeIDs)]; + } else { + this.reservedNodeIDs = options.reservedNodeIDs; + } + } + + public reservedNodeIDs: number[]; + + public serialize(): Buffer { + this.payload = Buffer.from([ + this.reservedNodeIDs.length, + ...this.reservedNodeIDs, + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCReserveNodeIDsOptions extends CCCommandOptions { + numNodeIDs: number; +} + +@CCCommand(ZWaveProtocolCommand.ReserveNodeIDs) +@expectedCCResponse(ZWaveProtocolCCReservedIDs) +export class ZWaveProtocolCCReserveNodeIDs extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCReserveNodeIDsOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 1); + this.numNodeIDs = this.payload[0]; + } else { + this.numNodeIDs = options.numNodeIDs; + } + } + + public numNodeIDs: number; + + public serialize(): Buffer { + this.payload = Buffer.from([this.numNodeIDs]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCNodesExistReplyOptions extends CCCommandOptions { + nodeMaskType: number; + nodeListUpdated: boolean; +} + +@CCCommand(ZWaveProtocolCommand.NodesExistReply) +export class ZWaveProtocolCCNodesExistReply extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCNodesExistReplyOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.nodeMaskType = this.payload[0]; + this.nodeListUpdated = this.payload[1] === 0x01; + } else { + this.nodeMaskType = options.nodeMaskType; + this.nodeListUpdated = options.nodeListUpdated; + } + } + + public nodeMaskType: number; + public nodeListUpdated: boolean; + + public serialize(): Buffer { + this.payload = Buffer.from([ + this.nodeMaskType, + this.nodeListUpdated ? 0x01 : 0x00, + ]); + return super.serialize(); + } +} + +function testResponseForZWaveProtocolNodesExist( + sent: ZWaveProtocolCCNodesExist, + received: ZWaveProtocolCCNodesExistReply, +) { + return received.nodeMaskType === sent.nodeMaskType; +} + +interface ZWaveProtocolCCNodesExistOptions extends CCCommandOptions { + nodeMaskType: number; + nodeIDs: number[]; +} + +@CCCommand(ZWaveProtocolCommand.NodesExist) +@expectedCCResponse( + ZWaveProtocolCCNodesExistReply, + testResponseForZWaveProtocolNodesExist, +) +export class ZWaveProtocolCCNodesExist extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCNodesExistOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.nodeMaskType = this.payload[0]; + const numNodeIDs = this.payload[1]; + validatePayload(this.payload.length >= 2 + numNodeIDs); + this.nodeIDs = [...this.payload.slice(2, 2 + numNodeIDs)]; + } else { + this.nodeMaskType = options.nodeMaskType; + this.nodeIDs = options.nodeIDs; + } + } + + public nodeMaskType: number; + public nodeIDs: number[]; + + public serialize(): Buffer { + this.payload = Buffer.from([ + this.nodeMaskType, + this.nodeIDs.length, + ...this.nodeIDs, + ]); + return super.serialize(); + } +} + +interface ZWaveProtocolCCSetNWIModeOptions extends CCCommandOptions { + enabled: boolean; + timeoutMinutes?: number; +} + +@CCCommand(ZWaveProtocolCommand.SetNWIMode) +export class ZWaveProtocolCCSetNWIMode extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCSetNWIModeOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.enabled = this.payload[0] === 0x01; + this.timeoutMinutes = this.payload[1] || undefined; + } else { + this.enabled = options.enabled; + this.timeoutMinutes = options.timeoutMinutes; + } + } + + public enabled: boolean; + public timeoutMinutes?: number; + + public serialize(): Buffer { + this.payload = Buffer.from([ + this.enabled ? 0x01 : 0x00, + this.timeoutMinutes ?? 0x00, + ]); + return super.serialize(); + } +} + +@CCCommand(ZWaveProtocolCommand.ExcludeRequest) +export class ZWaveProtocolCCExcludeRequest extends ZWaveProtocolCCNodeInformationFrame {} + +interface ZWaveProtocolCCAssignReturnRoutePriorityOptions + extends CCCommandOptions { + targetNodeId: number; + routeNumber: number; +} + +@CCCommand(ZWaveProtocolCommand.AssignReturnRoutePriority) +export class ZWaveProtocolCCAssignReturnRoutePriority extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCAssignReturnRoutePriorityOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 2); + this.targetNodeId = this.payload[0]; + this.routeNumber = this.payload[1]; + } else { + this.targetNodeId = options.targetNodeId; + this.routeNumber = options.routeNumber; + } + } + + public targetNodeId: number; + public routeNumber: number; + + public serialize(): Buffer { + this.payload = Buffer.from([this.targetNodeId, this.routeNumber]); + return super.serialize(); + } +} + +@CCCommand(ZWaveProtocolCommand.AssignSUCReturnRoutePriority) +export class ZWaveProtocolCCAssignSUCReturnRoutePriority extends ZWaveProtocolCCAssignReturnRoutePriority {} + +interface ZWaveProtocolCCSmartStartIncludedNodeInformationOptions + extends CCCommandOptions { + nwiHomeId: Buffer; +} + +@CCCommand(ZWaveProtocolCommand.SmartStartIncludedNodeInformation) +export class ZWaveProtocolCCSmartStartIncludedNodeInformation extends ZWaveProtocolCC { + public constructor( + host: ZWaveHost, + options: + | CommandClassDeserializationOptions + | ZWaveProtocolCCSmartStartIncludedNodeInformationOptions, + ) { + super(host, options); + if (gotDeserializationOptions(options)) { + validatePayload(this.payload.length >= 4); + this.nwiHomeId = this.payload.slice(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(); + } +} + +@CCCommand(ZWaveProtocolCommand.SmartStartPrime) +export class ZWaveProtocolCCSmartStartPrime extends ZWaveProtocolCCNodeInformationFrame {} + +@CCCommand(ZWaveProtocolCommand.SmartStartInclusionRequest) +export class ZWaveProtocolCCSmartStartInclusionRequest extends ZWaveProtocolCCNodeInformationFrame {} diff --git a/packages/cc/src/lib/_Types.ts b/packages/cc/src/lib/_Types.ts index 9a00d7693c7..fae53de2eb4 100644 --- a/packages/cc/src/lib/_Types.ts +++ b/packages/cc/src/lib/_Types.ts @@ -1,10 +1,13 @@ import type { Scale } from "@zwave-js/config/safe"; -import type { +import { CommandClasses, + DataRate, Duration, + FLiRS, IZWaveNode, Maybe, ValueMetadata, + ZWaveDataRate, } from "@zwave-js/core/safe"; import type { NotificationCCReport } from "../cc/NotificationCC"; @@ -1637,3 +1640,86 @@ export enum ZWavePlusNodeType { Node = 0x00, // ZWave+ Node IPGateway = 0x02, // ZWave+ for IP Gateway } + +/** @publicAPI */ +export enum ZWaveProtocolCommand { + NodeInformationFrame = 0x01, + RequestNodeInformationFrame = 0x02, + AssignIDs = 0x03, + FindNodesInRange = 0x04, + GetNodesInRange = 0x05, + RangeInfo = 0x06, + CommandComplete = 0x07, + TransferPresentation = 0x08, + TransferNodeInformation = 0x09, + TransferRangeInformation = 0x0a, + TransferEnd = 0x0b, + AssignReturnRoute = 0x0c, + NewNodeRegistered = 0x0d, + NewRangeRegistered = 0x0e, + TransferNewPrimaryControllerComplete = 0x0f, + AutomaticControllerUpdateStart = 0x10, + SUCNodeID = 0x11, + SetSUC = 0x12, + SetSUCAck = 0x13, + AssignSUCReturnRoute = 0x14, + StaticRouteRequest = 0x15, + Lost = 0x16, + AcceptLost = 0x17, + NOPPower = 0x18, + ReserveNodeIDs = 0x19, + ReservedIDs = 0x1a, + NodesExist = 0x1f, + NodesExistReply = 0x20, + SetNWIMode = 0x22, + ExcludeRequest = 0x23, + AssignReturnRoutePriority = 0x24, + AssignSUCReturnRoutePriority = 0x25, + SmartStartIncludedNodeInformation = 0x26, + SmartStartPrime = 0x27, + SmartStartInclusionRequest = 0x28, +} + +export enum WakeUpTime { + None, + "1000ms", + "250ms", +} + +export function FLiRS2WakeUpTime(value: FLiRS): WakeUpTime { + return value === "1000ms" ? 1 : value === "250ms" ? 2 : 0; +} + +export function wakeUpTime2FLiRS(value: WakeUpTime): FLiRS { + return value === 1 ? "1000ms" : value === 2 ? "250ms" : false; +} + +export function dataRate2ZWaveDataRate(dataRate: DataRate): ZWaveDataRate { + return dataRate === 100000 + ? ZWaveDataRate["100k"] + : dataRate === 40000 + ? ZWaveDataRate["40k"] + : ZWaveDataRate["9k6"]; +} + +export function ZWaveDataRate2DataRate(zdr: ZWaveDataRate): DataRate { + return zdr === ZWaveDataRate["100k"] + ? 100000 + : zdr === ZWaveDataRate["40k"] + ? 40000 + : 9600; +} + +export function parseWakeUpTime(value: number): WakeUpTime { + return value <= WakeUpTime["250ms"] ? value : 0; +} + +export enum NetworkTransferStatus { + Failed = 0, + Success, + UpdateDone, + UpdateAborted, + UpdateWait, + UpdateDisabled, + UpdateOverflow, +} diff --git a/packages/core/src/capabilities/CommandClasses.ts b/packages/core/src/capabilities/CommandClasses.ts index 5ed96832f3d..a1a4b8dd8fa 100644 --- a/packages/core/src/capabilities/CommandClasses.ts +++ b/packages/core/src/capabilities/CommandClasses.ts @@ -128,6 +128,8 @@ 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 + "Z-Wave Protocol" = 0x01, } export function getCCName(cc: number): string { @@ -141,7 +143,7 @@ export const allCCs: readonly CommandClasses[] = Object.freeze( Object.keys(CommandClasses) .filter((key) => /^\d+$/.test(key)) .map((key) => parseInt(key)) - .filter((key) => key >= 0), + .filter((key) => key >= 0 && key !== CommandClasses["Z-Wave Protocol"]), ); /** diff --git a/packages/core/src/capabilities/NodeInfo.test.ts b/packages/core/src/capabilities/NodeInfo.test.ts index 990ee80dda5..66881ad553d 100644 --- a/packages/core/src/capabilities/NodeInfo.test.ts +++ b/packages/core/src/capabilities/NodeInfo.test.ts @@ -1,8 +1,11 @@ import { CommandClasses } from "./CommandClasses"; -import { parseNodeInformationFrame, parseNodeUpdatePayload } from "./NodeInfo"; +import { + parseApplicationNodeInformation, + parseNodeUpdatePayload, +} from "./NodeInfo"; describe("lib/node/NodeInfo", () => { - describe("parseNodeInformationFrame()", () => { + describe("parseApplicationNodeInformation()", () => { const payload = Buffer.from([ 0x01, // Remote Controller 0x02, // Portable Scene Controller @@ -10,17 +13,17 @@ describe("lib/node/NodeInfo", () => { CommandClasses["Multi Channel"], CommandClasses["Multilevel Toggle Switch"], 0xef, // ====== - // Controlled CCs + // Controlled CCs (ignored in Application Node Info) CommandClasses["Multilevel Toggle Switch"], ]); - const eif = parseNodeInformationFrame(payload); + const eif = parseApplicationNodeInformation(payload); it("should extract the correct GenericDeviceClass", () => { - expect(eif.generic).toBe(0x01); + expect(eif.genericDeviceClass).toBe(0x01); }); it("should extract the correct SpecificDeviceClass", () => { - expect(eif.specific).toBe(0x02); + expect(eif.specificDeviceClass).toBe(0x02); }); it("should report the correct CCs as supported", () => { @@ -34,16 +37,13 @@ describe("lib/node/NodeInfo", () => { describe("parseNodeUpdatePayload()", () => { const payload = Buffer.from([ 5, // NodeID - 7, // Length (is ignored) + 2, // CC list length 0x03, // Slave 0x01, // Remote Controller 0x02, // Portable Scene Controller // Supported CCs CommandClasses["Multi Channel"], CommandClasses["Multilevel Toggle Switch"], - 0xef, // ====== - // Controlled CCs - CommandClasses["Multilevel Toggle Switch"], ]); const nup = parseNodeUpdatePayload(payload); @@ -56,11 +56,11 @@ describe("lib/node/NodeInfo", () => { }); it("should extract the correct GenericDeviceClass", () => { - expect(nup.generic).toBe(1); + expect(nup.genericDeviceClass).toBe(1); }); it("should extract the correct SpecificDeviceClass", () => { - expect(nup.specific).toBe(2); + expect(nup.specificDeviceClass).toBe(2); }); it("should report the correct CCs as supported", () => { @@ -70,16 +70,10 @@ describe("lib/node/NodeInfo", () => { ]); }); - it("should report the correct CCs as controlled", () => { - expect(nup.controlledCCs).toContainAllValues([ - CommandClasses["Multilevel Toggle Switch"], - ]); - }); - it("correctly parses extended CCs", () => { const payload = Buffer.from([ 5, // NodeID - 7, // Length (is ignored) + 6, // CC list length 0x03, 0x01, 0x02, // Portable Scene Controller @@ -88,10 +82,6 @@ describe("lib/node/NodeInfo", () => { 0xf1, 0x00, CommandClasses["Sensor Configuration"], - // ==== - 0xef, - // ==== - // Controlled CCs CommandClasses.Supervision, // --> some hypothetical CC 0xfe, @@ -101,8 +91,6 @@ describe("lib/node/NodeInfo", () => { expect(nup.supportedCCs).toContainAllValues([ CommandClasses["Security Mark"], CommandClasses["Sensor Configuration"], - ]); - expect(nup.controlledCCs).toContainAllValues([ 0xfedc, CommandClasses.Supervision, ]); diff --git a/packages/core/src/capabilities/NodeInfo.ts b/packages/core/src/capabilities/NodeInfo.ts index 03cd6c345ee..865c8036421 100644 --- a/packages/core/src/capabilities/NodeInfo.ts +++ b/packages/core/src/capabilities/NodeInfo.ts @@ -1,42 +1,37 @@ import { sum } from "@zwave-js/shared/safe"; -import { ZWaveError, ZWaveErrorCodes } from "../error/ZWaveError"; import { validatePayload } from "../util/misc"; import { CommandClasses } from "./CommandClasses"; -export interface NodeInformationFrame { - generic: number; - specific: number; +export interface ApplicationNodeInformation { + genericDeviceClass: number; + specificDeviceClass: number; supportedCCs: CommandClasses[]; } -export interface ExtendedNodeInformationFrame extends NodeInformationFrame { - // controlledCCs isn't actually included in a NIF, but this way we can reuse the parser code - controlledCCs: CommandClasses[]; +export function parseApplicationNodeInformation( + nif: Buffer, +): ApplicationNodeInformation { + validatePayload(nif.length >= 2); + return { + genericDeviceClass: nif[0], + specificDeviceClass: nif[1], + supportedCCs: parseCCList(nif.slice(2)).supportedCCs, + }; } -// This is sometimes used interchangeably with the NIF -export interface NodeUpdatePayload extends ExtendedNodeInformationFrame { +export interface NodeUpdatePayload extends ApplicationNodeInformation { nodeId: number; basic: number; } export function parseNodeUpdatePayload(nif: Buffer): NodeUpdatePayload { + const numCCs = nif[1]; + // The application node info starts at 3, and contains 2+N bytes + validatePayload(nif.length >= 3 + 2 + numCCs); return { nodeId: nif[0], - // length is byte 1 basic: nif[2], - ...internalParseNodeInformationFrame(nif.slice(3)), - }; -} - -function internalParseNodeInformationFrame( - nif: Buffer, -): ExtendedNodeInformationFrame { - validatePayload(nif.length >= 2); - return { - generic: nif[0], - specific: nif[1], - ...parseCCList(nif.slice(2)), + ...parseApplicationNodeInformation(nif.slice(3, 3 + 2 + numCCs)), }; } @@ -128,11 +123,6 @@ export function encodeCCList( return ret; } -export function parseNodeInformationFrame(nif: Buffer): NodeInformationFrame { - const { controlledCCs, ...ret } = internalParseNodeInformationFrame(nif); - return ret; -} - export enum ProtocolVersion { "unknown" = 0, "2.0" = 1, @@ -179,16 +169,14 @@ export interface NodeProtocolInfoAndDeviceClass specificDeviceClass: number; } +export type NodeInformationFrame = NodeProtocolInfoAndDeviceClass & + ApplicationNodeInformation; + export function parseNodeProtocolInfo( buffer: Buffer, offset: number, ): NodeProtocolInfo { - if (buffer.length < offset + 3) { - throw new ZWaveError( - "Cannot parse protocol info, the buffer is too short!", - ZWaveErrorCodes.PacketFormat_Truncated, - ); - } + validatePayload(buffer.length >= offset + 3); const isListening = !!(buffer[offset] & 0b10_000_000); const isRouting = !!(buffer[offset] & 0b01_000_000); @@ -278,3 +266,61 @@ export function encodeNodeProtocolInfo(info: NodeProtocolInfo): Buffer { return ret; } + +export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer): { + info: NodeProtocolInfoAndDeviceClass; + bytesRead: number; +} { + validatePayload(buffer.length >= 5); + const protocolInfo = parseNodeProtocolInfo(buffer, 0); + let offset = 3; + const basic = buffer[offset++]; + const generic = buffer[offset++]; + let specific = 0; + if (protocolInfo.hasSpecificDeviceClass) { + validatePayload(buffer.length >= offset + 1); + specific = buffer[offset++]; + } + return { + info: { + ...protocolInfo, + basicDeviceClass: basic, + genericDeviceClass: generic, + specificDeviceClass: specific, + }, + bytesRead: offset, + }; +} + +export function encodeNodeProtocolInfoAndDeviceClass( + info: NodeProtocolInfoAndDeviceClass, +): Buffer { + return Buffer.concat([ + encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), + Buffer.from([ + info.basicDeviceClass, + info.genericDeviceClass, + info.specificDeviceClass, + ]), + ]); +} + +export function parseNodeInformationFrame( + buffer: Buffer, +): NodeInformationFrame { + const { info, bytesRead: offset } = + parseNodeProtocolInfoAndDeviceClass(buffer); + const supportedCCs = parseCCList(buffer.slice(offset)).supportedCCs; + + return { + ...info, + supportedCCs, + }; +} + +export function encodeNodeInformationFrame(info: NodeInformationFrame): Buffer { + return Buffer.concat([ + encodeNodeProtocolInfoAndDeviceClass(info), + encodeCCList(info.supportedCCs, []), + ]); +} diff --git a/packages/core/src/capabilities/Protocols.ts b/packages/core/src/capabilities/Protocols.ts index 57647956d06..d8b1780dd0c 100644 --- a/packages/core/src/capabilities/Protocols.ts +++ b/packages/core/src/capabilities/Protocols.ts @@ -5,6 +5,12 @@ export enum Protocols { ZWaveLongRange = 1, } +export enum ZWaveDataRate { + "9k6" = 0x01, + "40k" = 0x02, + "100k" = 0x03, +} + export enum ProtocolDataRate { ZWave_9k6 = 0x01, ZWave_40k = 0x02, diff --git a/packages/zwave-js/src/lib/node/Node.test.ts b/packages/zwave-js/src/lib/node/Node.test.ts index 214be934a9e..8419ccdc931 100644 --- a/packages/zwave-js/src/lib/node/Node.test.ts +++ b/packages/zwave-js/src/lib/node/Node.test.ts @@ -805,15 +805,17 @@ describe("lib/node/Node", () => { isControlled: true, }); node.addCC(CommandClasses.Configuration, { - isSupported: true, + isControlled: true, }); node.updateNodeInfo({ - controlledCCs: [CommandClasses.Configuration], - supportedCCs: [CommandClasses.Battery], + supportedCCs: [ + CommandClasses.Battery, + CommandClasses.Configuration, + ], } as any); expect(node.supportsCC(CommandClasses.Battery)).toBeTrue(); - expect(node.controlsCC(CommandClasses.Configuration)).toBeTrue(); + expect(node.supportsCC(CommandClasses.Configuration)).toBeTrue(); node.destroy(); }); diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 732906b8ac8..7581f738d5b 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -1493,11 +1493,6 @@ protocol version: ${this.protocolVersion}`; const ccName = CommandClasses[cc]; logLines.push(`· ${ccName ? ccName : num2hex(cc)}`); } - logLines.push("controlled CCs:"); - for (const cc of resp.nodeInformation.controlledCCs) { - const ccName = CommandClasses[cc]; - logLines.push(`· ${ccName ? ccName : num2hex(cc)}`); - } this.driver.controllerLog.logNode(this.id, { message: logLines.join("\n"), direction: "inbound", @@ -1885,8 +1880,6 @@ protocol version: ${this.protocolVersion}`; if (this.interviewStage < InterviewStage.NodeInfo) { for (const cc of nodeInfo.supportedCCs) this.addCC(cc, { isSupported: true }); - for (const cc of nodeInfo.controlledCCs) - this.addCC(cc, { isControlled: true }); } // As the NIF is sent on wakeup, treat this as a sign that the node is awake