diff --git a/packages/cc/src/cc/AlarmSensorCC.ts b/packages/cc/src/cc/AlarmSensorCC.ts index 3962536b628d..9b1494726ba4 100644 --- a/packages/cc/src/cc/AlarmSensorCC.ts +++ b/packages/cc/src/cc/AlarmSensorCC.ts @@ -6,23 +6,26 @@ import { MessagePriority, type MessageRecord, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, parseBitMask, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, isEnumMember, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -130,7 +133,7 @@ export class AlarmSensorCCAPI extends PhysicalCCAPI { const cc = new AlarmSensorCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sensorType, }); const response = await this.host.sendCommand( @@ -149,7 +152,7 @@ export class AlarmSensorCCAPI extends PhysicalCCAPI { const cc = new AlarmSensorCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< AlarmSensorCCSupportedReport @@ -318,28 +321,53 @@ duration: ${currentValue.duration}`; } } +// @publicAPI +export interface AlarmSensorCCReportOptions { + sensorType: AlarmSensorType; + state: boolean; + severity?: number; + duration?: number; +} + @CCCommand(AlarmSensorCommand.Report) export class AlarmSensorCCReport extends AlarmSensorCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 5, this.payload[1] !== 0xff); - // Alarm Sensor reports may be forwarded by a different node, in this case - // (and only then!) the payload contains the original node ID - const sourceNodeId = this.payload[0]; - if (sourceNodeId !== 0) { - this.nodeId = sourceNodeId; - } - this.sensorType = this.payload[1]; + + // TODO: Check implementation: + this.sensorType = options.sensorType; + this.state = options.state; + this.severity = options.severity; + this.duration = options.duration; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): AlarmSensorCCReport { + validatePayload(raw.payload.length >= 5, raw.payload[1] !== 0xff); + const sourceNodeId = raw.payload[0]; + + const sensorType: AlarmSensorType = raw.payload[1]; // Any positive value gets interpreted as alarm - this.state = this.payload[2] > 0; + const state: boolean = raw.payload[2] > 0; // Severity only ranges from 1 to 100 - if (this.payload[2] > 0 && this.payload[2] <= 0x64) { - this.severity = this.payload[2]; + let severity: number | undefined; + if (raw.payload[2] > 0 && raw.payload[2] <= 0x64) { + severity = raw.payload[2]; } + // ignore zero durations - this.duration = this.payload.readUInt16BE(3) || undefined; + const duration = raw.payload.readUInt16BE(3) || undefined; + + return new AlarmSensorCCReport({ + // Alarm Sensor reports may be forwarded by a different node, in this case + // (and only then!) the payload contains the original node ID + nodeId: sourceNodeId || ctx.sourceNodeId, + sensorType, + state, + severity, + duration, + }); } public readonly sensorType: AlarmSensorType; @@ -393,7 +421,7 @@ function testResponseForAlarmSensorGet( } // @publicAPI -export interface AlarmSensorCCGetOptions extends CCCommandOptions { +export interface AlarmSensorCCGetOptions { sensorType?: AlarmSensorType; } @@ -401,18 +429,22 @@ export interface AlarmSensorCCGetOptions extends CCCommandOptions { @expectedCCResponse(AlarmSensorCCReport, testResponseForAlarmSensorGet) export class AlarmSensorCCGet extends AlarmSensorCC { public constructor( - options: CommandClassDeserializationOptions | AlarmSensorCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.sensorType = options.sensorType ?? AlarmSensorType.Any; - } + this.sensorType = options.sensorType ?? AlarmSensorType.Any; + } + + public static from(_raw: CCRaw, _ctx: CCParsingContext): AlarmSensorCCGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new AlarmSensorCCGet({ + // nodeId: ctx.sourceNodeId, + // }); } public sensorType: AlarmSensorType; @@ -435,31 +467,47 @@ export class AlarmSensorCCGet extends AlarmSensorCC { } } +// @publicAPI +export interface AlarmSensorCCSupportedReportOptions { + supportedSensorTypes: AlarmSensorType[]; +} + @CCCommand(AlarmSensorCommand.SupportedReport) export class AlarmSensorCCSupportedReport extends AlarmSensorCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - const bitMaskLength = this.payload[0]; - validatePayload(this.payload.length >= 1 + bitMaskLength); - this._supportedSensorTypes = parseBitMask( - this.payload.subarray(1, 1 + bitMaskLength), + + // TODO: Check implementation: + this.supportedSensorTypes = options.supportedSensorTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AlarmSensorCCSupportedReport { + validatePayload(raw.payload.length >= 1); + const bitMaskLength = raw.payload[0]; + validatePayload(raw.payload.length >= 1 + bitMaskLength); + const supportedSensorTypes: AlarmSensorType[] = parseBitMask( + raw.payload.subarray(1, 1 + bitMaskLength), AlarmSensorType["General Purpose"], ); + + return new AlarmSensorCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedSensorTypes, + }); } - private _supportedSensorTypes: AlarmSensorType[]; @ccValue(AlarmSensorCCValues.supportedSensorTypes) - public get supportedSensorTypes(): readonly AlarmSensorType[] { - return this._supportedSensorTypes; - } + public supportedSensorTypes: AlarmSensorType[]; public persistValues(ctx: PersistValuesContext): boolean { if (!super.persistValues(ctx)) return false; // Create metadata for each sensor type - for (const type of this._supportedSensorTypes) { + for (const type of this.supportedSensorTypes) { this.createMetadataForSensorType(ctx, type); } return true; @@ -469,7 +517,7 @@ export class AlarmSensorCCSupportedReport extends AlarmSensorCC { return { ...super.toLogEntry(ctx), message: { - "supported sensor types": this._supportedSensorTypes + "supported sensor types": this.supportedSensorTypes .map((t) => getEnumMemberName(AlarmSensorType, t)) .join(", "), }, diff --git a/packages/cc/src/cc/AssociationCC.ts b/packages/cc/src/cc/AssociationCC.ts index 31a9157602c6..276ba3732334 100644 --- a/packages/cc/src/cc/AssociationCC.ts +++ b/packages/cc/src/cc/AssociationCC.ts @@ -3,6 +3,7 @@ import type { MaybeNotKnown, MessageRecord, SupervisionResult, + WithAddress, } from "@zwave-js/core/safe"; import { CommandClasses, @@ -23,12 +24,10 @@ import { validateArgs } from "@zwave-js/transformers"; import { distinct } from "alcalzone-shared/arrays"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -108,7 +107,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCSupportedGroupingsGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< AssociationCCSupportedGroupingsReport @@ -128,7 +127,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCSupportedGroupingsReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupCount, }); await this.host.sendCommand(cc, this.commandOptions); @@ -144,7 +143,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, }); const response = await this.host.sendCommand( @@ -161,7 +160,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { @validateArgs() public async sendReport( - options: AssociationCCReportSpecificOptions, + options: AssociationCCReportOptions, ): Promise { this.assertSupportsCommand( AssociationCommand, @@ -170,7 +169,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -188,7 +187,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, nodeIds, }); @@ -224,7 +223,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCRemove({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand(cc, this.commandOptions); @@ -271,7 +270,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCSpecificGroupGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< AssociationCCSpecificGroupReport @@ -296,7 +295,7 @@ export class AssociationCCAPI extends PhysicalCCAPI { const cc = new AssociationCCSpecificGroupReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, group, }); await this.host.sendCommand(cc, this.commandOptions); @@ -497,7 +496,7 @@ currently assigned nodes: ${group.nodeIds.map(String).join(", ")}`; } // @publicAPI -export interface AssociationCCSetOptions extends CCCommandOptions { +export interface AssociationCCSetOptions { groupId: number; nodeIds: number[]; } @@ -506,29 +505,35 @@ export interface AssociationCCSetOptions extends CCCommandOptions { @useSupervision() export class AssociationCCSet extends AssociationCC { public constructor( - options: CommandClassDeserializationOptions | AssociationCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.groupId = this.payload[0]; - this.nodeIds = [...this.payload.subarray(1)]; - } else { - if (options.groupId < 1) { - throw new ZWaveError( - "The group id must be positive!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - 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.groupId = options.groupId; - this.nodeIds = options.nodeIds; + if (options.groupId < 1) { + throw new ZWaveError( + "The group id must be positive!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + 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.groupId = options.groupId; + this.nodeIds = options.nodeIds; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): AssociationCCSet { + validatePayload(raw.payload.length >= 2); + const groupId = raw.payload[0]; + const nodeIds = [...raw.payload.subarray(1)]; + + return new AssociationCCSet({ + nodeId: ctx.sourceNodeId, + groupId, + nodeIds, + }); } public groupId: number; @@ -565,23 +570,29 @@ export interface AssociationCCRemoveOptions { @useSupervision() export class AssociationCCRemove extends AssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (AssociationCCRemoveOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - if (this.payload[0] !== 0) { - this.groupId = this.payload[0]; - } - this.nodeIds = [...this.payload.subarray(1)]; - } else { - // When removing associations, we allow invalid node IDs. - // See GH#3606 - it is possible that those exist. - this.groupId = options.groupId; - this.nodeIds = options.nodeIds; + // When removing associations, we allow invalid node IDs. + // See GH#3606 - it is possible that those exist. + this.groupId = options.groupId; + this.nodeIds = options.nodeIds; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): AssociationCCRemove { + validatePayload(raw.payload.length >= 1); + + let groupId: number | undefined; + if (raw.payload[0] !== 0) { + groupId = raw.payload[0]; } + const nodeIds = [...raw.payload.subarray(1)]; + + return new AssociationCCRemove({ + nodeId: ctx.sourceNodeId, + groupId, + nodeIds, + }); } public groupId?: number; @@ -610,7 +621,7 @@ export class AssociationCCRemove extends AssociationCC { } // @publicAPI -export interface AssociationCCReportSpecificOptions { +export interface AssociationCCReportOptions { groupId: number; maxNodes: number; nodeIds: number[]; @@ -620,24 +631,30 @@ export interface AssociationCCReportSpecificOptions { @CCCommand(AssociationCommand.Report) export class AssociationCCReport extends AssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (AssociationCCReportSpecificOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.groupId = this.payload[0]; - this.maxNodes = this.payload[1]; - this.reportsToFollow = this.payload[2]; - this.nodeIds = [...this.payload.subarray(3)]; - } else { - this.groupId = options.groupId; - this.maxNodes = options.maxNodes; - this.nodeIds = options.nodeIds; - this.reportsToFollow = options.reportsToFollow; - } + this.groupId = options.groupId; + this.maxNodes = options.maxNodes; + this.nodeIds = options.nodeIds; + this.reportsToFollow = options.reportsToFollow; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): AssociationCCReport { + validatePayload(raw.payload.length >= 3); + const groupId = raw.payload[0]; + const maxNodes = raw.payload[1]; + const reportsToFollow = raw.payload[2]; + const nodeIds = [...raw.payload.subarray(3)]; + + return new AssociationCCReport({ + nodeId: ctx.sourceNodeId, + groupId, + maxNodes, + reportsToFollow, + nodeIds, + }); } public groupId: number; @@ -699,7 +716,7 @@ export class AssociationCCReport extends AssociationCC { } // @publicAPI -export interface AssociationCCGetOptions extends CCCommandOptions { +export interface AssociationCCGetOptions { groupId: number; } @@ -707,21 +724,26 @@ export interface AssociationCCGetOptions extends CCCommandOptions { @expectedCCResponse(AssociationCCReport) export class AssociationCCGet extends AssociationCC { public constructor( - options: CommandClassDeserializationOptions | AssociationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupId = this.payload[0]; - } else { - if (options.groupId < 1) { - throw new ZWaveError( - "The group id must be positive!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.groupId = options.groupId; + if (options.groupId < 1) { + throw new ZWaveError( + "The group id must be positive!", + ZWaveErrorCodes.Argument_Invalid, + ); } + this.groupId = options.groupId; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): AssociationCCGet { + validatePayload(raw.payload.length >= 1); + const groupId = raw.payload[0]; + + return new AssociationCCGet({ + nodeId: ctx.sourceNodeId, + groupId, + }); } public groupId: number; @@ -740,27 +762,31 @@ export class AssociationCCGet extends AssociationCC { } // @publicAPI -export interface AssociationCCSupportedGroupingsReportOptions - extends CCCommandOptions -{ +export interface AssociationCCSupportedGroupingsReportOptions { groupCount: number; } @CCCommand(AssociationCommand.SupportedGroupingsReport) export class AssociationCCSupportedGroupingsReport extends AssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | AssociationCCSupportedGroupingsReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupCount = this.payload[0]; - } else { - this.groupCount = options.groupCount; - } + this.groupCount = options.groupCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationCCSupportedGroupingsReport { + validatePayload(raw.payload.length >= 1); + const groupCount = raw.payload[0]; + + return new AssociationCCSupportedGroupingsReport({ + nodeId: ctx.sourceNodeId, + groupCount, + }); } @ccValue(AssociationCCValues.groupCount) @@ -791,18 +817,24 @@ export interface AssociationCCSpecificGroupReportOptions { @CCCommand(AssociationCommand.SpecificGroupReport) export class AssociationCCSpecificGroupReport extends AssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (AssociationCCSpecificGroupReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.group = this.payload[0]; - } else { - this.group = options.group; - } + this.group = options.group; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationCCSpecificGroupReport { + validatePayload(raw.payload.length >= 1); + const group = raw.payload[0]; + + return new AssociationCCSpecificGroupReport({ + nodeId: ctx.sourceNodeId, + group, + }); } public group: number; diff --git a/packages/cc/src/cc/AssociationGroupInfoCC.ts b/packages/cc/src/cc/AssociationGroupInfoCC.ts index 586443444817..0f421077551b 100644 --- a/packages/cc/src/cc/AssociationGroupInfoCC.ts +++ b/packages/cc/src/cc/AssociationGroupInfoCC.ts @@ -6,23 +6,26 @@ import { MessagePriority, type MessageRecord, type SupportsCC, + type WithAddress, encodeCCId, getCCName, parseCCId, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { cpp2js, getEnumMemberName, num2hex } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -111,7 +114,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { const cc = new AssociationGroupInfoCCNameGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, }); const response = await this.host.sendCommand< @@ -132,7 +135,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { const cc = new AssociationGroupInfoCCNameReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, name, }); @@ -150,7 +153,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { const cc = new AssociationGroupInfoCCInfoGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, refreshCache, }); @@ -174,7 +177,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { @validateArgs() public async reportGroupInfo( - options: AssociationGroupInfoCCInfoReportSpecificOptions, + options: AssociationGroupInfoCCInfoReportOptions, ): Promise { this.assertSupportsCommand( AssociationGroupInfoCommand, @@ -183,7 +186,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { const cc = new AssociationGroupInfoCCInfoReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -204,7 +207,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { const cc = new AssociationGroupInfoCCCommandListGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, allowCache, }); @@ -229,7 +232,7 @@ export class AssociationGroupInfoCCAPI extends PhysicalCCAPI { const cc = new AssociationGroupInfoCCCommandListReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, commands, }); @@ -463,9 +466,7 @@ profile: ${ } // @publicAPI -export interface AssociationGroupInfoCCNameReportOptions - extends CCCommandOptions -{ +export interface AssociationGroupInfoCCNameReportOptions { groupId: number; name: string; } @@ -473,26 +474,33 @@ export interface AssociationGroupInfoCCNameReportOptions @CCCommand(AssociationGroupInfoCommand.NameReport) export class AssociationGroupInfoCCNameReport extends AssociationGroupInfoCC { public constructor( - options: - | CommandClassDeserializationOptions - | AssociationGroupInfoCCNameReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.groupId = this.payload[0]; - const nameLength = this.payload[1]; - validatePayload(this.payload.length >= 2 + nameLength); - // The specs don't allow 0-terminated string, but some devices use them - // So we need to cut them off - this.name = cpp2js( - this.payload.subarray(2, 2 + nameLength).toString("utf8"), - ); - } else { - this.groupId = options.groupId; - this.name = options.name; - } + this.groupId = options.groupId; + this.name = options.name; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationGroupInfoCCNameReport { + validatePayload(raw.payload.length >= 2); + const groupId = raw.payload[0]; + const nameLength = raw.payload[1]; + validatePayload(raw.payload.length >= 2 + nameLength); + // The specs don't allow 0-terminated string, but some devices use them + // So we need to cut them off + const name = cpp2js( + raw.payload.subarray(2, 2 + nameLength).toString("utf8"), + ); + + return new AssociationGroupInfoCCNameReport({ + nodeId: ctx.sourceNodeId, + groupId, + name, + }); } public readonly groupId: number; @@ -532,7 +540,7 @@ export class AssociationGroupInfoCCNameReport extends AssociationGroupInfoCC { } // @publicAPI -export interface AssociationGroupInfoCCNameGetOptions extends CCCommandOptions { +export interface AssociationGroupInfoCCNameGetOptions { groupId: number; } @@ -540,17 +548,23 @@ export interface AssociationGroupInfoCCNameGetOptions extends CCCommandOptions { @expectedCCResponse(AssociationGroupInfoCCNameReport) export class AssociationGroupInfoCCNameGet extends AssociationGroupInfoCC { public constructor( - options: - | CommandClassDeserializationOptions - | AssociationGroupInfoCCNameGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupId = this.payload[0]; - } else { - this.groupId = options.groupId; - } + this.groupId = options.groupId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationGroupInfoCCNameGet { + validatePayload(raw.payload.length >= 1); + const groupId = raw.payload[0]; + + return new AssociationGroupInfoCCNameGet({ + nodeId: ctx.sourceNodeId, + groupId, + }); } public groupId: number; @@ -576,7 +590,7 @@ export interface AssociationGroupInfo { } // @publicAPI -export interface AssociationGroupInfoCCInfoReportSpecificOptions { +export interface AssociationGroupInfoCCInfoReportOptions { isListMode: boolean; hasDynamicInfo: boolean; groups: AssociationGroupInfo[]; @@ -585,40 +599,43 @@ export interface AssociationGroupInfoCCInfoReportSpecificOptions { @CCCommand(AssociationGroupInfoCommand.InfoReport) export class AssociationGroupInfoCCInfoReport extends AssociationGroupInfoCC { public constructor( - options: - | CommandClassDeserializationOptions - | ( - & AssociationGroupInfoCCInfoReportSpecificOptions - & CCCommandOptions - ), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.isListMode = !!(this.payload[0] & 0b1000_0000); - this.hasDynamicInfo = !!(this.payload[0] & 0b0100_0000); - - const groupCount = this.payload[0] & 0b0011_1111; - // each group requires 7 bytes of payload - validatePayload(this.payload.length >= 1 + groupCount * 7); - const _groups: AssociationGroupInfo[] = []; - for (let i = 0; i < groupCount; i++) { - const offset = 1 + i * 7; - // Parse the payload - const groupBytes = this.payload.subarray(offset, offset + 7); - const groupId = groupBytes[0]; - const mode = 0; // groupBytes[1]; - const profile = groupBytes.readUInt16BE(2); - const eventCode = 0; // groupBytes.readUInt16BE(5); - _groups.push({ groupId, mode, profile, eventCode }); - } - this.groups = _groups; - } else { - this.isListMode = options.isListMode; - this.hasDynamicInfo = options.hasDynamicInfo; - this.groups = options.groups; + this.isListMode = options.isListMode; + this.hasDynamicInfo = options.hasDynamicInfo; + this.groups = options.groups; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationGroupInfoCCInfoReport { + validatePayload(raw.payload.length >= 1); + const isListMode = !!(raw.payload[0] & 0b1000_0000); + const hasDynamicInfo = !!(raw.payload[0] & 0b0100_0000); + const groupCount = raw.payload[0] & 0b0011_1111; + // each group requires 7 bytes of payload + validatePayload(raw.payload.length >= 1 + groupCount * 7); + const groups: AssociationGroupInfo[] = []; + for (let i = 0; i < groupCount; i++) { + const offset = 1 + i * 7; + // Parse the payload + const groupBytes = raw.payload.subarray(offset, offset + 7); + const groupId = groupBytes[0]; + const mode = 0; // groupBytes[1]; + const profile = groupBytes.readUInt16BE(2); + const eventCode = 0; // groupBytes.readUInt16BE(5); + groups.push({ groupId, mode, profile, eventCode }); } + + return new AssociationGroupInfoCCInfoReport({ + nodeId: ctx.sourceNodeId, + isListMode, + hasDynamicInfo, + groups, + }); } public readonly isListMode: boolean; @@ -687,7 +704,6 @@ export class AssociationGroupInfoCCInfoReport extends AssociationGroupInfoCC { // @publicAPI export type AssociationGroupInfoCCInfoGetOptions = - & CCCommandOptions & { refreshCache: boolean; } @@ -704,24 +720,34 @@ export type AssociationGroupInfoCCInfoGetOptions = @expectedCCResponse(AssociationGroupInfoCCInfoReport) export class AssociationGroupInfoCCInfoGet extends AssociationGroupInfoCC { public constructor( - options: - | CommandClassDeserializationOptions - | AssociationGroupInfoCCInfoGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - const optionByte = this.payload[0]; - this.refreshCache = !!(optionByte & 0b1000_0000); - this.listMode = !!(optionByte & 0b0100_0000); - if (!this.listMode) { - this.groupId = this.payload[1]; - } - } else { - this.refreshCache = options.refreshCache; - if ("listMode" in options) this.listMode = options.listMode; - if ("groupId" in options) this.groupId = options.groupId; + this.refreshCache = options.refreshCache; + if ("listMode" in options) this.listMode = options.listMode; + if ("groupId" in options) this.groupId = options.groupId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationGroupInfoCCInfoGet { + validatePayload(raw.payload.length >= 2); + const optionByte = raw.payload[0]; + const refreshCache = !!(optionByte & 0b1000_0000); + const listMode: boolean | undefined = !!(optionByte & 0b0100_0000); + let groupId: number | undefined; + + if (!listMode) { + groupId = raw.payload[1]; } + + return new AssociationGroupInfoCCInfoGet({ + nodeId: ctx.sourceNodeId, + refreshCache, + listMode, + groupId, + }); } public refreshCache: boolean; @@ -756,9 +782,7 @@ export class AssociationGroupInfoCCInfoGet extends AssociationGroupInfoCC { } // @publicAPI -export interface AssociationGroupInfoCCCommandListReportOptions - extends CCCommandOptions -{ +export interface AssociationGroupInfoCCCommandListReportOptions { groupId: number; commands: ReadonlyMap; } @@ -768,34 +792,39 @@ export class AssociationGroupInfoCCCommandListReport extends AssociationGroupInfoCC { public constructor( - options: - | CommandClassDeserializationOptions - | AssociationGroupInfoCCCommandListReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.groupId = this.payload[0]; - const listLength = this.payload[1]; - validatePayload(this.payload.length >= 2 + listLength); - const listBytes = this.payload.subarray(2, 2 + listLength); - // Parse all CC ids and commands - let offset = 0; - const commands = new Map(); - while (offset < listLength) { - const { ccId, bytesRead } = parseCCId(listBytes, offset); - const command = listBytes[offset + bytesRead]; - if (!commands.has(ccId)) commands.set(ccId, []); - commands.get(ccId)!.push(command); - offset += bytesRead + 1; - } - - this.commands = commands; - } else { - this.groupId = options.groupId; - this.commands = options.commands; + this.groupId = options.groupId; + this.commands = options.commands; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationGroupInfoCCCommandListReport { + validatePayload(raw.payload.length >= 2); + const groupId = raw.payload[0]; + const listLength = raw.payload[1]; + validatePayload(raw.payload.length >= 2 + listLength); + const listBytes = raw.payload.subarray(2, 2 + listLength); + // Parse all CC ids and commands + let offset = 0; + const commands = new Map(); + while (offset < listLength) { + const { ccId, bytesRead } = parseCCId(listBytes, offset); + const command = listBytes[offset + bytesRead]; + if (!commands.has(ccId)) commands.set(ccId, []); + commands.get(ccId)!.push(command); + offset += bytesRead + 1; } + + return new AssociationGroupInfoCCCommandListReport({ + nodeId: ctx.sourceNodeId, + groupId, + commands, + }); } public readonly groupId: number; @@ -847,9 +876,7 @@ export class AssociationGroupInfoCCCommandListReport } // @publicAPI -export interface AssociationGroupInfoCCCommandListGetOptions - extends CCCommandOptions -{ +export interface AssociationGroupInfoCCCommandListGetOptions { allowCache: boolean; groupId: number; } @@ -860,19 +887,26 @@ export class AssociationGroupInfoCCCommandListGet extends AssociationGroupInfoCC { public constructor( - options: - | CommandClassDeserializationOptions - | AssociationGroupInfoCCCommandListGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.allowCache = !!(this.payload[0] & 0b1000_0000); - this.groupId = this.payload[1]; - } else { - this.allowCache = options.allowCache; - this.groupId = options.groupId; - } + this.allowCache = options.allowCache; + this.groupId = options.groupId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): AssociationGroupInfoCCCommandListGet { + validatePayload(raw.payload.length >= 2); + const allowCache = !!(raw.payload[0] & 0b1000_0000); + const groupId = raw.payload[1]; + + return new AssociationGroupInfoCCCommandListGet({ + nodeId: ctx.sourceNodeId, + allowCache, + groupId, + }); } public allowCache: boolean; diff --git a/packages/cc/src/cc/BarrierOperatorCC.ts b/packages/cc/src/cc/BarrierOperatorCC.ts index 0a15deaa326d..e61fcaabe740 100644 --- a/packages/cc/src/cc/BarrierOperatorCC.ts +++ b/packages/cc/src/cc/BarrierOperatorCC.ts @@ -7,6 +7,7 @@ import { type SupervisionResult, UNKNOWN_STATE, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, @@ -14,7 +15,11 @@ import { parseBitMask, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, isEnumMember, @@ -36,13 +41,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -147,7 +150,7 @@ export class BarrierOperatorCCAPI extends CCAPI { const cc = new BarrierOperatorCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< BarrierOperatorCCReport @@ -171,7 +174,7 @@ export class BarrierOperatorCCAPI extends CCAPI { const cc = new BarrierOperatorCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, targetState, }); return this.host.sendCommand(cc, this.commandOptions); @@ -188,7 +191,7 @@ export class BarrierOperatorCCAPI extends CCAPI { const cc = new BarrierOperatorCCSignalingCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< BarrierOperatorCCSignalingCapabilitiesReport @@ -210,7 +213,7 @@ export class BarrierOperatorCCAPI extends CCAPI { const cc = new BarrierOperatorCCEventSignalingGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, subsystemType, }); const response = await this.host.sendCommand< @@ -234,7 +237,7 @@ export class BarrierOperatorCCAPI extends CCAPI { const cc = new BarrierOperatorCCEventSignalingSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, subsystemType, subsystemState, }); @@ -538,7 +541,7 @@ export class BarrierOperatorCC extends CommandClass { } // @publicAPI -export interface BarrierOperatorCCSetOptions extends CCCommandOptions { +export interface BarrierOperatorCCSetOptions { targetState: BarrierState.Open | BarrierState.Closed; } @@ -546,19 +549,24 @@ export interface BarrierOperatorCCSetOptions extends CCCommandOptions { @useSupervision() export class BarrierOperatorCCSet extends BarrierOperatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | BarrierOperatorCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.targetState = options.targetState; - } + this.targetState = options.targetState; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): BarrierOperatorCCSet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new BarrierOperatorCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public targetState: BarrierState.Open | BarrierState.Closed; @@ -576,41 +584,63 @@ export class BarrierOperatorCCSet extends BarrierOperatorCC { } } +// @publicAPI +export interface BarrierOperatorCCReportOptions { + position: MaybeUnknown; + currentState: MaybeUnknown; +} + @CCCommand(BarrierOperatorCommand.Report) export class BarrierOperatorCCReport extends BarrierOperatorCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); + // TODO: Check implementation: + this.position = options.position; + this.currentState = options.currentState; + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BarrierOperatorCCReport { + validatePayload(raw.payload.length >= 1); // The payload byte encodes information about the state and position in a single value - const payloadValue = this.payload[0]; + const payloadValue = raw.payload[0]; + let position: MaybeUnknown; if (payloadValue <= 99) { // known position - this.position = payloadValue; + position = payloadValue; } else if (payloadValue === 255) { // known position, fully opened - this.position = 100; + position = 100; } else { // unknown position - this.position = UNKNOWN_STATE; + position = UNKNOWN_STATE; } + let currentState: MaybeUnknown; if ( payloadValue === BarrierState.Closed || payloadValue >= BarrierState.Closing ) { // predefined states - this.currentState = payloadValue; + currentState = payloadValue; } else if (payloadValue > 0 && payloadValue <= 99) { // stopped at exact position - this.currentState = BarrierState.Stopped; + currentState = BarrierState.Stopped; } else { // invalid value, assume unknown - this.currentState = UNKNOWN_STATE; + currentState = UNKNOWN_STATE; } + + return new BarrierOperatorCCReport({ + nodeId: ctx.sourceNodeId, + position, + currentState, + }); } @ccValue(BarrierOperatorCCValues.currentState) @@ -636,19 +666,39 @@ export class BarrierOperatorCCReport extends BarrierOperatorCC { @expectedCCResponse(BarrierOperatorCCReport) export class BarrierOperatorCCGet extends BarrierOperatorCC {} +// @publicAPI +export interface BarrierOperatorCCSignalingCapabilitiesReportOptions { + supportedSubsystemTypes: SubsystemType[]; +} + @CCCommand(BarrierOperatorCommand.SignalingCapabilitiesReport) export class BarrierOperatorCCSignalingCapabilitiesReport extends BarrierOperatorCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress< + BarrierOperatorCCSignalingCapabilitiesReportOptions + >, ) { super(options); - this.supportedSubsystemTypes = parseBitMask( - this.payload, + // TODO: Check implementation: + this.supportedSubsystemTypes = options.supportedSubsystemTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BarrierOperatorCCSignalingCapabilitiesReport { + const supportedSubsystemTypes: SubsystemType[] = parseBitMask( + raw.payload, SubsystemType.Audible, ); + + return new BarrierOperatorCCSignalingCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + supportedSubsystemTypes, + }); } @ccValue(BarrierOperatorCCValues.supportedSubsystemTypes) @@ -673,9 +723,7 @@ export class BarrierOperatorCCSignalingCapabilitiesGet {} // @publicAPI -export interface BarrierOperatorCCEventSignalingSetOptions - extends CCCommandOptions -{ +export interface BarrierOperatorCCEventSignalingSetOptions { subsystemType: SubsystemType; subsystemState: SubsystemState; } @@ -684,22 +732,28 @@ export interface BarrierOperatorCCEventSignalingSetOptions @useSupervision() export class BarrierOperatorCCEventSignalingSet extends BarrierOperatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | BarrierOperatorCCEventSignalingSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.subsystemType = options.subsystemType; - this.subsystemState = options.subsystemState; - } + this.subsystemType = options.subsystemType; + this.subsystemState = options.subsystemState; } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): BarrierOperatorCCEventSignalingSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new BarrierOperatorCCEventSignalingSet({ + // nodeId: ctx.sourceNodeId, + // }); + } + public subsystemType: SubsystemType; public subsystemState: SubsystemState; @@ -725,16 +779,37 @@ export class BarrierOperatorCCEventSignalingSet extends BarrierOperatorCC { } } +// @publicAPI +export interface BarrierOperatorCCEventSignalingReportOptions { + subsystemType: SubsystemType; + subsystemState: SubsystemState; +} + @CCCommand(BarrierOperatorCommand.EventSignalingReport) export class BarrierOperatorCCEventSignalingReport extends BarrierOperatorCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.subsystemType = this.payload[0]; - this.subsystemState = this.payload[1]; + // TODO: Check implementation: + this.subsystemType = options.subsystemType; + this.subsystemState = options.subsystemState; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BarrierOperatorCCEventSignalingReport { + validatePayload(raw.payload.length >= 2); + const subsystemType: SubsystemType = raw.payload[0]; + const subsystemState: SubsystemState = raw.payload[1]; + + return new BarrierOperatorCCEventSignalingReport({ + nodeId: ctx.sourceNodeId, + subsystemType, + subsystemState, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -771,9 +846,7 @@ export class BarrierOperatorCCEventSignalingReport extends BarrierOperatorCC { } // @publicAPI -export interface BarrierOperatorCCEventSignalingGetOptions - extends CCCommandOptions -{ +export interface BarrierOperatorCCEventSignalingGetOptions { subsystemType: SubsystemType; } @@ -781,20 +854,25 @@ export interface BarrierOperatorCCEventSignalingGetOptions @expectedCCResponse(BarrierOperatorCCEventSignalingReport) export class BarrierOperatorCCEventSignalingGet extends BarrierOperatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | BarrierOperatorCCEventSignalingGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.subsystemType = options.subsystemType; - } + this.subsystemType = options.subsystemType; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): BarrierOperatorCCEventSignalingGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new BarrierOperatorCCEventSignalingGet({ + // nodeId: ctx.sourceNodeId, + // }); } public subsystemType: SubsystemType; diff --git a/packages/cc/src/cc/BasicCC.ts b/packages/cc/src/cc/BasicCC.ts index b7746565e656..f25235ed58ba 100644 --- a/packages/cc/src/cc/BasicCC.ts +++ b/packages/cc/src/cc/BasicCC.ts @@ -14,18 +14,20 @@ import { type SupportsCC, type ValueID, ValueMetadata, + type WithAddress, maybeUnknownToString, parseMaybeNumber, validatePayload, } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetNode, GetSupportedCCVersion, GetValueDB, } from "@zwave-js/host/safe"; -import { type AllOrNone, pick } from "@zwave-js/shared/safe"; +import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, @@ -39,14 +41,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -226,7 +226,7 @@ export class BasicCCAPI extends CCAPI { const cc = new BasicCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -249,7 +249,7 @@ export class BasicCCAPI extends CCAPI { const cc = new BasicCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, targetValue, }); return this.host.sendCommand(cc, this.commandOptions); @@ -370,7 +370,7 @@ remaining duration: ${basicResponse.duration?.toString() ?? "undefined"}`; } // @publicAPI -export interface BasicCCSetOptions extends CCCommandOptions { +export interface BasicCCSetOptions { targetValue: number; } @@ -378,15 +378,20 @@ export interface BasicCCSetOptions extends CCCommandOptions { @useSupervision() export class BasicCCSet extends BasicCC { public constructor( - options: CommandClassDeserializationOptions | BasicCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.targetValue = this.payload[0]; - } else { - this.targetValue = options.targetValue; - } + this.targetValue = options.targetValue; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): BasicCCSet { + validatePayload(raw.payload.length >= 1); + const targetValue = raw.payload[0]; + + return new BasicCCSet({ + nodeId: ctx.sourceNodeId, + targetValue, + }); } public targetValue: number; @@ -405,50 +410,52 @@ export class BasicCCSet extends BasicCC { } // @publicAPI -export type BasicCCReportOptions = - & CCCommandOptions - & { - currentValue: number; - } - & AllOrNone<{ - targetValue: number; - duration: Duration; - }>; +export interface BasicCCReportOptions { + currentValue?: MaybeUnknown; + targetValue?: MaybeUnknown; + duration?: Duration; +} @CCCommand(BasicCommand.Report) export class BasicCCReport extends BasicCC { // @noCCValues See comment in the constructor public constructor( - options: CommandClassDeserializationOptions | BasicCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this._currentValue = - // 0xff is a legacy value for 100% (99) - this.payload[0] === 0xff - ? 99 - : parseMaybeNumber(this.payload[0]); - - if (this.payload.length >= 3) { - this.targetValue = parseMaybeNumber(this.payload[1]); - this.duration = Duration.parseReport(this.payload[2]); - } - } else { - this._currentValue = options.currentValue; - if ("targetValue" in options) { - this.targetValue = options.targetValue; - this.duration = options.duration; - } + this.currentValue = options.currentValue; + this.targetValue = options.targetValue; + this.duration = options.duration; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): BasicCCReport { + validatePayload(raw.payload.length >= 1); + const currentValue: MaybeUnknown | undefined = + // 0xff is a legacy value for 100% (99) + raw.payload[0] === 0xff + ? 99 + : parseMaybeNumber(raw.payload[0]); + validatePayload(currentValue !== undefined); + + let targetValue: MaybeUnknown | undefined; + let duration: Duration | undefined; + + if (raw.payload.length >= 3) { + targetValue = parseMaybeNumber(raw.payload[1]); + duration = Duration.parseReport(raw.payload[2]); } + + return new BasicCCReport({ + nodeId: ctx.sourceNodeId, + currentValue, + targetValue, + duration, + }); } - private _currentValue: MaybeUnknown | undefined; @ccValue(BasicCCValues.currentValue) - public get currentValue(): MaybeUnknown | undefined { - return this._currentValue; - } + public currentValue: MaybeUnknown | undefined; @ccValue(BasicCCValues.targetValue) public readonly targetValue: MaybeUnknown | undefined; diff --git a/packages/cc/src/cc/BatteryCC.ts b/packages/cc/src/cc/BatteryCC.ts index b4bb1a462cfa..ecae5e26e410 100644 --- a/packages/cc/src/cc/BatteryCC.ts +++ b/packages/cc/src/cc/BatteryCC.ts @@ -1,4 +1,4 @@ -import { timespan } from "@zwave-js/core"; +import { type WithAddress, timespan } from "@zwave-js/core"; import type { ControlsCC, EndpointId, @@ -20,6 +20,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetNode, GetSupportedCCVersion, @@ -34,13 +35,11 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -240,7 +239,7 @@ export class BatteryCCAPI extends PhysicalCCAPI { const cc = new BatteryCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -268,7 +267,7 @@ export class BatteryCCAPI extends PhysicalCCAPI { const cc = new BatteryCCHealthGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -401,7 +400,6 @@ temperature: ${batteryHealth.temperature} °C`; // @publicAPI export type BatteryCCReportOptions = - & CCCommandOptions & ( | { isLow?: false; @@ -430,47 +428,72 @@ export type BatteryCCReportOptions = @CCCommand(BatteryCommand.Report) export class BatteryCCReport extends BatteryCC { public constructor( - options: CommandClassDeserializationOptions | BatteryCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.level = this.payload[0]; - if (this.level === 0xff) { - this.level = 0; - this.isLow = true; - } else { - this.isLow = false; - } + this.level = options.isLow ? 0 : options.level; + this.isLow = !!options.isLow; + this.chargingStatus = options.chargingStatus; + this.rechargeable = options.rechargeable; + this.backup = options.backup; + this.overheating = options.overheating; + this.lowFluid = options.lowFluid; + this.rechargeOrReplace = options.rechargeOrReplace; + this.disconnected = options.disconnected; + this.lowTemperatureStatus = options.lowTemperatureStatus; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): BatteryCCReport { + let ccOptions: BatteryCCReportOptions; - if (this.payload.length >= 3) { - // Starting with V2 - this.chargingStatus = this.payload[1] >>> 6; - this.rechargeable = !!(this.payload[1] & 0b0010_0000); - this.backup = !!(this.payload[1] & 0b0001_0000); - this.overheating = !!(this.payload[1] & 0b1000); - this.lowFluid = !!(this.payload[1] & 0b0100); - this.rechargeOrReplace = !!(this.payload[1] & 0b10) + validatePayload(raw.payload.length >= 1); + const level = raw.payload[0]; + + if (level === 0xff) { + ccOptions = { + isLow: true, + }; + } else { + ccOptions = { + isLow: false, + level, + }; + } + + if (raw.payload.length >= 3) { + // Starting with V2 + const chargingStatus: BatteryChargingStatus = raw.payload[1] >>> 6; + const rechargeable = !!(raw.payload[1] & 0b0010_0000); + const backup = !!(raw.payload[1] & 0b0001_0000); + const overheating = !!(raw.payload[1] & 0b1000); + const lowFluid = !!(raw.payload[1] & 0b0100); + const rechargeOrReplace: BatteryReplacementStatus = + !!(raw.payload[1] & 0b10) ? BatteryReplacementStatus.Now - : !!(this.payload[1] & 0b1) + : !!(raw.payload[1] & 0b1) ? BatteryReplacementStatus.Soon : BatteryReplacementStatus.No; - this.lowTemperatureStatus = !!(this.payload[2] & 0b10); - this.disconnected = !!(this.payload[2] & 0b1); - } - } else { - this.level = options.isLow ? 0 : options.level; - this.isLow = !!options.isLow; - this.chargingStatus = options.chargingStatus; - this.rechargeable = options.rechargeable; - this.backup = options.backup; - this.overheating = options.overheating; - this.lowFluid = options.lowFluid; - this.rechargeOrReplace = options.rechargeOrReplace; - this.disconnected = options.disconnected; - this.lowTemperatureStatus = options.lowTemperatureStatus; + const lowTemperatureStatus = !!(raw.payload[2] & 0b10); + const disconnected = !!(raw.payload[2] & 0b1); + + ccOptions = { + ...ccOptions, + chargingStatus, + rechargeable, + backup, + overheating, + lowFluid, + rechargeOrReplace, + lowTemperatureStatus, + disconnected, + }; } + + return new BatteryCCReport({ + nodeId: ctx.sourceNodeId, + ...ccOptions, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -612,25 +635,48 @@ export class BatteryCCReport extends BatteryCC { @expectedCCResponse(BatteryCCReport) export class BatteryCCGet extends BatteryCC {} +// @publicAPI +export interface BatteryCCHealthReportOptions { + maximumCapacity?: number; + temperature?: number; + temperatureScale?: number; +} + @CCCommand(BatteryCommand.HealthReport) export class BatteryCCHealthReport extends BatteryCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); + // TODO: Check implementation: + this.maximumCapacity = options.maximumCapacity; + this.temperature = options.temperature; + this.temperatureScale = options.temperatureScale; + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BatteryCCHealthReport { + validatePayload(raw.payload.length >= 2); // Parse maximum capacity. 0xff means unknown - this.maximumCapacity = this.payload[0]; - if (this.maximumCapacity === 0xff) this.maximumCapacity = undefined; - - const { value: temperature, scale } = parseFloatWithScale( - this.payload.subarray(1), + let maximumCapacity: number | undefined = raw.payload[0]; + if (maximumCapacity === 0xff) maximumCapacity = undefined; + const { + value: temperature, + scale: temperatureScale, + } = parseFloatWithScale( + raw.payload.subarray(1), true, // The temperature field may be omitted ); - this.temperature = temperature; - this.temperatureScale = scale; + + return new BatteryCCHealthReport({ + nodeId: ctx.sourceNodeId, + maximumCapacity, + temperature, + temperatureScale, + }); } public persistValues(ctx: PersistValuesContext): boolean { diff --git a/packages/cc/src/cc/BinarySensorCC.ts b/packages/cc/src/cc/BinarySensorCC.ts index 49afdf1ce7df..e1f915135c81 100644 --- a/packages/cc/src/cc/BinarySensorCC.ts +++ b/packages/cc/src/cc/BinarySensorCC.ts @@ -6,11 +6,16 @@ import { MessagePriority, type SupervisionResult, ValueMetadata, + type WithAddress, encodeBitMask, parseBitMask, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, isEnumMember } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -21,13 +26,11 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -112,7 +115,7 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { const cc = new BinarySensorCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sensorType, }); const response = await this.host.sendCommand( @@ -135,7 +138,7 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { const cc = new BinarySensorCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, value, type: sensorType, }); @@ -152,7 +155,7 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { const cc = new BinarySensorCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< BinarySensorCCSupportedReport @@ -175,7 +178,7 @@ export class BinarySensorCCAPI extends PhysicalCCAPI { const cc = new BinarySensorCCSupportedReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, supportedSensorTypes: supported, }); return this.host.sendCommand(cc, this.commandOptions); @@ -336,7 +339,7 @@ export class BinarySensorCC extends CommandClass { } // @publicAPI -export interface BinarySensorCCReportOptions extends CCCommandOptions { +export interface BinarySensorCCReportOptions { type?: BinarySensorType; value: boolean; } @@ -344,23 +347,31 @@ export interface BinarySensorCCReportOptions extends CCCommandOptions { @CCCommand(BinarySensorCommand.Report) export class BinarySensorCCReport extends BinarySensorCC { public constructor( - options: - | BinarySensorCCReportOptions - | CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.value = this.payload[0] === 0xff; - this.type = BinarySensorType.Any; - if (this.payload.length >= 2) { - this.type = this.payload[1]; - } - } else { - this.type = options.type ?? BinarySensorType.Any; - this.value = options.value; + this.type = options.type ?? BinarySensorType.Any; + this.value = options.value; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BinarySensorCCReport { + validatePayload(raw.payload.length >= 1); + const value = raw.payload[0] === 0xff; + let type: BinarySensorType = BinarySensorType.Any; + + if (raw.payload.length >= 2) { + type = raw.payload[1]; } + + return new BinarySensorCCReport({ + nodeId: ctx.sourceNodeId, + value, + type, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -419,7 +430,7 @@ function testResponseForBinarySensorGet( } // @publicAPI -export interface BinarySensorCCGetOptions extends CCCommandOptions { +export interface BinarySensorCCGetOptions { sensorType?: BinarySensorType; } @@ -427,16 +438,23 @@ export interface BinarySensorCCGetOptions extends CCCommandOptions { @expectedCCResponse(BinarySensorCCReport, testResponseForBinarySensorGet) export class BinarySensorCCGet extends BinarySensorCC { public constructor( - options: CommandClassDeserializationOptions | BinarySensorCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - if (this.payload.length >= 1) { - this.sensorType = this.payload[0]; - } - } else { - this.sensorType = options.sensorType; + this.sensorType = options.sensorType; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): BinarySensorCCGet { + let sensorType: BinarySensorType | undefined; + + if (raw.payload.length >= 1) { + sensorType = raw.payload[0]; } + + return new BinarySensorCCGet({ + nodeId: ctx.sourceNodeId, + sensorType, + }); } public sensorType: BinarySensorType | undefined; @@ -467,22 +485,32 @@ export interface BinarySensorCCSupportedReportOptions { @CCCommand(BinarySensorCommand.SupportedReport) export class BinarySensorCCSupportedReport extends BinarySensorCC { public constructor( - options: - | CommandClassDeserializationOptions - | (BinarySensorCCSupportedReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - // The enumeration starts at 1, but the first (reserved) bit is included - // in the report - this.supportedSensorTypes = parseBitMask(this.payload, 0).filter( + this.supportedSensorTypes = options.supportedSensorTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BinarySensorCCSupportedReport { + validatePayload(raw.payload.length >= 1); + // The enumeration starts at 1, but the first (reserved) bit is included + // in the report + const supportedSensorTypes: BinarySensorType[] = parseBitMask( + raw.payload, + 0, + ) + .filter( (t) => t !== 0, ); - } else { - this.supportedSensorTypes = options.supportedSensorTypes; - } + + return new BinarySensorCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedSensorTypes, + }); } @ccValue(BinarySensorCCValues.supportedSensorTypes) diff --git a/packages/cc/src/cc/BinarySwitchCC.ts b/packages/cc/src/cc/BinarySwitchCC.ts index 9442d2938386..64be97c29aa1 100644 --- a/packages/cc/src/cc/BinarySwitchCC.ts +++ b/packages/cc/src/cc/BinarySwitchCC.ts @@ -9,13 +9,17 @@ import { type SupervisionResult, UNKNOWN_STATE, ValueMetadata, + type WithAddress, encodeMaybeBoolean, maybeUnknownToString, parseMaybeBoolean, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; -import type { AllOrNone } from "@zwave-js/shared"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, @@ -29,13 +33,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -101,7 +103,7 @@ export class BinarySwitchCCAPI extends CCAPI { const cc = new BinarySwitchCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -134,7 +136,7 @@ export class BinarySwitchCCAPI extends CCAPI { const cc = new BinarySwitchCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, targetValue, duration, }); @@ -302,7 +304,7 @@ remaining duration: ${resp.duration?.toString() ?? "undefined"}`; } // @publicAPI -export interface BinarySwitchCCSetOptions extends CCCommandOptions { +export interface BinarySwitchCCSetOptions { targetValue: boolean; duration?: Duration | string; } @@ -311,19 +313,27 @@ export interface BinarySwitchCCSetOptions extends CCCommandOptions { @useSupervision() export class BinarySwitchCCSet extends BinarySwitchCC { public constructor( - options: CommandClassDeserializationOptions | BinarySwitchCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.targetValue = !!this.payload[0]; - if (this.payload.length >= 2) { - this.duration = Duration.parseSet(this.payload[1]); - } - } else { - this.targetValue = options.targetValue; - this.duration = Duration.from(options.duration); + this.targetValue = options.targetValue; + this.duration = Duration.from(options.duration); + } + + public static from(raw: CCRaw, ctx: CCParsingContext): BinarySwitchCCSet { + validatePayload(raw.payload.length >= 1); + const targetValue = !!raw.payload[0]; + let duration: Duration | undefined; + + if (raw.payload.length >= 2) { + duration = Duration.parseSet(raw.payload[1]); } + + return new BinarySwitchCCSet({ + nodeId: ctx.sourceNodeId, + targetValue, + duration, + }); } public targetValue: boolean; @@ -363,38 +373,47 @@ export class BinarySwitchCCSet extends BinarySwitchCC { } // @publicAPI -export type BinarySwitchCCReportOptions = - & CCCommandOptions - & { - currentValue: MaybeUnknown; - } - & AllOrNone<{ - targetValue: MaybeUnknown; - duration: Duration | string; - }>; +export interface BinarySwitchCCReportOptions { + currentValue?: MaybeUnknown; + targetValue?: MaybeUnknown; + duration?: Duration | string; +} @CCCommand(BinarySwitchCommand.Report) export class BinarySwitchCCReport extends BinarySwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | BinarySwitchCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.currentValue = parseMaybeBoolean(this.payload[0]); + this.currentValue = options.currentValue; + this.targetValue = options.targetValue; + this.duration = Duration.from(options.duration); + } - if (this.payload.length >= 3) { - this.targetValue = parseMaybeBoolean(this.payload[1]); - this.duration = Duration.parseReport(this.payload[2]); - } - } else { - this.currentValue = options.currentValue; - this.targetValue = options.targetValue; - this.duration = Duration.from(options.duration); + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): BinarySwitchCCReport { + validatePayload(raw.payload.length >= 1); + const currentValue: MaybeUnknown | undefined = + parseMaybeBoolean( + raw.payload[0], + ); + let targetValue: MaybeUnknown | undefined; + let duration: Duration | undefined; + + if (raw.payload.length >= 3) { + targetValue = parseMaybeBoolean(raw.payload[1]); + duration = Duration.parseReport(raw.payload[2]); } + + return new BinarySwitchCCReport({ + nodeId: ctx.sourceNodeId, + currentValue, + targetValue, + duration, + }); } @ccValue(BinarySwitchCCValues.currentValue) diff --git a/packages/cc/src/cc/CRC16CC.ts b/packages/cc/src/cc/CRC16CC.ts index fe0a30c8437e..d8368a895400 100644 --- a/packages/cc/src/cc/CRC16CC.ts +++ b/packages/cc/src/cc/CRC16CC.ts @@ -4,16 +4,16 @@ import { EncapsulationFlags, type MaybeNotKnown, type MessageOrCCLogEntry, + type WithAddress, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { CCAPI } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -21,8 +21,14 @@ import { expectedCCResponse, implementedVersion, } from "../lib/CommandClassDecorators"; + import { CRC16Command } from "../lib/_Types"; +const headerBuffer = Buffer.from([ + CommandClasses["CRC-16 Encapsulation"], + CRC16Command.CommandEncapsulation, +]); + // @noSetValueAPI // @noInterview This CC only has a single encapsulation command @@ -48,7 +54,7 @@ export class CRC16CCAPI extends CCAPI { const cc = new CRC16CCCommandEncapsulation({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, encapsulated: encapsulatedCC, }); await this.host.sendCommand(cc, this.commandOptions); @@ -87,7 +93,7 @@ export class CRC16CC extends CommandClass { } // @publicAPI -export interface CRC16CCCommandEncapsulationOptions extends CCCommandOptions { +export interface CRC16CCCommandEncapsulationOptions { encapsulated: CommandClass; } @@ -106,39 +112,37 @@ function getCCResponseForCommandEncapsulation( ) export class CRC16CCCommandEncapsulation extends CRC16CC { public constructor( - options: - | CommandClassDeserializationOptions - | CRC16CCCommandEncapsulationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - - const ccBuffer = this.payload.subarray(0, -2); - - // Verify the CRC - let expectedCRC = CRC16_CCITT(this.headerBuffer); - expectedCRC = CRC16_CCITT(ccBuffer, expectedCRC); - const actualCRC = this.payload.readUInt16BE( - this.payload.length - 2, - ); - validatePayload(expectedCRC === actualCRC); - - this.encapsulated = CommandClass.from({ - data: ccBuffer, - fromEncapsulation: true, - encapCC: this, - origin: options.origin, - context: options.context, - }); - } else { - this.encapsulated = options.encapsulated; - options.encapsulated.encapsulatingCC = this as any; - } + this.encapsulated = options.encapsulated; + this.encapsulated.encapsulatingCC = this as any; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): CRC16CCCommandEncapsulation { + validatePayload(raw.payload.length >= 3); + + const ccBuffer = raw.payload.subarray(0, -2); + + // Verify the CRC + let expectedCRC = CRC16_CCITT(headerBuffer); + expectedCRC = CRC16_CCITT(ccBuffer, expectedCRC); + const actualCRC = raw.payload.readUInt16BE( + raw.payload.length - 2, + ); + validatePayload(expectedCRC === actualCRC); + + const encapsulated = CommandClass.parse(ccBuffer, ctx); + return new CRC16CCCommandEncapsulation({ + nodeId: ctx.sourceNodeId, + encapsulated, + }); } public encapsulated: CommandClass; - private readonly headerBuffer = Buffer.from([this.ccId, this.ccCommand]); public serialize(ctx: CCEncodingContext): Buffer { const commandBuffer = this.encapsulated.serialize(ctx); @@ -147,7 +151,7 @@ export class CRC16CCCommandEncapsulation extends CRC16CC { // Compute and save the CRC16 in the payload // The CC header is included in the CRC computation - let crc = CRC16_CCITT(this.headerBuffer); + let crc = CRC16_CCITT(headerBuffer); crc = CRC16_CCITT(commandBuffer, crc); this.payload.writeUInt16BE(crc, this.payload.length - 2); diff --git a/packages/cc/src/cc/CentralSceneCC.ts b/packages/cc/src/cc/CentralSceneCC.ts index 452b9f4da212..df16d120bbad 100644 --- a/packages/cc/src/cc/CentralSceneCC.ts +++ b/packages/cc/src/cc/CentralSceneCC.ts @@ -6,6 +6,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, @@ -14,7 +15,11 @@ import { parseBitMask, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { padStart } from "alcalzone-shared/strings"; @@ -28,12 +33,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -113,7 +116,7 @@ export class CentralSceneCCAPI extends CCAPI { const cc = new CentralSceneCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< CentralSceneCCSupportedReport @@ -139,7 +142,7 @@ export class CentralSceneCCAPI extends CCAPI { const cc = new CentralSceneCCConfigurationGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< CentralSceneCCConfigurationReport @@ -163,7 +166,7 @@ export class CentralSceneCCAPI extends CCAPI { const cc = new CentralSceneCCConfigurationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, slowRefresh, }); return this.host.sendCommand(cc, this.commandOptions); @@ -296,22 +299,50 @@ supports slow refresh: ${ccSupported.supportsSlowRefresh}`; } } +// @publicAPI +export interface CentralSceneCCNotificationOptions { + sequenceNumber: number; + keyAttribute: CentralSceneKeys; + sceneNumber: number; + slowRefresh?: boolean; +} + @CCCommand(CentralSceneCommand.Notification) export class CentralSceneCCNotification extends CentralSceneCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 3); - this.sequenceNumber = this.payload[0]; - this.keyAttribute = this.payload[1] & 0b111; - this.sceneNumber = this.payload[2]; - if (this.keyAttribute === CentralSceneKeys.KeyHeldDown) { + // TODO: Check implementation: + this.sequenceNumber = options.sequenceNumber; + this.keyAttribute = options.keyAttribute; + this.sceneNumber = options.sceneNumber; + this.slowRefresh = options.slowRefresh; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): CentralSceneCCNotification { + validatePayload(raw.payload.length >= 3); + const sequenceNumber = raw.payload[0]; + const keyAttribute: CentralSceneKeys = raw.payload[1] & 0b111; + const sceneNumber = raw.payload[2]; + let slowRefresh: boolean | undefined; + if (keyAttribute === CentralSceneKeys.KeyHeldDown) { // A receiving node MUST ignore this field if the command is not // carrying the Key Held Down key attribute. - this.slowRefresh = !!(this.payload[1] & 0b1000_0000); + slowRefresh = !!(raw.payload[1] & 0b1000_0000); } + + return new CentralSceneCCNotification({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + keyAttribute, + sceneNumber, + slowRefresh, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -352,40 +383,75 @@ export class CentralSceneCCNotification extends CentralSceneCC { } } +// @publicAPI +export interface CentralSceneCCSupportedReportOptions { + sceneCount: number; + supportsSlowRefresh: MaybeNotKnown; + supportedKeyAttributes: Record; +} + @CCCommand(CentralSceneCommand.SupportedReport) export class CentralSceneCCSupportedReport extends CentralSceneCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.sceneCount = this.payload[0]; - this.supportsSlowRefresh = !!(this.payload[1] & 0b1000_0000); - const bitMaskBytes = (this.payload[1] & 0b110) >>> 1; - const identicalKeyAttributes = !!(this.payload[1] & 0b1); - const numEntries = identicalKeyAttributes ? 1 : this.sceneCount; + // TODO: Check implementation: + this.sceneCount = options.sceneCount; + this.supportsSlowRefresh = options.supportsSlowRefresh; + for ( + const [scene, keys] of Object.entries( + options.supportedKeyAttributes, + ) + ) { + this._supportedKeyAttributes.set( + parseInt(scene), + keys, + ); + } + } - validatePayload(this.payload.length >= 2 + bitMaskBytes * numEntries); + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): CentralSceneCCSupportedReport { + validatePayload(raw.payload.length >= 2); + const sceneCount = raw.payload[0]; + const supportsSlowRefresh: MaybeNotKnown = + !!(raw.payload[1] & 0b1000_0000); + const bitMaskBytes = (raw.payload[1] & 0b110) >>> 1; + const identicalKeyAttributes = !!(raw.payload[1] & 0b1); + const numEntries = identicalKeyAttributes ? 1 : sceneCount; + validatePayload(raw.payload.length >= 2 + bitMaskBytes * numEntries); + const supportedKeyAttributes: Record< + number, + readonly CentralSceneKeys[] + > = {}; for (let i = 0; i < numEntries; i++) { - const mask = this.payload.subarray( + const mask = raw.payload.subarray( 2 + i * bitMaskBytes, 2 + (i + 1) * bitMaskBytes, ); - this._supportedKeyAttributes.set( - i + 1, - parseBitMask(mask, CentralSceneKeys.KeyPressed), + supportedKeyAttributes[i + 1] = parseBitMask( + mask, + CentralSceneKeys.KeyPressed, ); } + if (identicalKeyAttributes) { // The key attributes are only transmitted for scene 1, copy them to the others - for (let i = 2; i <= this.sceneCount; i++) { - this._supportedKeyAttributes.set( - i, - this._supportedKeyAttributes.get(1)!, - ); + for (let i = 2; i <= sceneCount; i++) { + supportedKeyAttributes[i] = supportedKeyAttributes[1]; } } + + return new CentralSceneCCSupportedReport({ + nodeId: ctx.sourceNodeId, + sceneCount, + supportsSlowRefresh, + supportedKeyAttributes, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -449,15 +515,33 @@ export class CentralSceneCCSupportedReport extends CentralSceneCC { @expectedCCResponse(CentralSceneCCSupportedReport) export class CentralSceneCCSupportedGet extends CentralSceneCC {} +// @publicAPI +export interface CentralSceneCCConfigurationReportOptions { + slowRefresh: boolean; +} + @CCCommand(CentralSceneCommand.ConfigurationReport) export class CentralSceneCCConfigurationReport extends CentralSceneCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.slowRefresh = !!(this.payload[0] & 0b1000_0000); + // TODO: Check implementation: + this.slowRefresh = options.slowRefresh; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): CentralSceneCCConfigurationReport { + validatePayload(raw.payload.length >= 1); + const slowRefresh = !!(raw.payload[0] & 0b1000_0000); + + return new CentralSceneCCConfigurationReport({ + nodeId: ctx.sourceNodeId, + slowRefresh, + }); } @ccValue(CentralSceneCCValues.slowRefresh) @@ -476,9 +560,7 @@ export class CentralSceneCCConfigurationReport extends CentralSceneCC { export class CentralSceneCCConfigurationGet extends CentralSceneCC {} // @publicAPI -export interface CentralSceneCCConfigurationSetOptions - extends CCCommandOptions -{ +export interface CentralSceneCCConfigurationSetOptions { slowRefresh: boolean; } @@ -486,19 +568,24 @@ export interface CentralSceneCCConfigurationSetOptions @useSupervision() export class CentralSceneCCConfigurationSet extends CentralSceneCC { public constructor( - options: - | CommandClassDeserializationOptions - | CentralSceneCCConfigurationSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.slowRefresh = options.slowRefresh; - } + this.slowRefresh = options.slowRefresh; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): CentralSceneCCConfigurationSet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new CentralSceneCCConfigurationSet({ + // nodeId: ctx.sourceNodeId, + // }); } public slowRefresh: boolean; diff --git a/packages/cc/src/cc/ClimateControlScheduleCC.ts b/packages/cc/src/cc/ClimateControlScheduleCC.ts index d02d8fd8ab95..17b9f27ce7f2 100644 --- a/packages/cc/src/cc/ClimateControlScheduleCC.ts +++ b/packages/cc/src/cc/ClimateControlScheduleCC.ts @@ -4,22 +4,22 @@ import { type MessageOrCCLogEntry, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { padStart } from "alcalzone-shared/strings"; import { CCAPI } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -112,7 +112,7 @@ export class ClimateControlScheduleCCAPI extends CCAPI { const cc = new ClimateControlScheduleCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, weekday, switchPoints, }); @@ -130,7 +130,7 @@ export class ClimateControlScheduleCCAPI extends CCAPI { const cc = new ClimateControlScheduleCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, weekday, }); const response = await this.host.sendCommand< @@ -150,7 +150,7 @@ export class ClimateControlScheduleCCAPI extends CCAPI { const cc = new ClimateControlScheduleCCChangedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ClimateControlScheduleCCChangedReport @@ -170,7 +170,7 @@ export class ClimateControlScheduleCCAPI extends CCAPI { const cc = new ClimateControlScheduleCCOverrideGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ClimateControlScheduleCCOverrideReport @@ -198,7 +198,7 @@ export class ClimateControlScheduleCCAPI extends CCAPI { const cc = new ClimateControlScheduleCCOverrideSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, overrideType: type, overrideState: state, }); @@ -214,7 +214,7 @@ export class ClimateControlScheduleCC extends CommandClass { } // @publicAPI -export interface ClimateControlScheduleCCSetOptions extends CCCommandOptions { +export interface ClimateControlScheduleCCSetOptions { weekday: Weekday; switchPoints: Switchpoint[]; } @@ -223,20 +223,25 @@ export interface ClimateControlScheduleCCSetOptions extends CCCommandOptions { @useSupervision() export class ClimateControlScheduleCCSet extends ClimateControlScheduleCC { public constructor( - options: - | CommandClassDeserializationOptions - | ClimateControlScheduleCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.switchPoints = options.switchPoints; - this.weekday = options.weekday; - } + this.switchPoints = options.switchPoints; + this.weekday = options.weekday; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ClimateControlScheduleCCSet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ClimateControlScheduleCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public switchPoints: Switchpoint[]; @@ -283,22 +288,46 @@ export class ClimateControlScheduleCCSet extends ClimateControlScheduleCC { } } +// @publicAPI +export interface ClimateControlScheduleCCReportOptions { + weekday: Weekday; + schedule: Switchpoint[]; +} + @CCCommand(ClimateControlScheduleCommand.Report) export class ClimateControlScheduleCCReport extends ClimateControlScheduleCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 28); // 1 + 9 * 3 - this.weekday = this.payload[0] & 0b111; + // TODO: Check implementation: + this.weekday = options.weekday; + this.schedule = options.schedule; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ClimateControlScheduleCCReport { + validatePayload(raw.payload.length >= 28); + const weekday: Weekday = raw.payload[0] & 0b111; const allSwitchpoints: Switchpoint[] = []; for (let i = 0; i <= 8; i++) { allSwitchpoints.push( - decodeSwitchpoint(this.payload.subarray(1 + 3 * i)), + decodeSwitchpoint(raw.payload.subarray(1 + 3 * i)), ); } - this.schedule = allSwitchpoints.filter((sp) => sp.state !== "Unused"); + + const schedule: Switchpoint[] = allSwitchpoints.filter((sp) => + sp.state !== "Unused" + ); + + return new ClimateControlScheduleCCReport({ + nodeId: ctx.sourceNodeId, + weekday, + schedule, + }); } public readonly weekday: Weekday; @@ -334,7 +363,7 @@ export class ClimateControlScheduleCCReport extends ClimateControlScheduleCC { } // @publicAPI -export interface ClimateControlScheduleCCGetOptions extends CCCommandOptions { +export interface ClimateControlScheduleCCGetOptions { weekday: Weekday; } @@ -342,19 +371,24 @@ export interface ClimateControlScheduleCCGetOptions extends CCCommandOptions { @expectedCCResponse(ClimateControlScheduleCCReport) export class ClimateControlScheduleCCGet extends ClimateControlScheduleCC { public constructor( - options: - | CommandClassDeserializationOptions - | ClimateControlScheduleCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.weekday = options.weekday; - } + this.weekday = options.weekday; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ClimateControlScheduleCCGet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ClimateControlScheduleCCGet({ + // nodeId: ctx.sourceNodeId, + // }); } public weekday: Weekday; @@ -372,17 +406,35 @@ export class ClimateControlScheduleCCGet extends ClimateControlScheduleCC { } } +// @publicAPI +export interface ClimateControlScheduleCCChangedReportOptions { + changeCounter: number; +} + @CCCommand(ClimateControlScheduleCommand.ChangedReport) export class ClimateControlScheduleCCChangedReport extends ClimateControlScheduleCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.changeCounter = this.payload[0]; + // TODO: Check implementation: + this.changeCounter = options.changeCounter; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ClimateControlScheduleCCChangedReport { + validatePayload(raw.payload.length >= 1); + const changeCounter = raw.payload[0]; + + return new ClimateControlScheduleCCChangedReport({ + nodeId: ctx.sourceNodeId, + changeCounter, + }); } public readonly changeCounter: number; @@ -401,19 +453,40 @@ export class ClimateControlScheduleCCChangedGet extends ClimateControlScheduleCC {} +// @publicAPI +export interface ClimateControlScheduleCCOverrideReportOptions { + overrideType: ScheduleOverrideType; + overrideState: SetbackState; +} + @CCCommand(ClimateControlScheduleCommand.OverrideReport) export class ClimateControlScheduleCCOverrideReport extends ClimateControlScheduleCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.overrideType = this.payload[0] & 0b11; - this.overrideState = decodeSetbackState(this.payload[1]) - || this.payload[1]; + // TODO: Check implementation: + this.overrideType = options.overrideType; + this.overrideState = options.overrideState; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ClimateControlScheduleCCOverrideReport { + validatePayload(raw.payload.length >= 2); + const overrideType: ScheduleOverrideType = raw.payload[0] & 0b11; + const overrideState: SetbackState = decodeSetbackState(raw.payload[1]) + || raw.payload[1]; + + return new ClimateControlScheduleCCOverrideReport({ + nodeId: ctx.sourceNodeId, + overrideType, + overrideState, + }); } @ccValue(ClimateControlScheduleCCValues.overrideType) @@ -443,9 +516,7 @@ export class ClimateControlScheduleCCOverrideGet {} // @publicAPI -export interface ClimateControlScheduleCCOverrideSetOptions - extends CCCommandOptions -{ +export interface ClimateControlScheduleCCOverrideSetOptions { overrideType: ScheduleOverrideType; overrideState: SetbackState; } @@ -456,20 +527,25 @@ export class ClimateControlScheduleCCOverrideSet extends ClimateControlScheduleCC { public constructor( - options: - | CommandClassDeserializationOptions - | ClimateControlScheduleCCOverrideSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.overrideType = options.overrideType; - this.overrideState = options.overrideState; - } + this.overrideType = options.overrideType; + this.overrideState = options.overrideState; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ClimateControlScheduleCCOverrideSet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ClimateControlScheduleCCOverrideSet({ + // nodeId: ctx.sourceNodeId, + // }); } public overrideType: ScheduleOverrideType; diff --git a/packages/cc/src/cc/ClockCC.ts b/packages/cc/src/cc/ClockCC.ts index 524422030d9d..73216530b91b 100644 --- a/packages/cc/src/cc/ClockCC.ts +++ b/packages/cc/src/cc/ClockCC.ts @@ -1,6 +1,7 @@ import type { MessageOrCCLogEntry, SupervisionResult, + WithAddress, } from "@zwave-js/core/safe"; import { CommandClasses, @@ -10,18 +11,20 @@ import { ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { padStart } from "alcalzone-shared/strings"; import { CCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -53,7 +56,7 @@ export class ClockCCAPI extends CCAPI { const cc = new ClockCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -74,7 +77,7 @@ export class ClockCCAPI extends CCAPI { const cc = new ClockCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, hour, minute, weekday: weekday ?? Weekday.Unknown, @@ -140,7 +143,7 @@ export class ClockCC extends CommandClass { } // @publicAPI -export interface ClockCCSetOptions extends CCCommandOptions { +export interface ClockCCSetOptions { weekday: Weekday; hour: number; minute: number; @@ -150,20 +153,24 @@ export interface ClockCCSetOptions extends CCCommandOptions { @useSupervision() export class ClockCCSet extends ClockCC { public constructor( - options: CommandClassDeserializationOptions | ClockCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.weekday = options.weekday; - this.hour = options.hour; - this.minute = options.minute; - } + this.weekday = options.weekday; + this.hour = options.hour; + this.minute = options.minute; + } + + public static from(_raw: CCRaw, _ctx: CCParsingContext): ClockCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ClockCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public weekday: Weekday; @@ -199,22 +206,43 @@ export class ClockCCSet extends ClockCC { } } +// @publicAPI +export interface ClockCCReportOptions { + weekday: Weekday; + hour: number; + minute: number; +} + @CCCommand(ClockCommand.Report) export class ClockCCReport extends ClockCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.weekday = this.payload[0] >>> 5; - this.hour = this.payload[0] & 0b11111; - this.minute = this.payload[1]; + // TODO: Check implementation: + this.weekday = options.weekday; + this.hour = options.hour; + this.minute = options.minute; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ClockCCReport { + validatePayload(raw.payload.length >= 2); + const weekday: Weekday = raw.payload[0] >>> 5; + const hour = raw.payload[0] & 0b11111; + const minute = raw.payload[1]; validatePayload( - this.weekday <= Weekday.Sunday, - this.hour <= 23, - this.minute <= 59, + weekday <= Weekday.Sunday, + hour <= 23, + minute <= 59, ); + + return new ClockCCReport({ + nodeId: ctx.sourceNodeId, + weekday, + hour, + minute, + }); } public readonly weekday: Weekday; diff --git a/packages/cc/src/cc/ColorSwitchCC.ts b/packages/cc/src/cc/ColorSwitchCC.ts index 0b163ece62c4..feaeb43a1cfe 100644 --- a/packages/cc/src/cc/ColorSwitchCC.ts +++ b/packages/cc/src/cc/ColorSwitchCC.ts @@ -8,6 +8,7 @@ import { type ValueDB, type ValueID, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, isUnsupervisedOrSucceeded, @@ -16,9 +17,12 @@ import { validatePayload, } from "@zwave-js/core"; import { type MaybeNotKnown, encodeBitMask } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { - type AllOrNone, getEnumMemberName, isEnumMember, keysOf, @@ -39,14 +43,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -209,7 +211,7 @@ export class ColorSwitchCCAPI extends CCAPI { const cc = new ColorSwitchCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ColorSwitchCCSupportedReport @@ -227,7 +229,7 @@ export class ColorSwitchCCAPI extends CCAPI { const cc = new ColorSwitchCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, colorComponent: component, }); const response = await this.host.sendCommand( @@ -247,7 +249,7 @@ export class ColorSwitchCCAPI extends CCAPI { const cc = new ColorSwitchCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -365,7 +367,7 @@ export class ColorSwitchCCAPI extends CCAPI { const cc = new ColorSwitchCCStartLevelChange({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -383,7 +385,7 @@ export class ColorSwitchCCAPI extends CCAPI { const cc = new ColorSwitchCCStopLevelChange({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, colorComponent, }); @@ -684,23 +686,28 @@ export interface ColorSwitchCCSupportedReportOptions { @CCCommand(ColorSwitchCommand.SupportedReport) export class ColorSwitchCCSupportedReport extends ColorSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (ColorSwitchCCSupportedReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // Docs say 'variable length', but the table shows 2 bytes. - validatePayload(this.payload.length >= 2); + this.supportedColorComponents = options.supportedColorComponents; + } - this.supportedColorComponents = parseBitMask( - this.payload.subarray(0, 2), - ColorComponent["Warm White"], - ); - } else { - this.supportedColorComponents = options.supportedColorComponents; - } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ColorSwitchCCSupportedReport { + // Docs say 'variable length', but the table shows 2 bytes. + validatePayload(raw.payload.length >= 2); + const supportedColorComponents: ColorComponent[] = parseBitMask( + raw.payload.subarray(0, 2), + ColorComponent["Warm White"], + ); + + return new ColorSwitchCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedColorComponents, + }); } @ccValue(ColorSwitchCCValues.supportedColorComponents) @@ -732,40 +739,45 @@ export class ColorSwitchCCSupportedReport extends ColorSwitchCC { export class ColorSwitchCCSupportedGet extends ColorSwitchCC {} // @publicAPI -export type ColorSwitchCCReportOptions = - & { - colorComponent: ColorComponent; - currentValue: number; - } - & AllOrNone<{ - targetValue: number; - duration: Duration | string; - }>; +export interface ColorSwitchCCReportOptions { + colorComponent: ColorComponent; + currentValue: number; + targetValue?: number; + duration?: Duration | string; +} @CCCommand(ColorSwitchCommand.Report) export class ColorSwitchCCReport extends ColorSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (ColorSwitchCCReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.colorComponent = this.payload[0]; - this.currentValue = this.payload[1]; + this.colorComponent = options.colorComponent; + this.currentValue = options.currentValue; + this.targetValue = options.targetValue; + this.duration = Duration.from(options.duration); + } - if (this.payload.length >= 4) { - this.targetValue = this.payload[2]; - this.duration = Duration.parseReport(this.payload[3]); - } - } else { - this.colorComponent = options.colorComponent; - this.currentValue = options.currentValue; - this.targetValue = options.targetValue; - this.duration = Duration.from(options.duration); + public static from(raw: CCRaw, ctx: CCParsingContext): ColorSwitchCCReport { + validatePayload(raw.payload.length >= 2); + const colorComponent: ColorComponent = raw.payload[0]; + const currentValue = raw.payload[1]; + let targetValue: number | undefined; + let duration: Duration | undefined; + + if (raw.payload.length >= 4) { + targetValue = raw.payload[2]; + duration = Duration.parseReport(raw.payload[3]); } + + return new ColorSwitchCCReport({ + nodeId: ctx.sourceNodeId, + colorComponent, + currentValue, + targetValue, + duration, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -885,7 +897,7 @@ export class ColorSwitchCCReport extends ColorSwitchCC { } // @publicAPI -export interface ColorSwitchCCGetOptions extends CCCommandOptions { +export interface ColorSwitchCCGetOptions { colorComponent: ColorComponent; } @@ -900,15 +912,20 @@ function testResponseForColorSwitchGet( @expectedCCResponse(ColorSwitchCCReport, testResponseForColorSwitchGet) export class ColorSwitchCCGet extends ColorSwitchCC { public constructor( - options: CommandClassDeserializationOptions | ColorSwitchCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this._colorComponent = this.payload[0]; - } else { - this._colorComponent = options.colorComponent; - } + this._colorComponent = options.colorComponent; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ColorSwitchCCGet { + validatePayload(raw.payload.length >= 1); + const colorComponent: ColorComponent = raw.payload[0]; + + return new ColorSwitchCCGet({ + nodeId: ctx.sourceNodeId, + colorComponent, + }); } private _colorComponent: ColorComponent; @@ -952,49 +969,55 @@ export type ColorSwitchCCSetOptions = (ColorTable | { hexColor: string }) & { @useSupervision() export class ColorSwitchCCSet extends ColorSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & ColorSwitchCCSetOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const populatedColorCount = this.payload[0] & 0b11111; - - validatePayload(this.payload.length >= 1 + populatedColorCount * 2); - this.colorTable = {}; - let offset = 1; - for (let color = 0; color < populatedColorCount; color++) { - const component = this.payload[offset]; - const value = this.payload[offset + 1]; - const key = colorComponentToTableKey(component); - // @ts-expect-error - if (key) this.colorTable[key] = value; - offset += 2; - } - if (this.payload.length > offset) { - this.duration = Duration.parseSet(this.payload[offset]); + // Populate properties from options object + if ("hexColor" in options) { + const match = hexColorRegex.exec(options.hexColor); + if (!match) { + throw new ZWaveError( + `${options.hexColor} is not a valid HEX color string`, + ZWaveErrorCodes.Argument_Invalid, + ); } + this.colorTable = { + red: parseInt(match.groups!.red, 16), + green: parseInt(match.groups!.green, 16), + blue: parseInt(match.groups!.blue, 16), + }; } else { - // Populate properties from options object - if ("hexColor" in options) { - const match = hexColorRegex.exec(options.hexColor); - if (!match) { - throw new ZWaveError( - `${options.hexColor} is not a valid HEX color string`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.colorTable = { - red: parseInt(match.groups!.red, 16), - green: parseInt(match.groups!.green, 16), - blue: parseInt(match.groups!.blue, 16), - }; - } else { - this.colorTable = pick(options, colorTableKeys as any[]); - } - this.duration = Duration.from(options.duration); + this.colorTable = pick(options, colorTableKeys as any[]); + } + this.duration = Duration.from(options.duration); + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ColorSwitchCCSet { + validatePayload(raw.payload.length >= 1); + const populatedColorCount = raw.payload[0] & 0b11111; + + validatePayload(raw.payload.length >= 1 + populatedColorCount * 2); + const colorTable: ColorTable = {}; + let offset = 1; + for (let color = 0; color < populatedColorCount; color++) { + const component = raw.payload[offset]; + const value = raw.payload[offset + 1]; + const key = colorComponentToTableKey(component); + // @ts-expect-error + if (key) this.colorTable[key] = value; + offset += 2; + } + + let duration: Duration | undefined; + if (raw.payload.length > offset) { + duration = Duration.parseSet(raw.payload[offset]); } + + return new ColorSwitchCCSet({ + nodeId: ctx.sourceNodeId, + ...colorTable, + duration, + }); } public colorTable: ColorTable; @@ -1073,31 +1096,40 @@ export type ColorSwitchCCStartLevelChangeOptions = @useSupervision() export class ColorSwitchCCStartLevelChange extends ColorSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & ColorSwitchCCStartLevelChangeOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - const ignoreStartLevel = (this.payload[0] & 0b0_0_1_00000) >>> 5; - this.ignoreStartLevel = !!ignoreStartLevel; - const direction = (this.payload[0] & 0b0_1_0_00000) >>> 6; - this.direction = direction ? "down" : "up"; - - this.colorComponent = this.payload[1]; - this.startLevel = this.payload[2]; - - if (this.payload.length >= 4) { - this.duration = Duration.parseSet(this.payload[3]); - } - } else { - this.duration = Duration.from(options.duration); - this.ignoreStartLevel = options.ignoreStartLevel; - this.startLevel = options.startLevel ?? 0; - this.direction = options.direction; - this.colorComponent = options.colorComponent; + this.duration = Duration.from(options.duration); + this.ignoreStartLevel = options.ignoreStartLevel; + this.startLevel = options.startLevel ?? 0; + this.direction = options.direction; + this.colorComponent = options.colorComponent; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ColorSwitchCCStartLevelChange { + validatePayload(raw.payload.length >= 3); + const ignoreStartLevel = !!((raw.payload[0] & 0b0_0_1_00000) >>> 5); + const direction = ((raw.payload[0] & 0b0_1_0_00000) >>> 6) + ? "down" + : "up"; + const colorComponent: ColorComponent = raw.payload[1]; + const startLevel = raw.payload[2]; + let duration: Duration | undefined; + if (raw.payload.length >= 4) { + duration = Duration.parseSet(raw.payload[3]); } + + return new ColorSwitchCCStartLevelChange({ + nodeId: ctx.sourceNodeId, + ignoreStartLevel, + direction, + colorComponent, + startLevel, + duration, + }); } public duration: Duration | undefined; @@ -1151,7 +1183,7 @@ export class ColorSwitchCCStartLevelChange extends ColorSwitchCC { } // @publicAPI -export interface ColorSwitchCCStopLevelChangeOptions extends CCCommandOptions { +export interface ColorSwitchCCStopLevelChangeOptions { colorComponent: ColorComponent; } @@ -1159,17 +1191,23 @@ export interface ColorSwitchCCStopLevelChangeOptions extends CCCommandOptions { @useSupervision() export class ColorSwitchCCStopLevelChange extends ColorSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | ColorSwitchCCStopLevelChangeOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.colorComponent = this.payload[0]; - } else { - this.colorComponent = options.colorComponent; - } + this.colorComponent = options.colorComponent; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ColorSwitchCCStopLevelChange { + validatePayload(raw.payload.length >= 1); + const colorComponent: ColorComponent = raw.payload[0]; + + return new ColorSwitchCCStopLevelChange({ + nodeId: ctx.sourceNodeId, + colorComponent, + }); } public readonly colorComponent: ColorComponent; diff --git a/packages/cc/src/cc/ConfigurationCC.ts b/packages/cc/src/cc/ConfigurationCC.ts index 422253fabd89..039c71fa07e9 100644 --- a/packages/cc/src/cc/ConfigurationCC.ts +++ b/packages/cc/src/cc/ConfigurationCC.ts @@ -16,6 +16,7 @@ import { type SupportsCC, type ValueID, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodePartial, @@ -54,14 +55,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -534,7 +533,7 @@ export class ConfigurationCCAPI extends CCAPI { const cc = new ConfigurationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, allowUnexpectedResponse, }); @@ -603,7 +602,7 @@ export class ConfigurationCCAPI extends CCAPI { ) { const cc = new ConfigurationCCBulkGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameters: distinctParameters, }); const response = await this.host.sendCommand< @@ -623,7 +622,7 @@ export class ConfigurationCCAPI extends CCAPI { for (const parameter of distinctParameters) { const cc = new ConfigurationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); const response = await this.host.sendCommand< @@ -693,7 +692,7 @@ export class ConfigurationCCAPI extends CCAPI { } const cc = new ConfigurationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, resetToDefault: false, parameter: normalized.parameter, value, @@ -737,7 +736,7 @@ export class ConfigurationCCAPI extends CCAPI { if (canUseBulkSet) { const cc = new ConfigurationCCBulkSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameters: allParams.map((v) => v.parameter), valueSize: allParams[0].valueSize, valueFormat: allParams[0].valueFormat, @@ -785,7 +784,7 @@ export class ConfigurationCCAPI extends CCAPI { ) { const cc = new ConfigurationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, value, valueSize, @@ -831,7 +830,7 @@ export class ConfigurationCCAPI extends CCAPI { const cc = new ConfigurationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, resetToDefault: true, }); @@ -853,7 +852,7 @@ export class ConfigurationCCAPI extends CCAPI { ) { const cc = new ConfigurationCCBulkSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameters, resetToDefault: true, }); @@ -867,7 +866,7 @@ export class ConfigurationCCAPI extends CCAPI { (parameter) => new ConfigurationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, resetToDefault: true, }), @@ -890,7 +889,7 @@ export class ConfigurationCCAPI extends CCAPI { const cc = new ConfigurationCCDefaultReset({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); await this.host.sendCommand(cc, this.commandOptions); } @@ -903,7 +902,7 @@ export class ConfigurationCCAPI extends CCAPI { const cc = new ConfigurationCCPropertiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); const response = await this.host.sendCommand< @@ -936,7 +935,7 @@ export class ConfigurationCCAPI extends CCAPI { const cc = new ConfigurationCCNameGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); const response = await this.host.sendCommand< @@ -956,7 +955,7 @@ export class ConfigurationCCAPI extends CCAPI { const cc = new ConfigurationCCInfoGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); const response = await this.host.sendCommand< @@ -1603,7 +1602,7 @@ alters capabilities: ${!!properties.altersCapabilities}`; } /** @publicAPI */ -export interface ConfigurationCCReportOptions extends CCCommandOptions { +export interface ConfigurationCCReportOptions { parameter: number; value: ConfigValue; valueSize: number; @@ -1613,36 +1612,45 @@ export interface ConfigurationCCReportOptions extends CCCommandOptions { @CCCommand(ConfigurationCommand.Report) export class ConfigurationCCReport extends ConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | ConfigurationCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // All fields must be present - validatePayload(this.payload.length > 2); - this.parameter = this.payload[0]; - this.valueSize = this.payload[1] & 0b111; - // Ensure we received a valid report - validatePayload( - this.valueSize >= 1, - this.valueSize <= 4, - this.payload.length >= 2 + this.valueSize, - ); - // Default to parsing the value as SignedInteger, like the specs say. - // We try to re-interpret the value in persistValues() - this.value = parseValue( - this.payload.subarray(2), - this.valueSize, - ConfigValueFormat.SignedInteger, - ); - } else { - this.parameter = options.parameter; - this.value = options.value; - this.valueSize = options.valueSize; - this.valueFormat = options.valueFormat; - } + this.parameter = options.parameter; + this.value = options.value; + this.valueSize = options.valueSize; + this.valueFormat = options.valueFormat; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCReport { + // All fields must be present + validatePayload(raw.payload.length > 2); + const parameter = raw.payload[0]; + const valueSize = raw.payload[1] & 0b111; + + // Ensure we received a valid report + validatePayload( + valueSize >= 1, + valueSize <= 4, + raw.payload.length >= 2 + valueSize, + ); + // Default to parsing the value as SignedInteger, like the specs say. + // We try to re-interpret the value in persistValues() + const value = parseValue( + raw.payload.subarray(2), + valueSize, + ConfigValueFormat.SignedInteger, + ); + + return new ConfigurationCCReport({ + nodeId: ctx.sourceNodeId, + parameter, + valueSize, + value, + }); } public parameter: number; @@ -1784,7 +1792,7 @@ function testResponseForConfigurationGet( } // @publicAPI -export interface ConfigurationCCGetOptions extends CCCommandOptions { +export interface ConfigurationCCGetOptions { parameter: number; /** * If this is `true`, responses with different parameters than expected are accepted @@ -1797,18 +1805,22 @@ export interface ConfigurationCCGetOptions extends CCCommandOptions { @expectedCCResponse(ConfigurationCCReport, testResponseForConfigurationGet) export class ConfigurationCCGet extends ConfigurationCC { public constructor( - options: CommandClassDeserializationOptions | ConfigurationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.parameter = this.payload[0]; - this.allowUnexpectedResponse = false; - } else { - this.parameter = options.parameter; - this.allowUnexpectedResponse = options.allowUnexpectedResponse - ?? false; - } + this.parameter = options.parameter; + this.allowUnexpectedResponse = options.allowUnexpectedResponse + ?? false; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ConfigurationCCGet { + validatePayload(raw.payload.length >= 1); + const parameter = raw.payload[0]; + + return new ConfigurationCCGet({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: number; @@ -1829,60 +1841,65 @@ export class ConfigurationCCGet extends ConfigurationCC { // @publicAPI export type ConfigurationCCSetOptions = - & CCCommandOptions - & ( - | { - parameter: number; - resetToDefault: true; - } - | { - parameter: number; - resetToDefault?: false; - valueSize: number; - /** How the value is encoded. Defaults to SignedInteger */ - valueFormat?: ConfigValueFormat; - value: ConfigValue; - } - ); + | { + parameter: number; + resetToDefault: true; + } + | { + parameter: number; + resetToDefault?: false; + valueSize: number; + /** How the value is encoded. Defaults to SignedInteger */ + valueFormat?: ConfigValueFormat; + value: ConfigValue; + }; @CCCommand(ConfigurationCommand.Set) @useSupervision() export class ConfigurationCCSet extends ConfigurationCC { public constructor( - options: CommandClassDeserializationOptions | ConfigurationCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.parameter = this.payload[0]; - this.resetToDefault = !!(this.payload[1] & 0b1000_0000); - this.valueSize = this.payload[1] & 0b111; - - // Ensure we received a valid report - validatePayload( - this.valueSize >= 1, - this.valueSize <= 4, - this.payload.length >= 2 + this.valueSize, - ); - // Parse the value as signed integer. We don't know the format here. - this.value = parseValue( - this.payload.subarray(2), - this.valueSize, - ConfigValueFormat.SignedInteger, - ); - } else { - this.parameter = options.parameter; - this.resetToDefault = !!options.resetToDefault; - if (!options.resetToDefault) { - // TODO: Default to the stored value size - this.valueSize = options.valueSize; - this.valueFormat = options.valueFormat - ?? ConfigValueFormat.SignedInteger; - this.value = options.value; - } + this.parameter = options.parameter; + this.resetToDefault = !!options.resetToDefault; + if (!options.resetToDefault) { + // TODO: Default to the stored value size + this.valueSize = options.valueSize; + this.valueFormat = options.valueFormat + ?? ConfigValueFormat.SignedInteger; + this.value = options.value; } } + public static from(raw: CCRaw, ctx: CCParsingContext): ConfigurationCCSet { + validatePayload(raw.payload.length >= 2); + const parameter = raw.payload[0]; + const resetToDefault = !!(raw.payload[1] & 0b1000_0000); + const valueSize: number | undefined = raw.payload[1] & 0b111; + + // Ensure we received a valid report + validatePayload( + valueSize >= 1, + valueSize <= 4, + raw.payload.length >= 2 + valueSize, + ); + // Parse the value as signed integer. We don't know the format here. + const value: number | undefined = parseValue( + raw.payload.subarray(2), + valueSize, + ConfigValueFormat.SignedInteger, + ); + + return new ConfigurationCCSet({ + nodeId: ctx.sourceNodeId, + parameter, + resetToDefault, + valueSize, + value, + }); + } + public resetToDefault: boolean; public parameter: number; public valueSize: number | undefined; @@ -1958,7 +1975,6 @@ export class ConfigurationCCSet extends ConfigurationCC { // @publicAPI export type ConfigurationCCBulkSetOptions = - & CCCommandOptions & { parameters: number[]; handshake?: boolean; @@ -1984,45 +2000,50 @@ function getResponseForBulkSet(cc: ConfigurationCCBulkSet) { @useSupervision() export class ConfigurationCCBulkSet extends ConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | ConfigurationCCBulkSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload + this._parameters = options.parameters; + if (this._parameters.length < 1) { + throw new ZWaveError( + `In a ConfigurationCC.BulkSet, parameters must be a non-empty array`, + ZWaveErrorCodes.CC_Invalid, + ); + } else if (!isConsecutiveArray(this._parameters)) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `A ConfigurationCC.BulkSet can only be used for consecutive parameters`, + ZWaveErrorCodes.CC_Invalid, ); + } + this._handshake = !!options.handshake; + this._resetToDefault = !!options.resetToDefault; + if (!!options.resetToDefault) { + this._valueSize = 1; + this._valueFormat = ConfigValueFormat.SignedInteger; + this._values = this._parameters.map(() => 0); } else { - this._parameters = options.parameters; - if (this._parameters.length < 1) { - throw new ZWaveError( - `In a ConfigurationCC.BulkSet, parameters must be a non-empty array`, - ZWaveErrorCodes.CC_Invalid, - ); - } else if (!isConsecutiveArray(this._parameters)) { - throw new ZWaveError( - `A ConfigurationCC.BulkSet can only be used for consecutive parameters`, - ZWaveErrorCodes.CC_Invalid, - ); - } - this._handshake = !!options.handshake; - this._resetToDefault = !!options.resetToDefault; - if (!!options.resetToDefault) { - this._valueSize = 1; - this._valueFormat = ConfigValueFormat.SignedInteger; - this._values = this._parameters.map(() => 0); - } else { - this._valueSize = options.valueSize; - this._valueFormat = options.valueFormat - ?? ConfigValueFormat.SignedInteger; - this._values = options.values; - } + this._valueSize = options.valueSize; + this._valueFormat = options.valueFormat + ?? ConfigValueFormat.SignedInteger; + this._values = options.values; } } + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ConfigurationCCBulkSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ConfigurationCCBulkSet({ + // nodeId: ctx.sourceNodeId, + // }); + } + private _parameters: number[]; public get parameters(): number[] { return this._parameters; @@ -2120,37 +2141,66 @@ export class ConfigurationCCBulkSet extends ConfigurationCC { } } +// @publicAPI +export interface ConfigurationCCBulkReportOptions { + reportsToFollow: number; + defaultValues: boolean; + isHandshakeResponse: boolean; + valueSize: number; + values: Record; +} + @CCCommand(ConfigurationCommand.BulkReport) export class ConfigurationCCBulkReport extends ConfigurationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - // Ensure we received enough bytes for the preamble - validatePayload(this.payload.length >= 5); - const firstParameter = this.payload.readUInt16BE(0); - const numParams = this.payload[2]; - this._reportsToFollow = this.payload[3]; - this._defaultValues = !!(this.payload[4] & 0b1000_0000); - this._isHandshakeResponse = !!(this.payload[4] & 0b0100_0000); - this._valueSize = this.payload[4] & 0b111; + // TODO: Check implementation: + this.reportsToFollow = options.reportsToFollow; + this.defaultValues = options.defaultValues; + this.isHandshakeResponse = options.isHandshakeResponse; + this.valueSize = options.valueSize; + for (const [param, value] of Object.entries(options.values)) { + this._values.set(parseInt(param), value); + } + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCBulkReport { + // Ensure we received enough bytes for the preamble + validatePayload(raw.payload.length >= 5); + const firstParameter = raw.payload.readUInt16BE(0); + const numParams = raw.payload[2]; + const reportsToFollow = raw.payload[3]; + const defaultValues = !!(raw.payload[4] & 0b1000_0000); + const isHandshakeResponse = !!(raw.payload[4] & 0b0100_0000); + const valueSize = raw.payload[4] & 0b111; // Ensure the payload is long enough for all reported values - validatePayload(this.payload.length >= 5 + numParams * this._valueSize); + validatePayload(raw.payload.length >= 5 + numParams * valueSize); + const values: Record = {}; for (let i = 0; i < numParams; i++) { const param = firstParameter + i; - this._values.set( - param, - // Default to parsing the value as SignedInteger, like the specs say. - // We try to re-interpret the value in persistValues() - parseValue( - this.payload.subarray(5 + i * this.valueSize), - this.valueSize, - ConfigValueFormat.SignedInteger, - ), + // Default to parsing the value as SignedInteger, like the specs say. + // We try to re-interpret the value in persistValues() + values[param] = parseValue( + raw.payload.subarray(5 + i * valueSize), + valueSize, + ConfigValueFormat.SignedInteger, ); } + + return new ConfigurationCCBulkReport({ + nodeId: ctx.sourceNodeId, + reportsToFollow, + defaultValues, + isHandshakeResponse, + valueSize, + values, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -2172,7 +2222,7 @@ export class ConfigurationCCBulkReport extends ConfigurationCC { // Re-interpret the value with the new format value = reInterpretSignedValue( value, - this._valueSize, + this.valueSize, oldParamInformation.format, ); this._values.set(parameter, value); @@ -2188,9 +2238,14 @@ export class ConfigurationCCBulkReport extends ConfigurationCC { return true; } - private _reportsToFollow: number; - public get reportsToFollow(): number { - return this._reportsToFollow; + public reportsToFollow: number; + public defaultValues: boolean; + public isHandshakeResponse: boolean; + public valueSize: number; + + private _values = new Map(); + public get values(): ReadonlyMap { + return this._values; } public getPartialCCSessionId(): Record | undefined { @@ -2199,34 +2254,14 @@ export class ConfigurationCCBulkReport extends ConfigurationCC { } public expectMoreMessages(): boolean { - return this._reportsToFollow > 0; - } - - private _defaultValues: boolean; - public get defaultValues(): boolean { - return this._defaultValues; - } - - private _isHandshakeResponse: boolean; - public get isHandshakeResponse(): boolean { - return this._isHandshakeResponse; - } - - private _valueSize: number; - public get valueSize(): number { - return this._valueSize; - } - - private _values = new Map(); - public get values(): ReadonlyMap { - return this._values; + return this.reportsToFollow > 0; } public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { const message: MessageRecord = { - "handshake response": this._isHandshakeResponse, - "default values": this._defaultValues, - "value size": this._valueSize, + "handshake response": this.isHandshakeResponse, + "default values": this.defaultValues, + "value size": this.valueSize, "reports to follow": this.reportsToFollow, }; if (this._values.size > 0) { @@ -2245,7 +2280,7 @@ export class ConfigurationCCBulkReport extends ConfigurationCC { } // @publicAPI -export interface ConfigurationCCBulkGetOptions extends CCCommandOptions { +export interface ConfigurationCCBulkGetOptions { parameters: number[]; } @@ -2253,28 +2288,33 @@ export interface ConfigurationCCBulkGetOptions extends CCCommandOptions { @expectedCCResponse(ConfigurationCCBulkReport) export class ConfigurationCCBulkGet extends ConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | ConfigurationCCBulkGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload + this._parameters = options.parameters.sort(); + if (!isConsecutiveArray(this.parameters)) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `A ConfigurationCC.BulkGet can only be used for consecutive parameters`, + ZWaveErrorCodes.CC_Invalid, ); - } else { - this._parameters = options.parameters.sort(); - if (!isConsecutiveArray(this.parameters)) { - throw new ZWaveError( - `A ConfigurationCC.BulkGet can only be used for consecutive parameters`, - ZWaveErrorCodes.CC_Invalid, - ); - } } } + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ConfigurationCCBulkGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ConfigurationCCBulkGet({ + // nodeId: ctx.sourceNodeId, + // }); + } + private _parameters: number[]; public get parameters(): number[] { return this._parameters; @@ -2296,7 +2336,7 @@ export class ConfigurationCCBulkGet extends ConfigurationCC { } /** @publicAPI */ -export interface ConfigurationCCNameReportOptions extends CCCommandOptions { +export interface ConfigurationCCNameReportOptions { parameter: number; name: string; reportsToFollow: number; @@ -2305,27 +2345,36 @@ export interface ConfigurationCCNameReportOptions extends CCCommandOptions { @CCCommand(ConfigurationCommand.NameReport) export class ConfigurationCCNameReport extends ConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | ConfigurationCCNameReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // Parameter and # of reports must be present - validatePayload(this.payload.length >= 3); - this.parameter = this.payload.readUInt16BE(0); - this.reportsToFollow = this.payload[2]; - if (this.reportsToFollow > 0) { - // If more reports follow, the info must at least be one byte - validatePayload(this.payload.length >= 4); - } - this.name = this.payload.subarray(3).toString("utf8"); - } else { - this.parameter = options.parameter; - this.name = options.name; - this.reportsToFollow = options.reportsToFollow; + this.parameter = options.parameter; + this.name = options.name; + this.reportsToFollow = options.reportsToFollow; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCNameReport { + // Parameter and # of reports must be present + validatePayload(raw.payload.length >= 3); + const parameter = raw.payload.readUInt16BE(0); + const reportsToFollow = raw.payload[2]; + + if (reportsToFollow > 0) { + // If more reports follow, the info must at least be one byte + validatePayload(raw.payload.length >= 4); } + const name: string = raw.payload.subarray(3).toString("utf8"); + + return new ConfigurationCCNameReport({ + nodeId: ctx.sourceNodeId, + parameter, + reportsToFollow, + name, + }); } public readonly parameter: number; @@ -2413,15 +2462,23 @@ export class ConfigurationCCNameReport extends ConfigurationCC { @expectedCCResponse(ConfigurationCCNameReport) export class ConfigurationCCNameGet extends ConfigurationCC { public constructor( - options: CommandClassDeserializationOptions | ConfigurationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.parameter = this.payload.readUInt16BE(0); - } else { - this.parameter = options.parameter; - } + this.parameter = options.parameter; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCNameGet { + validatePayload(raw.payload.length >= 2); + const parameter = raw.payload.readUInt16BE(0); + + return new ConfigurationCCNameGet({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: number; @@ -2441,7 +2498,7 @@ export class ConfigurationCCNameGet extends ConfigurationCC { } /** @publicAPI */ -export interface ConfigurationCCInfoReportOptions extends CCCommandOptions { +export interface ConfigurationCCInfoReportOptions { parameter: number; info: string; reportsToFollow: number; @@ -2450,27 +2507,36 @@ export interface ConfigurationCCInfoReportOptions extends CCCommandOptions { @CCCommand(ConfigurationCommand.InfoReport) export class ConfigurationCCInfoReport extends ConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | ConfigurationCCInfoReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // Parameter and # of reports must be present - validatePayload(this.payload.length >= 3); - this.parameter = this.payload.readUInt16BE(0); - this.reportsToFollow = this.payload[2]; - if (this.reportsToFollow > 0) { - // If more reports follow, the info must at least be one byte - validatePayload(this.payload.length >= 4); - } - this.info = this.payload.subarray(3).toString("utf8"); - } else { - this.parameter = options.parameter; - this.info = options.info; - this.reportsToFollow = options.reportsToFollow; + this.parameter = options.parameter; + this.info = options.info; + this.reportsToFollow = options.reportsToFollow; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCInfoReport { + // Parameter and # of reports must be present + validatePayload(raw.payload.length >= 3); + const parameter = raw.payload.readUInt16BE(0); + const reportsToFollow = raw.payload[2]; + + if (reportsToFollow > 0) { + // If more reports follow, the info must at least be one byte + validatePayload(raw.payload.length >= 4); } + const info: string = raw.payload.subarray(3).toString("utf8"); + + return new ConfigurationCCInfoReport({ + nodeId: ctx.sourceNodeId, + parameter, + reportsToFollow, + info, + }); } public readonly parameter: number; @@ -2571,15 +2637,23 @@ export class ConfigurationCCInfoReport extends ConfigurationCC { @expectedCCResponse(ConfigurationCCInfoReport) export class ConfigurationCCInfoGet extends ConfigurationCC { public constructor( - options: CommandClassDeserializationOptions | ConfigurationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.parameter = this.payload.readUInt16BE(0); - } else { - this.parameter = options.parameter; - } + this.parameter = options.parameter; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCInfoGet { + validatePayload(raw.payload.length >= 2); + const parameter = raw.payload.readUInt16BE(0); + + return new ConfigurationCCInfoGet({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: number; @@ -2599,9 +2673,7 @@ export class ConfigurationCCInfoGet extends ConfigurationCC { } /** @publicAPI */ -export interface ConfigurationCCPropertiesReportOptions - extends CCCommandOptions -{ +export interface ConfigurationCCPropertiesReportOptions { parameter: number; valueSize: number; valueFormat: ConfigValueFormat; @@ -2618,95 +2690,129 @@ export interface ConfigurationCCPropertiesReportOptions @CCCommand(ConfigurationCommand.PropertiesReport) export class ConfigurationCCPropertiesReport extends ConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | ConfigurationCCPropertiesReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.parameter = this.payload.readUInt16BE(0); - this.valueFormat = (this.payload[2] & 0b111000) >>> 3; - this.valueSize = this.payload[2] & 0b111; - - // GH#1309 Some devices don't tell us the first parameter if we query #0 - // Instead, they contain 0x000000 - if (this.valueSize === 0 && this.payload.length < 5) { - this.nextParameter = 0; - return; + this.parameter = options.parameter; + this.valueSize = options.valueSize; + this.valueFormat = options.valueFormat; + if (this.valueSize > 0) { + if (options.minValue == undefined) { + throw new ZWaveError( + "The minimum value must be set when the value size is non-zero", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (options.maxValue == undefined) { + throw new ZWaveError( + "The maximum value must be set when the value size is non-zero", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (options.defaultValue == undefined) { + throw new ZWaveError( + "The default value must be set when the value size is non-zero", + ZWaveErrorCodes.Argument_Invalid, + ); } + this.minValue = options.minValue; + this.maxValue = options.maxValue; + this.defaultValue = options.defaultValue; + } + this.nextParameter = options.nextParameter; + this.altersCapabilities = options.altersCapabilities; + this.isReadonly = options.isReadonly; + this.isAdvanced = options.isAdvanced; + this.noBulkSupport = options.noBulkSupport; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCPropertiesReport { + validatePayload(raw.payload.length >= 3); + const parameter = raw.payload.readUInt16BE(0); + const valueFormat: ConfigValueFormat = (raw.payload[2] & 0b111000) + >>> 3; + const valueSize = raw.payload[2] & 0b111; + + // GH#1309 Some devices don't tell us the first parameter if we query #0 + // Instead, they contain 0x000000 + let nextParameter; + + if (valueSize === 0 && raw.payload.length < 5) { + nextParameter = 0; + return new ConfigurationCCPropertiesReport({ + nodeId: ctx.sourceNodeId, + parameter, + valueFormat, + valueSize, + nextParameter, + }); + } - // Ensure the payload contains the two bytes for next parameter - const nextParameterOffset = 3 + 3 * this.valueSize; - validatePayload(this.payload.length >= nextParameterOffset + 2); + // Ensure the payload contains the two bytes for next parameter + const nextParameterOffset = 3 + 3 * valueSize; + validatePayload(raw.payload.length >= nextParameterOffset + 2); - if (this.valueSize > 0) { - if (this.valueFormat === ConfigValueFormat.BitField) { - this.minValue = 0; - } else { - this.minValue = parseValue( - this.payload.subarray(3), - this.valueSize, - this.valueFormat, - ); - } - this.maxValue = parseValue( - this.payload.subarray(3 + this.valueSize), - this.valueSize, - this.valueFormat, - ); - this.defaultValue = parseValue( - this.payload.subarray(3 + 2 * this.valueSize), - this.valueSize, - this.valueFormat, + let minValue: MaybeNotKnown; + let maxValue: MaybeNotKnown; + let defaultValue: MaybeNotKnown; + + if (valueSize > 0) { + if (valueFormat === ConfigValueFormat.BitField) { + minValue = 0; + } else { + minValue = parseValue( + raw.payload.subarray(3), + valueSize, + valueFormat, ); } - - this.nextParameter = this.payload.readUInt16BE( - nextParameterOffset, + maxValue = parseValue( + raw.payload.subarray(3 + valueSize), + valueSize, + valueFormat, + ); + defaultValue = parseValue( + raw.payload.subarray(3 + 2 * valueSize), + valueSize, + valueFormat, ); + } - if (this.payload.length >= nextParameterOffset + 3) { - // V4 adds an options byte after the next parameter and two bits in byte 2 - const options1 = this.payload[2]; - const options2 = this.payload[3 + 3 * this.valueSize + 2]; - this.altersCapabilities = !!(options1 & 0b1000_0000); - this.isReadonly = !!(options1 & 0b0100_0000); - this.isAdvanced = !!(options2 & 0b1); - this.noBulkSupport = !!(options2 & 0b10); - } - } else { - this.parameter = options.parameter; - this.valueSize = options.valueSize; - this.valueFormat = options.valueFormat; - if (this.valueSize > 0) { - if (options.minValue == undefined) { - throw new ZWaveError( - "The minimum value must be set when the value size is non-zero", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (options.maxValue == undefined) { - throw new ZWaveError( - "The maximum value must be set when the value size is non-zero", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (options.defaultValue == undefined) { - throw new ZWaveError( - "The default value must be set when the value size is non-zero", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.minValue = options.minValue; - this.maxValue = options.maxValue; - this.defaultValue = options.defaultValue; - } - this.nextParameter = options.nextParameter; - this.altersCapabilities = options.altersCapabilities; - this.isReadonly = options.isReadonly; - this.isAdvanced = options.isAdvanced; - this.noBulkSupport = options.noBulkSupport; + nextParameter = raw.payload.readUInt16BE( + nextParameterOffset, + ); + + let altersCapabilities: MaybeNotKnown; + let isReadonly: MaybeNotKnown; + let isAdvanced: MaybeNotKnown; + let noBulkSupport: MaybeNotKnown; + + if (raw.payload.length >= nextParameterOffset + 3) { + // V4 adds an options byte after the next parameter and two bits in byte 2 + const options1 = raw.payload[2]; + const options2 = raw.payload[3 + 3 * valueSize + 2]; + altersCapabilities = !!(options1 & 0b1000_0000); + isReadonly = !!(options1 & 0b0100_0000); + isAdvanced = !!(options2 & 0b1); + noBulkSupport = !!(options2 & 0b10); } + + return new ConfigurationCCPropertiesReport({ + nodeId: ctx.sourceNodeId, + parameter, + valueFormat, + valueSize, + nextParameter, + minValue, + maxValue, + defaultValue, + altersCapabilities, + isReadonly, + isAdvanced, + noBulkSupport, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -2893,15 +2999,23 @@ export class ConfigurationCCPropertiesReport extends ConfigurationCC { @expectedCCResponse(ConfigurationCCPropertiesReport) export class ConfigurationCCPropertiesGet extends ConfigurationCC { public constructor( - options: CommandClassDeserializationOptions | ConfigurationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.parameter = this.payload.readUInt16BE(0); - } else { - this.parameter = options.parameter; - } + this.parameter = options.parameter; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ConfigurationCCPropertiesGet { + validatePayload(raw.payload.length >= 2); + const parameter = raw.payload.readUInt16BE(0); + + return new ConfigurationCCPropertiesGet({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: number; diff --git a/packages/cc/src/cc/DeviceResetLocallyCC.ts b/packages/cc/src/cc/DeviceResetLocallyCC.ts index 105ac53672e5..88f330e12af7 100644 --- a/packages/cc/src/cc/DeviceResetLocallyCC.ts +++ b/packages/cc/src/cc/DeviceResetLocallyCC.ts @@ -4,12 +4,9 @@ import { TransmitOptions, validatePayload, } from "@zwave-js/core/safe"; +import { type CCParsingContext } from "@zwave-js/host"; import { CCAPI } from "../lib/API"; -import { - CommandClass, - type CommandClassOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -40,7 +37,7 @@ export class DeviceResetLocallyCCAPI extends CCAPI { const cc = new DeviceResetLocallyCCNotification({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); try { @@ -72,16 +69,18 @@ export class DeviceResetLocallyCC extends CommandClass { @CCCommand(DeviceResetLocallyCommand.Notification) export class DeviceResetLocallyCCNotification extends DeviceResetLocallyCC { - public constructor(options: CommandClassOptions) { - super(options); + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): DeviceResetLocallyCCNotification { + // We need to make sure this doesn't get parsed accidentally, e.g. because of a bit flip - if (gotDeserializationOptions(options)) { - // We need to make sure this doesn't get parsed accidentally, e.g. because of a bit flip + // This CC has no payload + validatePayload(raw.payload.length === 0); + // The driver ensures before handling it that it is only received from the root device - // This CC has no payload - validatePayload(this.payload.length === 0); - // It MUST be issued by the root device - validatePayload(this.endpointIndex === 0); - } + return new DeviceResetLocallyCCNotification({ + nodeId: ctx.sourceNodeId, + }); } } diff --git a/packages/cc/src/cc/DoorLockCC.ts b/packages/cc/src/cc/DoorLockCC.ts index 50f11ac03ad8..d20dbe4c926e 100644 --- a/packages/cc/src/cc/DoorLockCC.ts +++ b/packages/cc/src/cc/DoorLockCC.ts @@ -8,6 +8,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, @@ -15,7 +16,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { isArray } from "alcalzone-shared/typeguards"; @@ -30,13 +35,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -464,7 +467,7 @@ export class DoorLockCCAPI extends PhysicalCCAPI { const cc = new DoorLockCCCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< DoorLockCCCapabilitiesReport @@ -498,7 +501,7 @@ export class DoorLockCCAPI extends PhysicalCCAPI { const cc = new DoorLockCCOperationGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< DoorLockCCOperationReport @@ -532,7 +535,7 @@ export class DoorLockCCAPI extends PhysicalCCAPI { const cc = new DoorLockCCOperationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, mode, }); return this.host.sendCommand(cc, this.commandOptions); @@ -549,7 +552,7 @@ export class DoorLockCCAPI extends PhysicalCCAPI { const cc = new DoorLockCCConfigurationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...configuration, }); return this.host.sendCommand(cc, this.commandOptions); @@ -564,7 +567,7 @@ export class DoorLockCCAPI extends PhysicalCCAPI { const cc = new DoorLockCCConfigurationGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< DoorLockCCConfigurationReport @@ -818,7 +821,7 @@ latch status: ${status.latchStatus}`; } // @publicAPI -export interface DoorLockCCOperationSetOptions extends CCCommandOptions { +export interface DoorLockCCOperationSetOptions { mode: DoorLockMode; } @@ -826,26 +829,31 @@ export interface DoorLockCCOperationSetOptions extends CCCommandOptions { @useSupervision() export class DoorLockCCOperationSet extends DoorLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | DoorLockCCOperationSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload + if (options.mode === DoorLockMode.Unknown) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `Unknown is not a valid door lock target state!`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.mode === DoorLockMode.Unknown) { - throw new ZWaveError( - `Unknown is not a valid door lock target state!`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.mode = options.mode; } + this.mode = options.mode; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): DoorLockCCOperationSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new DoorLockCCOperationSet({ + // nodeId: ctx.sourceNodeId, + // }); } public mode: DoorLockMode; @@ -865,43 +873,93 @@ export class DoorLockCCOperationSet extends DoorLockCC { } } +// @publicAPI +export interface DoorLockCCOperationReportOptions { + currentMode: DoorLockMode; + outsideHandlesCanOpenDoor: DoorHandleStatus; + insideHandlesCanOpenDoor: DoorHandleStatus; + doorStatus?: "closed" | "open"; + boltStatus?: "unlocked" | "locked"; + latchStatus?: "closed" | "open"; + lockTimeout?: number; + targetMode?: DoorLockMode; + duration?: Duration; +} + @CCCommand(DoorLockCommand.OperationReport) export class DoorLockCCOperationReport extends DoorLockCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 5); - - this.currentMode = this.payload[0]; - this.outsideHandlesCanOpenDoor = [ - !!(this.payload[1] & 0b0001_0000), - !!(this.payload[1] & 0b0010_0000), - !!(this.payload[1] & 0b0100_0000), - !!(this.payload[1] & 0b1000_0000), - ]; - this.insideHandlesCanOpenDoor = [ - !!(this.payload[1] & 0b0001), - !!(this.payload[1] & 0b0010), - !!(this.payload[1] & 0b0100), - !!(this.payload[1] & 0b1000), - ]; - this.doorStatus = !!(this.payload[2] & 0b1) ? "closed" : "open"; - this.boltStatus = !!(this.payload[2] & 0b10) ? "unlocked" : "locked"; - this.latchStatus = !!(this.payload[2] & 0b100) ? "closed" : "open"; + // TODO: Check implementation: + this.currentMode = options.currentMode; + this.outsideHandlesCanOpenDoor = options.outsideHandlesCanOpenDoor; + this.insideHandlesCanOpenDoor = options.insideHandlesCanOpenDoor; + this.doorStatus = options.doorStatus; + this.boltStatus = options.boltStatus; + this.latchStatus = options.latchStatus; + this.lockTimeout = options.lockTimeout; + this.targetMode = options.targetMode; + this.duration = options.duration; + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): DoorLockCCOperationReport { + validatePayload(raw.payload.length >= 5); + const currentMode: DoorLockMode = raw.payload[0]; + const outsideHandlesCanOpenDoor: DoorHandleStatus = [ + !!(raw.payload[1] & 0b0001_0000), + !!(raw.payload[1] & 0b0010_0000), + !!(raw.payload[1] & 0b0100_0000), + !!(raw.payload[1] & 0b1000_0000), + ]; + const insideHandlesCanOpenDoor: DoorHandleStatus = [ + !!(raw.payload[1] & 0b0001), + !!(raw.payload[1] & 0b0010), + !!(raw.payload[1] & 0b0100), + !!(raw.payload[1] & 0b1000), + ]; + const doorStatus: "closed" | "open" | undefined = + !!(raw.payload[2] & 0b1) + ? "closed" + : "open"; + const boltStatus: "unlocked" | "locked" | undefined = + !!(raw.payload[2] & 0b10) ? "unlocked" : "locked"; + const latchStatus: "closed" | "open" | undefined = + !!(raw.payload[2] & 0b100) + ? "closed" + : "open"; // Ignore invalid timeout values - const lockTimeoutMinutes = this.payload[3]; - const lockTimeoutSeconds = this.payload[4]; + const lockTimeoutMinutes = raw.payload[3]; + const lockTimeoutSeconds = raw.payload[4]; + let lockTimeout: number | undefined; if (lockTimeoutMinutes <= 253 && lockTimeoutSeconds <= 59) { - this.lockTimeout = lockTimeoutSeconds + lockTimeoutMinutes * 60; + lockTimeout = lockTimeoutSeconds + lockTimeoutMinutes * 60; } - if (this.payload.length >= 7) { - this.targetMode = this.payload[5]; - this.duration = Duration.parseReport(this.payload[6]); + let targetMode: DoorLockMode | undefined; + let duration: Duration | undefined; + if (raw.payload.length >= 7) { + targetMode = raw.payload[5]; + duration = Duration.parseReport(raw.payload[6]); } + + return new DoorLockCCOperationReport({ + nodeId: ctx.sourceNodeId, + currentMode, + outsideHandlesCanOpenDoor, + insideHandlesCanOpenDoor, + doorStatus, + boltStatus, + latchStatus, + lockTimeout, + targetMode, + duration, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -1007,43 +1065,90 @@ export class DoorLockCCOperationReport extends DoorLockCC { @expectedCCResponse(DoorLockCCOperationReport) export class DoorLockCCOperationGet extends DoorLockCC {} +// @publicAPI +export interface DoorLockCCConfigurationReportOptions { + operationType: DoorLockOperationType; + outsideHandlesCanOpenDoorConfiguration: DoorHandleStatus; + insideHandlesCanOpenDoorConfiguration: DoorHandleStatus; + lockTimeoutConfiguration?: number; + autoRelockTime?: number; + holdAndReleaseTime?: number; + twistAssist?: boolean; + blockToBlock?: boolean; +} + @CCCommand(DoorLockCommand.ConfigurationReport) export class DoorLockCCConfigurationReport extends DoorLockCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - - this.operationType = this.payload[0]; - this.outsideHandlesCanOpenDoorConfiguration = [ - !!(this.payload[1] & 0b0001_0000), - !!(this.payload[1] & 0b0010_0000), - !!(this.payload[1] & 0b0100_0000), - !!(this.payload[1] & 0b1000_0000), + + // TODO: Check implementation: + this.operationType = options.operationType; + this.outsideHandlesCanOpenDoorConfiguration = + options.outsideHandlesCanOpenDoorConfiguration; + this.insideHandlesCanOpenDoorConfiguration = + options.insideHandlesCanOpenDoorConfiguration; + this.lockTimeoutConfiguration = options.lockTimeoutConfiguration; + this.autoRelockTime = options.autoRelockTime; + this.holdAndReleaseTime = options.holdAndReleaseTime; + this.twistAssist = options.twistAssist; + this.blockToBlock = options.blockToBlock; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): DoorLockCCConfigurationReport { + validatePayload(raw.payload.length >= 4); + const operationType: DoorLockOperationType = raw.payload[0]; + const outsideHandlesCanOpenDoorConfiguration: DoorHandleStatus = [ + !!(raw.payload[1] & 0b0001_0000), + !!(raw.payload[1] & 0b0010_0000), + !!(raw.payload[1] & 0b0100_0000), + !!(raw.payload[1] & 0b1000_0000), ]; - this.insideHandlesCanOpenDoorConfiguration = [ - !!(this.payload[1] & 0b0001), - !!(this.payload[1] & 0b0010), - !!(this.payload[1] & 0b0100), - !!(this.payload[1] & 0b1000), + const insideHandlesCanOpenDoorConfiguration: DoorHandleStatus = [ + !!(raw.payload[1] & 0b0001), + !!(raw.payload[1] & 0b0010), + !!(raw.payload[1] & 0b0100), + !!(raw.payload[1] & 0b1000), ]; - if (this.operationType === DoorLockOperationType.Timed) { - const lockTimeoutMinutes = this.payload[2]; - const lockTimeoutSeconds = this.payload[3]; + let lockTimeoutConfiguration: number | undefined; + if (operationType === DoorLockOperationType.Timed) { + const lockTimeoutMinutes = raw.payload[2]; + const lockTimeoutSeconds = raw.payload[3]; if (lockTimeoutMinutes <= 0xfd && lockTimeoutSeconds <= 59) { - this.lockTimeoutConfiguration = lockTimeoutSeconds + lockTimeoutConfiguration = lockTimeoutSeconds + lockTimeoutMinutes * 60; } } - if (this.payload.length >= 5) { - this.autoRelockTime = this.payload.readUInt16BE(4); - this.holdAndReleaseTime = this.payload.readUInt16BE(6); - const flags = this.payload[8]; - this.twistAssist = !!(flags & 0b1); - this.blockToBlock = !!(flags & 0b10); + let autoRelockTime: number | undefined; + let holdAndReleaseTime: number | undefined; + let twistAssist: boolean | undefined; + let blockToBlock: boolean | undefined; + if (raw.payload.length >= 5) { + autoRelockTime = raw.payload.readUInt16BE(4); + holdAndReleaseTime = raw.payload.readUInt16BE(6); + + const flags = raw.payload[8]; + twistAssist = !!(flags & 0b1); + blockToBlock = !!(flags & 0b10); } + + return new DoorLockCCConfigurationReport({ + nodeId: ctx.sourceNodeId, + operationType, + outsideHandlesCanOpenDoorConfiguration, + insideHandlesCanOpenDoorConfiguration, + lockTimeoutConfiguration, + autoRelockTime, + holdAndReleaseTime, + twistAssist, + blockToBlock, + }); } @ccValue(DoorLockCCValues.operationType) @@ -1184,29 +1289,34 @@ export type DoorLockCCConfigurationSetOptions = @useSupervision() export class DoorLockCCConfigurationSet extends DoorLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & DoorLockCCConfigurationSetOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.operationType = options.operationType; - this.outsideHandlesCanOpenDoorConfiguration = - options.outsideHandlesCanOpenDoorConfiguration; - this.insideHandlesCanOpenDoorConfiguration = - options.insideHandlesCanOpenDoorConfiguration; - this.lockTimeoutConfiguration = options.lockTimeoutConfiguration; - this.autoRelockTime = options.autoRelockTime; - this.holdAndReleaseTime = options.holdAndReleaseTime; - this.twistAssist = options.twistAssist; - this.blockToBlock = options.blockToBlock; - } + this.operationType = options.operationType; + this.outsideHandlesCanOpenDoorConfiguration = + options.outsideHandlesCanOpenDoorConfiguration; + this.insideHandlesCanOpenDoorConfiguration = + options.insideHandlesCanOpenDoorConfiguration; + this.lockTimeoutConfiguration = options.lockTimeoutConfiguration; + this.autoRelockTime = options.autoRelockTime; + this.holdAndReleaseTime = options.holdAndReleaseTime; + this.twistAssist = options.twistAssist; + this.blockToBlock = options.blockToBlock; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): DoorLockCCConfigurationSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new DoorLockCCConfigurationSet({ + // nodeId: ctx.sourceNodeId, + // }); } public operationType: DoorLockOperationType; @@ -1316,55 +1426,99 @@ export class DoorLockCCConfigurationSet extends DoorLockCC { } } +// @publicAPI +export interface DoorLockCCCapabilitiesReportOptions { + supportedOperationTypes: DoorLockOperationType[]; + supportedDoorLockModes: DoorLockMode[]; + supportedOutsideHandles: DoorHandleStatus; + supportedInsideHandles: DoorHandleStatus; + doorSupported: boolean; + boltSupported: boolean; + latchSupported: boolean; + blockToBlockSupported: boolean; + twistAssistSupported: boolean; + holdAndReleaseSupported: boolean; + autoRelockSupported: boolean; +} + @CCCommand(DoorLockCommand.CapabilitiesReport) export class DoorLockCCCapabilitiesReport extends DoorLockCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); + // TODO: Check implementation: + this.supportedOperationTypes = options.supportedOperationTypes; + this.supportedDoorLockModes = options.supportedDoorLockModes; + this.supportedOutsideHandles = options.supportedOutsideHandles; + this.supportedInsideHandles = options.supportedInsideHandles; + this.doorSupported = options.doorSupported; + this.boltSupported = options.boltSupported; + this.latchSupported = options.latchSupported; + this.blockToBlockSupported = options.blockToBlockSupported; + this.twistAssistSupported = options.twistAssistSupported; + this.holdAndReleaseSupported = options.holdAndReleaseSupported; + this.autoRelockSupported = options.autoRelockSupported; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): DoorLockCCCapabilitiesReport { // parse variable length operation type bit mask - validatePayload(this.payload.length >= 1); - const bitMaskLength = this.payload[0] & 0b11111; + validatePayload(raw.payload.length >= 1); + const bitMaskLength = raw.payload[0] & 0b11111; let offset = 1; - validatePayload(this.payload.length >= offset + bitMaskLength + 1); - this.supportedOperationTypes = parseBitMask( - this.payload.subarray(offset, offset + bitMaskLength), + validatePayload(raw.payload.length >= offset + bitMaskLength + 1); + const supportedOperationTypes: DoorLockOperationType[] = parseBitMask( + raw.payload.subarray(offset, offset + bitMaskLength), // bit 0 is reserved, bitmask starts at 1 0, ); offset += bitMaskLength; - // parse variable length door lock mode list - const listLength = this.payload[offset]; + const listLength = raw.payload[offset]; offset += 1; - validatePayload(this.payload.length >= offset + listLength + 3); - this.supportedDoorLockModes = [ - ...this.payload.subarray(offset, offset + listLength), + validatePayload(raw.payload.length >= offset + listLength + 3); + const supportedDoorLockModes: DoorLockMode[] = [ + ...raw.payload.subarray(offset, offset + listLength), ]; offset += listLength; - - this.supportedOutsideHandles = [ - !!(this.payload[offset] & 0b0001_0000), - !!(this.payload[offset] & 0b0010_0000), - !!(this.payload[offset] & 0b0100_0000), - !!(this.payload[offset] & 0b1000_0000), + const supportedOutsideHandles: DoorHandleStatus = [ + !!(raw.payload[offset] & 0b0001_0000), + !!(raw.payload[offset] & 0b0010_0000), + !!(raw.payload[offset] & 0b0100_0000), + !!(raw.payload[offset] & 0b1000_0000), ]; - this.supportedInsideHandles = [ - !!(this.payload[offset] & 0b0001), - !!(this.payload[offset] & 0b0010), - !!(this.payload[offset] & 0b0100), - !!(this.payload[offset] & 0b1000), + const supportedInsideHandles: DoorHandleStatus = [ + !!(raw.payload[offset] & 0b0001), + !!(raw.payload[offset] & 0b0010), + !!(raw.payload[offset] & 0b0100), + !!(raw.payload[offset] & 0b1000), ]; - - this.doorSupported = !!(this.payload[offset + 1] & 0b1); - this.boltSupported = !!(this.payload[offset + 1] & 0b10); - this.latchSupported = !!(this.payload[offset + 1] & 0b100); - - this.blockToBlockSupported = !!(this.payload[offset + 2] & 0b1); - this.twistAssistSupported = !!(this.payload[offset + 2] & 0b10); - this.holdAndReleaseSupported = !!(this.payload[offset + 2] & 0b100); - this.autoRelockSupported = !!(this.payload[offset + 2] & 0b1000); + const doorSupported = !!(raw.payload[offset + 1] & 0b1); + const boltSupported = !!(raw.payload[offset + 1] & 0b10); + const latchSupported = !!(raw.payload[offset + 1] & 0b100); + const blockToBlockSupported = !!(raw.payload[offset + 2] & 0b1); + const twistAssistSupported = !!(raw.payload[offset + 2] & 0b10); + const holdAndReleaseSupported = !!(raw.payload[offset + 2] & 0b100); + const autoRelockSupported = !!(raw.payload[offset + 2] & 0b1000); + + return new DoorLockCCCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + supportedOperationTypes, + supportedDoorLockModes, + supportedOutsideHandles, + supportedInsideHandles, + doorSupported, + boltSupported, + latchSupported, + blockToBlockSupported, + twistAssistSupported, + holdAndReleaseSupported, + autoRelockSupported, + }); } public readonly supportedOperationTypes: readonly DoorLockOperationType[]; diff --git a/packages/cc/src/cc/DoorLockLoggingCC.ts b/packages/cc/src/cc/DoorLockLoggingCC.ts index cbd016bef743..d8ba9ee64628 100644 --- a/packages/cc/src/cc/DoorLockLoggingCC.ts +++ b/packages/cc/src/cc/DoorLockLoggingCC.ts @@ -4,21 +4,24 @@ import { type MessageOrCCLogEntry, MessagePriority, type MessageRecord, + type WithAddress, ZWaveError, ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { isPrintableASCII, num2hex } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -126,7 +129,7 @@ export class DoorLockLoggingCCAPI extends PhysicalCCAPI { const cc = new DoorLockLoggingCCRecordsSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< DoorLockLoggingCCRecordsSupportedReport @@ -149,7 +152,7 @@ export class DoorLockLoggingCCAPI extends PhysicalCCAPI { const cc = new DoorLockLoggingCCRecordGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, recordNumber, }); const response = await this.host.sendCommand< @@ -226,15 +229,33 @@ export class DoorLockLoggingCC extends CommandClass { } } +// @publicAPI +export interface DoorLockLoggingCCRecordsSupportedReportOptions { + recordsCount: number; +} + @CCCommand(DoorLockLoggingCommand.RecordsSupportedReport) export class DoorLockLoggingCCRecordsSupportedReport extends DoorLockLoggingCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.recordsCount = this.payload[0]; + // TODO: Check implementation: + this.recordsCount = options.recordsCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): DoorLockLoggingCCRecordsSupportedReport { + validatePayload(raw.payload.length >= 1); + const recordsCount = raw.payload[0]; + + return new DoorLockLoggingCCRecordsSupportedReport({ + nodeId: ctx.sourceNodeId, + recordsCount, + }); } @ccValue(DoorLockLoggingCCValues.recordsCount) @@ -261,37 +282,51 @@ function eventTypeToLabel(eventType: DoorLockLoggingEventType): string { @expectedCCResponse(DoorLockLoggingCCRecordsSupportedReport) export class DoorLockLoggingCCRecordsSupportedGet extends DoorLockLoggingCC {} +// @publicAPI +export interface DoorLockLoggingCCRecordReportOptions { + recordNumber: number; + record?: DoorLockLoggingRecord; +} + @CCCommand(DoorLockLoggingCommand.RecordReport) export class DoorLockLoggingCCRecordReport extends DoorLockLoggingCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 11); - this.recordNumber = this.payload[0]; - const recordStatus = this.payload[5] >>> 5; - if (recordStatus === DoorLockLoggingRecordStatus.Empty) { - return; - } else { + // TODO: Check implementation: + this.recordNumber = options.recordNumber; + this.record = options.record; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): DoorLockLoggingCCRecordReport { + validatePayload(raw.payload.length >= 11); + const recordNumber = raw.payload[0]; + const recordStatus = raw.payload[5] >>> 5; + let record: DoorLockLoggingRecord | undefined; + if (recordStatus !== DoorLockLoggingRecordStatus.Empty) { const dateSegments = { - year: this.payload.readUInt16BE(1), - month: this.payload[3], - day: this.payload[4], - hour: this.payload[5] & 0b11111, - minute: this.payload[6], - second: this.payload[7], + year: raw.payload.readUInt16BE(1), + month: raw.payload[3], + day: raw.payload[4], + hour: raw.payload[5] & 0b11111, + minute: raw.payload[6], + second: raw.payload[7], }; - const eventType = this.payload[8]; - const recordUserID = this.payload[9]; - const userCodeLength = this.payload[10]; + const eventType = raw.payload[8]; + const recordUserID = raw.payload[9]; + const userCodeLength = raw.payload[10]; validatePayload( userCodeLength <= 10, - this.payload.length >= 11 + userCodeLength, + raw.payload.length >= 11 + userCodeLength, ); - const userCodeBuffer = this.payload.subarray( + const userCodeBuffer = raw.payload.subarray( 11, 11 + userCodeLength, ); @@ -302,7 +337,7 @@ export class DoorLockLoggingCCRecordReport extends DoorLockLoggingCC { ? userCodeString : userCodeBuffer; - this.record = { + record = { eventType: eventType, label: eventTypeToLabel(eventType), timestamp: segmentsToDate(dateSegments).toISOString(), @@ -310,6 +345,12 @@ export class DoorLockLoggingCCRecordReport extends DoorLockLoggingCC { userCode, }; } + + return new DoorLockLoggingCCRecordReport({ + nodeId: ctx.sourceNodeId, + recordNumber, + record, + }); } public readonly recordNumber: number; @@ -345,7 +386,7 @@ export class DoorLockLoggingCCRecordReport extends DoorLockLoggingCC { } // @publicAPI -export interface DoorLockLoggingCCRecordGetOptions extends CCCommandOptions { +export interface DoorLockLoggingCCRecordGetOptions { recordNumber: number; } @@ -366,19 +407,24 @@ function testResponseForDoorLockLoggingRecordGet( ) export class DoorLockLoggingCCRecordGet extends DoorLockLoggingCC { public constructor( - options: - | CommandClassDeserializationOptions - | DoorLockLoggingCCRecordGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.recordNumber = options.recordNumber; - } + this.recordNumber = options.recordNumber; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): DoorLockLoggingCCRecordGet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new DoorLockLoggingCCRecordGet({ + // nodeId: ctx.sourceNodeId, + // }); } public recordNumber: number; diff --git a/packages/cc/src/cc/EnergyProductionCC.ts b/packages/cc/src/cc/EnergyProductionCC.ts index 6754b286b5be..a45eb28c93a0 100644 --- a/packages/cc/src/cc/EnergyProductionCC.ts +++ b/packages/cc/src/cc/EnergyProductionCC.ts @@ -3,12 +3,17 @@ import { type MessageOrCCLogEntry, MessagePriority, ValueMetadata, + type WithAddress, encodeFloatWithScale, parseFloatWithScale, validatePayload, } from "@zwave-js/core"; import { type MaybeNotKnown } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host"; import { getEnumMemberName, pick } from "@zwave-js/shared"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -18,13 +23,11 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -110,7 +113,7 @@ export class EnergyProductionCCAPI extends CCAPI { const cc = new EnergyProductionCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); const response = await this.host.sendCommand< @@ -187,7 +190,7 @@ export class EnergyProductionCC extends CommandClass { } // @publicAPI -export interface EnergyProductionCCReportOptions extends CCCommandOptions { +export interface EnergyProductionCCReportOptions { parameter: EnergyProductionParameter; scale: EnergyProductionScale; value: number; @@ -196,24 +199,34 @@ export interface EnergyProductionCCReportOptions extends CCCommandOptions { @CCCommand(EnergyProductionCommand.Report) export class EnergyProductionCCReport extends EnergyProductionCC { public constructor( - options: - | CommandClassDeserializationOptions - | EnergyProductionCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.parameter = this.payload[0]; - const { value, scale } = parseFloatWithScale( - this.payload.subarray(1), - ); - this.value = value; - this.scale = getEnergyProductionScale(this.parameter, scale); - } else { - this.parameter = options.parameter; - this.value = options.value; - this.scale = options.scale; - } + this.parameter = options.parameter; + this.value = options.value; + this.scale = options.scale; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): EnergyProductionCCReport { + validatePayload(raw.payload.length >= 2); + const parameter: EnergyProductionParameter = raw.payload[0]; + const { value, scale: rawScale } = parseFloatWithScale( + raw.payload.subarray(1), + ); + const scale: EnergyProductionScale = getEnergyProductionScale( + parameter, + rawScale, + ); + + return new EnergyProductionCCReport({ + nodeId: ctx.sourceNodeId, + parameter, + value, + scale, + }); } public readonly parameter: EnergyProductionParameter; @@ -261,7 +274,7 @@ export class EnergyProductionCCReport extends EnergyProductionCC { } // @publicAPI -export interface EnergyProductionCCGetOptions extends CCCommandOptions { +export interface EnergyProductionCCGetOptions { parameter: EnergyProductionParameter; } @@ -279,17 +292,23 @@ function testResponseForEnergyProductionGet( ) export class EnergyProductionCCGet extends EnergyProductionCC { public constructor( - options: - | CommandClassDeserializationOptions - | EnergyProductionCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.parameter = this.payload[0]; - } else { - this.parameter = options.parameter; - } + this.parameter = options.parameter; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): EnergyProductionCCGet { + validatePayload(raw.payload.length >= 1); + const parameter: EnergyProductionParameter = raw.payload[0]; + + return new EnergyProductionCCGet({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: EnergyProductionParameter; diff --git a/packages/cc/src/cc/EntryControlCC.ts b/packages/cc/src/cc/EntryControlCC.ts index e48b916c9aae..e6d932e3e96b 100644 --- a/packages/cc/src/cc/EntryControlCC.ts +++ b/packages/cc/src/cc/EntryControlCC.ts @@ -6,6 +6,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, getCCName, @@ -13,7 +14,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { buffer2hex, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -26,13 +31,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -114,7 +117,7 @@ export class EntryControlCCAPI extends CCAPI { const cc = new EntryControlCCKeySupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< EntryControlCCKeySupportedReport @@ -134,7 +137,7 @@ export class EntryControlCCAPI extends CCAPI { const cc = new EntryControlCCEventSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< EntryControlCCEventSupportedReport @@ -163,7 +166,7 @@ export class EntryControlCCAPI extends CCAPI { const cc = new EntryControlCCConfigurationGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< EntryControlCCConfigurationReport @@ -188,7 +191,7 @@ export class EntryControlCCAPI extends CCAPI { const cc = new EntryControlCCConfigurationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, keyCacheSize, keyCacheTimeout, }); @@ -391,35 +394,54 @@ key cache timeout: ${conf.keyCacheTimeout} seconds`, } } +// @publicAPI +export interface EntryControlCCNotificationOptions { + sequenceNumber: number; + dataType: EntryControlDataTypes; + eventType: EntryControlEventTypes; + eventData?: string | Buffer; +} + @CCCommand(EntryControlCommand.Notification) export class EntryControlCCNotification extends EntryControlCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - this.sequenceNumber = this.payload[0]; - this.dataType = this.payload[1] & 0b11; - this.eventType = this.payload[2]; - const eventDataLength = this.payload[3]; - validatePayload(eventDataLength >= 0 && eventDataLength <= 32); + // TODO: Check implementation: + this.sequenceNumber = options.sequenceNumber; + this.dataType = options.dataType; + this.eventType = options.eventType; + this.eventData = options.eventData; + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): EntryControlCCNotification { + validatePayload(raw.payload.length >= 4); + const sequenceNumber = raw.payload[0]; + let dataType: EntryControlDataTypes = raw.payload[1] & 0b11; + const eventType: EntryControlEventTypes = raw.payload[2]; + const eventDataLength = raw.payload[3]; + validatePayload(eventDataLength >= 0 && eventDataLength <= 32); const offset = 4; - validatePayload(this.payload.length >= offset + eventDataLength); + validatePayload(raw.payload.length >= offset + eventDataLength); + let eventData: string | Buffer | undefined; if (eventDataLength > 0) { // We shouldn't need to check this, since the specs are pretty clear which format to expect. // But as always - manufacturers don't care and send ASCII data with 0 bytes... // We also need to disable the strict validation for some devices to make them work - const noStrictValidation = !!options.context.getDeviceConfig?.( - this.nodeId as number, + const noStrictValidation = !!ctx.getDeviceConfig?.( + ctx.sourceNodeId, )?.compat?.disableStrictEntryControlDataValidation; - const eventData = Buffer.from( - this.payload.subarray(offset, offset + eventDataLength), + eventData = Buffer.from( + raw.payload.subarray(offset, offset + eventDataLength), ); - switch (this.dataType) { + switch (dataType) { case EntryControlDataTypes.Raw: // RAW 1 to 32 bytes of arbitrary binary data if (!noStrictValidation) { @@ -427,7 +449,6 @@ export class EntryControlCCNotification extends EntryControlCC { eventDataLength >= 1 && eventDataLength <= 32, ); } - this.eventData = eventData; break; case EntryControlDataTypes.ASCII: // ASCII 1 to 32 ASCII encoded characters. ASCII codes MUST be in the value range 0x00-0xF7. @@ -438,26 +459,33 @@ export class EntryControlCCNotification extends EntryControlCC { ); } // Using toString("ascii") converts the padding bytes 0xff to 0x7f - this.eventData = eventData.toString("ascii"); + eventData = eventData.toString("ascii"); if (!noStrictValidation) { validatePayload( - /^[\u0000-\u007f]+[\u007f]*$/.test(this.eventData), + /^[\u0000-\u007f]+[\u007f]*$/.test(eventData), ); } // Trim padding - this.eventData = this.eventData.replace(/[\u007f]*$/, ""); + eventData = eventData.replace(/[\u007f]*$/, ""); break; case EntryControlDataTypes.MD5: // MD5 16 byte binary data encoded as a MD5 hash value. if (!noStrictValidation) { validatePayload(eventDataLength === 16); } - this.eventData = eventData; break; } } else { - this.dataType = EntryControlDataTypes.None; + dataType = EntryControlDataTypes.None; } + + return new EntryControlCCNotification({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + dataType, + eventType, + eventData, + }); } public readonly sequenceNumber: number; @@ -492,20 +520,38 @@ export class EntryControlCCNotification extends EntryControlCC { } } +// @publicAPI +export interface EntryControlCCKeySupportedReportOptions { + supportedKeys: number[]; +} + @CCCommand(EntryControlCommand.KeySupportedReport) export class EntryControlCCKeySupportedReport extends EntryControlCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - const length = this.payload[0]; - validatePayload(this.payload.length >= 1 + length); - this.supportedKeys = parseBitMask( - this.payload.subarray(1, 1 + length), + // TODO: Check implementation: + this.supportedKeys = options.supportedKeys; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): EntryControlCCKeySupportedReport { + validatePayload(raw.payload.length >= 1); + const length = raw.payload[0]; + validatePayload(raw.payload.length >= 1 + length); + const supportedKeys = parseBitMask( + raw.payload.subarray(1, 1 + length), 0, ); + + return new EntryControlCCKeySupportedReport({ + nodeId: ctx.sourceNodeId, + supportedKeys, + }); } @ccValue(EntryControlCCValues.supportedKeys) @@ -523,47 +569,76 @@ export class EntryControlCCKeySupportedReport extends EntryControlCC { @expectedCCResponse(EntryControlCCKeySupportedReport) export class EntryControlCCKeySupportedGet extends EntryControlCC {} +// @publicAPI +export interface EntryControlCCEventSupportedReportOptions { + supportedDataTypes: EntryControlDataTypes[]; + supportedEventTypes: EntryControlEventTypes[]; + minKeyCacheSize: number; + maxKeyCacheSize: number; + minKeyCacheTimeout: number; + maxKeyCacheTimeout: number; +} + @CCCommand(EntryControlCommand.EventSupportedReport) export class EntryControlCCEventSupportedReport extends EntryControlCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - const dataTypeLength = this.payload[0] & 0b11; - let offset = 1; + // TODO: Check implementation: + this.supportedDataTypes = options.supportedDataTypes; + this.supportedEventTypes = options.supportedEventTypes; + this.minKeyCacheSize = options.minKeyCacheSize; + this.maxKeyCacheSize = options.maxKeyCacheSize; + this.minKeyCacheTimeout = options.minKeyCacheTimeout; + this.maxKeyCacheTimeout = options.maxKeyCacheTimeout; + } - validatePayload(this.payload.length >= offset + dataTypeLength); - this.supportedDataTypes = parseBitMask( - this.payload.subarray(offset, offset + dataTypeLength), + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): EntryControlCCEventSupportedReport { + validatePayload(raw.payload.length >= 1); + const dataTypeLength = raw.payload[0] & 0b11; + let offset = 1; + validatePayload(raw.payload.length >= offset + dataTypeLength); + const supportedDataTypes: EntryControlDataTypes[] = parseBitMask( + raw.payload.subarray(offset, offset + dataTypeLength), EntryControlDataTypes.None, ); offset += dataTypeLength; - - validatePayload(this.payload.length >= offset + 1); - const eventTypeLength = this.payload[offset] & 0b11111; + validatePayload(raw.payload.length >= offset + 1); + const eventTypeLength = raw.payload[offset] & 0b11111; offset += 1; - - validatePayload(this.payload.length >= offset + eventTypeLength); - this.supportedEventTypes = parseBitMask( - this.payload.subarray(offset, offset + eventTypeLength), + validatePayload(raw.payload.length >= offset + eventTypeLength); + const supportedEventTypes: EntryControlEventTypes[] = parseBitMask( + raw.payload.subarray(offset, offset + eventTypeLength), EntryControlEventTypes.Caching, ); offset += eventTypeLength; - - validatePayload(this.payload.length >= offset + 4); - this.minKeyCacheSize = this.payload[offset]; + validatePayload(raw.payload.length >= offset + 4); + const minKeyCacheSize = raw.payload[offset]; validatePayload( - this.minKeyCacheSize >= 1 && this.minKeyCacheSize <= 32, + minKeyCacheSize >= 1 && minKeyCacheSize <= 32, ); - this.maxKeyCacheSize = this.payload[offset + 1]; + const maxKeyCacheSize = raw.payload[offset + 1]; validatePayload( - this.maxKeyCacheSize >= this.minKeyCacheSize - && this.maxKeyCacheSize <= 32, + maxKeyCacheSize >= minKeyCacheSize + && maxKeyCacheSize <= 32, ); - this.minKeyCacheTimeout = this.payload[offset + 2]; - this.maxKeyCacheTimeout = this.payload[offset + 3]; + const minKeyCacheTimeout = raw.payload[offset + 2]; + const maxKeyCacheTimeout = raw.payload[offset + 3]; + + return new EntryControlCCEventSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedDataTypes, + supportedEventTypes, + minKeyCacheSize, + maxKeyCacheSize, + minKeyCacheTimeout, + maxKeyCacheTimeout, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -621,18 +696,38 @@ export class EntryControlCCEventSupportedReport extends EntryControlCC { @expectedCCResponse(EntryControlCCEventSupportedReport) export class EntryControlCCEventSupportedGet extends EntryControlCC {} +// @publicAPI +export interface EntryControlCCConfigurationReportOptions { + keyCacheSize: number; + keyCacheTimeout: number; +} + @CCCommand(EntryControlCommand.ConfigurationReport) export class EntryControlCCConfigurationReport extends EntryControlCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); + // TODO: Check implementation: + this.keyCacheSize = options.keyCacheSize; + this.keyCacheTimeout = options.keyCacheTimeout; + } - this.keyCacheSize = this.payload[0]; - validatePayload(this.keyCacheSize >= 1 && this.keyCacheSize <= 32); - this.keyCacheTimeout = this.payload[1]; + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): EntryControlCCConfigurationReport { + validatePayload(raw.payload.length >= 2); + const keyCacheSize = raw.payload[0]; + validatePayload(keyCacheSize >= 1 && keyCacheSize <= 32); + const keyCacheTimeout = raw.payload[1]; + + return new EntryControlCCConfigurationReport({ + nodeId: ctx.sourceNodeId, + keyCacheSize, + keyCacheTimeout, + }); } @ccValue(EntryControlCCValues.keyCacheSize) @@ -657,9 +752,7 @@ export class EntryControlCCConfigurationReport extends EntryControlCC { export class EntryControlCCConfigurationGet extends EntryControlCC {} // @publicAPI -export interface EntryControlCCConfigurationSetOptions - extends CCCommandOptions -{ +export interface EntryControlCCConfigurationSetOptions { keyCacheSize: number; keyCacheTimeout: number; } @@ -668,21 +761,26 @@ export interface EntryControlCCConfigurationSetOptions @useSupervision() export class EntryControlCCConfigurationSet extends EntryControlCC { public constructor( - options: - | CommandClassDeserializationOptions - | EntryControlCCConfigurationSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.keyCacheSize = options.keyCacheSize; - this.keyCacheTimeout = options.keyCacheTimeout; - } + this.keyCacheSize = options.keyCacheSize; + this.keyCacheTimeout = options.keyCacheTimeout; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): EntryControlCCConfigurationSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new EntryControlCCConfigurationSet({ + // nodeId: ctx.sourceNodeId, + // }); } public readonly keyCacheSize: number; diff --git a/packages/cc/src/cc/FirmwareUpdateMetaDataCC.ts b/packages/cc/src/cc/FirmwareUpdateMetaDataCC.ts index 0d1ad2a74cec..282d27171710 100644 --- a/packages/cc/src/cc/FirmwareUpdateMetaDataCC.ts +++ b/packages/cc/src/cc/FirmwareUpdateMetaDataCC.ts @@ -5,11 +5,16 @@ import { type MessageOrCCLogEntry, MessagePriority, type MessageRecord, + type WithAddress, ZWaveError, ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { type AllOrNone, getEnumMemberName, @@ -19,12 +24,10 @@ import { import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -112,7 +115,7 @@ export class FirmwareUpdateMetaDataCCAPI extends PhysicalCCAPI { const cc = new FirmwareUpdateMetaDataCCMetaDataGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< FirmwareUpdateMetaDataCCMetaDataReport @@ -148,7 +151,7 @@ export class FirmwareUpdateMetaDataCCAPI extends PhysicalCCAPI { const cc = new FirmwareUpdateMetaDataCCMetaDataReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -169,7 +172,7 @@ export class FirmwareUpdateMetaDataCCAPI extends PhysicalCCAPI { const cc = new FirmwareUpdateMetaDataCCRequestGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -196,7 +199,7 @@ export class FirmwareUpdateMetaDataCCAPI extends PhysicalCCAPI { const cc = new FirmwareUpdateMetaDataCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, reportNumber: fragmentNumber, isLast: isLastFragment, firmwareData: data, @@ -220,7 +223,7 @@ export class FirmwareUpdateMetaDataCCAPI extends PhysicalCCAPI { const cc = new FirmwareUpdateMetaDataCCActivationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); const response = await this.host.sendCommand< @@ -326,73 +329,93 @@ export class FirmwareUpdateMetaDataCCMetaDataReport implements FirmwareUpdateMetaData { public constructor( - options: - | CommandClassDeserializationOptions - | ( - & FirmwareUpdateMetaDataCCMetaDataReportOptions - & CCCommandOptions - ), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 6); - this.manufacturerId = this.payload.readUInt16BE(0); - this.firmwareId = this.payload.readUInt16BE(2); - this.checksum = this.payload.readUInt16BE(4); - // V1/V2 only have a single firmware which must be upgradable - this.firmwareUpgradable = this.payload[6] === 0xff - || this.payload[6] == undefined; - - if (this.payload.length >= 10) { - // V3+ - this.maxFragmentSize = this.payload.readUInt16BE(8); - // Read variable length list of additional firmwares - const numAdditionalFirmwares = this.payload[7]; - const additionalFirmwareIDs = []; - validatePayload( - this.payload.length >= 10 + 2 * numAdditionalFirmwares, + this.manufacturerId = options.manufacturerId; + this.firmwareId = options.firmwareId ?? 0; + this.checksum = options.checksum ?? 0; + this.firmwareUpgradable = options.firmwareUpgradable; + this.maxFragmentSize = options.maxFragmentSize; + this.additionalFirmwareIDs = options.additionalFirmwareIDs ?? []; + this.hardwareVersion = options.hardwareVersion; + this.continuesToFunction = options.continuesToFunction; + this.supportsActivation = options.supportsActivation; + this.supportsResuming = options.supportsResuming; + this.supportsNonSecureTransfer = options.supportsNonSecureTransfer; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCMetaDataReport { + validatePayload(raw.payload.length >= 6); + const manufacturerId = raw.payload.readUInt16BE(0); + const firmwareId = raw.payload.readUInt16BE(2); + const checksum = raw.payload.readUInt16BE(4); + + // V1/V2 only have a single firmware which must be upgradable + const firmwareUpgradable = raw.payload[6] === 0xff + || raw.payload[6] == undefined; + + let maxFragmentSize: number | undefined; + let additionalFirmwareIDs: number[] | undefined; + let hardwareVersion: number | undefined; + let continuesToFunction: MaybeNotKnown; + let supportsActivation: MaybeNotKnown; + let supportsResuming: MaybeNotKnown; + let supportsNonSecureTransfer: MaybeNotKnown; + + if (raw.payload.length >= 10) { + // V3+ + maxFragmentSize = raw.payload.readUInt16BE(8); + // Read variable length list of additional firmwares + const numAdditionalFirmwares = raw.payload[7]; + additionalFirmwareIDs = []; + validatePayload( + raw.payload.length >= 10 + 2 * numAdditionalFirmwares, + ); + for (let i = 0; i < numAdditionalFirmwares; i++) { + additionalFirmwareIDs.push( + raw.payload.readUInt16BE(10 + 2 * i), ); - for (let i = 0; i < numAdditionalFirmwares; i++) { - additionalFirmwareIDs.push( - this.payload.readUInt16BE(10 + 2 * i), - ); - } - this.additionalFirmwareIDs = additionalFirmwareIDs; - // Read hardware version (if it exists) - let offset = 10 + 2 * numAdditionalFirmwares; - if (this.payload.length >= offset + 1) { - // V5+ - this.hardwareVersion = this.payload[offset]; + } + // Read hardware version (if it exists) + let offset = 10 + 2 * numAdditionalFirmwares; + if (raw.payload.length >= offset + 1) { + // V5+ + hardwareVersion = raw.payload[offset]; + offset++; + if (raw.payload.length >= offset + 1) { + // V6+ + const capabilities = raw.payload[offset]; offset++; - if (this.payload.length >= offset + 1) { - // V6+ - const capabilities = this.payload[offset]; - offset++; - - this.continuesToFunction = !!(capabilities & 0b1); - // V7+ - this.supportsActivation = !!(capabilities & 0b10); - // V8+ - this.supportsResuming = !!(capabilities & 0b1000); - this.supportsNonSecureTransfer = - !!(capabilities & 0b100); - } + + continuesToFunction = !!(capabilities & 0b1); + // V7+ + supportsActivation = !!(capabilities & 0b10); + // V8+ + supportsResuming = !!(capabilities & 0b1000); + supportsNonSecureTransfer = !!(capabilities & 0b100); } } - } else { - this.manufacturerId = options.manufacturerId; - this.firmwareId = options.firmwareId ?? 0; - this.checksum = options.checksum ?? 0; - this.firmwareUpgradable = options.firmwareUpgradable; - this.maxFragmentSize = options.maxFragmentSize; - this.additionalFirmwareIDs = options.additionalFirmwareIDs ?? []; - this.hardwareVersion = options.hardwareVersion; - this.continuesToFunction = options.continuesToFunction; - this.supportsActivation = options.supportsActivation; - this.supportsResuming = options.supportsResuming; - this.supportsNonSecureTransfer = options.supportsNonSecureTransfer; } + + return new FirmwareUpdateMetaDataCCMetaDataReport({ + nodeId: ctx.sourceNodeId, + manufacturerId, + firmwareId, + checksum, + firmwareUpgradable, + maxFragmentSize, + additionalFirmwareIDs, + hardwareVersion, + continuesToFunction, + supportsActivation, + supportsResuming, + supportsNonSecureTransfer, + }); } public readonly manufacturerId: number; @@ -482,20 +505,47 @@ export class FirmwareUpdateMetaDataCCMetaDataGet extends FirmwareUpdateMetaDataCC {} +// @publicAPI +export interface FirmwareUpdateMetaDataCCRequestReportOptions { + status: FirmwareUpdateRequestStatus; + resume?: boolean; + nonSecureTransfer?: boolean; +} + @CCCommand(FirmwareUpdateMetaDataCommand.RequestReport) export class FirmwareUpdateMetaDataCCRequestReport extends FirmwareUpdateMetaDataCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.status = this.payload[0]; - if (this.payload.length >= 2) { - this.resume = !!(this.payload[1] & 0b100); - this.nonSecureTransfer = !!(this.payload[1] & 0b10); + + // TODO: Check implementation: + this.status = options.status; + this.resume = options.resume; + this.nonSecureTransfer = options.nonSecureTransfer; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCRequestReport { + validatePayload(raw.payload.length >= 1); + const status: FirmwareUpdateRequestStatus = raw.payload[0]; + let resume: boolean | undefined; + let nonSecureTransfer: boolean | undefined; + if (raw.payload.length >= 2) { + resume = !!(raw.payload[1] & 0b100); + nonSecureTransfer = !!(raw.payload[1] & 0b10); } + + return new FirmwareUpdateMetaDataCCRequestReport({ + nodeId: ctx.sourceNodeId, + status, + resume, + nonSecureTransfer, + }); } public readonly status: FirmwareUpdateRequestStatus; @@ -549,32 +599,37 @@ export class FirmwareUpdateMetaDataCCRequestGet extends FirmwareUpdateMetaDataCC { public constructor( - options: - | CommandClassDeserializationOptions - | (FirmwareUpdateMetaDataCCRequestGetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.manufacturerId = options.manufacturerId; - this.firmwareId = options.firmwareId; - this.checksum = options.checksum; - if ("firmwareTarget" in options) { - this.firmwareTarget = options.firmwareTarget; - this.fragmentSize = options.fragmentSize; - this.activation = options.activation ?? false; - this.hardwareVersion = options.hardwareVersion; - this.resume = options.resume; - this.nonSecureTransfer = options.nonSecureTransfer; - } + this.manufacturerId = options.manufacturerId; + this.firmwareId = options.firmwareId; + this.checksum = options.checksum; + if ("firmwareTarget" in options) { + this.firmwareTarget = options.firmwareTarget; + this.fragmentSize = options.fragmentSize; + this.activation = options.activation ?? false; + this.hardwareVersion = options.hardwareVersion; + this.resume = options.resume; + this.nonSecureTransfer = options.nonSecureTransfer; } } + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCRequestGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new FirmwareUpdateMetaDataCCRequestGet({ + // nodeId: ctx.sourceNodeId, + // }); + } + public manufacturerId: number; public firmwareId: number; public checksum: number; @@ -633,16 +688,38 @@ export class FirmwareUpdateMetaDataCCRequestGet } } +// @publicAPI +export interface FirmwareUpdateMetaDataCCGetOptions { + numReports: number; + reportNumber: number; +} + @CCCommand(FirmwareUpdateMetaDataCommand.Get) // This is sent to us from the node, so we expect no response export class FirmwareUpdateMetaDataCCGet extends FirmwareUpdateMetaDataCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 3); - this.numReports = this.payload[0]; - this.reportNumber = this.payload.readUInt16BE(1) & 0x7fff; + + // TODO: Check implementation: + this.numReports = options.numReports; + this.reportNumber = options.reportNumber; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCGet { + validatePayload(raw.payload.length >= 3); + const numReports = raw.payload[0]; + const reportNumber = raw.payload.readUInt16BE(1) & 0x7fff; + + return new FirmwareUpdateMetaDataCCGet({ + nodeId: ctx.sourceNodeId, + numReports, + reportNumber, + }); } public readonly numReports: number; @@ -660,9 +737,7 @@ export class FirmwareUpdateMetaDataCCGet extends FirmwareUpdateMetaDataCC { } // @publicAPI -export interface FirmwareUpdateMetaDataCCReportOptions - extends CCCommandOptions -{ +export interface FirmwareUpdateMetaDataCCReportOptions { isLast: boolean; reportNumber: number; firmwareData: Buffer; @@ -672,22 +747,27 @@ export interface FirmwareUpdateMetaDataCCReportOptions // We send this in reply to the Get command and expect no response export class FirmwareUpdateMetaDataCCReport extends FirmwareUpdateMetaDataCC { public constructor( - options: - | CommandClassDeserializationOptions - | FirmwareUpdateMetaDataCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.reportNumber = options.reportNumber; - this.firmwareData = options.firmwareData; - this.isLast = options.isLast; - } + this.reportNumber = options.reportNumber; + this.firmwareData = options.firmwareData; + this.isLast = options.isLast; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCReport { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new FirmwareUpdateMetaDataCCReport({ + // nodeId: ctx.sourceNodeId, + // }); } public isLast: boolean; @@ -735,19 +815,42 @@ export class FirmwareUpdateMetaDataCCReport extends FirmwareUpdateMetaDataCC { } } +// @publicAPI +export interface FirmwareUpdateMetaDataCCStatusReportOptions { + status: FirmwareUpdateStatus; + waitTime?: number; +} + @CCCommand(FirmwareUpdateMetaDataCommand.StatusReport) export class FirmwareUpdateMetaDataCCStatusReport extends FirmwareUpdateMetaDataCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.status = this.payload[0]; - if (this.payload.length >= 3) { - this.waitTime = this.payload.readUInt16BE(1); + + // TODO: Check implementation: + this.status = options.status; + this.waitTime = options.waitTime; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCStatusReport { + validatePayload(raw.payload.length >= 1); + const status: FirmwareUpdateStatus = raw.payload[0]; + let waitTime: number | undefined; + if (raw.payload.length >= 3) { + waitTime = raw.payload.readUInt16BE(1); } + + return new FirmwareUpdateMetaDataCCStatusReport({ + nodeId: ctx.sourceNodeId, + status, + waitTime, + }); } public readonly status: FirmwareUpdateStatus; @@ -768,24 +871,59 @@ export class FirmwareUpdateMetaDataCCStatusReport } } +// @publicAPI +export interface FirmwareUpdateMetaDataCCActivationReportOptions { + manufacturerId: number; + firmwareId: number; + checksum: number; + firmwareTarget: number; + activationStatus: FirmwareUpdateActivationStatus; + hardwareVersion?: number; +} + @CCCommand(FirmwareUpdateMetaDataCommand.ActivationReport) export class FirmwareUpdateMetaDataCCActivationReport extends FirmwareUpdateMetaDataCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 8); - this.manufacturerId = this.payload.readUInt16BE(0); - this.firmwareId = this.payload.readUInt16BE(2); - this.checksum = this.payload.readUInt16BE(4); - this.firmwareTarget = this.payload[6]; - this.activationStatus = this.payload[7]; - if (this.payload.length >= 9) { + + // TODO: Check implementation: + this.manufacturerId = options.manufacturerId; + this.firmwareId = options.firmwareId; + this.checksum = options.checksum; + this.firmwareTarget = options.firmwareTarget; + this.activationStatus = options.activationStatus; + this.hardwareVersion = options.hardwareVersion; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCActivationReport { + validatePayload(raw.payload.length >= 8); + const manufacturerId = raw.payload.readUInt16BE(0); + const firmwareId = raw.payload.readUInt16BE(2); + const checksum = raw.payload.readUInt16BE(4); + const firmwareTarget = raw.payload[6]; + const activationStatus: FirmwareUpdateActivationStatus = raw.payload[7]; + let hardwareVersion: number | undefined; + if (raw.payload.length >= 9) { // V5+ - this.hardwareVersion = this.payload[8]; + hardwareVersion = raw.payload[8]; } + + return new FirmwareUpdateMetaDataCCActivationReport({ + nodeId: ctx.sourceNodeId, + manufacturerId, + firmwareId, + checksum, + firmwareTarget, + activationStatus, + hardwareVersion, + }); } public readonly manufacturerId: number; @@ -832,24 +970,29 @@ export class FirmwareUpdateMetaDataCCActivationSet extends FirmwareUpdateMetaDataCC { public constructor( - options: - | CommandClassDeserializationOptions - | (FirmwareUpdateMetaDataCCActivationSetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.manufacturerId = options.manufacturerId; - this.firmwareId = options.firmwareId; - this.checksum = options.checksum; - this.firmwareTarget = options.firmwareTarget; - this.hardwareVersion = options.hardwareVersion; - } + this.manufacturerId = options.manufacturerId; + this.firmwareId = options.firmwareId; + this.checksum = options.checksum; + this.firmwareTarget = options.firmwareTarget; + this.hardwareVersion = options.hardwareVersion; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCActivationSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new FirmwareUpdateMetaDataCCActivationSet({ + // nodeId: ctx.sourceNodeId, + // }); } public manufacturerId: number; @@ -885,17 +1028,39 @@ export class FirmwareUpdateMetaDataCCActivationSet } } +// @publicAPI +export interface FirmwareUpdateMetaDataCCPrepareReportOptions { + status: FirmwareDownloadStatus; + checksum: number; +} + @CCCommand(FirmwareUpdateMetaDataCommand.PrepareReport) export class FirmwareUpdateMetaDataCCPrepareReport extends FirmwareUpdateMetaDataCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 3); - this.status = this.payload[0]; - this.checksum = this.payload.readUInt16BE(1); + + // TODO: Check implementation: + this.status = options.status; + this.checksum = options.checksum; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCPrepareReport { + validatePayload(raw.payload.length >= 3); + const status: FirmwareDownloadStatus = raw.payload[0]; + const checksum = raw.payload.readUInt16BE(1); + + return new FirmwareUpdateMetaDataCCPrepareReport({ + nodeId: ctx.sourceNodeId, + status, + checksum, + }); } public readonly status: FirmwareDownloadStatus; @@ -913,9 +1078,7 @@ export class FirmwareUpdateMetaDataCCPrepareReport } // @publicAPI -export interface FirmwareUpdateMetaDataCCPrepareGetOptions - extends CCCommandOptions -{ +export interface FirmwareUpdateMetaDataCCPrepareGetOptions { manufacturerId: number; firmwareId: number; firmwareTarget: number; @@ -929,24 +1092,29 @@ export class FirmwareUpdateMetaDataCCPrepareGet extends FirmwareUpdateMetaDataCC { public constructor( - options: - | CommandClassDeserializationOptions - | FirmwareUpdateMetaDataCCPrepareGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.manufacturerId = options.manufacturerId; - this.firmwareId = options.firmwareId; - this.firmwareTarget = options.firmwareTarget; - this.fragmentSize = options.fragmentSize; - this.hardwareVersion = options.hardwareVersion; - } + this.manufacturerId = options.manufacturerId; + this.firmwareId = options.firmwareId; + this.firmwareTarget = options.firmwareTarget; + this.fragmentSize = options.fragmentSize; + this.hardwareVersion = options.hardwareVersion; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): FirmwareUpdateMetaDataCCPrepareGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new FirmwareUpdateMetaDataCCPrepareGet({ + // nodeId: ctx.sourceNodeId, + // }); } public manufacturerId: number; diff --git a/packages/cc/src/cc/HumidityControlModeCC.ts b/packages/cc/src/cc/HumidityControlModeCC.ts index 940c1735315c..14618002c504 100644 --- a/packages/cc/src/cc/HumidityControlModeCC.ts +++ b/packages/cc/src/cc/HumidityControlModeCC.ts @@ -5,6 +5,7 @@ import { MessagePriority, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, @@ -12,7 +13,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -25,13 +30,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -126,7 +129,7 @@ export class HumidityControlModeCCAPI extends CCAPI { const cc = new HumidityControlModeCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< HumidityControlModeCCReport @@ -150,7 +153,7 @@ export class HumidityControlModeCCAPI extends CCAPI { const cc = new HumidityControlModeCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, mode, }); return this.host.sendCommand(cc, this.commandOptions); @@ -166,7 +169,7 @@ export class HumidityControlModeCCAPI extends CCAPI { const cc = new HumidityControlModeCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< HumidityControlModeCCSupportedReport @@ -275,7 +278,7 @@ export class HumidityControlModeCC extends CommandClass { } // @publicAPI -export interface HumidityControlModeCCSetOptions extends CCCommandOptions { +export interface HumidityControlModeCCSetOptions { mode: HumidityControlMode; } @@ -283,20 +286,25 @@ export interface HumidityControlModeCCSetOptions extends CCCommandOptions { @useSupervision() export class HumidityControlModeCCSet extends HumidityControlModeCC { public constructor( - options: - | CommandClassDeserializationOptions - | HumidityControlModeCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.mode = options.mode; - } + this.mode = options.mode; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): HumidityControlModeCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new HumidityControlModeCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public mode: HumidityControlMode; @@ -316,15 +324,33 @@ export class HumidityControlModeCCSet extends HumidityControlModeCC { } } +// @publicAPI +export interface HumidityControlModeCCReportOptions { + mode: HumidityControlMode; +} + @CCCommand(HumidityControlModeCommand.Report) export class HumidityControlModeCCReport extends HumidityControlModeCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.mode = this.payload[0] & 0b1111; + // TODO: Check implementation: + this.mode = options.mode; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlModeCCReport { + validatePayload(raw.payload.length >= 1); + const mode: HumidityControlMode = raw.payload[0] & 0b1111; + + return new HumidityControlModeCCReport({ + nodeId: ctx.sourceNodeId, + mode, + }); } @ccValue(HumidityControlModeCCValues.mode) @@ -344,24 +370,41 @@ export class HumidityControlModeCCReport extends HumidityControlModeCC { @expectedCCResponse(HumidityControlModeCCReport) export class HumidityControlModeCCGet extends HumidityControlModeCC {} +// @publicAPI +export interface HumidityControlModeCCSupportedReportOptions { + supportedModes: HumidityControlMode[]; +} + @CCCommand(HumidityControlModeCommand.SupportedReport) export class HumidityControlModeCCSupportedReport extends HumidityControlModeCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this._supportedModes = parseBitMask( - this.payload, + // TODO: Check implementation: + this.supportedModes = options.supportedModes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlModeCCSupportedReport { + validatePayload(raw.payload.length >= 1); + const supportedModes: HumidityControlMode[] = parseBitMask( + raw.payload, HumidityControlMode.Off, ); - - if (!this._supportedModes.includes(HumidityControlMode.Off)) { - this._supportedModes.unshift(HumidityControlMode.Off); + if (!supportedModes.includes(HumidityControlMode.Off)) { + supportedModes.unshift(HumidityControlMode.Off); } + + return new HumidityControlModeCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedModes, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -373,18 +416,15 @@ export class HumidityControlModeCCSupportedReport ...modeValue.meta, states: enumValuesToMetadataStates( HumidityControlMode, - this._supportedModes, + this.supportedModes, ), }); return true; } - private _supportedModes: HumidityControlMode[]; @ccValue(HumidityControlModeCCValues.supportedModes) - public get supportedModes(): readonly HumidityControlMode[] { - return this._supportedModes; - } + public supportedModes: HumidityControlMode[]; public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { return { diff --git a/packages/cc/src/cc/HumidityControlOperatingStateCC.ts b/packages/cc/src/cc/HumidityControlOperatingStateCC.ts index 95cd60109923..868a14d6ebef 100644 --- a/packages/cc/src/cc/HumidityControlOperatingStateCC.ts +++ b/packages/cc/src/cc/HumidityControlOperatingStateCC.ts @@ -4,10 +4,11 @@ import { type MessageOrCCLogEntry, MessagePriority, ValueMetadata, + type WithAddress, enumValuesToMetadataStates, validatePayload, } from "@zwave-js/core/safe"; -import type { GetValueDB } from "@zwave-js/host/safe"; +import type { CCParsingContext, GetValueDB } from "@zwave-js/host/safe"; import { getEnumMemberName } from "@zwave-js/shared/safe"; import { CCAPI, @@ -16,8 +17,8 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, } from "../lib/CommandClass"; @@ -89,7 +90,7 @@ export class HumidityControlOperatingStateCCAPI extends CCAPI { const cc = new HumidityControlOperatingStateCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< HumidityControlOperatingStateCCReport @@ -160,17 +161,35 @@ export class HumidityControlOperatingStateCC extends CommandClass { } } +// @publicAPI +export interface HumidityControlOperatingStateCCReportOptions { + state: HumidityControlOperatingState; +} + @CCCommand(HumidityControlOperatingStateCommand.Report) export class HumidityControlOperatingStateCCReport extends HumidityControlOperatingStateCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.state = this.payload[0] & 0b1111; + // TODO: Check implementation: + this.state = options.state; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlOperatingStateCCReport { + validatePayload(raw.payload.length >= 1); + const state: HumidityControlOperatingState = raw.payload[0] & 0b1111; + + return new HumidityControlOperatingStateCCReport({ + nodeId: ctx.sourceNodeId, + state, + }); } @ccValue(HumidityControlOperatingStateCCValues.state) diff --git a/packages/cc/src/cc/HumidityControlSetpointCC.ts b/packages/cc/src/cc/HumidityControlSetpointCC.ts index 82eb4d7d199c..0c4b9ef57980 100644 --- a/packages/cc/src/cc/HumidityControlSetpointCC.ts +++ b/packages/cc/src/cc/HumidityControlSetpointCC.ts @@ -7,6 +7,7 @@ import { type SupervisionResult, ValueMetadata, type ValueMetadataNumeric, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeFloatWithScale, @@ -17,7 +18,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -30,13 +35,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -207,7 +210,7 @@ export class HumidityControlSetpointCCAPI extends CCAPI { const cc = new HumidityControlSetpointCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, }); const response = await this.host.sendCommand< @@ -240,7 +243,7 @@ export class HumidityControlSetpointCCAPI extends CCAPI { const cc = new HumidityControlSetpointCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, value, scale, @@ -259,7 +262,7 @@ export class HumidityControlSetpointCCAPI extends CCAPI { const cc = new HumidityControlSetpointCCCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, }); const response = await this.host.sendCommand< @@ -288,7 +291,7 @@ export class HumidityControlSetpointCCAPI extends CCAPI { const cc = new HumidityControlSetpointCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< HumidityControlSetpointCCSupportedReport @@ -310,7 +313,7 @@ export class HumidityControlSetpointCCAPI extends CCAPI { const cc = new HumidityControlSetpointCCScaleSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, }); const response = await this.host.sendCommand< @@ -511,7 +514,7 @@ maximum value: ${setpointCaps.maxValue} ${maxValueUnit}`; } // @publicAPI -export interface HumidityControlSetpointCCSetOptions extends CCCommandOptions { +export interface HumidityControlSetpointCCSetOptions { setpointType: HumidityControlSetpointType; value: number; scale: number; @@ -521,22 +524,27 @@ export interface HumidityControlSetpointCCSetOptions extends CCCommandOptions { @useSupervision() export class HumidityControlSetpointCCSet extends HumidityControlSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | HumidityControlSetpointCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.setpointType = options.setpointType; - this.value = options.value; - this.scale = options.scale; - } + this.setpointType = options.setpointType; + this.value = options.value; + this.scale = options.scale; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): HumidityControlSetpointCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new HumidityControlSetpointCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public setpointType: HumidityControlSetpointType; @@ -566,27 +574,53 @@ export class HumidityControlSetpointCCSet extends HumidityControlSetpointCC { } } +// @publicAPI +export interface HumidityControlSetpointCCReportOptions { + type: HumidityControlSetpointType; + scale: number; + value: number; +} + @CCCommand(HumidityControlSetpointCommand.Report) export class HumidityControlSetpointCCReport extends HumidityControlSetpointCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this._type = this.payload[0] & 0b1111; + // TODO: Check implementation: + this.type = options.type; + this.value = options.value; + this.scale = options.scale; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlSetpointCCReport { + validatePayload(raw.payload.length >= 1); + const type: HumidityControlSetpointType = raw.payload[0] & 0b1111; + // Setpoint type 0 is not defined in the spec, prevent devices from using it. - if (this._type === 0) { + if (type === 0) { // Not supported - this._value = 0; - this.scale = 0; - return; + return new HumidityControlSetpointCCReport({ + nodeId: ctx.sourceNodeId, + type, + value: 0, + scale: 0, + }); } // parseFloatWithScale does its own validation - const { value, scale } = parseFloatWithScale(this.payload.subarray(1)); - this._value = value; - this.scale = scale; + const { value, scale } = parseFloatWithScale(raw.payload.subarray(1)); + + return new HumidityControlSetpointCCReport({ + nodeId: ctx.sourceNodeId, + type, + value, + scale, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -609,7 +643,7 @@ export class HumidityControlSetpointCCReport extends HumidityControlSetpointCC { unit: scale.unit, }); } - this.setValue(ctx, setpointValue, this._value); + this.setValue(ctx, setpointValue, this.value); // Remember the device-preferred setpoint scale so it can be used in SET commands this.setValue( @@ -620,17 +654,9 @@ export class HumidityControlSetpointCCReport extends HumidityControlSetpointCC { return true; } - private _type: HumidityControlSetpointType; - public get type(): HumidityControlSetpointType { - return this._type; - } - - public readonly scale: number; - - private _value: number; - public get value(): number { - return this._value; - } + public type: HumidityControlSetpointType; + public scale: number; + public value: number; public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { const scale = getScale(this.scale); @@ -656,7 +682,7 @@ function testResponseForHumidityControlSetpointGet( } // @publicAPI -export interface HumidityControlSetpointCCGetOptions extends CCCommandOptions { +export interface HumidityControlSetpointCCGetOptions { setpointType: HumidityControlSetpointType; } @@ -667,20 +693,25 @@ export interface HumidityControlSetpointCCGetOptions extends CCCommandOptions { ) export class HumidityControlSetpointCCGet extends HumidityControlSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | HumidityControlSetpointCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.setpointType = options.setpointType; - } + this.setpointType = options.setpointType; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): HumidityControlSetpointCCGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new HumidityControlSetpointCCGet({ + // nodeId: ctx.sourceNodeId, + // }); } public setpointType: HumidityControlSetpointType; @@ -703,20 +734,39 @@ export class HumidityControlSetpointCCGet extends HumidityControlSetpointCC { } } +// @publicAPI +export interface HumidityControlSetpointCCSupportedReportOptions { + supportedSetpointTypes: HumidityControlSetpointType[]; +} + @CCCommand(HumidityControlSetpointCommand.SupportedReport) export class HumidityControlSetpointCCSupportedReport extends HumidityControlSetpointCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.supportedSetpointTypes = parseBitMask( - this.payload, - HumidityControlSetpointType["N/A"], - ); + // TODO: Check implementation: + this.supportedSetpointTypes = options.supportedSetpointTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlSetpointCCSupportedReport { + validatePayload(raw.payload.length >= 1); + const supportedSetpointTypes: HumidityControlSetpointType[] = + parseBitMask( + raw.payload, + HumidityControlSetpointType["N/A"], + ); + + return new HumidityControlSetpointCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedSetpointTypes, + }); } @ccValue(HumidityControlSetpointCCValues.supportedSetpointTypes) @@ -749,21 +799,40 @@ export class HumidityControlSetpointCCSupportedGet extends HumidityControlSetpointCC {} +// @publicAPI +export interface HumidityControlSetpointCCScaleSupportedReportOptions { + supportedScales: number[]; +} + @CCCommand(HumidityControlSetpointCommand.ScaleSupportedReport) export class HumidityControlSetpointCCScaleSupportedReport extends HumidityControlSetpointCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress< + HumidityControlSetpointCCScaleSupportedReportOptions + >, ) { super(options); - validatePayload(this.payload.length >= 1); + // TODO: Check implementation: + this.supportedScales = options.supportedScales; + } - this.supportedScales = parseBitMask( - Buffer.from([this.payload[0] & 0b1111]), + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlSetpointCCScaleSupportedReport { + validatePayload(raw.payload.length >= 1); + const supportedScales = parseBitMask( + Buffer.from([raw.payload[0] & 0b1111]), 0, ); + + return new HumidityControlSetpointCCScaleSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedScales, + }); } public readonly supportedScales: readonly number[]; @@ -784,9 +853,7 @@ export class HumidityControlSetpointCCScaleSupportedReport } // @publicAPI -export interface HumidityControlSetpointCCScaleSupportedGetOptions - extends CCCommandOptions -{ +export interface HumidityControlSetpointCCScaleSupportedGetOptions { setpointType: HumidityControlSetpointType; } @@ -796,20 +863,25 @@ export class HumidityControlSetpointCCScaleSupportedGet extends HumidityControlSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | HumidityControlSetpointCCScaleSupportedGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.setpointType = options.setpointType; - } + this.setpointType = options.setpointType; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): HumidityControlSetpointCCScaleSupportedGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new HumidityControlSetpointCCScaleSupportedGet({ + // nodeId: ctx.sourceNodeId, + // }); } public setpointType: HumidityControlSetpointType; @@ -832,26 +904,58 @@ export class HumidityControlSetpointCCScaleSupportedGet } } +// @publicAPI +export interface HumidityControlSetpointCCCapabilitiesReportOptions { + type: HumidityControlSetpointType; + minValue: number; + maxValue: number; + minValueScale: number; + maxValueScale: number; +} + @CCCommand(HumidityControlSetpointCommand.CapabilitiesReport) export class HumidityControlSetpointCCCapabilitiesReport extends HumidityControlSetpointCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress< + HumidityControlSetpointCCCapabilitiesReportOptions + >, ) { super(options); - validatePayload(this.payload.length >= 1); - this._type = this.payload[0] & 0b1111; - let bytesRead: number; + this.type = options.type; + this.minValue = options.minValue; + this.maxValue = options.maxValue; + this.minValueScale = options.minValueScale; + this.maxValueScale = options.maxValueScale; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): HumidityControlSetpointCCCapabilitiesReport { + validatePayload(raw.payload.length >= 1); + const type: HumidityControlSetpointType = raw.payload[0] & 0b1111; + // parseFloatWithScale does its own validation - ({ - value: this._minValue, - scale: this._minValueScale, + const { + value: minValue, + scale: minValueScale, bytesRead, - } = parseFloatWithScale(this.payload.subarray(1))); - ({ value: this._maxValue, scale: this._maxValueScale } = - parseFloatWithScale(this.payload.subarray(1 + bytesRead))); + } = parseFloatWithScale(raw.payload.subarray(1)); + const { value: maxValue, scale: maxValueScale } = parseFloatWithScale( + raw.payload.subarray(1 + bytesRead), + ); + + return new HumidityControlSetpointCCCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + type, + minValue, + minValueScale, + maxValue, + maxValueScale, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -863,39 +967,20 @@ export class HumidityControlSetpointCCCapabilitiesReport ); this.setMetadata(ctx, setpointValue, { ...setpointValue.meta, - min: this._minValue, - max: this._maxValue, - unit: getSetpointUnit(this._minValueScale) - || getSetpointUnit(this._maxValueScale), + min: this.minValue, + max: this.maxValue, + unit: getSetpointUnit(this.minValueScale) + || getSetpointUnit(this.maxValueScale), }); return true; } - private _type: HumidityControlSetpointType; - public get type(): HumidityControlSetpointType { - return this._type; - } - - private _minValue: number; - public get minValue(): number { - return this._minValue; - } - - private _maxValue: number; - public get maxValue(): number { - return this._maxValue; - } - - private _minValueScale: number; - public get minValueScale(): number { - return this._minValueScale; - } - - private _maxValueScale: number; - public get maxValueScale(): number { - return this._maxValueScale; - } + public type: HumidityControlSetpointType; + public minValue: number; + public maxValue: number; + public minValueScale: number; + public maxValueScale: number; public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { const minValueScale = getScale(this.minValueScale); @@ -915,9 +1000,7 @@ export class HumidityControlSetpointCCCapabilitiesReport } // @publicAPI -export interface HumidityControlSetpointCCCapabilitiesGetOptions - extends CCCommandOptions -{ +export interface HumidityControlSetpointCCCapabilitiesGetOptions { setpointType: HumidityControlSetpointType; } @@ -927,20 +1010,25 @@ export class HumidityControlSetpointCCCapabilitiesGet extends HumidityControlSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | HumidityControlSetpointCCCapabilitiesGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.setpointType = options.setpointType; - } + this.setpointType = options.setpointType; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): HumidityControlSetpointCCCapabilitiesGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new HumidityControlSetpointCCCapabilitiesGet({ + // nodeId: ctx.sourceNodeId, + // }); } public setpointType: HumidityControlSetpointType; diff --git a/packages/cc/src/cc/InclusionControllerCC.ts b/packages/cc/src/cc/InclusionControllerCC.ts index ba26ef111bcc..e295314cb6ac 100644 --- a/packages/cc/src/cc/InclusionControllerCC.ts +++ b/packages/cc/src/cc/InclusionControllerCC.ts @@ -1,18 +1,18 @@ import { CommandClasses, type MessageOrCCLogEntry, + type WithAddress, validatePayload, } from "@zwave-js/core"; import { type MaybeNotKnown } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host"; import { getEnumMemberName } from "@zwave-js/shared"; import { CCAPI } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -59,7 +59,7 @@ export class InclusionControllerCCAPI extends CCAPI { const cc = new InclusionControllerCCInitiate({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, includedNodeId: nodeId, step, }); @@ -78,7 +78,7 @@ export class InclusionControllerCCAPI extends CCAPI { const cc = new InclusionControllerCCComplete({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, step, status, }); @@ -87,7 +87,7 @@ export class InclusionControllerCCAPI extends CCAPI { } // @publicAPI -export interface InclusionControllerCCCompleteOptions extends CCCommandOptions { +export interface InclusionControllerCCCompleteOptions { step: InclusionControllerStep; status: InclusionControllerStatus; } @@ -95,22 +95,30 @@ export interface InclusionControllerCCCompleteOptions extends CCCommandOptions { @CCCommand(InclusionControllerCommand.Complete) export class InclusionControllerCCComplete extends InclusionControllerCC { public constructor( - options: - | CommandClassDeserializationOptions - | InclusionControllerCCCompleteOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.step = this.payload[0]; - validatePayload.withReason("Invalid inclusion controller step")( - this.step in InclusionControllerStep, - ); - this.status = this.payload[1]; - } else { - this.step = options.step; - this.status = options.status; - } + this.step = options.step; + this.status = options.status; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): InclusionControllerCCComplete { + validatePayload(raw.payload.length >= 2); + const step: InclusionControllerStep = raw.payload[0]; + + validatePayload.withReason("Invalid inclusion controller step")( + step in InclusionControllerStep, + ); + const status: InclusionControllerStatus = raw.payload[1]; + + return new InclusionControllerCCComplete({ + nodeId: ctx.sourceNodeId, + step, + status, + }); } public step: InclusionControllerStep; @@ -136,7 +144,7 @@ export class InclusionControllerCCComplete extends InclusionControllerCC { } // @publicAPI -export interface InclusionControllerCCInitiateOptions extends CCCommandOptions { +export interface InclusionControllerCCInitiateOptions { includedNodeId: number; step: InclusionControllerStep; } @@ -144,22 +152,30 @@ export interface InclusionControllerCCInitiateOptions extends CCCommandOptions { @CCCommand(InclusionControllerCommand.Initiate) export class InclusionControllerCCInitiate extends InclusionControllerCC { public constructor( - options: - | CommandClassDeserializationOptions - | InclusionControllerCCInitiateOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.includedNodeId = this.payload[0]; - this.step = this.payload[1]; - validatePayload.withReason("Invalid inclusion controller step")( - this.step in InclusionControllerStep, - ); - } else { - this.includedNodeId = options.includedNodeId; - this.step = options.step; - } + this.includedNodeId = options.includedNodeId; + this.step = options.step; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): InclusionControllerCCInitiate { + validatePayload(raw.payload.length >= 2); + const includedNodeId = raw.payload[0]; + const step: InclusionControllerStep = raw.payload[1]; + + validatePayload.withReason("Invalid inclusion controller step")( + step in InclusionControllerStep, + ); + + return new InclusionControllerCCInitiate({ + nodeId: ctx.sourceNodeId, + includedNodeId, + step, + }); } public includedNodeId: number; diff --git a/packages/cc/src/cc/IndicatorCC.ts b/packages/cc/src/cc/IndicatorCC.ts index 8e8e75e03d13..e54b09efa770 100644 --- a/packages/cc/src/cc/IndicatorCC.ts +++ b/packages/cc/src/cc/IndicatorCC.ts @@ -8,6 +8,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeBitMask, @@ -15,7 +16,11 @@ import { parseBitMask, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { num2hex } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { clamp, roundTo } from "alcalzone-shared/math"; @@ -30,13 +35,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -358,7 +361,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, indicatorId, }); const response = await this.host.sendCommand( @@ -395,7 +398,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...(typeof value === "number" ? { value } : { values: value }), }); return this.host.sendCommand(cc, this.commandOptions); @@ -403,7 +406,7 @@ export class IndicatorCCAPI extends CCAPI { @validateArgs() public async sendReport( - options: IndicatorCCReportSpecificOptions, + options: IndicatorCCReportOptions, ): Promise { this.assertSupportsCommand( IndicatorCommand, @@ -412,7 +415,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -435,7 +438,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, indicatorId, }); const response = await this.host.sendCommand< @@ -469,7 +472,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCSupportedReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, indicatorId, supportedProperties, nextIndicatorId, @@ -490,7 +493,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCDescriptionReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, indicatorId, description, }); @@ -670,7 +673,7 @@ export class IndicatorCCAPI extends CCAPI { const cc = new IndicatorCCDescriptionGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, indicatorId, }); const response = await this.host.sendCommand< @@ -880,6 +883,7 @@ export class IndicatorCC extends CommandClass { } } +// @publicAPI export interface IndicatorObject { indicatorId: number; propertyId: number; @@ -899,39 +903,49 @@ export type IndicatorCCSetOptions = @useSupervision() export class IndicatorCCSet extends IndicatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | (IndicatorCCSetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - - const objCount = this.payload.length >= 2 - ? this.payload[1] & 0b11111 - : 0; - if (objCount === 0) { - this.indicator0Value = this.payload[0]; - } else { - validatePayload(this.payload.length >= 2 + 3 * objCount); - this.values = []; - for (let i = 0; i < objCount; i++) { - const offset = 2 + 3 * i; - const value: IndicatorObject = { - indicatorId: this.payload[offset], - propertyId: this.payload[offset + 1], - value: this.payload[offset + 2], - }; - this.values.push(value); - } - } + if ("value" in options) { + this.indicator0Value = options.value; } else { - if ("value" in options) { - this.indicator0Value = options.value; - } else { - this.values = options.values; - } + this.values = options.values; + } + } + + public static from(raw: CCRaw, ctx: CCParsingContext): IndicatorCCSet { + validatePayload(raw.payload.length >= 1); + + const objCount = raw.payload.length >= 2 + ? raw.payload[1] & 0b11111 + : 0; + + if (objCount === 0) { + const indicator0Value = raw.payload[0]; + + return new IndicatorCCSet({ + nodeId: ctx.sourceNodeId, + value: indicator0Value, + }); + } + + validatePayload(raw.payload.length >= 2 + 3 * objCount); + + const values: IndicatorObject[] = []; + for (let i = 0; i < objCount; i++) { + const offset = 2 + 3 * i; + const value: IndicatorObject = { + indicatorId: raw.payload[offset], + propertyId: raw.payload[offset + 1], + value: raw.payload[offset + 2], + }; + values.push(value); } + + return new IndicatorCCSet({ + nodeId: ctx.sourceNodeId, + values, + }); } public indicator0Value: number | undefined; @@ -987,7 +1001,7 @@ export class IndicatorCCSet extends IndicatorCC { } // @publicAPI -export type IndicatorCCReportSpecificOptions = +export type IndicatorCCReportOptions = | { value: number; } @@ -998,72 +1012,81 @@ export type IndicatorCCReportSpecificOptions = @CCCommand(IndicatorCommand.Report) export class IndicatorCCReport extends IndicatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | (IndicatorCCReportSpecificOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); + if ("value" in options) { + this.indicator0Value = options.value; + } else if ("values" in options) { + if (options.values.length > MAX_INDICATOR_OBJECTS) { + throw new ZWaveError( + `Only ${MAX_INDICATOR_OBJECTS} indicator values can be set at a time!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.values = options.values; + } + } + + public static from(raw: CCRaw, ctx: CCParsingContext): IndicatorCCReport { + validatePayload(raw.payload.length >= 1); - const objCount = this.payload.length >= 2 - ? this.payload[1] & 0b11111 - : 0; - if (objCount === 0) { - this.indicator0Value = this.payload[0]; - } else { - validatePayload(this.payload.length >= 2 + 3 * objCount); - this.values = []; - for (let i = 0; i < objCount; i++) { - const offset = 2 + 3 * i; - const value: IndicatorObject = { - indicatorId: this.payload[offset], - propertyId: this.payload[offset + 1], - value: this.payload[offset + 2], - }; - this.values.push(value); - } + const objCount = raw.payload.length >= 2 + ? raw.payload[1] & 0b11111 + : 0; - // TODO: Think if we want this: - - // // If not all Property IDs are included in the command for the actual Indicator ID, - // // a controlling node MUST assume non-specified Property IDs values to be 0x00. - // const indicatorId = this.values[0].indicatorId; - // const supportedIndicatorProperties = - // valueDB.getValue( - // getSupportedPropertyIDsValueID( - // this.endpointIndex, - // indicatorId, - // ), - // ) ?? []; - // // Find out which ones are missing - // const missingIndicatorProperties = supportedIndicatorProperties.filter( - // prop => - // !this.values!.find(({ propertyId }) => prop === propertyId), - // ); - // // And assume they are 0 (false) - // for (const missing of missingIndicatorProperties) { - // this.setIndicatorValue({ - // indicatorId, - // propertyId: missing, - // value: 0, - // }); - // } - } - } else { - if ("value" in options) { - this.indicator0Value = options.value; - } else if ("values" in options) { - if (options.values.length > MAX_INDICATOR_OBJECTS) { - throw new ZWaveError( - `Only ${MAX_INDICATOR_OBJECTS} indicator values can be set at a time!`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.values = options.values; - } + if (objCount === 0) { + const indicator0Value = raw.payload[0]; + return new IndicatorCCReport({ + nodeId: ctx.sourceNodeId, + value: indicator0Value, + }); } + + validatePayload(raw.payload.length >= 2 + 3 * objCount); + + const values: IndicatorObject[] = []; + for (let i = 0; i < objCount; i++) { + const offset = 2 + 3 * i; + const value: IndicatorObject = { + indicatorId: raw.payload[offset], + propertyId: raw.payload[offset + 1], + value: raw.payload[offset + 2], + }; + values.push(value); + } + + return new IndicatorCCReport({ + nodeId: ctx.sourceNodeId, + values, + }); + + // TODO: Think if we want this: + + // // If not all Property IDs are included in the command for the actual Indicator ID, + // // a controlling node MUST assume non-specified Property IDs values to be 0x00. + // const indicatorId = this.values[0].indicatorId; + // const supportedIndicatorProperties = + // valueDB.getValue( + // getSupportedPropertyIDsValueID( + // this.endpointIndex, + // indicatorId, + // ), + // ) ?? []; + // // Find out which ones are missing + // const missingIndicatorProperties = supportedIndicatorProperties.filter( + // prop => + // !this.values!.find(({ propertyId }) => prop === propertyId), + // ); + // // And assume they are 0 (false) + // for (const missing of missingIndicatorProperties) { + // this.setIndicatorValue({ + // indicatorId, + // propertyId: missing, + // value: 0, + // }); + // } } public persistValues(ctx: PersistValuesContext): boolean { @@ -1199,7 +1222,7 @@ export class IndicatorCCReport extends IndicatorCC { } // @publicAPI -export interface IndicatorCCGetOptions extends CCCommandOptions { +export interface IndicatorCCGetOptions { indicatorId?: number; } @@ -1207,16 +1230,23 @@ export interface IndicatorCCGetOptions extends CCCommandOptions { @expectedCCResponse(IndicatorCCReport) export class IndicatorCCGet extends IndicatorCC { public constructor( - options: CommandClassDeserializationOptions | IndicatorCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - if (this.payload.length > 0) { - this.indicatorId = this.payload[0]; - } - } else { - this.indicatorId = options.indicatorId; + this.indicatorId = options.indicatorId; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): IndicatorCCGet { + let indicatorId: number | undefined; + + if (raw.payload.length > 0) { + indicatorId = raw.payload[0]; } + + return new IndicatorCCGet({ + nodeId: ctx.sourceNodeId, + indicatorId, + }); } public indicatorId: number | undefined; @@ -1239,7 +1269,7 @@ export class IndicatorCCGet extends IndicatorCC { } // @publicAPI -export interface IndicatorCCSupportedReportOptions extends CCCommandOptions { +export interface IndicatorCCSupportedReportOptions { indicatorId: number; nextIndicatorId: number; supportedProperties: readonly number[]; @@ -1248,32 +1278,42 @@ export interface IndicatorCCSupportedReportOptions extends CCCommandOptions { @CCCommand(IndicatorCommand.SupportedReport) export class IndicatorCCSupportedReport extends IndicatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | IndicatorCCSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.indicatorId = this.payload[0]; - this.nextIndicatorId = this.payload[1]; - const bitMaskLength = this.payload[2] & 0b11111; - if (bitMaskLength === 0) { - this.supportedProperties = []; - } else { - validatePayload(this.payload.length >= 3 + bitMaskLength); - // The bit mask starts at 0, but bit 0 is not used - this.supportedProperties = parseBitMask( - this.payload.subarray(3, 3 + bitMaskLength), - 0, - ).filter((v) => v !== 0); - } + this.indicatorId = options.indicatorId; + this.nextIndicatorId = options.nextIndicatorId; + this.supportedProperties = options.supportedProperties; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IndicatorCCSupportedReport { + validatePayload(raw.payload.length >= 3); + const indicatorId = raw.payload[0]; + const nextIndicatorId = raw.payload[1]; + const bitMaskLength = raw.payload[2] & 0b11111; + let supportedProperties: readonly number[]; + + if (bitMaskLength === 0) { + supportedProperties = []; } else { - this.indicatorId = options.indicatorId; - this.nextIndicatorId = options.nextIndicatorId; - this.supportedProperties = options.supportedProperties; + validatePayload(raw.payload.length >= 3 + bitMaskLength); + // The bit mask starts at 0, but bit 0 is not used + supportedProperties = parseBitMask( + raw.payload.subarray(3, 3 + bitMaskLength), + 0, + ).filter((v) => v !== 0); } + + return new IndicatorCCSupportedReport({ + nodeId: ctx.sourceNodeId, + indicatorId, + nextIndicatorId, + supportedProperties, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -1331,7 +1371,7 @@ export class IndicatorCCSupportedReport extends IndicatorCC { } // @publicAPI -export interface IndicatorCCSupportedGetOptions extends CCCommandOptions { +export interface IndicatorCCSupportedGetOptions { indicatorId: number; } @@ -1349,17 +1389,23 @@ function testResponseForIndicatorSupportedGet( ) export class IndicatorCCSupportedGet extends IndicatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | IndicatorCCSupportedGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.indicatorId = this.payload[0]; - } else { - this.indicatorId = options.indicatorId; - } + this.indicatorId = options.indicatorId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IndicatorCCSupportedGet { + validatePayload(raw.payload.length >= 1); + const indicatorId = raw.payload[0]; + + return new IndicatorCCSupportedGet({ + nodeId: ctx.sourceNodeId, + indicatorId, + }); } public indicatorId: number; @@ -1388,24 +1434,31 @@ export interface IndicatorCCDescriptionReportOptions { @CCCommand(IndicatorCommand.DescriptionReport) export class IndicatorCCDescriptionReport extends IndicatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | (IndicatorCCDescriptionReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.indicatorId = this.payload[0]; - const descrptionLength = this.payload[1]; - validatePayload(this.payload.length >= 2 + descrptionLength); - this.description = this.payload - .subarray(2, 2 + descrptionLength) - .toString("utf8"); - } else { - this.indicatorId = options.indicatorId; - this.description = options.description; - } + this.indicatorId = options.indicatorId; + this.description = options.description; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IndicatorCCDescriptionReport { + validatePayload(raw.payload.length >= 2); + const indicatorId = raw.payload[0]; + const descrptionLength = raw.payload[1]; + validatePayload(raw.payload.length >= 2 + descrptionLength); + const description: string = raw.payload + .subarray(2, 2 + descrptionLength) + .toString("utf8"); + + return new IndicatorCCDescriptionReport({ + nodeId: ctx.sourceNodeId, + indicatorId, + description, + }); } public indicatorId: number; @@ -1446,7 +1499,7 @@ export class IndicatorCCDescriptionReport extends IndicatorCC { } // @publicAPI -export interface IndicatorCCDescriptionGetOptions extends CCCommandOptions { +export interface IndicatorCCDescriptionGetOptions { indicatorId: number; } @@ -1464,25 +1517,31 @@ function testResponseForIndicatorDescriptionGet( ) export class IndicatorCCDescriptionGet extends IndicatorCC { public constructor( - options: - | CommandClassDeserializationOptions - | IndicatorCCDescriptionGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.indicatorId = this.payload[0]; - } else { - this.indicatorId = options.indicatorId; - if (!isManufacturerDefinedIndicator(this.indicatorId)) { - throw new ZWaveError( - "The indicator ID must be between 0x80 and 0x9f", - ZWaveErrorCodes.Argument_Invalid, - ); - } + this.indicatorId = options.indicatorId; + if (!isManufacturerDefinedIndicator(this.indicatorId)) { + throw new ZWaveError( + "The indicator ID must be between 0x80 and 0x9f", + ZWaveErrorCodes.Argument_Invalid, + ); } } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IndicatorCCDescriptionGet { + validatePayload(raw.payload.length >= 1); + const indicatorId = raw.payload[0]; + + return new IndicatorCCDescriptionGet({ + nodeId: ctx.sourceNodeId, + indicatorId, + }); + } + public indicatorId: number; public serialize(ctx: CCEncodingContext): Buffer { diff --git a/packages/cc/src/cc/IrrigationCC.ts b/packages/cc/src/cc/IrrigationCC.ts index 5a58c8e1e4b4..5543583e7a4c 100644 --- a/packages/cc/src/cc/IrrigationCC.ts +++ b/packages/cc/src/cc/IrrigationCC.ts @@ -8,6 +8,7 @@ import { type SupervisionResult, type ValueID, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeFloatWithScale, @@ -15,7 +16,11 @@ import { parseFloatWithScale, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { padStart } from "alcalzone-shared/strings"; @@ -31,13 +36,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -603,7 +606,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCSystemInfoGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< IrrigationCCSystemInfoReport @@ -630,7 +633,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCSystemStatusGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< IrrigationCCSystemStatusReport @@ -668,7 +671,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCSystemConfigGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< IrrigationCCSystemConfigReport @@ -698,7 +701,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCSystemConfigSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...config, }); @@ -715,7 +718,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveInfoGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, valveId, }); const response = await this.host.sendCommand< @@ -749,7 +752,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveConfigSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -766,7 +769,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveConfigGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, valveId, }); const response = await this.host.sendCommand< @@ -800,7 +803,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveRun({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, valveId, duration, }); @@ -843,7 +846,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveTableSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, tableId, entries, }); @@ -862,7 +865,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveTableGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, tableId, }); const response = await this.host.sendCommand< @@ -887,7 +890,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCValveTableRun({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, tableIDs, }); @@ -909,7 +912,7 @@ export class IrrigationCCAPI extends CCAPI { const cc = new IrrigationCCSystemShutoff({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, duration, }); @@ -1344,17 +1347,45 @@ moisture sensor polarity: ${ } } +// @publicAPI +export interface IrrigationCCSystemInfoReportOptions { + supportsMasterValve: boolean; + numValves: number; + numValveTables: number; + maxValveTableSize: number; +} + @CCCommand(IrrigationCommand.SystemInfoReport) export class IrrigationCCSystemInfoReport extends IrrigationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - this.supportsMasterValve = !!(this.payload[0] & 0x01); - this.numValves = this.payload[1]; - this.numValveTables = this.payload[2]; - this.maxValveTableSize = this.payload[3] & 0b1111; + + // TODO: Check implementation: + this.supportsMasterValve = options.supportsMasterValve; + this.numValves = options.numValves; + this.numValveTables = options.numValveTables; + this.maxValveTableSize = options.maxValveTableSize; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IrrigationCCSystemInfoReport { + validatePayload(raw.payload.length >= 4); + const supportsMasterValve = !!(raw.payload[0] & 0x01); + const numValves = raw.payload[1]; + const numValveTables = raw.payload[2]; + const maxValveTableSize = raw.payload[3] & 0b1111; + + return new IrrigationCCSystemInfoReport({ + nodeId: ctx.sourceNodeId, + supportsMasterValve, + numValves, + numValveTables, + maxValveTableSize, + }); } @ccValue(IrrigationCCValues.numValves) @@ -1386,48 +1417,112 @@ export class IrrigationCCSystemInfoReport extends IrrigationCC { @expectedCCResponse(IrrigationCCSystemInfoReport) export class IrrigationCCSystemInfoGet extends IrrigationCC {} +// @publicAPI +export interface IrrigationCCSystemStatusReportOptions { + systemVoltage: number; + flowSensorActive: boolean; + pressureSensorActive: boolean; + rainSensorActive: boolean; + moistureSensorActive: boolean; + flow?: number; + pressure?: number; + shutoffDuration: number; + errorNotProgrammed: boolean; + errorEmergencyShutdown: boolean; + errorHighPressure: boolean; + errorLowPressure: boolean; + errorValve: boolean; + masterValveOpen: boolean; + firstOpenZoneId?: number; +} + @CCCommand(IrrigationCommand.SystemStatusReport) export class IrrigationCCSystemStatusReport extends IrrigationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.systemVoltage = this.payload[0]; - this.flowSensorActive = !!(this.payload[1] & 0x01); - this.pressureSensorActive = !!(this.payload[1] & 0x02); - this.rainSensorActive = !!(this.payload[1] & 0x04); - this.moistureSensorActive = !!(this.payload[1] & 0x08); + // TODO: Check implementation: + this.systemVoltage = options.systemVoltage; + this.flowSensorActive = options.flowSensorActive; + this.pressureSensorActive = options.pressureSensorActive; + this.rainSensorActive = options.rainSensorActive; + this.moistureSensorActive = options.moistureSensorActive; + this.flow = options.flow; + this.pressure = options.pressure; + this.shutoffDuration = options.shutoffDuration; + this.errorNotProgrammed = options.errorNotProgrammed; + this.errorEmergencyShutdown = options.errorEmergencyShutdown; + this.errorHighPressure = options.errorHighPressure; + this.errorLowPressure = options.errorLowPressure; + this.errorValve = options.errorValve; + this.masterValveOpen = options.masterValveOpen; + this.firstOpenZoneId = options.firstOpenZoneId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IrrigationCCSystemStatusReport { + validatePayload(raw.payload.length >= 2); + const systemVoltage = raw.payload[0]; + const flowSensorActive = !!(raw.payload[1] & 0x01); + const pressureSensorActive = !!(raw.payload[1] & 0x02); + const rainSensorActive = !!(raw.payload[1] & 0x04); + const moistureSensorActive = !!(raw.payload[1] & 0x08); let offset = 2; + let flow: number | undefined; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0); - if (this.flowSensorActive) this.flow = value; + if (flowSensorActive) flow = value; offset += bytesRead; } + + let pressure: number | undefined; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0); - if (this.pressureSensorActive) this.pressure = value; + if (pressureSensorActive) pressure = value; offset += bytesRead; } - validatePayload(this.payload.length >= offset + 4); - this.shutoffDuration = this.payload[offset]; - this.errorNotProgrammed = !!(this.payload[offset + 1] & 0x01); - this.errorEmergencyShutdown = !!(this.payload[offset + 1] & 0x02); - this.errorHighPressure = !!(this.payload[offset + 1] & 0x04); - this.errorLowPressure = !!(this.payload[offset + 1] & 0x08); - this.errorValve = !!(this.payload[offset + 1] & 0x10); - this.masterValveOpen = !!(this.payload[offset + 2] & 0x01); - if (this.payload[offset + 3]) { - this.firstOpenZoneId = this.payload[offset + 3]; + validatePayload(raw.payload.length >= offset + 4); + const shutoffDuration = raw.payload[offset]; + const errorNotProgrammed = !!(raw.payload[offset + 1] & 0x01); + const errorEmergencyShutdown = !!(raw.payload[offset + 1] & 0x02); + const errorHighPressure = !!(raw.payload[offset + 1] & 0x04); + const errorLowPressure = !!(raw.payload[offset + 1] & 0x08); + const errorValve = !!(raw.payload[offset + 1] & 0x10); + const masterValveOpen = !!(raw.payload[offset + 2] & 0x01); + let firstOpenZoneId: number | undefined; + if (raw.payload[offset + 3]) { + firstOpenZoneId = raw.payload[offset + 3]; } + + return new IrrigationCCSystemStatusReport({ + nodeId: ctx.sourceNodeId, + systemVoltage, + flowSensorActive, + pressureSensorActive, + rainSensorActive, + moistureSensorActive, + flow, + pressure, + shutoffDuration, + errorNotProgrammed, + errorEmergencyShutdown, + errorHighPressure, + errorLowPressure, + errorValve, + masterValveOpen, + firstOpenZoneId, + }); } @ccValue(IrrigationCCValues.systemVoltage) @@ -1539,24 +1634,29 @@ export type IrrigationCCSystemConfigSetOptions = { @useSupervision() export class IrrigationCCSystemConfigSet extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (IrrigationCCSystemConfigSetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.masterValveDelay = options.masterValveDelay; - this.highPressureThreshold = options.highPressureThreshold; - this.lowPressureThreshold = options.lowPressureThreshold; - this.rainSensorPolarity = options.rainSensorPolarity; - this.moistureSensorPolarity = options.moistureSensorPolarity; - } + this.masterValveDelay = options.masterValveDelay; + this.highPressureThreshold = options.highPressureThreshold; + this.lowPressureThreshold = options.lowPressureThreshold; + this.rainSensorPolarity = options.rainSensorPolarity; + this.moistureSensorPolarity = options.moistureSensorPolarity; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCSystemConfigSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCSystemConfigSet({ + // nodeId: ctx.sourceNodeId, + // }); } public masterValveDelay: number; @@ -1610,38 +1710,75 @@ export class IrrigationCCSystemConfigSet extends IrrigationCC { } } +// @publicAPI +export interface IrrigationCCSystemConfigReportOptions { + masterValveDelay: number; + highPressureThreshold: number; + lowPressureThreshold: number; + rainSensorPolarity?: IrrigationSensorPolarity; + moistureSensorPolarity?: IrrigationSensorPolarity; +} + @CCCommand(IrrigationCommand.SystemConfigReport) export class IrrigationCCSystemConfigReport extends IrrigationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.masterValveDelay = this.payload[0]; + + // TODO: Check implementation: + this.masterValveDelay = options.masterValveDelay; + this.highPressureThreshold = options.highPressureThreshold; + this.lowPressureThreshold = options.lowPressureThreshold; + this.rainSensorPolarity = options.rainSensorPolarity; + this.moistureSensorPolarity = options.moistureSensorPolarity; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IrrigationCCSystemConfigReport { + validatePayload(raw.payload.length >= 2); + const masterValveDelay = raw.payload[0]; let offset = 1; + let highPressureThreshold; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0 /* kPa */); - this.highPressureThreshold = value; + highPressureThreshold = value; offset += bytesRead; } + + let lowPressureThreshold; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0 /* kPa */); - this.lowPressureThreshold = value; + lowPressureThreshold = value; offset += bytesRead; } - validatePayload(this.payload.length >= offset + 1); - const polarity = this.payload[offset]; + + validatePayload(raw.payload.length >= offset + 1); + const polarity = raw.payload[offset]; + let rainSensorPolarity: IrrigationSensorPolarity | undefined; + let moistureSensorPolarity: IrrigationSensorPolarity | undefined; if (!!(polarity & 0b1000_0000)) { // The valid bit is set - this.rainSensorPolarity = polarity & 0b1; - this.moistureSensorPolarity = (polarity & 0b10) >>> 1; + rainSensorPolarity = polarity & 0b1; + moistureSensorPolarity = (polarity & 0b10) >>> 1; } + + return new IrrigationCCSystemConfigReport({ + nodeId: ctx.sourceNodeId, + masterValveDelay, + highPressureThreshold, + lowPressureThreshold, + rainSensorPolarity, + moistureSensorPolarity, + }); } @ccValue(IrrigationCCValues.masterValveDelay) @@ -1688,29 +1825,76 @@ export class IrrigationCCSystemConfigReport extends IrrigationCC { @expectedCCResponse(IrrigationCCSystemConfigReport) export class IrrigationCCSystemConfigGet extends IrrigationCC {} +// @publicAPI +export interface IrrigationCCValveInfoReportOptions { + valveId: ValveId; + connected: boolean; + nominalCurrent: number; + errorShortCircuit: boolean; + errorHighCurrent: boolean; + errorLowCurrent: boolean; + errorMaximumFlow?: boolean; + errorHighFlow?: boolean; + errorLowFlow?: boolean; +} + @CCCommand(IrrigationCommand.ValveInfoReport) export class IrrigationCCValveInfoReport extends IrrigationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - if ((this.payload[0] & 0b1) === ValveType.MasterValve) { - this.valveId = "master"; + + // TODO: Check implementation: + this.valveId = options.valveId; + this.connected = options.connected; + this.nominalCurrent = options.nominalCurrent; + this.errorShortCircuit = options.errorShortCircuit; + this.errorHighCurrent = options.errorHighCurrent; + this.errorLowCurrent = options.errorLowCurrent; + this.errorMaximumFlow = options.errorMaximumFlow; + this.errorHighFlow = options.errorHighFlow; + this.errorLowFlow = options.errorLowFlow; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IrrigationCCValveInfoReport { + validatePayload(raw.payload.length >= 4); + let valveId: ValveId; + if ((raw.payload[0] & 0b1) === ValveType.MasterValve) { + valveId = "master"; } else { - this.valveId = this.payload[1]; + valveId = raw.payload[1]; } - this.connected = !!(this.payload[0] & 0b10); - this.nominalCurrent = 10 * this.payload[2]; - this.errorShortCircuit = !!(this.payload[3] & 0b1); - this.errorHighCurrent = !!(this.payload[3] & 0b10); - this.errorLowCurrent = !!(this.payload[3] & 0b100); - if (this.valveId === "master") { - this.errorMaximumFlow = !!(this.payload[3] & 0b1000); - this.errorHighFlow = !!(this.payload[3] & 0b1_0000); - this.errorLowFlow = !!(this.payload[3] & 0b10_0000); + const connected = !!(raw.payload[0] & 0b10); + const nominalCurrent = 10 * raw.payload[2]; + const errorShortCircuit = !!(raw.payload[3] & 0b1); + const errorHighCurrent = !!(raw.payload[3] & 0b10); + const errorLowCurrent = !!(raw.payload[3] & 0b100); + let errorMaximumFlow: boolean | undefined; + let errorHighFlow: boolean | undefined; + let errorLowFlow: boolean | undefined; + if (valveId === "master") { + errorMaximumFlow = !!(raw.payload[3] & 0b1000); + errorHighFlow = !!(raw.payload[3] & 0b1_0000); + errorLowFlow = !!(raw.payload[3] & 0b10_0000); } + + return new IrrigationCCValveInfoReport({ + nodeId: ctx.sourceNodeId, + valveId, + connected, + nominalCurrent, + errorShortCircuit, + errorHighCurrent, + errorLowCurrent, + errorMaximumFlow, + errorHighFlow, + errorLowFlow, + }); } public readonly valveId: ValveId; @@ -1818,7 +2002,7 @@ export class IrrigationCCValveInfoReport extends IrrigationCC { } // @publicAPI -export interface IrrigationCCValveInfoGetOptions extends CCCommandOptions { +export interface IrrigationCCValveInfoGetOptions { valveId: ValveId; } @@ -1840,20 +2024,25 @@ function testResponseForIrrigationCommandWithValveId( ) export class IrrigationCCValveInfoGet extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCValveInfoGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.valveId = options.valveId; - } + this.valveId = options.valveId; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveInfoGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveInfoGet({ + // nodeId: ctx.sourceNodeId, + // }); } public valveId: ValveId; @@ -1892,29 +2081,32 @@ export type IrrigationCCValveConfigSetOptions = { @useSupervision() export class IrrigationCCValveConfigSet extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (IrrigationCCValveConfigSetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.valveId = options.valveId; - this.nominalCurrentHighThreshold = - options.nominalCurrentHighThreshold; - this.nominalCurrentLowThreshold = - options.nominalCurrentLowThreshold; - this.maximumFlow = options.maximumFlow; - this.highFlowThreshold = options.highFlowThreshold; - this.lowFlowThreshold = options.lowFlowThreshold; - this.useRainSensor = options.useRainSensor; - this.useMoistureSensor = options.useMoistureSensor; - } + this.valveId = options.valveId; + this.nominalCurrentHighThreshold = options.nominalCurrentHighThreshold; + this.nominalCurrentLowThreshold = options.nominalCurrentLowThreshold; + this.maximumFlow = options.maximumFlow; + this.highFlowThreshold = options.highFlowThreshold; + this.lowFlowThreshold = options.lowFlowThreshold; + this.useRainSensor = options.useRainSensor; + this.useMoistureSensor = options.useMoistureSensor; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveConfigSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveConfigSet({ + // nodeId: ctx.sourceNodeId, + // }); } public valveId: ValveId; @@ -1964,49 +2156,96 @@ export class IrrigationCCValveConfigSet extends IrrigationCC { } } +// @publicAPI +export interface IrrigationCCValveConfigReportOptions { + valveId: ValveId; + nominalCurrentHighThreshold: number; + nominalCurrentLowThreshold: number; + maximumFlow: number; + highFlowThreshold: number; + lowFlowThreshold: number; + useRainSensor: boolean; + useMoistureSensor: boolean; +} + @CCCommand(IrrigationCommand.ValveConfigReport) export class IrrigationCCValveConfigReport extends IrrigationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - if ((this.payload[0] & 0b1) === ValveType.MasterValve) { - this.valveId = "master"; + + // TODO: Check implementation: + this.valveId = options.valveId; + this.nominalCurrentHighThreshold = options.nominalCurrentHighThreshold; + this.nominalCurrentLowThreshold = options.nominalCurrentLowThreshold; + this.maximumFlow = options.maximumFlow; + this.highFlowThreshold = options.highFlowThreshold; + this.lowFlowThreshold = options.lowFlowThreshold; + this.useRainSensor = options.useRainSensor; + this.useMoistureSensor = options.useMoistureSensor; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IrrigationCCValveConfigReport { + validatePayload(raw.payload.length >= 4); + let valveId: ValveId; + if ((raw.payload[0] & 0b1) === ValveType.MasterValve) { + valveId = "master"; } else { - this.valveId = this.payload[1]; + valveId = raw.payload[1]; } - this.nominalCurrentHighThreshold = 10 * this.payload[2]; - this.nominalCurrentLowThreshold = 10 * this.payload[3]; + const nominalCurrentHighThreshold = 10 * raw.payload[2]; + const nominalCurrentLowThreshold = 10 * raw.payload[3]; let offset = 4; + let maximumFlow; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0 /* l/h */); - this.maximumFlow = value; + maximumFlow = value; offset += bytesRead; } + + let highFlowThreshold; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0 /* l/h */); - this.highFlowThreshold = value; + highFlowThreshold = value; offset += bytesRead; } + + let lowFlowThreshold; { const { value, scale, bytesRead } = parseFloatWithScale( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); validatePayload(scale === 0 /* l/h */); - this.lowFlowThreshold = value; + lowFlowThreshold = value; offset += bytesRead; } - validatePayload(this.payload.length >= offset + 1); - this.useRainSensor = !!(this.payload[offset] & 0b1); - this.useMoistureSensor = !!(this.payload[offset] & 0b10); + + validatePayload(raw.payload.length >= offset + 1); + const useRainSensor = !!(raw.payload[offset] & 0b1); + const useMoistureSensor = !!(raw.payload[offset] & 0b10); + + return new IrrigationCCValveConfigReport({ + nodeId: ctx.sourceNodeId, + valveId, + nominalCurrentHighThreshold, + nominalCurrentLowThreshold, + maximumFlow, + highFlowThreshold, + lowFlowThreshold, + useRainSensor, + useMoistureSensor, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -2097,7 +2336,7 @@ export class IrrigationCCValveConfigReport extends IrrigationCC { } // @publicAPI -export interface IrrigationCCValveConfigGetOptions extends CCCommandOptions { +export interface IrrigationCCValveConfigGetOptions { valveId: ValveId; } @@ -2108,20 +2347,25 @@ export interface IrrigationCCValveConfigGetOptions extends CCCommandOptions { ) export class IrrigationCCValveConfigGet extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCValveConfigGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.valveId = options.valveId; - } + this.valveId = options.valveId; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveConfigGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveConfigGet({ + // nodeId: ctx.sourceNodeId, + // }); } public valveId: ValveId; @@ -2145,7 +2389,7 @@ export class IrrigationCCValveConfigGet extends IrrigationCC { } // @publicAPI -export interface IrrigationCCValveRunOptions extends CCCommandOptions { +export interface IrrigationCCValveRunOptions { valveId: ValveId; duration: number; } @@ -2154,21 +2398,26 @@ export interface IrrigationCCValveRunOptions extends CCCommandOptions { @useSupervision() export class IrrigationCCValveRun extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCValveRunOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.valveId = options.valveId; - this.duration = options.duration; - } + this.valveId = options.valveId; + this.duration = options.duration; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveRun { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveRun({ + // nodeId: ctx.sourceNodeId, + // }); } public valveId: ValveId; @@ -2202,7 +2451,7 @@ export class IrrigationCCValveRun extends IrrigationCC { } // @publicAPI -export interface IrrigationCCValveTableSetOptions extends CCCommandOptions { +export interface IrrigationCCValveTableSetOptions { tableId: number; entries: ValveTableEntry[]; } @@ -2211,21 +2460,26 @@ export interface IrrigationCCValveTableSetOptions extends CCCommandOptions { @useSupervision() export class IrrigationCCValveTableSet extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCValveTableSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.tableId = options.tableId; - this.entries = options.entries; - } + this.tableId = options.tableId; + this.entries = options.entries; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveTableSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveTableSet({ + // nodeId: ctx.sourceNodeId, + // }); } public tableId: number; @@ -2263,21 +2517,43 @@ export class IrrigationCCValveTableSet extends IrrigationCC { } } +// @publicAPI +export interface IrrigationCCValveTableReportOptions { + tableId: number; + entries: ValveTableEntry[]; +} + @CCCommand(IrrigationCommand.ValveTableReport) export class IrrigationCCValveTableReport extends IrrigationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload((this.payload.length - 1) % 3 === 0); - this.tableId = this.payload[0]; - this.entries = []; - for (let offset = 1; offset < this.payload.length; offset += 3) { - this.entries.push({ - valveId: this.payload[offset], - duration: this.payload.readUInt16BE(offset + 1), + + // TODO: Check implementation: + this.tableId = options.tableId; + this.entries = options.entries; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): IrrigationCCValveTableReport { + validatePayload((raw.payload.length - 1) % 3 === 0); + const tableId = raw.payload[0]; + const entries: ValveTableEntry[] = []; + for (let offset = 1; offset < raw.payload.length; offset += 3) { + entries.push({ + valveId: raw.payload[offset], + duration: raw.payload.readUInt16BE(offset + 1), }); } + + return new IrrigationCCValveTableReport({ + nodeId: ctx.sourceNodeId, + tableId, + entries, + }); } public readonly tableId: number; @@ -2304,7 +2580,7 @@ export class IrrigationCCValveTableReport extends IrrigationCC { } // @publicAPI -export interface IrrigationCCValveTableGetOptions extends CCCommandOptions { +export interface IrrigationCCValveTableGetOptions { tableId: number; } @@ -2322,20 +2598,25 @@ function testResponseForIrrigationValveTableGet( ) export class IrrigationCCValveTableGet extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCValveTableGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.tableId = options.tableId; - } + this.tableId = options.tableId; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveTableGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveTableGet({ + // nodeId: ctx.sourceNodeId, + // }); } public tableId: number; @@ -2356,7 +2637,7 @@ export class IrrigationCCValveTableGet extends IrrigationCC { } // @publicAPI -export interface IrrigationCCValveTableRunOptions extends CCCommandOptions { +export interface IrrigationCCValveTableRunOptions { tableIDs: number[]; } @@ -2364,28 +2645,33 @@ export interface IrrigationCCValveTableRunOptions extends CCCommandOptions { @useSupervision() export class IrrigationCCValveTableRun extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCValveTableRunOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload + this.tableIDs = options.tableIDs; + if (this.tableIDs.length < 1) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `${this.constructor.name}: At least one table ID must be specified.`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - this.tableIDs = options.tableIDs; - if (this.tableIDs.length < 1) { - throw new ZWaveError( - `${this.constructor.name}: At least one table ID must be specified.`, - ZWaveErrorCodes.Argument_Invalid, - ); - } } } + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCValveTableRun { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCValveTableRun({ + // nodeId: ctx.sourceNodeId, + // }); + } + public tableIDs: number[]; public serialize(ctx: CCEncodingContext): Buffer { @@ -2406,7 +2692,7 @@ export class IrrigationCCValveTableRun extends IrrigationCC { } // @publicAPI -export interface IrrigationCCSystemShutoffOptions extends CCCommandOptions { +export interface IrrigationCCSystemShutoffOptions { /** * The duration in minutes the system must stay off. * 255 or `undefined` will prevent schedules from running. @@ -2418,20 +2704,25 @@ export interface IrrigationCCSystemShutoffOptions extends CCCommandOptions { @useSupervision() export class IrrigationCCSystemShutoff extends IrrigationCC { public constructor( - options: - | CommandClassDeserializationOptions - | IrrigationCCSystemShutoffOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.duration = options.duration; - } + this.duration = options.duration; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): IrrigationCCSystemShutoff { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new IrrigationCCSystemShutoff({ + // nodeId: ctx.sourceNodeId, + // }); } public duration?: number; diff --git a/packages/cc/src/cc/LanguageCC.ts b/packages/cc/src/cc/LanguageCC.ts index 907727807b21..10bc366cd897 100644 --- a/packages/cc/src/cc/LanguageCC.ts +++ b/packages/cc/src/cc/LanguageCC.ts @@ -2,6 +2,7 @@ import type { MessageOrCCLogEntry, MessageRecord, SupervisionResult, + WithAddress, } from "@zwave-js/core/safe"; import { CommandClasses, @@ -12,17 +13,19 @@ import { ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -77,7 +80,7 @@ export class LanguageCCAPI extends CCAPI { const cc = new LanguageCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -97,7 +100,7 @@ export class LanguageCCAPI extends CCAPI { const cc = new LanguageCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, language, country, }); @@ -160,7 +163,7 @@ export class LanguageCC extends CommandClass { } // @publicAPI -export interface LanguageCCSetOptions extends CCCommandOptions { +export interface LanguageCCSetOptions { language: string; country?: string; } @@ -169,20 +172,24 @@ export interface LanguageCCSetOptions extends CCCommandOptions { @useSupervision() export class LanguageCCSet extends LanguageCC { public constructor( - options: CommandClassDeserializationOptions | LanguageCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - // Populate properties from options object - this._language = options.language; - this._country = options.country; - } + // Populate properties from options object + this._language = options.language; + this._country = options.country; + } + + public static from(_raw: CCRaw, _ctx: CCParsingContext): LanguageCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new LanguageCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } private _language: string; @@ -235,19 +242,38 @@ export class LanguageCCSet extends LanguageCC { } } +// @publicAPI +export interface LanguageCCReportOptions { + language: string; + country: MaybeNotKnown; +} + @CCCommand(LanguageCommand.Report) export class LanguageCCReport extends LanguageCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); + + // TODO: Check implementation: + this.language = options.language; + this.country = options.country; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): LanguageCCReport { // if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.language = this.payload.toString("ascii", 0, 3); - if (this.payload.length >= 5) { - this.country = this.payload.toString("ascii", 3, 5); + validatePayload(raw.payload.length >= 3); + const language = raw.payload.toString("ascii", 0, 3); + let country: MaybeNotKnown; + if (raw.payload.length >= 5) { + country = raw.payload.toString("ascii", 3, 5); } - // } + + return new LanguageCCReport({ + nodeId: ctx.sourceNodeId, + language, + country, + }); } @ccValue(LanguageCCValues.language) diff --git a/packages/cc/src/cc/LockCC.ts b/packages/cc/src/cc/LockCC.ts index 41a1cc32f734..08e96741adc3 100644 --- a/packages/cc/src/cc/LockCC.ts +++ b/packages/cc/src/cc/LockCC.ts @@ -5,12 +5,17 @@ import { MessagePriority, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, @@ -23,12 +28,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -72,7 +75,7 @@ export class LockCCAPI extends PhysicalCCAPI { const cc = new LockCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -91,7 +94,7 @@ export class LockCCAPI extends PhysicalCCAPI { const cc = new LockCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, locked, }); return this.host.sendCommand(cc, this.commandOptions); @@ -179,7 +182,7 @@ export class LockCC extends CommandClass { } // @publicAPI -export interface LockCCSetOptions extends CCCommandOptions { +export interface LockCCSetOptions { locked: boolean; } @@ -187,18 +190,22 @@ export interface LockCCSetOptions extends CCCommandOptions { @useSupervision() export class LockCCSet extends LockCC { public constructor( - options: CommandClassDeserializationOptions | LockCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.locked = options.locked; - } + this.locked = options.locked; + } + + public static from(_raw: CCRaw, _ctx: CCParsingContext): LockCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new LockCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public locked: boolean; @@ -216,14 +223,30 @@ export class LockCCSet extends LockCC { } } +// @publicAPI +export interface LockCCReportOptions { + locked: boolean; +} + @CCCommand(LockCommand.Report) export class LockCCReport extends LockCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.locked = this.payload[0] === 1; + + // TODO: Check implementation: + this.locked = options.locked; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): LockCCReport { + validatePayload(raw.payload.length >= 1); + const locked = raw.payload[0] === 1; + + return new LockCCReport({ + nodeId: ctx.sourceNodeId, + locked, + }); } @ccValue(LockCCValues.locked) diff --git a/packages/cc/src/cc/ManufacturerProprietaryCC.ts b/packages/cc/src/cc/ManufacturerProprietaryCC.ts index e16969216443..c6d42b35f5ec 100644 --- a/packages/cc/src/cc/ManufacturerProprietaryCC.ts +++ b/packages/cc/src/cc/ManufacturerProprietaryCC.ts @@ -1,20 +1,18 @@ import { CommandClasses, + type WithAddress, ZWaveError, ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext } from "@zwave-js/host/safe"; -import { staticExtends } from "@zwave-js/shared/safe"; +import type { CCEncodingContext, CCParsingContext } from "@zwave-js/host/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, type CCAPIEndpoint, type CCAPIHost } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -70,7 +68,7 @@ export class ManufacturerProprietaryCCAPI extends CCAPI { ): Promise { const cc = new ManufacturerProprietaryCC({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, manufacturerId, }); cc.payload = data ?? Buffer.allocUnsafe(0); @@ -83,7 +81,7 @@ export class ManufacturerProprietaryCCAPI extends CCAPI { public async sendAndReceiveData(manufacturerId: number, data?: Buffer) { const cc = new ManufacturerProprietaryCC({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, manufacturerId, unspecifiedExpectsResponse: true, }); @@ -105,7 +103,7 @@ export class ManufacturerProprietaryCCAPI extends CCAPI { } // @publicAPI -export interface ManufacturerProprietaryCCOptions extends CCCommandOptions { +export interface ManufacturerProprietaryCCOptions { manufacturerId?: number; unspecifiedExpectsResponse?: boolean; } @@ -134,42 +132,39 @@ export class ManufacturerProprietaryCC extends CommandClass { declare ccCommand: undefined; public constructor( - options: - | CommandClassDeserializationOptions - | ManufacturerProprietaryCCOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - // ManufacturerProprietaryCC has no CC command, so the first byte is stored in ccCommand. - this.manufacturerId = ((this.ccCommand as unknown as number) << 8) - + this.payload[0]; + this.manufacturerId = options.manufacturerId + ?? getManufacturerId(this); + this.unspecifiedExpectsResponse = options.unspecifiedExpectsResponse; - // Try to parse the proprietary command - const PCConstructor = getManufacturerProprietaryCCConstructor( - this.manufacturerId, - ); - if ( - PCConstructor - && new.target !== PCConstructor - && !staticExtends(new.target, PCConstructor) - ) { - return new PCConstructor(options); - } - - // If the constructor is correct, update the payload for subclass deserialization - this.payload = this.payload.subarray(1); - } else { - this.manufacturerId = options.manufacturerId - ?? getManufacturerId(this); - - this.unspecifiedExpectsResponse = - options.unspecifiedExpectsResponse; + // To use this CC, a manufacturer ID must exist in the value DB + // If it doesn't, the interview procedure will throw. + } - // To use this CC, a manufacturer ID must exist in the value DB - // If it doesn't, the interview procedure will throw. + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ManufacturerProprietaryCC { + validatePayload(raw.payload.length >= 1); + const manufacturerId = raw.payload.readUint16BE(0); + // Try to parse the proprietary command + const PCConstructor = getManufacturerProprietaryCCConstructor( + manufacturerId, + ); + if (PCConstructor) { + return PCConstructor.from( + raw.withPayload(raw.payload.subarray(2)), + ctx, + ); } + + return new ManufacturerProprietaryCC({ + nodeId: ctx.sourceNodeId, + manufacturerId, + }); } public manufacturerId?: number; diff --git a/packages/cc/src/cc/ManufacturerSpecificCC.ts b/packages/cc/src/cc/ManufacturerSpecificCC.ts index 9f993fab4418..ac8f51fc6c32 100644 --- a/packages/cc/src/cc/ManufacturerSpecificCC.ts +++ b/packages/cc/src/cc/ManufacturerSpecificCC.ts @@ -1,4 +1,4 @@ -import type { MessageOrCCLogEntry } from "@zwave-js/core/safe"; +import type { MessageOrCCLogEntry, WithAddress } from "@zwave-js/core/safe"; import { CommandClasses, type MaybeNotKnown, @@ -8,16 +8,18 @@ import { ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -106,7 +108,7 @@ export class ManufacturerSpecificCCAPI extends PhysicalCCAPI { const cc = new ManufacturerSpecificCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ManufacturerSpecificCCReport @@ -134,7 +136,7 @@ export class ManufacturerSpecificCCAPI extends PhysicalCCAPI { const cc = new ManufacturerSpecificCCDeviceSpecificGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, deviceIdType, }); const response = await this.host.sendCommand< @@ -157,7 +159,7 @@ export class ManufacturerSpecificCCAPI extends PhysicalCCAPI { const cc = new ManufacturerSpecificCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -233,22 +235,30 @@ export interface ManufacturerSpecificCCReportOptions { @CCCommand(ManufacturerSpecificCommand.Report) export class ManufacturerSpecificCCReport extends ManufacturerSpecificCC { public constructor( - options: - | (ManufacturerSpecificCCReportOptions & CCCommandOptions) - | CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 6); - this.manufacturerId = this.payload.readUInt16BE(0); - this.productType = this.payload.readUInt16BE(2); - this.productId = this.payload.readUInt16BE(4); - } else { - this.manufacturerId = options.manufacturerId; - this.productType = options.productType; - this.productId = options.productId; - } + this.manufacturerId = options.manufacturerId; + this.productType = options.productType; + this.productId = options.productId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ManufacturerSpecificCCReport { + validatePayload(raw.payload.length >= 6); + const manufacturerId = raw.payload.readUInt16BE(0); + const productType = raw.payload.readUInt16BE(2); + const productId = raw.payload.readUInt16BE(4); + + return new ManufacturerSpecificCCReport({ + nodeId: ctx.sourceNodeId, + manufacturerId, + productType, + productId, + }); } @ccValue(ManufacturerSpecificCCValues.manufacturerId) @@ -284,25 +294,45 @@ export class ManufacturerSpecificCCReport extends ManufacturerSpecificCC { @expectedCCResponse(ManufacturerSpecificCCReport) export class ManufacturerSpecificCCGet extends ManufacturerSpecificCC {} +// @publicAPI +export interface ManufacturerSpecificCCDeviceSpecificReportOptions { + type: DeviceIdType; + deviceId: string; +} + @CCCommand(ManufacturerSpecificCommand.DeviceSpecificReport) export class ManufacturerSpecificCCDeviceSpecificReport extends ManufacturerSpecificCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.type = this.payload[0] & 0b111; - const dataFormat = this.payload[1] >>> 5; - const dataLength = this.payload[1] & 0b11111; + // TODO: Check implementation: + this.type = options.type; + this.deviceId = options.deviceId; + } - validatePayload(dataLength > 0, this.payload.length >= 2 + dataLength); - const deviceIdData = this.payload.subarray(2, 2 + dataLength); - this.deviceId = dataFormat === 0 + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ManufacturerSpecificCCDeviceSpecificReport { + validatePayload(raw.payload.length >= 2); + const type: DeviceIdType = raw.payload[0] & 0b111; + const dataFormat = raw.payload[1] >>> 5; + const dataLength = raw.payload[1] & 0b11111; + validatePayload(dataLength > 0, raw.payload.length >= 2 + dataLength); + const deviceIdData = raw.payload.subarray(2, 2 + dataLength); + const deviceId: string = dataFormat === 0 ? deviceIdData.toString("utf8") : "0x" + deviceIdData.toString("hex"); + + return new ManufacturerSpecificCCDeviceSpecificReport({ + nodeId: ctx.sourceNodeId, + type, + deviceId, + }); } public readonly type: DeviceIdType; @@ -326,9 +356,7 @@ export class ManufacturerSpecificCCDeviceSpecificReport } // @publicAPI -export interface ManufacturerSpecificCCDeviceSpecificGetOptions - extends CCCommandOptions -{ +export interface ManufacturerSpecificCCDeviceSpecificGetOptions { deviceIdType: DeviceIdType; } @@ -338,19 +366,24 @@ export class ManufacturerSpecificCCDeviceSpecificGet extends ManufacturerSpecificCC { public constructor( - options: - | CommandClassDeserializationOptions - | ManufacturerSpecificCCDeviceSpecificGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.deviceIdType = options.deviceIdType; - } + this.deviceIdType = options.deviceIdType; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ManufacturerSpecificCCDeviceSpecificGet { + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ManufacturerSpecificCCDeviceSpecificGet({ + // nodeId: ctx.sourceNodeId, + // }); } public deviceIdType: DeviceIdType; diff --git a/packages/cc/src/cc/MeterCC.ts b/packages/cc/src/cc/MeterCC.ts index fc0cfd497f11..685aab8df739 100644 --- a/packages/cc/src/cc/MeterCC.ts +++ b/packages/cc/src/cc/MeterCC.ts @@ -1,6 +1,7 @@ import { type FloatParameters, type MaybeUnknown, + type WithAddress, encodeBitMask, encodeFloatWithScale, getFloatParameters, @@ -31,6 +32,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetNode, GetSupportedCCVersion, @@ -58,14 +60,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -371,7 +371,7 @@ export class MeterCCAPI extends PhysicalCCAPI { const cc = new MeterCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); const response = await this.host.sendCommand( @@ -401,7 +401,7 @@ export class MeterCCAPI extends PhysicalCCAPI { const cc = new MeterCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand(cc, this.commandOptions); @@ -462,7 +462,7 @@ export class MeterCCAPI extends PhysicalCCAPI { const cc = new MeterCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< MeterCCSupportedReport @@ -488,7 +488,7 @@ export class MeterCCAPI extends PhysicalCCAPI { const cc = new MeterCCSupportedReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -502,7 +502,7 @@ export class MeterCCAPI extends PhysicalCCAPI { const cc = new MeterCCReset({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand(cc, this.commandOptions); @@ -898,56 +898,63 @@ export interface MeterCCReportOptions { @CCCommand(MeterCommand.Report) export class MeterCCReport extends MeterCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MeterCCReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - const { type, rateType, scale1, value, bytesRead } = - parseMeterValueAndInfo(this.payload, 0); - this.type = type; - this.rateType = rateType; - this.value = value; - let offset = bytesRead; - const floatSize = bytesRead - 2; - - if (this.payload.length >= offset + 2) { - this.deltaTime = this.payload.readUInt16BE(offset); - offset += 2; - if (this.deltaTime === 0xffff) { - this.deltaTime = UNKNOWN_STATE; - } + this.type = options.type; + this.scale = options.scale; + this.value = options.value; + this.previousValue = options.previousValue; + this.rateType = options.rateType ?? RateType.Unspecified; + this.deltaTime = options.deltaTime ?? UNKNOWN_STATE; + } - if ( - // Previous value is included only if delta time is not 0 - this.deltaTime !== 0 - && this.payload.length >= offset + floatSize - ) { - const { value: prevValue } = parseFloatWithScale( - // This float is split in the payload - Buffer.concat([ - Buffer.from([this.payload[1]]), - this.payload.subarray(offset), - ]), - ); - offset += floatSize; - this.previousValue = prevValue; - } - } else { - // 0 means that no previous value is included - this.deltaTime = 0; + public static from(raw: CCRaw, ctx: CCParsingContext): MeterCCReport { + const { type, rateType, scale1, value, bytesRead } = + parseMeterValueAndInfo(raw.payload, 0); + let offset = bytesRead; + const floatSize = bytesRead - 2; + let deltaTime: MaybeUnknown; + let previousValue: MaybeNotKnown; + + if (raw.payload.length >= offset + 2) { + deltaTime = raw.payload.readUInt16BE(offset); + offset += 2; + if (deltaTime === 0xffff) { + deltaTime = UNKNOWN_STATE; + } + + if ( + // Previous value is included only if delta time is not 0 + deltaTime !== 0 + && raw.payload.length >= offset + floatSize + ) { + const { value: prevValue } = parseFloatWithScale( + // This float is split in the payload + Buffer.concat([ + Buffer.from([raw.payload[1]]), + raw.payload.subarray(offset), + ]), + ); + offset += floatSize; + previousValue = prevValue; } - this.scale = parseScale(scale1, this.payload, offset); } else { - this.type = options.type; - this.scale = options.scale; - this.value = options.value; - this.previousValue = options.previousValue; - this.rateType = options.rateType ?? RateType.Unspecified; - this.deltaTime = options.deltaTime ?? UNKNOWN_STATE; + // 0 means that no previous value is included + deltaTime = 0; } + const scale = parseScale(scale1, raw.payload, offset); + + return new MeterCCReport({ + nodeId: ctx.sourceNodeId, + type, + rateType, + value, + deltaTime, + previousValue, + scale, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -1122,24 +1129,31 @@ export interface MeterCCGetOptions { @expectedCCResponse(MeterCCReport, testResponseForMeterGet) export class MeterCCGet extends MeterCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MeterCCGetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - if (this.payload.length >= 1) { - this.rateType = (this.payload[0] & 0b11_000_000) >>> 6; - this.scale = (this.payload[0] & 0b00_111_000) >>> 3; - if (this.scale === 7) { - validatePayload(this.payload.length >= 2); - this.scale += this.payload[1]; - } + this.rateType = options.rateType; + this.scale = options.scale; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): MeterCCGet { + let rateType: RateType | undefined; + let scale: number | undefined; + + if (raw.payload.length >= 1) { + rateType = (raw.payload[0] & 0b11_000_000) >>> 6; + scale = (raw.payload[0] & 0b00_111_000) >>> 3; + if (scale === 7) { + validatePayload(raw.payload.length >= 2); + scale += raw.payload[1]; } - } else { - this.rateType = options.rateType; - this.scale = options.scale; } + + return new MeterCCGet({ + nodeId: ctx.sourceNodeId, + rateType, + scale, + }); } public rateType: RateType | undefined; @@ -1215,49 +1229,60 @@ export interface MeterCCSupportedReportOptions { @CCCommand(MeterCommand.SupportedReport) export class MeterCCSupportedReport extends MeterCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MeterCCSupportedReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.type = this.payload[0] & 0b0_00_11111; - this.supportsReset = !!(this.payload[0] & 0b1_00_00000); - const hasMoreScales = !!(this.payload[1] & 0b1_0000000); - if (hasMoreScales) { - // The bitmask is spread out - validatePayload(this.payload.length >= 3); - const extraBytes = this.payload[2]; - validatePayload(this.payload.length >= 3 + extraBytes); - // The bitmask is the original payload byte plus all following bytes - // Since the first byte only has 7 bits, we need to reduce all following bits by 1 - this.supportedScales = parseBitMask( - Buffer.concat([ - Buffer.from([this.payload[1] & 0b0_1111111]), - this.payload.subarray(3, 3 + extraBytes), - ]), - 0, - ).map((scale) => (scale >= 8 ? scale - 1 : scale)); - } else { - // only 7 bits in the bitmask. Bit 7 is 0, so no need to mask it out - this.supportedScales = parseBitMask( - Buffer.from([this.payload[1]]), - 0, - ); - } - // This is only present in V4+ - this.supportedRateTypes = parseBitMask( - Buffer.from([(this.payload[0] & 0b0_11_00000) >>> 5]), - 1, - ); + this.type = options.type; + this.supportsReset = options.supportsReset; + this.supportedScales = options.supportedScales; + this.supportedRateTypes = options.supportedRateTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MeterCCSupportedReport { + validatePayload(raw.payload.length >= 2); + const type = raw.payload[0] & 0b0_00_11111; + const supportsReset = !!(raw.payload[0] & 0b1_00_00000); + const hasMoreScales = !!(raw.payload[1] & 0b1_0000000); + + let supportedScales: number[] | undefined; + if (hasMoreScales) { + // The bitmask is spread out + validatePayload(raw.payload.length >= 3); + const extraBytes = raw.payload[2]; + validatePayload(raw.payload.length >= 3 + extraBytes); + // The bitmask is the original payload byte plus all following bytes + // Since the first byte only has 7 bits, we need to reduce all following bits by 1 + supportedScales = parseBitMask( + Buffer.concat([ + Buffer.from([raw.payload[1] & 0b0_1111111]), + raw.payload.subarray(3, 3 + extraBytes), + ]), + 0, + ).map((scale) => (scale >= 8 ? scale - 1 : scale)); } else { - this.type = options.type; - this.supportsReset = options.supportsReset; - this.supportedScales = options.supportedScales; - this.supportedRateTypes = options.supportedRateTypes; + // only 7 bits in the bitmask. Bit 7 is 0, so no need to mask it out + supportedScales = parseBitMask( + Buffer.from([raw.payload[1]]), + 0, + ); } + // This is only present in V4+ + const supportedRateTypes: RateType[] = parseBitMask( + Buffer.from([(raw.payload[0] & 0b0_11_00000) >>> 5]), + 1, + ); + + return new MeterCCSupportedReport({ + nodeId: ctx.sourceNodeId, + type, + supportsReset, + supportedScales, + supportedRateTypes, + }); } @ccValue(MeterCCValues.type) @@ -1382,31 +1407,38 @@ export type MeterCCResetOptions = AllOrNone<{ @useSupervision() export class MeterCCReset extends MeterCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MeterCCResetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - if (this.payload.length > 0) { - const { - type, - rateType, - scale1, - value, - bytesRead: scale2Offset, - } = parseMeterValueAndInfo(this.payload, 0); - this.type = type; - this.rateType = rateType; - this.targetValue = value; - this.scale = parseScale(scale1, this.payload, scale2Offset); - } - } else { - this.type = options.type; - this.scale = options.scale; - this.rateType = options.rateType; - this.targetValue = options.targetValue; + this.type = options.type; + this.scale = options.scale; + this.rateType = options.rateType; + this.targetValue = options.targetValue; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): MeterCCReset { + if (raw.payload.length === 0) { + return new MeterCCReset({ + nodeId: ctx.sourceNodeId, + }); } + + const { + type, + rateType, + scale1, + value: targetValue, + bytesRead: scale2Offset, + } = parseMeterValueAndInfo(raw.payload, 0); + const scale = parseScale(scale1, raw.payload, scale2Offset); + + return new MeterCCReset({ + nodeId: ctx.sourceNodeId, + type, + rateType, + targetValue, + scale, + }); } public type: number | undefined; diff --git a/packages/cc/src/cc/MultiChannelAssociationCC.ts b/packages/cc/src/cc/MultiChannelAssociationCC.ts index 15dd6dd36a81..b6ed2b7fac76 100644 --- a/packages/cc/src/cc/MultiChannelAssociationCC.ts +++ b/packages/cc/src/cc/MultiChannelAssociationCC.ts @@ -2,6 +2,7 @@ import type { EndpointId, MessageRecord, SupervisionResult, + WithAddress, } from "@zwave-js/core/safe"; import { CommandClasses, @@ -24,12 +25,10 @@ import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -206,7 +205,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { const cc = new MultiChannelAssociationCCSupportedGroupingsGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< MultiChannelAssociationCCSupportedGroupingsReport @@ -226,7 +225,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { const cc = new MultiChannelAssociationCCSupportedGroupingsReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupCount, }); await this.host.sendCommand(cc, this.commandOptions); @@ -245,7 +244,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { const cc = new MultiChannelAssociationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, }); const response = await this.host.sendCommand< @@ -270,7 +269,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { const cc = new MultiChannelAssociationCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -290,7 +289,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { const cc = new MultiChannelAssociationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand(cc, this.commandOptions); @@ -319,7 +318,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { for (const [group, destinations] of currentDestinations) { const cc = new MultiChannelAssociationCCRemove({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId: group, nodeIds: destinations .filter((d) => d.endpoint != undefined) @@ -340,7 +339,7 @@ export class MultiChannelAssociationCCAPI extends PhysicalCCAPI { } else { const cc = new MultiChannelAssociationCCRemove({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand(cc, this.commandOptions); @@ -623,36 +622,45 @@ export type MultiChannelAssociationCCSetOptions = @useSupervision() export class MultiChannelAssociationCCSet extends MultiChannelAssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MultiChannelAssociationCCSetOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupId = this.payload[0]; - ({ nodeIds: this.nodeIds, endpoints: this.endpoints } = - deserializeMultiChannelAssociationDestination( - this.payload.subarray(1), - )); - } else { - if (options.groupId < 1) { - throw new ZWaveError( - "The group id must be positive!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.groupId = options.groupId; - this.nodeIds = ("nodeIds" in options && options.nodeIds) || []; - if (this.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.endpoints = ("endpoints" in options && options.endpoints) - || []; + if (options.groupId < 1) { + throw new ZWaveError( + "The group id must be positive!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.groupId = options.groupId; + this.nodeIds = ("nodeIds" in options && options.nodeIds) || []; + if (this.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.endpoints = ("endpoints" in options && options.endpoints) + || []; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelAssociationCCSet { + validatePayload(raw.payload.length >= 1); + const groupId = raw.payload[0]; + + const { nodeIds, endpoints } = + deserializeMultiChannelAssociationDestination( + raw.payload.subarray(1), + ); + + return new MultiChannelAssociationCCSet({ + nodeId: ctx.sourceNodeId, + groupId, + nodeIds, + endpoints, + }); } public groupId: number; @@ -696,25 +704,34 @@ export interface MultiChannelAssociationCCRemoveOptions { @useSupervision() export class MultiChannelAssociationCCRemove extends MultiChannelAssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MultiChannelAssociationCCRemoveOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupId = this.payload[0]; - ({ nodeIds: this.nodeIds, endpoints: this.endpoints } = - deserializeMultiChannelAssociationDestination( - this.payload.subarray(1), - )); - } else { - // When removing associations, we allow invalid node IDs. - // See GH#3606 - it is possible that those exist. - this.groupId = options.groupId; - this.nodeIds = options.nodeIds; - this.endpoints = options.endpoints; - } + // When removing associations, we allow invalid node IDs. + // See GH#3606 - it is possible that those exist. + this.groupId = options.groupId; + this.nodeIds = options.nodeIds; + this.endpoints = options.endpoints; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelAssociationCCRemove { + validatePayload(raw.payload.length >= 1); + const groupId: number | undefined = raw.payload[0]; + + const { nodeIds, endpoints } = + deserializeMultiChannelAssociationDestination( + raw.payload.subarray(1), + ); + + return new MultiChannelAssociationCCRemove({ + nodeId: ctx.sourceNodeId, + groupId, + nodeIds, + endpoints, + }); } public groupId?: number; @@ -761,28 +778,39 @@ export interface MultiChannelAssociationCCReportOptions { @CCCommand(MultiChannelAssociationCommand.Report) export class MultiChannelAssociationCCReport extends MultiChannelAssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (MultiChannelAssociationCCReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.groupId = this.payload[0]; - this.maxNodes = this.payload[1]; - this.reportsToFollow = this.payload[2]; - ({ nodeIds: this.nodeIds, endpoints: this.endpoints } = - deserializeMultiChannelAssociationDestination( - this.payload.subarray(3), - )); - } else { - this.groupId = options.groupId; - this.maxNodes = options.maxNodes; - this.nodeIds = options.nodeIds; - this.endpoints = options.endpoints; - this.reportsToFollow = options.reportsToFollow; - } + this.groupId = options.groupId; + this.maxNodes = options.maxNodes; + this.nodeIds = options.nodeIds; + this.endpoints = options.endpoints; + this.reportsToFollow = options.reportsToFollow; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelAssociationCCReport { + validatePayload(raw.payload.length >= 3); + const groupId = raw.payload[0]; + const maxNodes = raw.payload[1]; + const reportsToFollow = raw.payload[2]; + + const { nodeIds, endpoints } = + deserializeMultiChannelAssociationDestination( + raw.payload.subarray(3), + ); + + return new MultiChannelAssociationCCReport({ + nodeId: ctx.sourceNodeId, + groupId, + maxNodes, + nodeIds, + endpoints, + reportsToFollow, + }); } public readonly groupId: number; @@ -860,7 +888,7 @@ export class MultiChannelAssociationCCReport extends MultiChannelAssociationCC { } // @publicAPI -export interface MultiChannelAssociationCCGetOptions extends CCCommandOptions { +export interface MultiChannelAssociationCCGetOptions { groupId: number; } @@ -868,23 +896,29 @@ export interface MultiChannelAssociationCCGetOptions extends CCCommandOptions { @expectedCCResponse(MultiChannelAssociationCCReport) export class MultiChannelAssociationCCGet extends MultiChannelAssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelAssociationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupId = this.payload[0]; - } else { - if (options.groupId < 1) { - throw new ZWaveError( - "The group id must be positive!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.groupId = options.groupId; + if (options.groupId < 1) { + throw new ZWaveError( + "The group id must be positive!", + ZWaveErrorCodes.Argument_Invalid, + ); } + this.groupId = options.groupId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelAssociationCCGet { + validatePayload(raw.payload.length >= 1); + const groupId = raw.payload[0]; + + return new MultiChannelAssociationCCGet({ + nodeId: ctx.sourceNodeId, + groupId, + }); } public groupId: number; @@ -903,9 +937,7 @@ export class MultiChannelAssociationCCGet extends MultiChannelAssociationCC { } // @publicAPI -export interface MultiChannelAssociationCCSupportedGroupingsReportOptions - extends CCCommandOptions -{ +export interface MultiChannelAssociationCCSupportedGroupingsReportOptions { groupCount: number; } @@ -914,18 +946,26 @@ export class MultiChannelAssociationCCSupportedGroupingsReport extends MultiChannelAssociationCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelAssociationCCSupportedGroupingsReportOptions, + options: WithAddress< + MultiChannelAssociationCCSupportedGroupingsReportOptions + >, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.groupCount = this.payload[0]; - } else { - this.groupCount = options.groupCount; - } + this.groupCount = options.groupCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelAssociationCCSupportedGroupingsReport { + validatePayload(raw.payload.length >= 1); + const groupCount = raw.payload[0]; + + return new MultiChannelAssociationCCSupportedGroupingsReport({ + nodeId: ctx.sourceNodeId, + groupCount, + }); } @ccValue(MultiChannelAssociationCCValues.groupCount) diff --git a/packages/cc/src/cc/MultiChannelCC.ts b/packages/cc/src/cc/MultiChannelCC.ts index af3069711f05..a33082797b0d 100644 --- a/packages/cc/src/cc/MultiChannelCC.ts +++ b/packages/cc/src/cc/MultiChannelCC.ts @@ -7,6 +7,7 @@ import { MessagePriority, type MessageRecord, type SpecificDeviceClass, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeApplicationNodeInformation, @@ -27,13 +28,11 @@ import { validateArgs } from "@zwave-js/transformers"; import { distinct } from "alcalzone-shared/arrays"; import { CCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -44,6 +43,10 @@ import { expectedCCResponse, implementedVersion, } from "../lib/CommandClassDecorators"; +import { + isEncapsulatingCommandClass, + isMultiEncapsulatingCommandClass, +} from "../lib/EncapsulatingCommandClass"; import { V } from "../lib/Values"; import { MultiChannelCommand } from "../lib/_Types"; @@ -218,7 +221,7 @@ export class MultiChannelCCAPI extends CCAPI { const cc = new MultiChannelCCEndPointGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< MultiChannelCCEndPointReport @@ -247,7 +250,7 @@ export class MultiChannelCCAPI extends CCAPI { const cc = new MultiChannelCCCapabilityGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, requestedEndpoint: endpoint, }); const response = await this.host.sendCommand< @@ -286,7 +289,7 @@ export class MultiChannelCCAPI extends CCAPI { const cc = new MultiChannelCCEndPointFind({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, genericClass, specificClass, }); @@ -310,7 +313,7 @@ export class MultiChannelCCAPI extends CCAPI { const cc = new MultiChannelCCAggregatedMembersGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, requestedEndpoint: endpoint, }); const response = await this.host.sendCommand< @@ -326,10 +329,7 @@ export class MultiChannelCCAPI extends CCAPI { // want to pay the cost of validating each call // eslint-disable-next-line @zwave-js/ccapi-validate-args public async sendEncapsulated( - options: Omit< - MultiChannelCCCommandEncapsulationOptions, - keyof CCCommandOptions - >, + options: MultiChannelCCCommandEncapsulationOptions, ): Promise { this.assertSupportsCommand( MultiChannelCommand, @@ -806,7 +806,7 @@ supported CCs:`; } // @publicAPI -export interface MultiChannelCCEndPointReportOptions extends CCCommandOptions { +export interface MultiChannelCCEndPointReportOptions { countIsDynamic: boolean; identicalCapabilities: boolean; individualCount: number; @@ -816,26 +816,37 @@ export interface MultiChannelCCEndPointReportOptions extends CCCommandOptions { @CCCommand(MultiChannelCommand.EndPointReport) export class MultiChannelCCEndPointReport extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCEndPointReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.countIsDynamic = !!(this.payload[0] & 0b10000000); - this.identicalCapabilities = !!(this.payload[0] & 0b01000000); - this.individualCount = this.payload[1] & 0b01111111; - if (this.payload.length >= 3) { - this.aggregatedCount = this.payload[2] & 0b01111111; - } - } else { - this.countIsDynamic = options.countIsDynamic; - this.identicalCapabilities = options.identicalCapabilities; - this.individualCount = options.individualCount; - this.aggregatedCount = options.aggregatedCount; + this.countIsDynamic = options.countIsDynamic; + this.identicalCapabilities = options.identicalCapabilities; + this.individualCount = options.individualCount; + this.aggregatedCount = options.aggregatedCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCEndPointReport { + validatePayload(raw.payload.length >= 2); + const countIsDynamic = !!(raw.payload[0] & 0b10000000); + const identicalCapabilities = !!(raw.payload[0] & 0b01000000); + const individualCount = raw.payload[1] & 0b01111111; + let aggregatedCount: MaybeNotKnown; + + if (raw.payload.length >= 3) { + aggregatedCount = raw.payload[2] & 0b01111111; } + + return new MultiChannelCCEndPointReport({ + nodeId: ctx.sourceNodeId, + countIsDynamic, + identicalCapabilities, + individualCount, + aggregatedCount, + }); } @ccValue(MultiChannelCCValues.endpointCountIsDynamic) @@ -882,9 +893,7 @@ export class MultiChannelCCEndPointReport extends MultiChannelCC { export class MultiChannelCCEndPointGet extends MultiChannelCC {} // @publicAPI -export interface MultiChannelCCCapabilityReportOptions - extends CCCommandOptions -{ +export interface MultiChannelCCCapabilityReportOptions { endpointIndex: number; genericDeviceClass: number; specificDeviceClass: number; @@ -898,38 +907,48 @@ export class MultiChannelCCCapabilityReport extends MultiChannelCC implements ApplicationNodeInformation { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCCapabilityReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // Only validate the bytes we expect to see here - // parseApplicationNodeInformation does its own validation - validatePayload(this.payload.length >= 1); - this.endpointIndex = this.payload[0] & 0b01111111; - this.isDynamic = !!(this.payload[0] & 0b10000000); + this.endpointIndex = options.endpointIndex; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + this.supportedCCs = options.supportedCCs; + this.isDynamic = options.isDynamic; + this.wasRemoved = options.wasRemoved; + } - const NIF = parseApplicationNodeInformation( - this.payload.subarray(1), - ); - this.genericDeviceClass = NIF.genericDeviceClass; - this.specificDeviceClass = NIF.specificDeviceClass; - this.supportedCCs = NIF.supportedCCs; - - // Removal reports have very specific information - this.wasRemoved = this.isDynamic - && this.genericDeviceClass === 0xff // "Non-Interoperable" - && this.specificDeviceClass === 0x00; - } else { - this.endpointIndex = options.endpointIndex; - this.genericDeviceClass = options.genericDeviceClass; - this.specificDeviceClass = options.specificDeviceClass; - this.supportedCCs = options.supportedCCs; - this.isDynamic = options.isDynamic; - this.wasRemoved = options.wasRemoved; - } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCCapabilityReport { + // Only validate the bytes we expect to see here + // parseApplicationNodeInformation does its own validation + validatePayload(raw.payload.length >= 1); + const endpointIndex = raw.payload[0] & 0b01111111; + const isDynamic = !!(raw.payload[0] & 0b10000000); + const NIF = parseApplicationNodeInformation( + raw.payload.subarray(1), + ); + const genericDeviceClass = NIF.genericDeviceClass; + const specificDeviceClass = NIF.specificDeviceClass; + const supportedCCs: CommandClasses[] = NIF.supportedCCs; + + // Removal reports have very specific information + const wasRemoved: boolean = isDynamic + && genericDeviceClass === 0xff // "Non-Interoperable" + && specificDeviceClass === 0x00; + + return new MultiChannelCCCapabilityReport({ + nodeId: ctx.sourceNodeId, + endpointIndex, + isDynamic, + genericDeviceClass, + specificDeviceClass, + supportedCCs, + wasRemoved, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -991,7 +1010,7 @@ export class MultiChannelCCCapabilityReport extends MultiChannelCC } // @publicAPI -export interface MultiChannelCCCapabilityGetOptions extends CCCommandOptions { +export interface MultiChannelCCCapabilityGetOptions { requestedEndpoint: number; } @@ -1009,17 +1028,23 @@ function testResponseForMultiChannelCapabilityGet( ) export class MultiChannelCCCapabilityGet extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCCapabilityGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.requestedEndpoint = this.payload[0] & 0b01111111; - } else { - this.requestedEndpoint = options.requestedEndpoint; - } + this.requestedEndpoint = options.requestedEndpoint; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCCapabilityGet { + validatePayload(raw.payload.length >= 1); + const requestedEndpoint = raw.payload[0] & 0b01111111; + + return new MultiChannelCCCapabilityGet({ + nodeId: ctx.sourceNodeId, + requestedEndpoint, + }); } public requestedEndpoint: number; @@ -1038,9 +1063,7 @@ export class MultiChannelCCCapabilityGet extends MultiChannelCC { } // @publicAPI -export interface MultiChannelCCEndPointFindReportOptions - extends CCCommandOptions -{ +export interface MultiChannelCCEndPointFindReportOptions { genericClass: number; specificClass: number; foundEndpoints: number[]; @@ -1050,29 +1073,38 @@ export interface MultiChannelCCEndPointFindReportOptions @CCCommand(MultiChannelCommand.EndPointFindReport) export class MultiChannelCCEndPointFindReport extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCEndPointFindReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.reportsToFollow = this.payload[0]; - this.genericClass = this.payload[1]; - this.specificClass = this.payload[2]; - - // Some devices omit the endpoint list although that is not allowed in the specs - // therefore don't validatePayload here. - this.foundEndpoints = [...this.payload.subarray(3)] - .map((e) => e & 0b01111111) - .filter((e) => e !== 0); - } else { - this.genericClass = options.genericClass; - this.specificClass = options.specificClass; - this.foundEndpoints = options.foundEndpoints; - this.reportsToFollow = options.reportsToFollow; - } + this.genericClass = options.genericClass; + this.specificClass = options.specificClass; + this.foundEndpoints = options.foundEndpoints; + this.reportsToFollow = options.reportsToFollow; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCEndPointFindReport { + validatePayload(raw.payload.length >= 3); + const reportsToFollow = raw.payload[0]; + const genericClass = raw.payload[1]; + const specificClass = raw.payload[2]; + + // Some devices omit the endpoint list although that is not allowed in the specs + // therefore don't validatePayload here. + const foundEndpoints = [...raw.payload.subarray(3)] + .map((e) => e & 0b01111111) + .filter((e) => e !== 0); + + return new MultiChannelCCEndPointFindReport({ + nodeId: ctx.sourceNodeId, + reportsToFollow, + genericClass, + specificClass, + foundEndpoints, + }); } public genericClass: number; @@ -1133,7 +1165,7 @@ export class MultiChannelCCEndPointFindReport extends MultiChannelCC { } // @publicAPI -export interface MultiChannelCCEndPointFindOptions extends CCCommandOptions { +export interface MultiChannelCCEndPointFindOptions { genericClass: number; specificClass: number; } @@ -1142,19 +1174,26 @@ export interface MultiChannelCCEndPointFindOptions extends CCCommandOptions { @expectedCCResponse(MultiChannelCCEndPointFindReport) export class MultiChannelCCEndPointFind extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCEndPointFindOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.genericClass = this.payload[0]; - this.specificClass = this.payload[1]; - } else { - this.genericClass = options.genericClass; - this.specificClass = options.specificClass; - } + this.genericClass = options.genericClass; + this.specificClass = options.specificClass; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCEndPointFind { + validatePayload(raw.payload.length >= 2); + const genericClass = raw.payload[0]; + const specificClass = raw.payload[1]; + + return new MultiChannelCCEndPointFind({ + nodeId: ctx.sourceNodeId, + genericClass, + specificClass, + }); } public genericClass: number; @@ -1180,19 +1219,40 @@ export class MultiChannelCCEndPointFind extends MultiChannelCC { } } +// @publicAPI +export interface MultiChannelCCAggregatedMembersReportOptions { + aggregatedEndpointIndex: number; + members: number[]; +} + @CCCommand(MultiChannelCommand.AggregatedMembersReport) export class MultiChannelCCAggregatedMembersReport extends MultiChannelCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 2); - this.aggregatedEndpointIndex = this.payload[0] & 0b0111_1111; - const bitMaskLength = this.payload[1]; - validatePayload(this.payload.length >= 2 + bitMaskLength); - const bitMask = this.payload.subarray(2, 2 + bitMaskLength); - this.members = parseBitMask(bitMask); + // TODO: Check implementation: + this.aggregatedEndpointIndex = options.aggregatedEndpointIndex; + this.members = options.members; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCAggregatedMembersReport { + validatePayload(raw.payload.length >= 2); + const aggregatedEndpointIndex = raw.payload[0] & 0b0111_1111; + const bitMaskLength = raw.payload[1]; + validatePayload(raw.payload.length >= 2 + bitMaskLength); + const bitMask = raw.payload.subarray(2, 2 + bitMaskLength); + const members = parseBitMask(bitMask); + + return new MultiChannelCCAggregatedMembersReport({ + nodeId: ctx.sourceNodeId, + aggregatedEndpointIndex, + members, + }); } public readonly aggregatedEndpointIndex: number; @@ -1216,9 +1276,7 @@ export class MultiChannelCCAggregatedMembersReport extends MultiChannelCC { } // @publicAPI -export interface MultiChannelCCAggregatedMembersGetOptions - extends CCCommandOptions -{ +export interface MultiChannelCCAggregatedMembersGetOptions { requestedEndpoint: number; } @@ -1226,20 +1284,25 @@ export interface MultiChannelCCAggregatedMembersGetOptions @expectedCCResponse(MultiChannelCCAggregatedMembersReport) export class MultiChannelCCAggregatedMembersGet extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCAggregatedMembersGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.requestedEndpoint = options.requestedEndpoint; - } + this.requestedEndpoint = options.requestedEndpoint; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): MultiChannelCCAggregatedMembersGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new MultiChannelCCAggregatedMembersGet({ + // nodeId: ctx.sourceNodeId, + // }); } public requestedEndpoint: number; @@ -1260,9 +1323,7 @@ export class MultiChannelCCAggregatedMembersGet extends MultiChannelCC { type MultiChannelCCDestination = number | (1 | 2 | 3 | 4 | 5 | 6 | 7)[]; // @publicAPI -export interface MultiChannelCCCommandEncapsulationOptions - extends CCCommandOptions -{ +export interface MultiChannelCCCommandEncapsulationOptions { encapsulated: CommandClass; destination: MultiChannelCCDestination; } @@ -1311,46 +1372,64 @@ function testResponseForCommandEncapsulation( ) export class MultiChannelCCCommandEncapsulation extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCCommandEncapsulationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - if ( - options.context.getDeviceConfig?.(this.nodeId as number)?.compat - ?.treatDestinationEndpointAsSource - ) { - // This device incorrectly uses the destination field to indicate the source endpoint - this.endpointIndex = this.payload[1] & 0b0111_1111; - this.destination = 0; - } else { - // Parse normally - this.endpointIndex = this.payload[0] & 0b0111_1111; - const isBitMask = !!(this.payload[1] & 0b1000_0000); - const destination = this.payload[1] & 0b0111_1111; - if (isBitMask) { - this.destination = parseBitMask( - Buffer.from([destination]), - ) as any; - } else { - this.destination = destination; + this.encapsulated = options.encapsulated; + this.encapsulated.encapsulatingCC = this as any; + // Propagate the endpoint index all the way down + let cur: CommandClass = this; + while (cur) { + if (isMultiEncapsulatingCommandClass(cur)) { + for (const cc of cur.encapsulated) { + cc.endpointIndex = this.endpointIndex; } + break; + } else if (isEncapsulatingCommandClass(cur)) { + cur.encapsulated.endpointIndex = this.endpointIndex; + cur = cur.encapsulated; + } else { + break; } - // No need to validate further, each CC does it for itself - this.encapsulated = CommandClass.from({ - data: this.payload.subarray(2), - fromEncapsulation: true, - encapCC: this, - origin: options.origin, - context: options.context, - }); + } + this.destination = options.destination; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCCommandEncapsulation { + validatePayload(raw.payload.length >= 2); + + let endpointIndex: number; + let destination: MultiChannelCCDestination; + + if ( + ctx.getDeviceConfig?.(ctx.sourceNodeId) + ?.compat?.treatDestinationEndpointAsSource + ) { + // This device incorrectly uses the destination field to indicate the source endpoint + endpointIndex = raw.payload[1] & 0b0111_1111; + destination = 0; } else { - this.encapsulated = options.encapsulated; - options.encapsulated.encapsulatingCC = this as any; - this.destination = options.destination; + // Parse normally + endpointIndex = raw.payload[0] & 0b0111_1111; + const isBitMask = !!(raw.payload[1] & 0b1000_0000); + destination = raw.payload[1] & 0b0111_1111; + if (isBitMask) { + destination = parseBitMask( + Buffer.from([destination]), + ) as any; + } } + // No need to validate further, each CC does it for itself + const encapsulated = CommandClass.parse(raw.payload.subarray(2), ctx); + return new MultiChannelCCCommandEncapsulation({ + nodeId: ctx.sourceNodeId, + endpointIndex, + destination, + encapsulated, + }); } public encapsulated: CommandClass; @@ -1398,16 +1477,38 @@ export class MultiChannelCCCommandEncapsulation extends MultiChannelCC { } } +// @publicAPI +export interface MultiChannelCCV1ReportOptions { + requestedCC: CommandClasses; + endpointCount: number; +} + @CCCommand(MultiChannelCommand.ReportV1) export class MultiChannelCCV1Report extends MultiChannelCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); + + // TODO: Check implementation: + this.requestedCC = options.requestedCC; + this.endpointCount = options.endpointCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCV1Report { // V1 won't be extended in the future, so do an exact check - validatePayload(this.payload.length === 2); - this.requestedCC = this.payload[0]; - this.endpointCount = this.payload[1]; + validatePayload(raw.payload.length === 2); + const requestedCC: CommandClasses = raw.payload[0]; + const endpointCount = raw.payload[1]; + + return new MultiChannelCCV1Report({ + nodeId: ctx.sourceNodeId, + requestedCC, + endpointCount, + }); } public readonly requestedCC: CommandClasses; @@ -1432,7 +1533,7 @@ function testResponseForMultiChannelV1Get( } // @publicAPI -export interface MultiChannelCCV1GetOptions extends CCCommandOptions { +export interface MultiChannelCCV1GetOptions { requestedCC: CommandClasses; } @@ -1440,20 +1541,25 @@ export interface MultiChannelCCV1GetOptions extends CCCommandOptions { @expectedCCResponse(MultiChannelCCV1Report, testResponseForMultiChannelV1Get) export class MultiChannelCCV1Get extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCV1GetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.requestedCC = options.requestedCC; - } + this.requestedCC = options.requestedCC; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): MultiChannelCCV1Get { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new MultiChannelCCV1Get({ + // nodeId: ctx.sourceNodeId, + // }); } public requestedCC: CommandClasses; @@ -1490,9 +1596,7 @@ function testResponseForV1CommandEncapsulation( } // @publicAPI -export interface MultiChannelCCV1CommandEncapsulationOptions - extends CCCommandOptions -{ +export interface MultiChannelCCV1CommandEncapsulationOptions { encapsulated: CommandClass; } @@ -1503,33 +1607,37 @@ export interface MultiChannelCCV1CommandEncapsulationOptions ) export class MultiChannelCCV1CommandEncapsulation extends MultiChannelCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiChannelCCV1CommandEncapsulationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.endpointIndex = this.payload[0]; - - // Some devices send invalid reports, i.e. MultiChannelCCV1CommandEncapsulation, but with V2+ binary format - // This would be a NoOp CC, but it makes no sense to encapsulate that. - const isV2withV1Header = this.payload.length >= 2 - && this.payload[1] === 0x00; - - // No need to validate further, each CC does it for itself - this.encapsulated = CommandClass.from({ - data: this.payload.subarray(isV2withV1Header ? 2 : 1), - fromEncapsulation: true, - encapCC: this, - origin: options.origin, - context: options.context, - }); - } else { - this.encapsulated = options.encapsulated; - // No need to distinguish between source and destination in V1 - this.endpointIndex = this.encapsulated.endpointIndex; - } + this.encapsulated = options.encapsulated; + // No need to distinguish between source and destination in V1 + this.endpointIndex = this.encapsulated.endpointIndex; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiChannelCCV1CommandEncapsulation { + validatePayload(raw.payload.length >= 1); + const endpointIndex = raw.payload[0]; + + // Some devices send invalid reports, i.e. MultiChannelCCV1CommandEncapsulation, but with V2+ binary format + // This would be a NoOp CC, but it makes no sense to encapsulate that. + const isV2withV1Header = raw.payload.length >= 2 + && raw.payload[1] === 0x00; + + // No need to validate further, each CC does it for itself + const encapsulated = CommandClass.parse( + raw.payload.subarray(isV2withV1Header ? 2 : 1), + ctx, + ); + + return new MultiChannelCCV1CommandEncapsulation({ + nodeId: ctx.sourceNodeId, + endpointIndex, + encapsulated, + }); } public encapsulated!: CommandClass; diff --git a/packages/cc/src/cc/MultiCommandCC.ts b/packages/cc/src/cc/MultiCommandCC.ts index 5d620e0737e3..adecb65a6f01 100644 --- a/packages/cc/src/cc/MultiCommandCC.ts +++ b/packages/cc/src/cc/MultiCommandCC.ts @@ -3,17 +3,17 @@ import { EncapsulationFlags, type MaybeNotKnown, type MessageOrCCLogEntry, + type WithAddress, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -94,9 +94,7 @@ export class MultiCommandCC extends CommandClass { } // @publicAPI -export interface MultiCommandCCCommandEncapsulationOptions - extends CCCommandOptions -{ +export interface MultiCommandCCCommandEncapsulationOptions { encapsulated: CommandClass[]; } @@ -104,40 +102,45 @@ export interface MultiCommandCCCommandEncapsulationOptions // When sending commands encapsulated in this CC, responses to GET-type commands likely won't be encapsulated export class MultiCommandCCCommandEncapsulation extends MultiCommandCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultiCommandCCCommandEncapsulationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const numCommands = this.payload[0]; - this.encapsulated = []; - let offset = 1; - for (let i = 0; i < numCommands; i++) { - validatePayload(this.payload.length >= offset + 1); - const cmdLength = this.payload[offset]; - validatePayload(this.payload.length >= offset + 1 + cmdLength); - this.encapsulated.push( - CommandClass.from({ - data: this.payload.subarray( - offset + 1, - offset + 1 + cmdLength, - ), - fromEncapsulation: true, - encapCC: this, - origin: options.origin, - context: options.context, - }), - ); - offset += 1 + cmdLength; - } - } else { - this.encapsulated = options.encapsulated; - for (const cc of options.encapsulated) { - cc.encapsulatingCC = this as any; - } + this.encapsulated = options.encapsulated; + for (const cc of options.encapsulated) { + cc.encapsulatingCC = this as any; + // Multi Command CC is inside Multi Channel CC, so the endpoint must be copied + cc.endpointIndex = this.endpointIndex; + } + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultiCommandCCCommandEncapsulation { + validatePayload(raw.payload.length >= 1); + const numCommands = raw.payload[0]; + const encapsulated: CommandClass[] = []; + let offset = 1; + for (let i = 0; i < numCommands; i++) { + validatePayload(raw.payload.length >= offset + 1); + const cmdLength = raw.payload[offset]; + validatePayload(raw.payload.length >= offset + 1 + cmdLength); + encapsulated.push( + CommandClass.parse( + raw.payload.subarray( + offset + 1, + offset + 1 + cmdLength, + ), + ctx, + ), + ); + offset += 1 + cmdLength; } + + return new MultiCommandCCCommandEncapsulation({ + nodeId: ctx.sourceNodeId, + encapsulated, + }); } public encapsulated: CommandClass[]; diff --git a/packages/cc/src/cc/MultilevelSensorCC.ts b/packages/cc/src/cc/MultilevelSensorCC.ts index f19e272f2714..c83aa03dbff5 100644 --- a/packages/cc/src/cc/MultilevelSensorCC.ts +++ b/packages/cc/src/cc/MultilevelSensorCC.ts @@ -1,4 +1,5 @@ import { + type WithAddress, encodeBitMask, getSensor, getSensorName, @@ -31,6 +32,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetNode, GetSupportedCCVersion, @@ -38,7 +40,7 @@ import type { GetValueDB, LogNode, } from "@zwave-js/host/safe"; -import { num2hex } from "@zwave-js/shared/safe"; +import { type AllOrNone, num2hex } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, @@ -48,15 +50,13 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, type CCResponsePredicate, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -290,9 +290,9 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { const cc = new MultilevelSensorCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, - sensorType, - scale: scale ?? preferredScale, + endpointIndex: this.endpoint.index, + sensorType: sensorType!, + scale: (scale ?? preferredScale)!, }); const response = await this.host.sendCommand< MultilevelSensorCCReport @@ -333,7 +333,7 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { const cc = new MultilevelSensorCCGetSupportedSensor({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< MultilevelSensorCCSupportedSensorReport @@ -355,7 +355,7 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { const cc = new MultilevelSensorCCGetSupportedScale({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sensorType, }); const response = await this.host.sendCommand< @@ -380,7 +380,7 @@ export class MultilevelSensorCCAPI extends PhysicalCCAPI { const cc = new MultilevelSensorCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, type: sensorType, scale, value, @@ -635,7 +635,7 @@ value: ${mlsResponse.value}${ } // @publicAPI -export interface MultilevelSensorCCReportOptions extends CCCommandOptions { +export interface MultilevelSensorCCReportOptions { type: number; scale: number | Scale; value: number; @@ -645,28 +645,35 @@ export interface MultilevelSensorCCReportOptions extends CCCommandOptions { @useSupervision() export class MultilevelSensorCCReport extends MultilevelSensorCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSensorCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.type = this.payload[0]; - // parseFloatWithScale does its own validation - const { value, scale } = parseFloatWithScale( - this.payload.subarray(1), - ); - this.value = value; - this.scale = scale; - } else { - this.type = options.type; - this.value = options.value; - this.scale = typeof options.scale === "number" - ? options.scale - : options.scale.key; - } + this.type = options.type; + this.value = options.value; + this.scale = typeof options.scale === "number" + ? options.scale + : options.scale.key; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSensorCCReport { + validatePayload(raw.payload.length >= 1); + const type = raw.payload[0]; + + // parseFloatWithScale does its own validation + const { value, scale } = parseFloatWithScale( + raw.payload.subarray(1), + ); + + return new MultilevelSensorCCReport({ + nodeId: ctx.sourceNodeId, + type, + value, + scale, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -775,14 +782,11 @@ const testResponseForMultilevelSensorGet: CCResponsePredicate< }; // These options are supported starting in V5 -interface MultilevelSensorCCGetSpecificOptions { +// @publicAPI +export type MultilevelSensorCCGetOptions = AllOrNone<{ sensorType: number; scale: number; -} -// @publicAPI -export type MultilevelSensorCCGetOptions = - | CCCommandOptions - | (CCCommandOptions & MultilevelSensorCCGetSpecificOptions); +}>; @CCCommand(MultilevelSensorCommand.Get) @expectedCCResponse( @@ -791,21 +795,31 @@ export type MultilevelSensorCCGetOptions = ) export class MultilevelSensorCCGet extends MultilevelSensorCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSensorCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - if (this.payload.length >= 2) { - this.sensorType = this.payload[0]; - this.scale = (this.payload[1] >> 3) & 0b11; - } + if ("sensorType" in options) { + this.sensorType = options.sensorType; + this.scale = options.scale; + } + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSensorCCGet { + if (raw.payload.length >= 2) { + const sensorType = raw.payload[0]; + const scale = (raw.payload[1] >> 3) & 0b11; + return new MultilevelSensorCCGet({ + nodeId: ctx.sourceNodeId, + sensorType, + scale, + }); } else { - if ("sensorType" in options) { - this.sensorType = options.sensorType; - this.scale = options.scale; - } + return new MultilevelSensorCCGet({ + nodeId: ctx.sourceNodeId, + }); } } @@ -849,9 +863,7 @@ export class MultilevelSensorCCGet extends MultilevelSensorCC { } // @publicAPI -export interface MultilevelSensorCCSupportedSensorReportOptions - extends CCCommandOptions -{ +export interface MultilevelSensorCCSupportedSensorReportOptions { supportedSensorTypes: readonly number[]; } @@ -860,18 +872,24 @@ export class MultilevelSensorCCSupportedSensorReport extends MultilevelSensorCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSensorCCSupportedSensorReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.supportedSensorTypes = parseBitMask(this.payload); - } else { - this.supportedSensorTypes = options.supportedSensorTypes; - } + this.supportedSensorTypes = options.supportedSensorTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSensorCCSupportedSensorReport { + validatePayload(raw.payload.length >= 1); + const supportedSensorTypes = parseBitMask(raw.payload); + + return new MultilevelSensorCCSupportedSensorReport({ + nodeId: ctx.sourceNodeId, + supportedSensorTypes, + }); } // TODO: Use this during interview to precreate values @@ -900,9 +918,7 @@ export class MultilevelSensorCCSupportedSensorReport export class MultilevelSensorCCGetSupportedSensor extends MultilevelSensorCC {} // @publicAPI -export interface MultilevelSensorCCSupportedScaleReportOptions - extends CCCommandOptions -{ +export interface MultilevelSensorCCSupportedScaleReportOptions { sensorType: number; supportedScales: readonly number[]; } @@ -910,23 +926,30 @@ export interface MultilevelSensorCCSupportedScaleReportOptions @CCCommand(MultilevelSensorCommand.SupportedScaleReport) export class MultilevelSensorCCSupportedScaleReport extends MultilevelSensorCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSensorCCSupportedScaleReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.sensorType = this.payload[0]; - this.supportedScales = parseBitMask( - Buffer.from([this.payload[1] & 0b1111]), - 0, - ); - } else { - this.sensorType = options.sensorType; - this.supportedScales = options.supportedScales; - } + this.sensorType = options.sensorType; + this.supportedScales = options.supportedScales; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSensorCCSupportedScaleReport { + validatePayload(raw.payload.length >= 2); + const sensorType = raw.payload[0]; + const supportedScales = parseBitMask( + Buffer.from([raw.payload[1] & 0b1111]), + 0, + ); + + return new MultilevelSensorCCSupportedScaleReport({ + nodeId: ctx.sourceNodeId, + sensorType, + supportedScales, + }); } public readonly sensorType: number; @@ -966,9 +989,7 @@ export class MultilevelSensorCCSupportedScaleReport extends MultilevelSensorCC { } // @publicAPI -export interface MultilevelSensorCCGetSupportedScaleOptions - extends CCCommandOptions -{ +export interface MultilevelSensorCCGetSupportedScaleOptions { sensorType: number; } @@ -976,17 +997,23 @@ export interface MultilevelSensorCCGetSupportedScaleOptions @expectedCCResponse(MultilevelSensorCCSupportedScaleReport) export class MultilevelSensorCCGetSupportedScale extends MultilevelSensorCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSensorCCGetSupportedScaleOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.sensorType = this.payload[0]; - } else { - this.sensorType = options.sensorType; - } + this.sensorType = options.sensorType; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSensorCCGetSupportedScale { + validatePayload(raw.payload.length >= 1); + const sensorType = raw.payload[0]; + + return new MultilevelSensorCCGetSupportedScale({ + nodeId: ctx.sourceNodeId, + sensorType, + }); } public sensorType: number; diff --git a/packages/cc/src/cc/MultilevelSwitchCC.ts b/packages/cc/src/cc/MultilevelSwitchCC.ts index a7a188d8e4f4..2c60c409b752 100644 --- a/packages/cc/src/cc/MultilevelSwitchCC.ts +++ b/packages/cc/src/cc/MultilevelSwitchCC.ts @@ -9,11 +9,16 @@ import { NOT_KNOWN, type SupervisionResult, ValueMetadata, + type WithAddress, maybeUnknownToString, parseMaybeNumber, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -28,14 +33,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -230,7 +233,7 @@ export class MultilevelSwitchCCAPI extends CCAPI { const cc = new MultilevelSwitchCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< MultilevelSwitchCCReport @@ -261,7 +264,7 @@ export class MultilevelSwitchCCAPI extends CCAPI { const cc = new MultilevelSwitchCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, targetValue, duration, }); @@ -279,7 +282,7 @@ export class MultilevelSwitchCCAPI extends CCAPI { const cc = new MultilevelSwitchCCStartLevelChange({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); @@ -294,7 +297,7 @@ export class MultilevelSwitchCCAPI extends CCAPI { const cc = new MultilevelSwitchCCStopLevelChange({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); return this.host.sendCommand(cc, this.commandOptions); @@ -308,7 +311,7 @@ export class MultilevelSwitchCCAPI extends CCAPI { const cc = new MultilevelSwitchCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< MultilevelSwitchCCSupportedReport @@ -615,7 +618,7 @@ export class MultilevelSwitchCC extends CommandClass { } // @publicAPI -export interface MultilevelSwitchCCSetOptions extends CCCommandOptions { +export interface MultilevelSwitchCCSetOptions { targetValue: number; // Version >= 2: duration?: Duration | string; @@ -625,22 +628,30 @@ export interface MultilevelSwitchCCSetOptions extends CCCommandOptions { @useSupervision() export class MultilevelSwitchCCSet extends MultilevelSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSwitchCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.targetValue = this.payload[0]; + this.targetValue = options.targetValue; + this.duration = Duration.from(options.duration); + } - if (this.payload.length >= 2) { - this.duration = Duration.parseReport(this.payload[1]); - } - } else { - this.targetValue = options.targetValue; - this.duration = Duration.from(options.duration); + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSwitchCCSet { + validatePayload(raw.payload.length >= 1); + const targetValue = raw.payload[0]; + let duration: Duration | undefined; + + if (raw.payload.length >= 2) { + duration = Duration.parseReport(raw.payload[1]); } + + return new MultilevelSwitchCCSet({ + nodeId: ctx.sourceNodeId, + targetValue, + duration, + }); } public targetValue: number; @@ -680,37 +691,48 @@ export class MultilevelSwitchCCSet extends MultilevelSwitchCC { } // @publicAPI -export interface MultilevelSwitchCCReportOptions extends CCCommandOptions { - currentValue: MaybeUnknown; - targetValue: MaybeUnknown; +export interface MultilevelSwitchCCReportOptions { + currentValue?: MaybeUnknown; + targetValue?: MaybeUnknown; duration?: Duration | string; } @CCCommand(MultilevelSwitchCommand.Report) export class MultilevelSwitchCCReport extends MultilevelSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | MultilevelSwitchCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.currentValue = - // 0xff is a legacy value for 100% (99) - this.payload[0] === 0xff - ? 99 - : parseMaybeNumber(this.payload[0]); - if (this.payload.length >= 3) { - this.targetValue = parseMaybeNumber(this.payload[1]); - this.duration = Duration.parseReport(this.payload[2]); - } - } else { - this.currentValue = options.currentValue; - this.targetValue = options.targetValue; - this.duration = Duration.from(options.duration); + this.currentValue = options.currentValue; + this.targetValue = options.targetValue; + this.duration = Duration.from(options.duration); + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSwitchCCReport { + validatePayload(raw.payload.length >= 1); + const currentValue: MaybeUnknown | undefined = + // 0xff is a legacy value for 100% (99) + raw.payload[0] === 0xff + ? 99 + : parseMaybeNumber(raw.payload[0]); + let targetValue: MaybeUnknown | undefined; + let duration: Duration | undefined; + + if (raw.payload.length >= 3) { + targetValue = parseMaybeNumber(raw.payload[1]); + duration = Duration.parseReport(raw.payload[2]); } + + return new MultilevelSwitchCCReport({ + nodeId: ctx.sourceNodeId, + currentValue, + targetValue, + duration, + }); } @ccValue(MultilevelSwitchCCValues.targetValue) @@ -774,29 +796,37 @@ export type MultilevelSwitchCCStartLevelChangeOptions = @useSupervision() export class MultilevelSwitchCCStartLevelChange extends MultilevelSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & MultilevelSwitchCCStartLevelChangeOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - const ignoreStartLevel = (this.payload[0] & 0b0_0_1_00000) >>> 5; - this.ignoreStartLevel = !!ignoreStartLevel; - const direction = (this.payload[0] & 0b0_1_0_00000) >>> 6; - this.direction = direction ? "down" : "up"; - - this.startLevel = this.payload[1]; + this.duration = Duration.from(options.duration); + this.ignoreStartLevel = options.ignoreStartLevel; + this.startLevel = options.startLevel ?? 0; + this.direction = options.direction; + } - if (this.payload.length >= 3) { - this.duration = Duration.parseSet(this.payload[2]); - } - } else { - this.duration = Duration.from(options.duration); - this.ignoreStartLevel = options.ignoreStartLevel; - this.startLevel = options.startLevel ?? 0; - this.direction = options.direction; + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSwitchCCStartLevelChange { + validatePayload(raw.payload.length >= 2); + const ignoreStartLevel = !!((raw.payload[0] & 0b0_0_1_00000) >>> 5); + const direction = ((raw.payload[0] & 0b0_1_0_00000) >>> 6) + ? "down" + : "up"; + const startLevel = raw.payload[1]; + let duration: Duration | undefined; + if (raw.payload.length >= 3) { + duration = Duration.parseSet(raw.payload[2]); } + + return new MultilevelSwitchCCStartLevelChange({ + nodeId: ctx.sourceNodeId, + ignoreStartLevel, + direction, + startLevel, + duration, + }); } public duration: Duration | undefined; @@ -855,19 +885,24 @@ export interface MultilevelSwitchCCSupportedReportOptions { @CCCommand(MultilevelSwitchCommand.SupportedReport) export class MultilevelSwitchCCSupportedReport extends MultilevelSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & MultilevelSwitchCCSupportedReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.switchType = this.payload[0] & 0b11111; - // We do not support the deprecated secondary switch type - } else { - this.switchType = options.switchType; - } + this.switchType = options.switchType; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): MultilevelSwitchCCSupportedReport { + validatePayload(raw.payload.length >= 1); + const switchType: SwitchType = raw.payload[0] & 0b11111; + + return new MultilevelSwitchCCSupportedReport({ + nodeId: ctx.sourceNodeId, + switchType, + }); } // This is the primary switch type. We're not supporting secondary switch types diff --git a/packages/cc/src/cc/NoOperationCC.ts b/packages/cc/src/cc/NoOperationCC.ts index c81d89f72737..65ac9fa282fa 100644 --- a/packages/cc/src/cc/NoOperationCC.ts +++ b/packages/cc/src/cc/NoOperationCC.ts @@ -18,7 +18,7 @@ export class NoOperationCCAPI extends PhysicalCCAPI { await this.host.sendCommand( new NoOperationCC({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }), { ...this.commandOptions, diff --git a/packages/cc/src/cc/NodeNamingCC.ts b/packages/cc/src/cc/NodeNamingCC.ts index 78a52401ffa3..fa265d742faf 100644 --- a/packages/cc/src/cc/NodeNamingCC.ts +++ b/packages/cc/src/cc/NodeNamingCC.ts @@ -5,11 +5,16 @@ import { MessagePriority, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, @@ -22,12 +27,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -133,7 +136,7 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { const cc = new NodeNamingAndLocationCCNameGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< NodeNamingAndLocationCCNameReport @@ -153,7 +156,7 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { const cc = new NodeNamingAndLocationCCNameSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, name, }); return this.host.sendCommand(cc, this.commandOptions); @@ -167,7 +170,7 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { const cc = new NodeNamingAndLocationCCLocationGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< NodeNamingAndLocationCCLocationReport @@ -189,7 +192,7 @@ export class NodeNamingAndLocationCCAPI extends PhysicalCCAPI { const cc = new NodeNamingAndLocationCCLocationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, location, }); return this.host.sendCommand(cc, this.commandOptions); @@ -264,9 +267,7 @@ export class NodeNamingAndLocationCC extends CommandClass { } // @publicAPI -export interface NodeNamingAndLocationCCNameSetOptions - extends CCCommandOptions -{ +export interface NodeNamingAndLocationCCNameSetOptions { name: string; } @@ -274,20 +275,25 @@ export interface NodeNamingAndLocationCCNameSetOptions @useSupervision() export class NodeNamingAndLocationCCNameSet extends NodeNamingAndLocationCC { public constructor( - options: - | CommandClassDeserializationOptions - | NodeNamingAndLocationCCNameSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.name = options.name; - } + this.name = options.name; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): NodeNamingAndLocationCCNameSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new NodeNamingAndLocationCCNameSet({ + // nodeId: ctx.sourceNodeId, + // }); } public name: string; @@ -321,20 +327,37 @@ export class NodeNamingAndLocationCCNameSet extends NodeNamingAndLocationCC { } } +// @publicAPI +export interface NodeNamingAndLocationCCNameReportOptions { + name: string; +} + @CCCommand(NodeNamingAndLocationCommand.NameReport) export class NodeNamingAndLocationCCNameReport extends NodeNamingAndLocationCC { public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, + options: WithAddress, ) { super(options); - const encoding = this.payload[0] === 2 ? "utf16le" : "ascii"; - let nameBuffer = this.payload.subarray(1); + this.name = options.name; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): NodeNamingAndLocationCCNameReport { + validatePayload(raw.payload.length >= 1); + const encoding = raw.payload[0] === 2 ? "utf16le" : "ascii"; + let nameBuffer = raw.payload.subarray(1); if (encoding === "utf16le") { validatePayload(nameBuffer.length % 2 === 0); // Z-Wave expects UTF16 BE nameBuffer = nameBuffer.swap16(); } - this.name = nameBuffer.toString(encoding); + + return new NodeNamingAndLocationCCNameReport({ + nodeId: ctx.sourceNodeId, + name: nameBuffer.toString(encoding), + }); } @ccValue(NodeNamingAndLocationCCValues.name) @@ -353,9 +376,7 @@ export class NodeNamingAndLocationCCNameReport extends NodeNamingAndLocationCC { export class NodeNamingAndLocationCCNameGet extends NodeNamingAndLocationCC {} // @publicAPI -export interface NodeNamingAndLocationCCLocationSetOptions - extends CCCommandOptions -{ +export interface NodeNamingAndLocationCCLocationSetOptions { location: string; } @@ -365,20 +386,25 @@ export class NodeNamingAndLocationCCLocationSet extends NodeNamingAndLocationCC { public constructor( - options: - | CommandClassDeserializationOptions - | NodeNamingAndLocationCCLocationSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.location = options.location; - } + this.location = options.location; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): NodeNamingAndLocationCCLocationSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new NodeNamingAndLocationCCLocationSet({ + // nodeId: ctx.sourceNodeId, + // }); } public location: string; @@ -412,22 +438,39 @@ export class NodeNamingAndLocationCCLocationSet } } +// @publicAPI +export interface NodeNamingAndLocationCCLocationReportOptions { + location: string; +} + @CCCommand(NodeNamingAndLocationCommand.LocationReport) export class NodeNamingAndLocationCCLocationReport extends NodeNamingAndLocationCC { public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, + options: WithAddress, ) { super(options); - const encoding = this.payload[0] === 2 ? "utf16le" : "ascii"; - let locationBuffer = this.payload.subarray(1); + this.location = options.location; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): NodeNamingAndLocationCCLocationReport { + validatePayload(raw.payload.length >= 1); + const encoding = raw.payload[0] === 2 ? "utf16le" : "ascii"; + let locationBuffer = raw.payload.subarray(1); if (encoding === "utf16le") { validatePayload(locationBuffer.length % 2 === 0); // Z-Wave expects UTF16 BE locationBuffer = locationBuffer.swap16(); } - this.location = locationBuffer.toString(encoding); + + return new NodeNamingAndLocationCCLocationReport({ + nodeId: ctx.sourceNodeId, + location: locationBuffer.toString(encoding), + }); } @ccValue(NodeNamingAndLocationCCValues.location) diff --git a/packages/cc/src/cc/NotificationCC.ts b/packages/cc/src/cc/NotificationCC.ts index f0704a5d06cf..335a68e0cb78 100644 --- a/packages/cc/src/cc/NotificationCC.ts +++ b/packages/cc/src/cc/NotificationCC.ts @@ -2,6 +2,7 @@ import { type Notification, type NotificationState, type NotificationValue, + type WithAddress, getNotification, getNotificationEventName, getNotificationName, @@ -20,6 +21,7 @@ import { MessagePriority, type MessageRecord, type NodeId, + SecurityClass, type SinglecastCC, type SupervisionResult, type SupportsCC, @@ -36,6 +38,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetNode, GetSupportedCCVersion, @@ -53,15 +56,13 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { - type CCCommandOptions, + CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, InvalidCC, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -293,7 +294,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { * @internal */ public async getInternal( - options: NotificationCCGetSpecificOptions, + options: NotificationCCGetOptions, ): Promise { this.assertSupportsCommand( NotificationCommand, @@ -302,7 +303,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { const cc = new NotificationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand( @@ -322,7 +323,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { const cc = new NotificationCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); return this.host.sendCommand(cc, this.commandOptions); @@ -330,7 +331,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { @validateArgs() // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - public async get(options: NotificationCCGetSpecificOptions) { + public async get(options: NotificationCCGetOptions) { const response = await this.getInternal(options); if (response) { return pick(response, [ @@ -355,7 +356,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { const cc = new NotificationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, notificationType, notificationStatus, }); @@ -371,7 +372,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { const cc = new NotificationCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< NotificationCCSupportedReport @@ -398,7 +399,7 @@ export class NotificationCCAPI extends PhysicalCCAPI { const cc = new NotificationCCEventSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, notificationType, }); const response = await this.host.sendCommand< @@ -921,7 +922,7 @@ export class NotificationCC extends CommandClass { } // @publicAPI -export interface NotificationCCSetOptions extends CCCommandOptions { +export interface NotificationCCSetOptions { notificationType: number; notificationStatus: boolean; } @@ -930,18 +931,25 @@ export interface NotificationCCSetOptions extends CCCommandOptions { @useSupervision() export class NotificationCCSet extends NotificationCC { public constructor( - options: CommandClassDeserializationOptions | NotificationCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.notificationType = this.payload[0]; - this.notificationStatus = this.payload[1] === 0xff; - } else { - this.notificationType = options.notificationType; - this.notificationStatus = options.notificationStatus; - } + this.notificationType = options.notificationType; + this.notificationStatus = options.notificationStatus; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): NotificationCCSet { + validatePayload(raw.payload.length >= 2); + const notificationType = raw.payload[0]; + const notificationStatus = raw.payload[1] === 0xff; + + return new NotificationCCSet({ + nodeId: ctx.sourceNodeId, + notificationType, + notificationStatus, + }); } + public notificationType: number; public notificationStatus: boolean; @@ -965,74 +973,93 @@ export class NotificationCCSet extends NotificationCC { } // @publicAPI -export type NotificationCCReportOptions = - | { - alarmType: number; - alarmLevel: number; - } - | { - notificationType: number; - notificationEvent: number; - eventParameters?: Buffer; - sequenceNumber?: number; - }; +export type NotificationCCReportOptions = { + alarmType?: number; + alarmLevel?: number; + notificationType?: number; + notificationEvent?: number; + notificationStatus?: number; + eventParameters?: Buffer; + sequenceNumber?: number; +}; @CCCommand(NotificationCommand.Report) @useSupervision() export class NotificationCCReport extends NotificationCC { public constructor( - options: - | CommandClassDeserializationOptions - | (NotificationCCReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.alarmType = this.payload[0]; - this.alarmLevel = this.payload[1]; - // Byte 2 used to be zensorNetSourceNodeId in V2 and V3, but we don't care about that - - // V2+ requires the alarm bytes to be zero. Manufacturers don't care though, so we don't enforce that. - // Don't use the version to decide because we might discard notifications - // before the interview is complete - if (this.payload.length >= 7) { - this.notificationStatus = this.payload[3]; - this.notificationType = this.payload[4]; - this.notificationEvent = this.payload[5]; - - const containsSeqNum = !!(this.payload[6] & 0b1000_0000); - const numEventParams = this.payload[6] & 0b11111; - if (numEventParams > 0) { - validatePayload(this.payload.length >= 7 + numEventParams); - this.eventParameters = Buffer.from( - this.payload.subarray(7, 7 + numEventParams), - ); - } - if (containsSeqNum) { - validatePayload( - this.payload.length >= 7 + numEventParams + 1, - ); - this.sequenceNumber = this.payload[7 + numEventParams]; - } - } + if (options.alarmType != undefined) { + this.alarmType = options.alarmType; + this.alarmLevel = options.alarmLevel; + } - // Store the V1 alarm values if they exist - } else { - // Create a notification to send - if ("alarmType" in options) { - this.alarmType = options.alarmType; - this.alarmLevel = options.alarmLevel; - } else { - this.notificationType = options.notificationType; - this.notificationStatus = true; - this.notificationEvent = options.notificationEvent; - this.eventParameters = options.eventParameters; - this.sequenceNumber = options.sequenceNumber; - } + if (options.notificationType != undefined) { + this.notificationType = options.notificationType; + this.notificationStatus = options.notificationStatus ?? true; + this.notificationEvent = options.notificationEvent; + this.eventParameters = options.eventParameters; + this.sequenceNumber = options.sequenceNumber; } } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): NotificationCCReport { + validatePayload(raw.payload.length >= 2); + const alarmType = raw.payload[0]; + const alarmLevel = raw.payload[1]; + + // Byte 2 used to be zensorNetSourceNodeId in V2 and V3, but we don't care about that + + if (raw.payload.length < 7) { + return new NotificationCCReport({ + nodeId: ctx.sourceNodeId, + alarmType, + alarmLevel, + }); + } + + // V2+ requires the alarm bytes to be zero. Manufacturers don't care though, so we don't enforce that. + // Don't use the version to decide because we might discard notifications + // before the interview is complete + + const notificationStatus = raw.payload[3]; + const notificationType = raw.payload[4]; + const notificationEvent = raw.payload[5]; + + const containsSeqNum = !!(raw.payload[6] & 0b1000_0000); + const numEventParams = raw.payload[6] & 0b11111; + let eventParameters: Buffer | undefined; + if (numEventParams > 0) { + validatePayload(raw.payload.length >= 7 + numEventParams); + eventParameters = Buffer.from( + raw.payload.subarray(7, 7 + numEventParams), + ); + } + let sequenceNumber: number | undefined; + if (containsSeqNum) { + validatePayload( + raw.payload.length >= 7 + numEventParams + 1, + ); + sequenceNumber = raw.payload[7 + numEventParams]; + } + + return new NotificationCCReport({ + nodeId: ctx.sourceNodeId, + alarmType, + alarmLevel, + notificationStatus, + notificationType, + notificationEvent, + eventParameters, + sequenceNumber, + }); + } + public persistValues(ctx: PersistValuesContext): boolean { if (!super.persistValues(ctx)) return false; @@ -1240,7 +1267,7 @@ export class NotificationCCReport extends NotificationCC { }; } - private parseEventParameters(ctx: LogNode): void { + private parseEventParameters(ctx: PersistValuesContext): void { // This only makes sense for V2+ notifications if ( this.notificationType == undefined @@ -1289,14 +1316,20 @@ export class NotificationCCReport extends NotificationCC { // Try to parse the event parameters - if this fails, we should still handle the notification report try { // Convert CommandClass instances to a standardized object representation - const cc = CommandClass.from({ - data: this.eventParameters, - fromEncapsulation: true, - encapCC: this, - // FIXME: persistValues needs access to the CCParsingContext - context: {} as any, + const cc = CommandClass.parse(this.eventParameters, { + ...ctx, + sourceNodeId: this.nodeId as number, + // Security encapsulation is handled outside of this CC, + // so it is not needed here: + hasSecurityClass: () => false, + getHighestSecurityClass: () => SecurityClass.None, + setSecurityClass: () => {}, + securityManager: undefined, + securityManager2: undefined, + securityManagerLR: undefined, }); validatePayload(!(cc instanceof InvalidCC)); + cc.encapsulatingCC = this as any; if (isNotificationEventPayload(cc)) { this.eventParameters = cc @@ -1320,10 +1353,7 @@ export class NotificationCCReport extends NotificationCC { === ZWaveErrorCodes.PacketFormat_InvalidPayload && Buffer.isBuffer(this.eventParameters) ) { - const ccId = CommandClass.getCommandClass( - this.eventParameters, - ); - const ccCommand = CommandClass.getCCCommand( + const { ccId, ccCommand } = CCRaw.parse( this.eventParameters, ); if ( @@ -1435,7 +1465,8 @@ export class NotificationCCReport extends NotificationCC { } } -type NotificationCCGetSpecificOptions = +// @publicAPI +export type NotificationCCGetOptions = | { alarmType: number; } @@ -1443,34 +1474,42 @@ type NotificationCCGetSpecificOptions = notificationType: number; notificationEvent?: number; }; -// @publicAPI -export type NotificationCCGetOptions = - & CCCommandOptions - & NotificationCCGetSpecificOptions; @CCCommand(NotificationCommand.Get) @expectedCCResponse(NotificationCCReport) export class NotificationCCGet extends NotificationCC { public constructor( - options: CommandClassDeserializationOptions | NotificationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.alarmType = this.payload[0] || undefined; - if (this.payload.length >= 2) { - this.notificationType = this.payload[1] || undefined; - if (this.payload.length >= 3 && this.notificationType != 0xff) { - this.notificationEvent = this.payload[2]; - } - } + if ("alarmType" in options) { + this.alarmType = options.alarmType; } else { - if ("alarmType" in options) { - this.alarmType = options.alarmType; - } else { - this.notificationType = options.notificationType; - this.notificationEvent = options.notificationEvent; + this.notificationType = options.notificationType; + this.notificationEvent = options.notificationEvent; + } + } + + public static from(raw: CCRaw, ctx: CCParsingContext): NotificationCCGet { + validatePayload(raw.payload.length >= 1); + + if (raw.payload.length >= 2) { + const notificationType = raw.payload[1]; + let notificationEvent: number | undefined; + if (raw.payload.length >= 3 && notificationType != 0xff) { + notificationEvent = raw.payload[2]; } + return new NotificationCCGet({ + nodeId: ctx.sourceNodeId, + notificationType, + notificationEvent, + }); + } else { + const alarmType = raw.payload[0]; + return new NotificationCCGet({ + nodeId: ctx.sourceNodeId, + alarmType, + }); } } @@ -1516,7 +1555,7 @@ export class NotificationCCGet extends NotificationCC { } // @publicAPI -export interface NotificationCCSupportedReportOptions extends CCCommandOptions { +export interface NotificationCCSupportedReportOptions { supportsV1Alarm: boolean; supportedNotificationTypes: number[]; } @@ -1524,34 +1563,40 @@ export interface NotificationCCSupportedReportOptions extends CCCommandOptions { @CCCommand(NotificationCommand.SupportedReport) export class NotificationCCSupportedReport extends NotificationCC { public constructor( - options: - | NotificationCCSupportedReportOptions - | CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.supportsV1Alarm = !!(this.payload[0] & 0b1000_0000); - const numBitMaskBytes = this.payload[0] & 0b0001_1111; - validatePayload( - numBitMaskBytes > 0, - this.payload.length >= 1 + numBitMaskBytes, - ); - const notificationBitMask = this.payload.subarray( - 1, - 1 + numBitMaskBytes, - ); - this.supportedNotificationTypes = parseBitMask( - notificationBitMask, - // bit 0 is ignored, but counting still starts at 1, so the first bit must have the value 0 - 0, - ); - } else { - this.supportsV1Alarm = options.supportsV1Alarm; - this.supportedNotificationTypes = - options.supportedNotificationTypes; - } + this.supportsV1Alarm = options.supportsV1Alarm; + this.supportedNotificationTypes = options.supportedNotificationTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): NotificationCCSupportedReport { + validatePayload(raw.payload.length >= 1); + const supportsV1Alarm = !!(raw.payload[0] & 0b1000_0000); + const numBitMaskBytes = raw.payload[0] & 0b0001_1111; + validatePayload( + numBitMaskBytes > 0, + raw.payload.length >= 1 + numBitMaskBytes, + ); + const notificationBitMask = raw.payload.subarray( + 1, + 1 + numBitMaskBytes, + ); + const supportedNotificationTypes = parseBitMask( + notificationBitMask, + // bit 0 is ignored, but counting still starts at 1, so the first bit must have the value 0 + 0, + ); + + return new NotificationCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportsV1Alarm, + supportedNotificationTypes, + }); } @ccValue(NotificationCCValues.supportsV1Alarm) @@ -1595,9 +1640,7 @@ export class NotificationCCSupportedReport extends NotificationCC { export class NotificationCCSupportedGet extends NotificationCC {} // @publicAPI -export interface NotificationCCEventSupportedReportOptions - extends CCCommandOptions -{ +export interface NotificationCCEventSupportedReportOptions { notificationType: number; supportedEvents: number[]; } @@ -1605,33 +1648,44 @@ export interface NotificationCCEventSupportedReportOptions @CCCommand(NotificationCommand.EventSupportedReport) export class NotificationCCEventSupportedReport extends NotificationCC { public constructor( - options: - | CommandClassDeserializationOptions - | NotificationCCEventSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.notificationType = this.payload[0]; - const numBitMaskBytes = this.payload[1] & 0b000_11111; - if (numBitMaskBytes === 0) { - // Notification type is not supported - this.supportedEvents = []; - return; - } + this.notificationType = options.notificationType; + this.supportedEvents = options.supportedEvents; + } - validatePayload(this.payload.length >= 2 + numBitMaskBytes); - const eventBitMask = this.payload.subarray(2, 2 + numBitMaskBytes); - this.supportedEvents = parseBitMask( - eventBitMask, - // In this mask, bit 0 is ignored, but counting still starts at 1, so the first bit must have the value 0 - 0, - ); - } else { - this.notificationType = options.notificationType; - this.supportedEvents = options.supportedEvents; + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): NotificationCCEventSupportedReport { + validatePayload(raw.payload.length >= 1); + const notificationType = raw.payload[0]; + const numBitMaskBytes = raw.payload[1] & 0b000_11111; + + if (numBitMaskBytes === 0) { + // Notification type is not supported + return new NotificationCCEventSupportedReport({ + nodeId: ctx.sourceNodeId, + notificationType, + supportedEvents: [], + }); } + + validatePayload(raw.payload.length >= 2 + numBitMaskBytes); + const eventBitMask = raw.payload.subarray(2, 2 + numBitMaskBytes); + const supportedEvents = parseBitMask( + eventBitMask, + // In this mask, bit 0 is ignored, but counting still starts at 1, so the first bit must have the value 0 + 0, + ); + + return new NotificationCCEventSupportedReport({ + nodeId: ctx.sourceNodeId, + notificationType, + supportedEvents, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -1729,9 +1783,7 @@ export class NotificationCCEventSupportedReport extends NotificationCC { } // @publicAPI -export interface NotificationCCEventSupportedGetOptions - extends CCCommandOptions -{ +export interface NotificationCCEventSupportedGetOptions { notificationType: number; } @@ -1739,17 +1791,23 @@ export interface NotificationCCEventSupportedGetOptions @expectedCCResponse(NotificationCCEventSupportedReport) export class NotificationCCEventSupportedGet extends NotificationCC { public constructor( - options: - | CommandClassDeserializationOptions - | NotificationCCEventSupportedGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.notificationType = this.payload[0]; - } else { - this.notificationType = options.notificationType; - } + this.notificationType = options.notificationType; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): NotificationCCEventSupportedGet { + validatePayload(raw.payload.length >= 1); + const notificationType = raw.payload[0]; + + return new NotificationCCEventSupportedGet({ + nodeId: ctx.sourceNodeId, + notificationType, + }); } public notificationType: number; diff --git a/packages/cc/src/cc/PowerlevelCC.ts b/packages/cc/src/cc/PowerlevelCC.ts index 49e461caa8d7..ce06831e3180 100644 --- a/packages/cc/src/cc/PowerlevelCC.ts +++ b/packages/cc/src/cc/PowerlevelCC.ts @@ -5,20 +5,20 @@ import { type MessageRecord, NodeStatus, type SupervisionResult, + type WithAddress, ZWaveError, ZWaveErrorCodes, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { PhysicalCCAPI } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -55,7 +55,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, powerlevel: Powerlevel["Normal Power"], }); return this.host.sendCommand(cc, this.commandOptions); @@ -70,7 +70,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, powerlevel, timeout, }); @@ -84,7 +84,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -103,7 +103,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -142,7 +142,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCTestNodeSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, testNodeId, powerlevel, testFrameCount, @@ -165,7 +165,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCTestNodeGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< PowerlevelCCTestNodeReport @@ -193,7 +193,7 @@ export class PowerlevelCCAPI extends PhysicalCCAPI { const cc = new PowerlevelCCTestNodeReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -208,42 +208,51 @@ export class PowerlevelCC extends CommandClass { // @publicAPI export type PowerlevelCCSetOptions = - & CCCommandOptions - & ( - | { - powerlevel: Powerlevel; - timeout: number; - } - | { - powerlevel: (typeof Powerlevel)["Normal Power"]; - timeout?: undefined; - } - ); + | { + powerlevel: Powerlevel; + timeout: number; + } + | { + powerlevel: (typeof Powerlevel)["Normal Power"]; + timeout?: undefined; + }; @CCCommand(PowerlevelCommand.Set) @useSupervision() export class PowerlevelCCSet extends PowerlevelCC { public constructor( - options: CommandClassDeserializationOptions | PowerlevelCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.powerlevel = this.payload[0]; - if (this.powerlevel !== Powerlevel["Normal Power"]) { - this.timeout = this.payload[1]; + this.powerlevel = options.powerlevel; + if (options.powerlevel !== Powerlevel["Normal Power"]) { + if (options.timeout < 1 || options.timeout > 255) { + throw new ZWaveError( + `The timeout parameter must be between 1 and 255.`, + ZWaveErrorCodes.Argument_Invalid, + ); } + this.timeout = options.timeout; + } + } + + public static from(raw: CCRaw, ctx: CCParsingContext): PowerlevelCCSet { + validatePayload(raw.payload.length >= 1); + const powerlevel: Powerlevel = raw.payload[0]; + + if (powerlevel === Powerlevel["Normal Power"]) { + return new PowerlevelCCSet({ + nodeId: ctx.sourceNodeId, + powerlevel, + }); } else { - this.powerlevel = options.powerlevel; - if (options.powerlevel !== Powerlevel["Normal Power"]) { - if (options.timeout < 1 || options.timeout > 255) { - throw new ZWaveError( - `The timeout parameter must be between 1 and 255.`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.timeout = options.timeout; - } + validatePayload(raw.payload.length >= 2); + const timeout = raw.payload[1]; + return new PowerlevelCCSet({ + nodeId: ctx.sourceNodeId, + powerlevel, + timeout, + }); } } @@ -281,20 +290,31 @@ export type PowerlevelCCReportOptions = { @CCCommand(PowerlevelCommand.Report) export class PowerlevelCCReport extends PowerlevelCC { public constructor( - options: - | CommandClassDeserializationOptions - | (PowerlevelCCReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - this.powerlevel = this.payload[0]; - if (this.powerlevel !== Powerlevel["Normal Power"]) { - this.timeout = this.payload[1]; - } + this.powerlevel = options.powerlevel; + this.timeout = options.timeout; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): PowerlevelCCReport { + validatePayload(raw.payload.length >= 1); + const powerlevel: Powerlevel = raw.payload[0]; + + if (powerlevel === Powerlevel["Normal Power"]) { + return new PowerlevelCCReport({ + nodeId: ctx.sourceNodeId, + powerlevel, + }); } else { - this.powerlevel = options.powerlevel; - this.timeout = options.timeout; + validatePayload(raw.payload.length >= 2); + const timeout = raw.payload[1]; + return new PowerlevelCCReport({ + nodeId: ctx.sourceNodeId, + powerlevel, + timeout, + }); } } @@ -325,7 +345,7 @@ export class PowerlevelCCReport extends PowerlevelCC { export class PowerlevelCCGet extends PowerlevelCC {} // @publicAPI -export interface PowerlevelCCTestNodeSetOptions extends CCCommandOptions { +export interface PowerlevelCCTestNodeSetOptions { testNodeId: number; powerlevel: Powerlevel; testFrameCount: number; @@ -335,21 +355,29 @@ export interface PowerlevelCCTestNodeSetOptions extends CCCommandOptions { @useSupervision() export class PowerlevelCCTestNodeSet extends PowerlevelCC { public constructor( - options: - | CommandClassDeserializationOptions - | PowerlevelCCTestNodeSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.testNodeId = this.payload[0]; - this.powerlevel = this.payload[1]; - this.testFrameCount = this.payload.readUInt16BE(2); - } else { - this.testNodeId = options.testNodeId; - this.powerlevel = options.powerlevel; - this.testFrameCount = options.testFrameCount; - } + this.testNodeId = options.testNodeId; + this.powerlevel = options.powerlevel; + this.testFrameCount = options.testFrameCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): PowerlevelCCTestNodeSet { + validatePayload(raw.payload.length >= 4); + const testNodeId = raw.payload[0]; + const powerlevel: Powerlevel = raw.payload[1]; + const testFrameCount = raw.payload.readUInt16BE(2); + + return new PowerlevelCCTestNodeSet({ + nodeId: ctx.sourceNodeId, + testNodeId, + powerlevel, + testFrameCount, + }); } public testNodeId: number; @@ -384,22 +412,30 @@ export interface PowerlevelCCTestNodeReportOptions { @CCCommand(PowerlevelCommand.TestNodeReport) export class PowerlevelCCTestNodeReport extends PowerlevelCC { public constructor( - options: - | CommandClassDeserializationOptions - | (PowerlevelCCTestNodeReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.testNodeId = this.payload[0]; - this.status = this.payload[1]; - this.acknowledgedFrames = this.payload.readUInt16BE(2); - } else { - this.testNodeId = options.testNodeId; - this.status = options.status; - this.acknowledgedFrames = options.acknowledgedFrames; - } + this.testNodeId = options.testNodeId; + this.status = options.status; + this.acknowledgedFrames = options.acknowledgedFrames; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): PowerlevelCCTestNodeReport { + validatePayload(raw.payload.length >= 4); + const testNodeId = raw.payload[0]; + const status: PowerlevelTestStatus = raw.payload[1]; + const acknowledgedFrames = raw.payload.readUInt16BE(2); + + return new PowerlevelCCTestNodeReport({ + nodeId: ctx.sourceNodeId, + testNodeId, + status, + acknowledgedFrames, + }); } public testNodeId: number; diff --git a/packages/cc/src/cc/ProtectionCC.ts b/packages/cc/src/cc/ProtectionCC.ts index 12a29d5e0a1c..dd3a6b8cfbf3 100644 --- a/packages/cc/src/cc/ProtectionCC.ts +++ b/packages/cc/src/cc/ProtectionCC.ts @@ -8,13 +8,18 @@ import { type SupervisionResult, Timeout, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, parseBitMask, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { padStart } from "alcalzone-shared/strings"; @@ -28,14 +33,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -222,7 +225,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -242,7 +245,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, local, rf, }); @@ -258,7 +261,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ProtectionCCSupportedReport @@ -284,7 +287,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCExclusiveControlGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ProtectionCCExclusiveControlReport @@ -306,7 +309,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCExclusiveControlSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, exclusiveControlNodeId: nodeId, }); return this.host.sendCommand(cc, this.commandOptions); @@ -320,7 +323,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCTimeoutGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ProtectionCCTimeoutReport @@ -342,7 +345,7 @@ export class ProtectionCCAPI extends CCAPI { const cc = new ProtectionCCTimeoutSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, timeout, }); return this.host.sendCommand(cc, this.commandOptions); @@ -496,7 +499,7 @@ rf ${getEnumMemberName(RFProtectionState, protectionResp.rf)}`; } // @publicAPI -export interface ProtectionCCSetOptions extends CCCommandOptions { +export interface ProtectionCCSetOptions { local: LocalProtectionState; rf?: RFProtectionState; } @@ -505,19 +508,23 @@ export interface ProtectionCCSetOptions extends CCCommandOptions { @useSupervision() export class ProtectionCCSet extends ProtectionCC { public constructor( - options: CommandClassDeserializationOptions | ProtectionCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.local = options.local; - this.rf = options.rf; - } + this.local = options.local; + this.rf = options.rf; + } + + public static from(_raw: CCRaw, _ctx: CCParsingContext): ProtectionCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ProtectionCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public local: LocalProtectionState; @@ -556,17 +563,37 @@ export class ProtectionCCSet extends ProtectionCC { } } +// @publicAPI +export interface ProtectionCCReportOptions { + local: LocalProtectionState; + rf?: RFProtectionState; +} + @CCCommand(ProtectionCommand.Report) export class ProtectionCCReport extends ProtectionCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.local = this.payload[0] & 0b1111; - if (this.payload.length >= 2) { - this.rf = this.payload[1] & 0b1111; + + // TODO: Check implementation: + this.local = options.local; + this.rf = options.rf; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ProtectionCCReport { + validatePayload(raw.payload.length >= 1); + const local: LocalProtectionState = raw.payload[0] & 0b1111; + let rf: RFProtectionState | undefined; + if (raw.payload.length >= 2) { + rf = raw.payload[1] & 0b1111; } + + return new ProtectionCCReport({ + nodeId: ctx.sourceNodeId, + local, + rf, + }); } @ccValue(ProtectionCCValues.localProtectionState) @@ -593,23 +620,51 @@ export class ProtectionCCReport extends ProtectionCC { @expectedCCResponse(ProtectionCCReport) export class ProtectionCCGet extends ProtectionCC {} +// @publicAPI +export interface ProtectionCCSupportedReportOptions { + supportsTimeout: boolean; + supportsExclusiveControl: boolean; + supportedLocalStates: LocalProtectionState[]; + supportedRFStates: RFProtectionState[]; +} + @CCCommand(ProtectionCommand.SupportedReport) export class ProtectionCCSupportedReport extends ProtectionCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 5); - this.supportsTimeout = !!(this.payload[0] & 0b1); - this.supportsExclusiveControl = !!(this.payload[0] & 0b10); - this.supportedLocalStates = parseBitMask( - this.payload.subarray(1, 3), + + // TODO: Check implementation: + this.supportsTimeout = options.supportsTimeout; + this.supportsExclusiveControl = options.supportsExclusiveControl; + this.supportedLocalStates = options.supportedLocalStates; + this.supportedRFStates = options.supportedRFStates; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ProtectionCCSupportedReport { + validatePayload(raw.payload.length >= 5); + const supportsTimeout = !!(raw.payload[0] & 0b1); + const supportsExclusiveControl = !!(raw.payload[0] & 0b10); + const supportedLocalStates: LocalProtectionState[] = parseBitMask( + raw.payload.subarray(1, 3), LocalProtectionState.Unprotected, ); - this.supportedRFStates = parseBitMask( - this.payload.subarray(3, 5), + const supportedRFStates: RFProtectionState[] = parseBitMask( + raw.payload.subarray(3, 5), RFProtectionState.Unprotected, ); + + return new ProtectionCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportsTimeout, + supportsExclusiveControl, + supportedLocalStates, + supportedRFStates, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -674,14 +729,33 @@ export class ProtectionCCSupportedReport extends ProtectionCC { @expectedCCResponse(ProtectionCCSupportedReport) export class ProtectionCCSupportedGet extends ProtectionCC {} +// @publicAPI +export interface ProtectionCCExclusiveControlReportOptions { + exclusiveControlNodeId: number; +} + @CCCommand(ProtectionCommand.ExclusiveControlReport) export class ProtectionCCExclusiveControlReport extends ProtectionCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.exclusiveControlNodeId = this.payload[0]; + + // TODO: Check implementation: + this.exclusiveControlNodeId = options.exclusiveControlNodeId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ProtectionCCExclusiveControlReport { + validatePayload(raw.payload.length >= 1); + const exclusiveControlNodeId = raw.payload[0]; + + return new ProtectionCCExclusiveControlReport({ + nodeId: ctx.sourceNodeId, + exclusiveControlNodeId, + }); } @ccValue(ProtectionCCValues.exclusiveControlNodeId) @@ -702,9 +776,7 @@ export class ProtectionCCExclusiveControlReport extends ProtectionCC { export class ProtectionCCExclusiveControlGet extends ProtectionCC {} // @publicAPI -export interface ProtectionCCExclusiveControlSetOptions - extends CCCommandOptions -{ +export interface ProtectionCCExclusiveControlSetOptions { exclusiveControlNodeId: number; } @@ -713,20 +785,25 @@ export interface ProtectionCCExclusiveControlSetOptions @useSupervision() export class ProtectionCCExclusiveControlSet extends ProtectionCC { public constructor( - options: - | CommandClassDeserializationOptions - | ProtectionCCExclusiveControlSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.exclusiveControlNodeId = options.exclusiveControlNodeId; - } + this.exclusiveControlNodeId = options.exclusiveControlNodeId; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ProtectionCCExclusiveControlSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ProtectionCCExclusiveControlSet({ + // nodeId: ctx.sourceNodeId, + // }); } public exclusiveControlNodeId: number; @@ -746,14 +823,33 @@ export class ProtectionCCExclusiveControlSet extends ProtectionCC { } } +// @publicAPI +export interface ProtectionCCTimeoutReportOptions { + timeout: Timeout; +} + @CCCommand(ProtectionCommand.TimeoutReport) export class ProtectionCCTimeoutReport extends ProtectionCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.timeout = Timeout.parse(this.payload[0]); + + // TODO: Check implementation: + this.timeout = options.timeout; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ProtectionCCTimeoutReport { + validatePayload(raw.payload.length >= 1); + const timeout: Timeout = Timeout.parse(raw.payload[0]); + + return new ProtectionCCTimeoutReport({ + nodeId: ctx.sourceNodeId, + timeout, + }); } @ccValue(ProtectionCCValues.timeout) @@ -772,7 +868,7 @@ export class ProtectionCCTimeoutReport extends ProtectionCC { export class ProtectionCCTimeoutGet extends ProtectionCC {} // @publicAPI -export interface ProtectionCCTimeoutSetOptions extends CCCommandOptions { +export interface ProtectionCCTimeoutSetOptions { timeout: Timeout; } @@ -781,20 +877,25 @@ export interface ProtectionCCTimeoutSetOptions extends CCCommandOptions { @useSupervision() export class ProtectionCCTimeoutSet extends ProtectionCC { public constructor( - options: - | CommandClassDeserializationOptions - | ProtectionCCTimeoutSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.timeout = options.timeout; - } + this.timeout = options.timeout; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ProtectionCCTimeoutSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ProtectionCCTimeoutSet({ + // nodeId: ctx.sourceNodeId, + // }); } public timeout: Timeout; diff --git a/packages/cc/src/cc/SceneActivationCC.ts b/packages/cc/src/cc/SceneActivationCC.ts index 0793bd8204ad..1800d3df1bf7 100644 --- a/packages/cc/src/cc/SceneActivationCC.ts +++ b/packages/cc/src/cc/SceneActivationCC.ts @@ -2,6 +2,7 @@ import type { MessageOrCCLogEntry, MessageRecord, SupervisionResult, + WithAddress, } from "@zwave-js/core/safe"; import { CommandClasses, @@ -10,7 +11,11 @@ import { ValueMetadata, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, @@ -19,12 +24,7 @@ import { throwUnsupportedProperty, throwWrongValueType, } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -110,7 +110,7 @@ export class SceneActivationCCAPI extends CCAPI { const cc = new SceneActivationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sceneId, dimmingDuration, }); @@ -126,7 +126,7 @@ export class SceneActivationCC extends CommandClass { } // @publicAPI -export interface SceneActivationCCSetOptions extends CCCommandOptions { +export interface SceneActivationCCSetOptions { sceneId: number; dimmingDuration?: Duration | string; } @@ -135,24 +135,32 @@ export interface SceneActivationCCSetOptions extends CCCommandOptions { @useSupervision() export class SceneActivationCCSet extends SceneActivationCC { public constructor( - options: - | CommandClassDeserializationOptions - | SceneActivationCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.sceneId = this.payload[0]; - // Per the specs, dimmingDuration is required, but as always the real world is different... - if (this.payload.length >= 2) { - this.dimmingDuration = Duration.parseSet(this.payload[1]); - } + this.sceneId = options.sceneId; + this.dimmingDuration = Duration.from(options.dimmingDuration); + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SceneActivationCCSet { + validatePayload(raw.payload.length >= 1); + const sceneId = raw.payload[0]; + validatePayload(sceneId >= 1, sceneId <= 255); - validatePayload(this.sceneId >= 1, this.sceneId <= 255); - } else { - this.sceneId = options.sceneId; - this.dimmingDuration = Duration.from(options.dimmingDuration); + // Per the specs, dimmingDuration is required, but as always the real world is different... + let dimmingDuration: Duration | undefined; + if (raw.payload.length >= 2) { + dimmingDuration = Duration.parseSet(raw.payload[1]); } + + return new SceneActivationCCSet({ + nodeId: ctx.sourceNodeId, + sceneId, + dimmingDuration, + }); } @ccValue(SceneActivationCCValues.sceneId) diff --git a/packages/cc/src/cc/SceneActuatorConfigurationCC.ts b/packages/cc/src/cc/SceneActuatorConfigurationCC.ts index d2d6640025bf..0188870dbec2 100644 --- a/packages/cc/src/cc/SceneActuatorConfigurationCC.ts +++ b/packages/cc/src/cc/SceneActuatorConfigurationCC.ts @@ -6,12 +6,17 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, getCCName, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -26,12 +31,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -207,7 +210,7 @@ export class SceneActuatorConfigurationCCAPI extends CCAPI { // Undefined `level` uses the actuator's current value (override = 0). const cc = new SceneActuatorConfigurationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sceneId, dimmingDuration: Duration.from(dimmingDuration) ?? new Duration(0, "seconds"), @@ -232,7 +235,7 @@ export class SceneActuatorConfigurationCCAPI extends CCAPI { const cc = new SceneActuatorConfigurationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sceneId: 0, }); const response = await this.host.sendCommand< @@ -272,7 +275,7 @@ export class SceneActuatorConfigurationCCAPI extends CCAPI { const cc = new SceneActuatorConfigurationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, sceneId: sceneId, }); const response = await this.host.sendCommand< @@ -342,9 +345,7 @@ export class SceneActuatorConfigurationCC extends CommandClass { } // @publicAPI -export interface SceneActuatorConfigurationCCSetOptions - extends CCCommandOptions -{ +export interface SceneActuatorConfigurationCCSetOptions { sceneId: number; dimmingDuration: Duration; level?: number; @@ -356,28 +357,33 @@ export class SceneActuatorConfigurationCCSet extends SceneActuatorConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | SceneActuatorConfigurationCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload + if (options.sceneId < 1 || options.sceneId > 255) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The scene id ${options.sceneId} must be between 1 and 255!`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.sceneId < 1 || options.sceneId > 255) { - throw new ZWaveError( - `The scene id ${options.sceneId} must be between 1 and 255!`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.sceneId = options.sceneId; - this.dimmingDuration = options.dimmingDuration; - this.level = options.level; } + this.sceneId = options.sceneId; + this.dimmingDuration = options.dimmingDuration; + this.level = options.level; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): SceneActuatorConfigurationCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SceneActuatorConfigurationCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public sceneId: number; @@ -410,22 +416,49 @@ export class SceneActuatorConfigurationCCSet } } +// @publicAPI +export interface SceneActuatorConfigurationCCReportOptions { + sceneId: number; + level?: number; + dimmingDuration?: Duration; +} + @CCCommand(SceneActuatorConfigurationCommand.Report) export class SceneActuatorConfigurationCCReport extends SceneActuatorConfigurationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 3); - this.sceneId = this.payload[0]; - if (this.sceneId !== 0) { - this.level = this.payload[1]; - this.dimmingDuration = Duration.parseReport(this.payload[2]) + // TODO: Check implementation: + this.sceneId = options.sceneId; + this.level = options.level; + this.dimmingDuration = options.dimmingDuration; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SceneActuatorConfigurationCCReport { + validatePayload(raw.payload.length >= 3); + const sceneId = raw.payload[0]; + + let level: number | undefined; + let dimmingDuration: Duration | undefined; + if (sceneId !== 0) { + level = raw.payload[1]; + dimmingDuration = Duration.parseReport(raw.payload[2]) ?? Duration.unknown(); } + + return new SceneActuatorConfigurationCCReport({ + nodeId: ctx.sourceNodeId, + sceneId, + level, + dimmingDuration, + }); } public readonly sceneId: number; @@ -487,9 +520,7 @@ function testResponseForSceneActuatorConfigurationGet( } // @publicAPI -export interface SceneActuatorConfigurationCCGetOptions - extends CCCommandOptions -{ +export interface SceneActuatorConfigurationCCGetOptions { sceneId: number; } @@ -502,20 +533,25 @@ export class SceneActuatorConfigurationCCGet extends SceneActuatorConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | SceneActuatorConfigurationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.sceneId = options.sceneId; - } + this.sceneId = options.sceneId; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): SceneActuatorConfigurationCCGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SceneActuatorConfigurationCCGet({ + // nodeId: ctx.sourceNodeId, + // }); } public sceneId: number; diff --git a/packages/cc/src/cc/SceneControllerConfigurationCC.ts b/packages/cc/src/cc/SceneControllerConfigurationCC.ts index f028bf3dfa4c..1c2623a02ffb 100644 --- a/packages/cc/src/cc/SceneControllerConfigurationCC.ts +++ b/packages/cc/src/cc/SceneControllerConfigurationCC.ts @@ -7,6 +7,7 @@ import { MessagePriority, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, getCCName, @@ -14,6 +15,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetValueDB, } from "@zwave-js/host/safe"; @@ -31,13 +33,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -270,7 +270,7 @@ export class SceneControllerConfigurationCCAPI extends CCAPI { const cc = new SceneControllerConfigurationCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, sceneId, dimmingDuration, @@ -294,7 +294,7 @@ export class SceneControllerConfigurationCCAPI extends CCAPI { const cc = new SceneControllerConfigurationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId: 0, }); const response = await this.host.sendCommand< @@ -342,7 +342,7 @@ export class SceneControllerConfigurationCCAPI extends CCAPI { const cc = new SceneControllerConfigurationCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, groupId, }); const response = await this.host.sendCommand< @@ -482,9 +482,7 @@ dimming duration: ${group.dimmingDuration.toString()}`; } // @publicAPI -export interface SceneControllerConfigurationCCSetOptions - extends CCCommandOptions -{ +export interface SceneControllerConfigurationCCSetOptions { groupId: number; sceneId: number; dimmingDuration?: Duration | string; @@ -496,24 +494,29 @@ export class SceneControllerConfigurationCCSet extends SceneControllerConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | SceneControllerConfigurationCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.groupId = options.groupId; - this.sceneId = options.sceneId; - // if dimmingDuration was missing, use default duration. - this.dimmingDuration = Duration.from(options.dimmingDuration) - ?? Duration.default(); - } + this.groupId = options.groupId; + this.sceneId = options.sceneId; + // if dimmingDuration was missing, use default duration. + this.dimmingDuration = Duration.from(options.dimmingDuration) + ?? Duration.default(); + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): SceneControllerConfigurationCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SceneControllerConfigurationCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public groupId: number; @@ -541,19 +544,44 @@ export class SceneControllerConfigurationCCSet } } +// @publicAPI +export interface SceneControllerConfigurationCCReportOptions { + groupId: number; + sceneId: number; + dimmingDuration: Duration; +} + @CCCommand(SceneControllerConfigurationCommand.Report) export class SceneControllerConfigurationCCReport extends SceneControllerConfigurationCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 3); - this.groupId = this.payload[0]; - this.sceneId = this.payload[1]; - this.dimmingDuration = Duration.parseReport(this.payload[2]) + + // TODO: Check implementation: + this.groupId = options.groupId; + this.sceneId = options.sceneId; + this.dimmingDuration = options.dimmingDuration; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SceneControllerConfigurationCCReport { + validatePayload(raw.payload.length >= 3); + const groupId = raw.payload[0]; + const sceneId = raw.payload[1]; + const dimmingDuration: Duration = Duration.parseReport(raw.payload[2]) ?? Duration.unknown(); + + return new SceneControllerConfigurationCCReport({ + nodeId: ctx.sourceNodeId, + groupId, + sceneId, + dimmingDuration, + }); } public readonly groupId: number; @@ -602,9 +630,7 @@ function testResponseForSceneControllerConfigurationGet( } // @publicAPI -export interface SceneControllerConfigurationCCGetOptions - extends CCCommandOptions -{ +export interface SceneControllerConfigurationCCGetOptions { groupId: number; } @@ -617,20 +643,25 @@ export class SceneControllerConfigurationCCGet extends SceneControllerConfigurationCC { public constructor( - options: - | CommandClassDeserializationOptions - | SceneControllerConfigurationCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.groupId = options.groupId; - } + this.groupId = options.groupId; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): SceneControllerConfigurationCCGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SceneControllerConfigurationCCGet({ + // nodeId: ctx.sourceNodeId, + // }); } public groupId: number; diff --git a/packages/cc/src/cc/ScheduleEntryLockCC.ts b/packages/cc/src/cc/ScheduleEntryLockCC.ts index f8e0782526bd..5ac6c9150d1d 100644 --- a/packages/cc/src/cc/ScheduleEntryLockCC.ts +++ b/packages/cc/src/cc/ScheduleEntryLockCC.ts @@ -4,6 +4,7 @@ import { MessagePriority, type MessageRecord, type SupervisionResult, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeBitMask, @@ -13,7 +14,11 @@ import { validatePayload, } from "@zwave-js/core"; import { type EndpointId, type MaybeNotKnown } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host"; import { type AllOrNone, formatDate, @@ -24,12 +29,10 @@ import { import { validateArgs } from "@zwave-js/transformers"; import { CCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -230,7 +233,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCEnableSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, userId, enabled, }); @@ -244,7 +247,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCEnableAllSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, enabled, }); @@ -273,7 +276,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const result = await this.host.sendCommand< @@ -331,7 +334,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCWeekDayScheduleSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...slot, ...(schedule ? { @@ -388,7 +391,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCWeekDayScheduleGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...slot, }); const result = await this.host.sendCommand< @@ -458,7 +461,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCYearDayScheduleSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...slot, ...(schedule ? { @@ -515,7 +518,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCYearDayScheduleGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...slot, }); const result = await this.host.sendCommand< @@ -568,7 +571,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCDailyRepeatingScheduleSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...slot, ...(schedule ? { @@ -625,7 +628,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCDailyRepeatingScheduleGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...slot, }); const result = await this.host.sendCommand< @@ -654,7 +657,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCTimeOffsetGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const result = await this.host.sendCommand< ScheduleEntryLockCCTimeOffsetReport @@ -679,7 +682,7 @@ export class ScheduleEntryLockCCAPI extends CCAPI { const cc = new ScheduleEntryLockCCTimeOffsetSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...timezone, }); @@ -919,7 +922,7 @@ daily repeating: ${slotsResp.numDailyRepeatingSlots}`; } // @publicAPI -export interface ScheduleEntryLockCCEnableSetOptions extends CCCommandOptions { +export interface ScheduleEntryLockCCEnableSetOptions { userId: number; enabled: boolean; } @@ -928,19 +931,26 @@ export interface ScheduleEntryLockCCEnableSetOptions extends CCCommandOptions { @useSupervision() export class ScheduleEntryLockCCEnableSet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCEnableSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.enabled = this.payload[1] === 0x01; - } else { - this.userId = options.userId; - this.enabled = options.enabled; - } + this.userId = options.userId; + this.enabled = options.enabled; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCEnableSet { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const enabled: boolean = raw.payload[1] === 0x01; + + return new ScheduleEntryLockCCEnableSet({ + nodeId: ctx.sourceNodeId, + userId, + enabled, + }); } public userId: number; @@ -963,9 +973,7 @@ export class ScheduleEntryLockCCEnableSet extends ScheduleEntryLockCC { } // @publicAPI -export interface ScheduleEntryLockCCEnableAllSetOptions - extends CCCommandOptions -{ +export interface ScheduleEntryLockCCEnableAllSetOptions { enabled: boolean; } @@ -973,17 +981,23 @@ export interface ScheduleEntryLockCCEnableAllSetOptions @useSupervision() export class ScheduleEntryLockCCEnableAllSet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCEnableAllSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.enabled = this.payload[0] === 0x01; - } else { - this.enabled = options.enabled; - } + this.enabled = options.enabled; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCEnableAllSet { + validatePayload(raw.payload.length >= 1); + const enabled: boolean = raw.payload[0] === 0x01; + + return new ScheduleEntryLockCCEnableAllSet({ + nodeId: ctx.sourceNodeId, + enabled, + }); } public enabled: boolean; @@ -1004,9 +1018,7 @@ export class ScheduleEntryLockCCEnableAllSet extends ScheduleEntryLockCC { } // @publicAPI -export interface ScheduleEntryLockCCSupportedReportOptions - extends CCCommandOptions -{ +export interface ScheduleEntryLockCCSupportedReportOptions { numWeekDaySlots: number; numYearDaySlots: number; numDailyRepeatingSlots?: number; @@ -1015,23 +1027,32 @@ export interface ScheduleEntryLockCCSupportedReportOptions @CCCommand(ScheduleEntryLockCommand.SupportedReport) export class ScheduleEntryLockCCSupportedReport extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.numWeekDaySlots = this.payload[0]; - this.numYearDaySlots = this.payload[1]; - if (this.payload.length >= 3) { - this.numDailyRepeatingSlots = this.payload[2]; - } - } else { - this.numWeekDaySlots = options.numWeekDaySlots; - this.numYearDaySlots = options.numYearDaySlots; - this.numDailyRepeatingSlots = options.numDailyRepeatingSlots; + this.numWeekDaySlots = options.numWeekDaySlots; + this.numYearDaySlots = options.numYearDaySlots; + this.numDailyRepeatingSlots = options.numDailyRepeatingSlots; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCSupportedReport { + validatePayload(raw.payload.length >= 2); + const numWeekDaySlots = raw.payload[0]; + const numYearDaySlots = raw.payload[1]; + let numDailyRepeatingSlots: number | undefined; + if (raw.payload.length >= 3) { + numDailyRepeatingSlots = raw.payload[2]; } + + return new ScheduleEntryLockCCSupportedReport({ + nodeId: ctx.sourceNodeId, + numWeekDaySlots, + numYearDaySlots, + numDailyRepeatingSlots, + }); } @ccValue(ScheduleEntryLockCCValues.numWeekDaySlots) @@ -1072,7 +1093,6 @@ export class ScheduleEntryLockCCSupportedGet extends ScheduleEntryLockCC {} /** @publicAPI */ export type ScheduleEntryLockCCWeekDayScheduleSetOptions = - & CCCommandOptions & ScheduleEntryLockSlotId & ( | { @@ -1087,40 +1107,62 @@ export type ScheduleEntryLockCCWeekDayScheduleSetOptions = @useSupervision() export class ScheduleEntryLockCCWeekDayScheduleSet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCWeekDayScheduleSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.action = this.payload[0]; - validatePayload( - this.action === ScheduleEntryLockSetAction.Set - || this.action === ScheduleEntryLockSetAction.Erase, - ); - this.userId = this.payload[1]; - this.slotId = this.payload[2]; - if (this.action === ScheduleEntryLockSetAction.Set) { - validatePayload(this.payload.length >= 8); - this.weekday = this.payload[3]; - this.startHour = this.payload[4]; - this.startMinute = this.payload[5]; - this.stopHour = this.payload[6]; - this.stopMinute = this.payload[7]; - } - } else { - this.userId = options.userId; - this.slotId = options.slotId; - this.action = options.action; - if (options.action === ScheduleEntryLockSetAction.Set) { - this.weekday = options.weekday; - this.startHour = options.startHour; - this.startMinute = options.startMinute; - this.stopHour = options.stopHour; - this.stopMinute = options.stopMinute; - } + this.userId = options.userId; + this.slotId = options.slotId; + this.action = options.action; + if (options.action === ScheduleEntryLockSetAction.Set) { + this.weekday = options.weekday; + this.startHour = options.startHour; + this.startMinute = options.startMinute; + this.stopHour = options.stopHour; + this.stopMinute = options.stopMinute; + } + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCWeekDayScheduleSet { + validatePayload(raw.payload.length >= 3); + const action: ScheduleEntryLockSetAction = raw.payload[0]; + + validatePayload( + action === ScheduleEntryLockSetAction.Set + || action === ScheduleEntryLockSetAction.Erase, + ); + const userId = raw.payload[1]; + const slotId = raw.payload[2]; + + if (action !== ScheduleEntryLockSetAction.Set) { + return new ScheduleEntryLockCCWeekDayScheduleSet({ + nodeId: ctx.sourceNodeId, + action, + userId, + slotId, + }); } + + validatePayload(raw.payload.length >= 8); + const weekday: ScheduleEntryLockWeekday = raw.payload[3]; + const startHour = raw.payload[4]; + const startMinute = raw.payload[5]; + const stopHour = raw.payload[6]; + const stopMinute = raw.payload[7]; + + return new ScheduleEntryLockCCWeekDayScheduleSet({ + nodeId: ctx.sourceNodeId, + action, + userId, + slotId, + weekday, + startHour, + startMinute, + stopHour, + stopMinute, + }); } public userId: number; @@ -1187,7 +1229,6 @@ export class ScheduleEntryLockCCWeekDayScheduleSet extends ScheduleEntryLockCC { // @publicAPI export type ScheduleEntryLockCCWeekDayScheduleReportOptions = - & CCCommandOptions & ScheduleEntryLockSlotId & AllOrNone; @@ -1196,41 +1237,76 @@ export class ScheduleEntryLockCCWeekDayScheduleReport extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCWeekDayScheduleReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.slotId = this.payload[1]; - if (this.payload.length >= 7) { - if (this.payload[2] !== 0xff) { - this.weekday = this.payload[2]; - } - if (this.payload[3] !== 0xff) { - this.startHour = this.payload[3]; - } - if (this.payload[4] !== 0xff) { - this.startMinute = this.payload[4]; - } - if (this.payload[5] !== 0xff) { - this.stopHour = this.payload[5]; - } - if (this.payload[6] !== 0xff) { - this.stopMinute = this.payload[6]; - } + this.userId = options.userId; + this.slotId = options.slotId; + this.weekday = options.weekday; + this.startHour = options.startHour; + this.startMinute = options.startMinute; + this.stopHour = options.stopHour; + this.stopMinute = options.stopMinute; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCWeekDayScheduleReport { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const slotId = raw.payload[1]; + + let ccOptions: ScheduleEntryLockCCWeekDayScheduleReportOptions = { + userId, + slotId, + }; + + let weekday: ScheduleEntryLockWeekday | undefined; + let startHour: number | undefined; + let startMinute: number | undefined; + let stopHour: number | undefined; + let stopMinute: number | undefined; + + if (raw.payload.length >= 7) { + if (raw.payload[2] !== 0xff) { + weekday = raw.payload[2]; + } + if (raw.payload[3] !== 0xff) { + startHour = raw.payload[3]; + } + if (raw.payload[4] !== 0xff) { + startMinute = raw.payload[4]; + } + if (raw.payload[5] !== 0xff) { + stopHour = raw.payload[5]; + } + if (raw.payload[6] !== 0xff) { + stopMinute = raw.payload[6]; } - } else { - this.userId = options.userId; - this.slotId = options.slotId; - this.weekday = options.weekday; - this.startHour = options.startHour; - this.startMinute = options.startMinute; - this.stopHour = options.stopHour; - this.stopMinute = options.stopMinute; } + + if ( + weekday != undefined + && startHour != undefined + && startMinute != undefined + && stopHour != undefined + && stopMinute != undefined + ) { + ccOptions = { + ...ccOptions, + weekday, + startHour, + startMinute, + stopHour, + stopMinute, + }; + } + + return new ScheduleEntryLockCCWeekDayScheduleReport({ + nodeId: ctx.sourceNodeId, + ...ccOptions, + }); } public userId: number; @@ -1312,26 +1388,32 @@ export class ScheduleEntryLockCCWeekDayScheduleReport // @publicAPI export type ScheduleEntryLockCCWeekDayScheduleGetOptions = - & CCCommandOptions - & ScheduleEntryLockSlotId; + ScheduleEntryLockSlotId; @CCCommand(ScheduleEntryLockCommand.WeekDayScheduleGet) @expectedCCResponse(ScheduleEntryLockCCWeekDayScheduleReport) export class ScheduleEntryLockCCWeekDayScheduleGet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCWeekDayScheduleGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.slotId = this.payload[1]; - } else { - this.userId = options.userId; - this.slotId = options.slotId; - } + this.userId = options.userId; + this.slotId = options.slotId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCWeekDayScheduleGet { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const slotId = raw.payload[1]; + + return new ScheduleEntryLockCCWeekDayScheduleGet({ + nodeId: ctx.sourceNodeId, + userId, + slotId, + }); } public userId: number; @@ -1355,7 +1437,6 @@ export class ScheduleEntryLockCCWeekDayScheduleGet extends ScheduleEntryLockCC { /** @publicAPI */ export type ScheduleEntryLockCCYearDayScheduleSetOptions = - & CCCommandOptions & ScheduleEntryLockSlotId & ( | { @@ -1370,50 +1451,77 @@ export type ScheduleEntryLockCCYearDayScheduleSetOptions = @useSupervision() export class ScheduleEntryLockCCYearDayScheduleSet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCYearDayScheduleSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.action = this.payload[0]; - validatePayload( - this.action === ScheduleEntryLockSetAction.Set - || this.action === ScheduleEntryLockSetAction.Erase, - ); - this.userId = this.payload[1]; - this.slotId = this.payload[2]; - if (this.action === ScheduleEntryLockSetAction.Set) { - validatePayload(this.payload.length >= 13); - this.startYear = this.payload[3]; - this.startMonth = this.payload[4]; - this.startDay = this.payload[5]; - this.startHour = this.payload[6]; - this.startMinute = this.payload[7]; - this.stopYear = this.payload[8]; - this.stopMonth = this.payload[9]; - this.stopDay = this.payload[10]; - this.stopHour = this.payload[11]; - this.stopMinute = this.payload[12]; - } - } else { - this.userId = options.userId; - this.slotId = options.slotId; - this.action = options.action; - if (options.action === ScheduleEntryLockSetAction.Set) { - this.startYear = options.startYear; - this.startMonth = options.startMonth; - this.startDay = options.startDay; - this.startHour = options.startHour; - this.startMinute = options.startMinute; - this.stopYear = options.stopYear; - this.stopMonth = options.stopMonth; - this.stopDay = options.stopDay; - this.stopHour = options.stopHour; - this.stopMinute = options.stopMinute; - } + this.userId = options.userId; + this.slotId = options.slotId; + this.action = options.action; + if (options.action === ScheduleEntryLockSetAction.Set) { + this.startYear = options.startYear; + this.startMonth = options.startMonth; + this.startDay = options.startDay; + this.startHour = options.startHour; + this.startMinute = options.startMinute; + this.stopYear = options.stopYear; + this.stopMonth = options.stopMonth; + this.stopDay = options.stopDay; + this.stopHour = options.stopHour; + this.stopMinute = options.stopMinute; + } + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCYearDayScheduleSet { + validatePayload(raw.payload.length >= 3); + const action: ScheduleEntryLockSetAction = raw.payload[0]; + + validatePayload( + action === ScheduleEntryLockSetAction.Set + || action === ScheduleEntryLockSetAction.Erase, + ); + const userId = raw.payload[1]; + const slotId = raw.payload[2]; + + if (action !== ScheduleEntryLockSetAction.Set) { + return new ScheduleEntryLockCCYearDayScheduleSet({ + nodeId: ctx.sourceNodeId, + action, + userId, + slotId, + }); } + + validatePayload(raw.payload.length >= 13); + const startYear = raw.payload[3]; + const startMonth = raw.payload[4]; + const startDay = raw.payload[5]; + const startHour = raw.payload[6]; + const startMinute = raw.payload[7]; + const stopYear = raw.payload[8]; + const stopMonth = raw.payload[9]; + const stopDay = raw.payload[10]; + const stopHour = raw.payload[11]; + const stopMinute = raw.payload[12]; + + return new ScheduleEntryLockCCYearDayScheduleSet({ + nodeId: ctx.sourceNodeId, + action, + userId, + slotId, + startYear, + startMonth, + startDay, + startHour, + startMinute, + stopYear, + stopMonth, + stopDay, + stopHour, + stopMinute, + }); } public userId: number; @@ -1492,7 +1600,6 @@ export class ScheduleEntryLockCCYearDayScheduleSet extends ScheduleEntryLockCC { // @publicAPI export type ScheduleEntryLockCCYearDayScheduleReportOptions = - & CCCommandOptions & ScheduleEntryLockSlotId & AllOrNone; @@ -1501,61 +1608,111 @@ export class ScheduleEntryLockCCYearDayScheduleReport extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCYearDayScheduleReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.slotId = this.payload[1]; - if (this.payload.length >= 12) { - if (this.payload[2] !== 0xff) { - this.startYear = this.payload[2]; - } - if (this.payload[3] !== 0xff) { - this.startMonth = this.payload[3]; - } - if (this.payload[4] !== 0xff) { - this.startDay = this.payload[4]; - } - if (this.payload[5] !== 0xff) { - this.startHour = this.payload[5]; - } - if (this.payload[6] !== 0xff) { - this.startMinute = this.payload[6]; - } - if (this.payload[7] !== 0xff) { - this.stopYear = this.payload[7]; - } - if (this.payload[8] !== 0xff) { - this.stopMonth = this.payload[8]; - } - if (this.payload[9] !== 0xff) { - this.stopDay = this.payload[9]; - } - if (this.payload[10] !== 0xff) { - this.stopHour = this.payload[10]; - } - if (this.payload[11] !== 0xff) { - this.stopMinute = this.payload[11]; - } + this.userId = options.userId; + this.slotId = options.slotId; + this.startYear = options.startYear; + this.startMonth = options.startMonth; + this.startDay = options.startDay; + this.startHour = options.startHour; + this.startMinute = options.startMinute; + this.stopYear = options.stopYear; + this.stopMonth = options.stopMonth; + this.stopDay = options.stopDay; + this.stopHour = options.stopHour; + this.stopMinute = options.stopMinute; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCYearDayScheduleReport { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const slotId = raw.payload[1]; + + let ccOptions: ScheduleEntryLockCCYearDayScheduleReportOptions = { + userId, + slotId, + }; + + let startYear: number | undefined; + let startMonth: number | undefined; + let startDay: number | undefined; + let startHour: number | undefined; + let startMinute: number | undefined; + let stopYear: number | undefined; + let stopMonth: number | undefined; + let stopDay: number | undefined; + let stopHour: number | undefined; + let stopMinute: number | undefined; + + if (raw.payload.length >= 12) { + if (raw.payload[2] !== 0xff) { + startYear = raw.payload[2]; } - } else { - this.userId = options.userId; - this.slotId = options.slotId; - this.startYear = options.startYear; - this.startMonth = options.startMonth; - this.startDay = options.startDay; - this.startHour = options.startHour; - this.startMinute = options.startMinute; - this.stopYear = options.stopYear; - this.stopMonth = options.stopMonth; - this.stopDay = options.stopDay; - this.stopHour = options.stopHour; - this.stopMinute = options.stopMinute; + if (raw.payload[3] !== 0xff) { + startMonth = raw.payload[3]; + } + if (raw.payload[4] !== 0xff) { + startDay = raw.payload[4]; + } + if (raw.payload[5] !== 0xff) { + startHour = raw.payload[5]; + } + if (raw.payload[6] !== 0xff) { + startMinute = raw.payload[6]; + } + if (raw.payload[7] !== 0xff) { + stopYear = raw.payload[7]; + } + if (raw.payload[8] !== 0xff) { + stopMonth = raw.payload[8]; + } + if (raw.payload[9] !== 0xff) { + stopDay = raw.payload[9]; + } + if (raw.payload[10] !== 0xff) { + stopHour = raw.payload[10]; + } + if (raw.payload[11] !== 0xff) { + stopMinute = raw.payload[11]; + } + } + + if ( + startYear != undefined + && startMonth != undefined + && startDay != undefined + && startHour != undefined + && startMinute != undefined + && stopYear != undefined + && stopMonth != undefined + && stopDay != undefined + && stopHour != undefined + && stopMinute != undefined + ) { + ccOptions = { + ...ccOptions, + startYear, + startMonth, + startDay, + startHour, + startMinute, + stopYear, + stopMonth, + stopDay, + stopHour, + stopMinute, + }; } + + return new ScheduleEntryLockCCYearDayScheduleReport({ + nodeId: ctx.sourceNodeId, + ...ccOptions, + }); } public userId: number; @@ -1655,26 +1812,32 @@ export class ScheduleEntryLockCCYearDayScheduleReport // @publicAPI export type ScheduleEntryLockCCYearDayScheduleGetOptions = - & CCCommandOptions - & ScheduleEntryLockSlotId; + ScheduleEntryLockSlotId; @CCCommand(ScheduleEntryLockCommand.YearDayScheduleGet) @expectedCCResponse(ScheduleEntryLockCCYearDayScheduleReport) export class ScheduleEntryLockCCYearDayScheduleGet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCYearDayScheduleGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.slotId = this.payload[1]; - } else { - this.userId = options.userId; - this.slotId = options.slotId; - } + this.userId = options.userId; + this.slotId = options.slotId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCYearDayScheduleGet { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const slotId = raw.payload[1]; + + return new ScheduleEntryLockCCYearDayScheduleGet({ + nodeId: ctx.sourceNodeId, + userId, + slotId, + }); } public userId: number; @@ -1697,9 +1860,7 @@ export class ScheduleEntryLockCCYearDayScheduleGet extends ScheduleEntryLockCC { } // @publicAPI -export interface ScheduleEntryLockCCTimeOffsetSetOptions - extends CCCommandOptions -{ +export interface ScheduleEntryLockCCTimeOffsetSetOptions { standardOffset: number; dstOffset: number; } @@ -1708,19 +1869,24 @@ export interface ScheduleEntryLockCCTimeOffsetSetOptions @useSupervision() export class ScheduleEntryLockCCTimeOffsetSet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCTimeOffsetSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - const { standardOffset, dstOffset } = parseTimezone(this.payload); - this.standardOffset = standardOffset; - this.dstOffset = dstOffset; - } else { - this.standardOffset = options.standardOffset; - this.dstOffset = options.dstOffset; - } + this.standardOffset = options.standardOffset; + this.dstOffset = options.dstOffset; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCTimeOffsetSet { + const { standardOffset, dstOffset } = parseTimezone(raw.payload); + + return new ScheduleEntryLockCCTimeOffsetSet({ + nodeId: ctx.sourceNodeId, + standardOffset, + dstOffset, + }); } public standardOffset: number; @@ -1746,9 +1912,7 @@ export class ScheduleEntryLockCCTimeOffsetSet extends ScheduleEntryLockCC { } // @publicAPI -export interface ScheduleEntryLockCCTimeOffsetReportOptions - extends CCCommandOptions -{ +export interface ScheduleEntryLockCCTimeOffsetReportOptions { standardOffset: number; dstOffset: number; } @@ -1756,19 +1920,24 @@ export interface ScheduleEntryLockCCTimeOffsetReportOptions @CCCommand(ScheduleEntryLockCommand.TimeOffsetReport) export class ScheduleEntryLockCCTimeOffsetReport extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCTimeOffsetReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - const { standardOffset, dstOffset } = parseTimezone(this.payload); - this.standardOffset = standardOffset; - this.dstOffset = dstOffset; - } else { - this.standardOffset = options.standardOffset; - this.dstOffset = options.dstOffset; - } + this.standardOffset = options.standardOffset; + this.dstOffset = options.dstOffset; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCTimeOffsetReport { + const { standardOffset, dstOffset } = parseTimezone(raw.payload); + + return new ScheduleEntryLockCCTimeOffsetReport({ + nodeId: ctx.sourceNodeId, + standardOffset, + dstOffset, + }); } public standardOffset: number; @@ -1799,7 +1968,6 @@ export class ScheduleEntryLockCCTimeOffsetGet extends ScheduleEntryLockCC {} /** @publicAPI */ export type ScheduleEntryLockCCDailyRepeatingScheduleSetOptions = - & CCCommandOptions & ScheduleEntryLockSlotId & ( | { @@ -1816,45 +1984,69 @@ export class ScheduleEntryLockCCDailyRepeatingScheduleSet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCDailyRepeatingScheduleSetOptions, + options: WithAddress< + ScheduleEntryLockCCDailyRepeatingScheduleSetOptions + >, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.action = this.payload[0]; - validatePayload( - this.action === ScheduleEntryLockSetAction.Set - || this.action === ScheduleEntryLockSetAction.Erase, - ); - this.userId = this.payload[1]; - this.slotId = this.payload[2]; - if (this.action === ScheduleEntryLockSetAction.Set) { - validatePayload(this.payload.length >= 8); - this.weekdays = parseBitMask( - this.payload.subarray(3, 4), - ScheduleEntryLockWeekday.Sunday, - ); - this.startHour = this.payload[4]; - this.startMinute = this.payload[5]; - this.durationHour = this.payload[6]; - this.durationMinute = this.payload[7]; - } - } else { - this.userId = options.userId; - this.slotId = options.slotId; - this.action = options.action; - if (options.action === ScheduleEntryLockSetAction.Set) { - this.weekdays = options.weekdays; - this.startHour = options.startHour; - this.startMinute = options.startMinute; - this.durationHour = options.durationHour; - this.durationMinute = options.durationMinute; - } + this.userId = options.userId; + this.slotId = options.slotId; + this.action = options.action; + if (options.action === ScheduleEntryLockSetAction.Set) { + this.weekdays = options.weekdays; + this.startHour = options.startHour; + this.startMinute = options.startMinute; + this.durationHour = options.durationHour; + this.durationMinute = options.durationMinute; } } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCDailyRepeatingScheduleSet { + validatePayload(raw.payload.length >= 3); + const action: ScheduleEntryLockSetAction = raw.payload[0]; + + validatePayload( + action === ScheduleEntryLockSetAction.Set + || action === ScheduleEntryLockSetAction.Erase, + ); + const userId = raw.payload[1]; + const slotId = raw.payload[2]; + + if (action !== ScheduleEntryLockSetAction.Set) { + return new ScheduleEntryLockCCDailyRepeatingScheduleSet({ + nodeId: ctx.sourceNodeId, + action, + userId, + slotId, + }); + } + + validatePayload(raw.payload.length >= 8); + const weekdays: ScheduleEntryLockWeekday[] = parseBitMask( + raw.payload.subarray(3, 4), + ScheduleEntryLockWeekday.Sunday, + ); + const startHour = raw.payload[4]; + const startMinute = raw.payload[5]; + const durationHour = raw.payload[6]; + const durationMinute = raw.payload[7]; + + return new ScheduleEntryLockCCDailyRepeatingScheduleSet({ + nodeId: ctx.sourceNodeId, + action, + userId, + slotId, + weekdays, + startHour, + startMinute, + durationHour, + durationMinute, + }); + } + public userId: number; public slotId: number; @@ -1934,37 +2126,55 @@ export class ScheduleEntryLockCCDailyRepeatingScheduleReport extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ( - & CCCommandOptions - & ScheduleEntryLockCCDailyRepeatingScheduleReportOptions - ), + options: WithAddress< + ScheduleEntryLockCCDailyRepeatingScheduleReportOptions + >, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.slotId = this.payload[1]; + this.userId = options.userId; + this.slotId = options.slotId; + this.weekdays = options.weekdays; + this.startHour = options.startHour; + this.startMinute = options.startMinute; + this.durationHour = options.durationHour; + this.durationMinute = options.durationMinute; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCDailyRepeatingScheduleReport { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const slotId = raw.payload[1]; + + if (raw.payload.length >= 7 && raw.payload[2] !== 0) { // Only parse the schedule if it is present and some weekday is selected - if (this.payload.length >= 7 && this.payload[2] !== 0) { - this.weekdays = parseBitMask( - this.payload.subarray(2, 3), - ScheduleEntryLockWeekday.Sunday, - ); - this.startHour = this.payload[3]; - this.startMinute = this.payload[4]; - this.durationHour = this.payload[5]; - this.durationMinute = this.payload[6]; - } + const weekdays: ScheduleEntryLockWeekday[] = parseBitMask( + raw.payload.subarray(2, 3), + ScheduleEntryLockWeekday.Sunday, + ); + const startHour = raw.payload[3]; + const startMinute = raw.payload[4]; + const durationHour = raw.payload[5]; + const durationMinute = raw.payload[6]; + + return new ScheduleEntryLockCCDailyRepeatingScheduleReport({ + nodeId: ctx.sourceNodeId, + userId, + slotId, + weekdays, + startHour, + startMinute, + durationHour, + durationMinute, + }); } else { - this.userId = options.userId; - this.slotId = options.slotId; - this.weekdays = options.weekdays; - this.startHour = options.startHour; - this.startMinute = options.startMinute; - this.durationHour = options.durationHour; - this.durationMinute = options.durationMinute; + return new ScheduleEntryLockCCDailyRepeatingScheduleReport({ + nodeId: ctx.sourceNodeId, + userId, + slotId, + }); } } @@ -2060,8 +2270,7 @@ export class ScheduleEntryLockCCDailyRepeatingScheduleReport // @publicAPI export type ScheduleEntryLockCCDailyRepeatingScheduleGetOptions = - & CCCommandOptions - & ScheduleEntryLockSlotId; + ScheduleEntryLockSlotId; @CCCommand(ScheduleEntryLockCommand.DailyRepeatingScheduleGet) @expectedCCResponse(ScheduleEntryLockCCDailyRepeatingScheduleReport) @@ -2069,19 +2278,28 @@ export class ScheduleEntryLockCCDailyRepeatingScheduleGet extends ScheduleEntryLockCC { public constructor( - options: - | CommandClassDeserializationOptions - | ScheduleEntryLockCCDailyRepeatingScheduleGetOptions, + options: WithAddress< + ScheduleEntryLockCCDailyRepeatingScheduleGetOptions + >, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.slotId = this.payload[1]; - } else { - this.userId = options.userId; - this.slotId = options.slotId; - } + this.userId = options.userId; + this.slotId = options.slotId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ScheduleEntryLockCCDailyRepeatingScheduleGet { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const slotId = raw.payload[1]; + + return new ScheduleEntryLockCCDailyRepeatingScheduleGet({ + nodeId: ctx.sourceNodeId, + userId, + slotId, + }); } public userId: number; diff --git a/packages/cc/src/cc/Security2CC.ts b/packages/cc/src/cc/Security2CC.ts index f550ff7f87b0..8328e277b7e0 100644 --- a/packages/cc/src/cc/Security2CC.ts +++ b/packages/cc/src/cc/Security2CC.ts @@ -12,6 +12,7 @@ import { SecurityClass, type SecurityManager2, TransmitOptions, + type WithAddress, ZWaveError, ZWaveErrorCodes, decryptAES128CCM, @@ -46,12 +47,10 @@ import { wait } from "alcalzone-shared/async"; import { isArray } from "alcalzone-shared/typeguards"; import { CCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, type CCResponseRole, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -123,11 +122,11 @@ function getAuthenticationData( function getSecurityManager( ownNodeId: number, securityManagers: SecurityManagers, - destination: MulticastDestination | number, + otherNodeId: MulticastDestination | number, ): SecurityManager2 | undefined { const longRange = isLongRangeNodeId(ownNodeId) || isLongRangeNodeId( - isArray(destination) ? destination[0] : destination, + isArray(otherNodeId) ? otherNodeId[0] : otherNodeId, ); return longRange ? securityManagers.securityManagerLR @@ -136,21 +135,21 @@ function getSecurityManager( /** Validates that a sequence number is not a duplicate and updates the SPAN table if it is accepted. Returns the previous sequence number if there is one. */ function validateSequenceNumber( - this: Security2CC, securityManager: SecurityManager2, + sourceNodeId: number, sequenceNumber: number, ): number | undefined { validatePayload.withReason( `Duplicate command (sequence number ${sequenceNumber})`, )( !securityManager.isDuplicateSinglecast( - this.nodeId as number, + sourceNodeId, sequenceNumber, ), ); // Not a duplicate, store it return securityManager.storeSequenceNumber( - this.nodeId as number, + sourceNodeId, sequenceNumber, ); } @@ -168,6 +167,297 @@ export interface DecryptionResult { securityClass: SecurityClass | undefined; } +function assertSecurityRX( + ctx: CCParsingContext, +): SecurityManager2 { + if (!ctx.ownNodeId) { + throw new ZWaveError( + `Secure commands (S2) can only be decoded when the controller's node id is known!`, + ZWaveErrorCodes.Driver_NotReady, + ); + } + + const ret = getSecurityManager(ctx.ownNodeId, ctx, ctx.sourceNodeId); + + if (!ret) { + throw new ZWaveError( + `Secure commands (S2) can only be decoded when the security manager is set up!`, + ZWaveErrorCodes.Driver_NoSecurity, + ); + } + + return ret; +} + +function assertSecurityTX( + ctx: CCEncodingContext, + destination: MulticastDestination | number, +): SecurityManager2 { + if (!ctx.ownNodeId) { + throw new ZWaveError( + `Secure commands (S2) can only be sent when the controller's node id is known!`, + ZWaveErrorCodes.Driver_NotReady, + ); + } + + const ret = getSecurityManager(ctx.ownNodeId, ctx, destination); + + if (!ret) { + throw new ZWaveError( + `Secure commands (S2) can only be sent when the security manager is set up!`, + ZWaveErrorCodes.Driver_NoSecurity, + ); + } + + return ret; +} + +function decryptSinglecast( + ctx: CCParsingContext, + securityManager: SecurityManager2, + sendingNodeId: number, + curSequenceNumber: number, + prevSequenceNumber: number, + ciphertext: Buffer, + authData: Buffer, + authTag: Buffer, + spanState: SPANTableEntry & { + type: SPANState.SPAN | SPANState.LocalEI; + }, + extensions: Security2Extension[], +): DecryptionResult { + const decryptWithNonce = (nonce: Buffer) => { + const { keyCCM: key } = securityManager.getKeysForNode( + sendingNodeId, + ); + + const iv = nonce; + return { + key, + iv, + ...decryptAES128CCM(key, iv, ciphertext, authData, authTag), + }; + }; + const getNonceAndDecrypt = () => { + const iv = securityManager.nextNonce(sendingNodeId); + return decryptWithNonce(iv); + }; + + if (spanState.type === SPANState.SPAN) { + // There SHOULD be a shared SPAN between both parties. But experience has shown that both could have + // sent a command at roughly the same time, using the same SPAN for encryption. + // To avoid a nasty desync and both nodes trying to resync at the same time, causing message loss, + // we accept commands encrypted with the previous SPAN under very specific circumstances: + if ( + // The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted + !!spanState.currentSPAN + // it is still valid + && spanState.currentSPAN.expires > highResTimestamp() + // The received command is exactly the next, expected one + && prevSequenceNumber != undefined + && curSequenceNumber === ((prevSequenceNumber + 1) & 0xff) + // And in case of a mock-based test, do this only on the controller + && !ctx.__internalIsMockNode + ) { + const nonce = spanState.currentSPAN.nonce; + spanState.currentSPAN = undefined; + + // If we could decrypt this way, we're done... + const result = decryptWithNonce(nonce); + if (result.authOK) { + return { + ...result, + securityClass: spanState.securityClass, + }; + } + // ...otherwise, we need to try the normal way + } else { + // forgetting the current SPAN shouldn't be necessary but better be safe than sorry + spanState.currentSPAN = undefined; + } + + // This can only happen if the security class is known + return { + ...getNonceAndDecrypt(), + securityClass: spanState.securityClass, + }; + } else if (spanState.type === SPANState.LocalEI) { + // We've sent the other our receiver's EI and received its sender's EI, + // meaning we can now establish an SPAN + const senderEI = getSenderEI(extensions); + if (!senderEI) failNoSPAN(); + const receiverEI = spanState.receiverEI; + + // How we do this depends on whether we know the security class of the other node + const isBootstrappingNode = securityManager.tempKeys.has( + sendingNodeId, + ); + if (isBootstrappingNode) { + // We're currently bootstrapping the node, it might be using a temporary key + securityManager.initializeTempSPAN( + sendingNodeId, + senderEI, + receiverEI, + ); + + const ret = getNonceAndDecrypt(); + // Decryption with the temporary key worked + if (ret.authOK) { + return { + ...ret, + securityClass: SecurityClass.Temporary, + }; + } + + // Reset the SPAN state and try with the recently granted security class + securityManager.setSPANState( + sendingNodeId, + spanState, + ); + } + + // When ending up here, one of two situations has occured: + // a) We've taken over an existing network and do not know the node's security class + // b) We know the security class, but we're about to establish a new SPAN. This may happen at a lower + // security class than the one the node normally uses, e.g. when we're being queried for securely + // supported CCs. + // In both cases, we should simply try decoding with multiple security classes, starting from the highest one. + // If this fails, we restore the previous (partial) SPAN state. + + // Try all security classes where we do not definitely know that it was not granted + // While bootstrapping a node, we consider the key that is being exchanged (including S0) to be the highest. No need to look at others + const possibleSecurityClasses = isBootstrappingNode + ? [ctx.getHighestSecurityClass(sendingNodeId)!] + : securityClassOrder.filter( + (s) => + ctx.hasSecurityClass(sendingNodeId, s) + !== false, + ); + + for (const secClass of possibleSecurityClasses) { + // Skip security classes we don't have keys for + if ( + !securityManager.hasKeysForSecurityClass( + secClass, + ) + ) { + continue; + } + + // Initialize an SPAN with that security class + securityManager.initializeSPAN( + sendingNodeId, + secClass, + senderEI, + receiverEI, + ); + const ret = getNonceAndDecrypt(); + + // It worked, return the result + if (ret.authOK) { + // Also if we weren't sure before, we now know that the security class is granted + if ( + ctx.hasSecurityClass(sendingNodeId, secClass) + === undefined + ) { + ctx.setSecurityClass(sendingNodeId, secClass, true); + } + return { + ...ret, + securityClass: secClass, + }; + } else { + // Reset the SPAN state and try with the next security class + securityManager.setSPANState( + sendingNodeId, + spanState, + ); + } + } + } + + // Nothing worked, fail the decryption + return { + plaintext: Buffer.from([]), + authOK: false, + securityClass: undefined, + }; +} + +function decryptMulticast( + sendingNodeId: number, + securityManager: SecurityManager2, + groupId: number, + ciphertext: Buffer, + authData: Buffer, + authTag: Buffer, +): DecryptionResult { + const iv = securityManager.nextPeerMPAN( + sendingNodeId, + groupId, + ); + const { keyCCM: key } = securityManager.getKeysForNode( + sendingNodeId, + ); + return { + key, + iv, + ...decryptAES128CCM(key, iv, ciphertext, authData, authTag), + // The security class is irrelevant when decrypting multicast commands + securityClass: undefined, + }; +} + +function getDestinationIDTX( + this: Security2CC & { extensions: Security2Extension[] }, +): number { + if (this.isSinglecast()) return this.nodeId; + + const ret = getMulticastGroupId(this.extensions); + if (ret == undefined) { + throw new ZWaveError( + "Multicast Security S2 encapsulation requires the MGRP extension", + ZWaveErrorCodes.Security2CC_MissingExtension, + ); + } + return ret; +} + +function getDestinationIDRX( + ctx: CCParsingContext, + extensions: Security2Extension[], +): number { + if (ctx.frameType === "singlecast") { + return ctx.ownNodeId; + } + + const ret = getMulticastGroupId(extensions); + if (ret == undefined) { + throw new ZWaveError( + "Multicast Security S2 encapsulation requires the MGRP extension", + ZWaveErrorCodes.Security2CC_MissingExtension, + ); + } + return ret; +} + +function getMulticastGroupId( + extensions: Security2Extension[], +): number | undefined { + const mgrpExtension = extensions.find( + (e) => e instanceof MGRPExtension, + ); + return mgrpExtension?.groupId; +} + +/** Returns the Sender's Entropy Input if this command contains an SPAN extension */ +function getSenderEI(extensions: Security2Extension[]): Buffer | undefined { + const spanExtension = extensions.find( + (e) => e instanceof SPANExtension, + ); + return spanExtension?.senderEI; +} + // Encapsulation CCs are used internally and too frequently that we // want to pay the cost of validating each call /* eslint-disable @zwave-js/ccapi-validate-args */ @@ -210,9 +500,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCNonceReport({ nodeId: this.endpoint.nodeId, - ownNodeId: this.host.ownNodeId, - endpoint: this.endpoint.index, - securityManagers: this.host, + endpointIndex: this.endpoint.index, SOS: true, MOS: false, receiverEI, @@ -258,9 +546,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCNonceReport({ nodeId: this.endpoint.nodeId, - ownNodeId: this.host.ownNodeId, - endpoint: this.endpoint.index, - securityManagers: this.host, + endpointIndex: this.endpoint.index, SOS: false, MOS: true, }); @@ -303,9 +589,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCMessageEncapsulation({ nodeId: this.endpoint.nodeId, - ownNodeId: this.host.ownNodeId, - endpoint: this.endpoint.index, - securityManagers: this.host, + endpointIndex: this.endpoint.index, extensions: [ new MPANExtension({ groupId, @@ -355,7 +639,7 @@ export class Security2CCAPI extends CCAPI { let cc: CommandClass = new Security2CCCommandsSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); // Security2CCCommandsSupportedGet is special because we cannot reply on the applHost to do the automatic // encapsulation because it would use a different security class. Therefore the entire possible stack @@ -399,7 +683,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCCommandsSupportedReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, supportedCCs, }); await this.host.sendCommand(cc, this.commandOptions); @@ -411,7 +695,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCKEXGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -440,7 +724,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCKEXReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...params, echo: false, }); @@ -455,7 +739,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCKEXSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...params, echo: false, }); @@ -473,7 +757,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCKEXReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...params, echo: true, }); @@ -491,7 +775,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCKEXSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...params, echo: true, }); @@ -504,7 +788,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCKEXFail({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, failType, }); await this.host.sendCommand(cc, this.commandOptions); @@ -521,7 +805,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCPublicKeyReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, includingNode, publicKey, }); @@ -538,7 +822,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCNetworkKeyGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, requestedKey: securityClass, }); await this.host.sendCommand(cc, this.commandOptions); @@ -555,7 +839,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCNetworkKeyReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, grantedKey: securityClass, networkKey, }); @@ -570,7 +854,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCNetworkKeyVerify({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); await this.host.sendCommand(cc, this.commandOptions); } @@ -583,7 +867,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCTransferEnd({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, keyVerified: true, keyRequestComplete: false, }); @@ -602,7 +886,7 @@ export class Security2CCAPI extends CCAPI { const cc = new Security2CCTransferEnd({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, keyVerified: false, keyRequestComplete: true, }); @@ -615,50 +899,6 @@ export class Security2CCAPI extends CCAPI { export class Security2CC extends CommandClass { declare ccCommand: Security2Command; - protected assertSecurity( - options: - | (CCCommandOptions & { - ownNodeId: number; - securityManagers: SecurityManagers; - }) - | CommandClassDeserializationOptions, - ): SecurityManager2 { - const verb = gotDeserializationOptions(options) ? "decoded" : "sent"; - const ownNodeId = gotDeserializationOptions(options) - ? options.context.ownNodeId - : options.ownNodeId; - if (!ownNodeId) { - throw new ZWaveError( - `Secure commands (S2) can only be ${verb} when the controller's node id is known!`, - ZWaveErrorCodes.Driver_NotReady, - ); - } - - let ret: SecurityManager2 | undefined; - if (gotDeserializationOptions(options)) { - ret = getSecurityManager( - ownNodeId, - options.context, - this.nodeId, - )!; - } else { - ret = getSecurityManager( - ownNodeId, - options.securityManagers, - this.nodeId, - )!; - } - - if (!ret) { - throw new ZWaveError( - `Secure commands (S2) can only be ${verb} when the security manager is set up!`, - ZWaveErrorCodes.Driver_NoSecurity, - ); - } - - return ret; - } - public async interview( ctx: InterviewContext, ): Promise { @@ -987,9 +1227,7 @@ export class Security2CC extends CommandClass { const ret = new Security2CCMessageEncapsulation({ nodeId, - ownNodeId, encapsulated: cc, - securityManagers, securityClass: options?.securityClass, extensions, verifyDelivery: options?.verifyDelivery, @@ -1004,12 +1242,28 @@ export class Security2CC extends CommandClass { } } +function failNoSPAN(): never { + validatePayload.fail(ZWaveErrorCodes.Security2CC_NoSPAN); +} + +function failNoMPAN(): never { + validatePayload.fail(ZWaveErrorCodes.Security2CC_NoMPAN); +} + +// @publicAPI +export type MulticastContext = + | { + isMulticast: true; + groupId: number; + } + | { + isMulticast: false; + groupId?: number; + }; + // @publicAPI -export interface Security2CCMessageEncapsulationOptions - extends CCCommandOptions -{ - ownNodeId: number; - securityManagers: Readonly; +export interface Security2CCMessageEncapsulationOptions { + sequenceNumber?: number; /** Can be used to override the default security class for the command */ securityClass?: SecurityClass; extensions?: Security2Extension[]; @@ -1057,25 +1311,6 @@ function testCCResponseForMessageEncapsulation( } } -function failNoSPAN(): never { - validatePayload.fail(ZWaveErrorCodes.Security2CC_NoSPAN); -} - -function failNoMPAN(): never { - validatePayload.fail(ZWaveErrorCodes.Security2CC_NoMPAN); -} - -// @publicAPI -export type MulticastContext = - | { - isMulticast: true; - groupId: number; - } - | { - isMulticast: false; - groupId?: number; - }; - @CCCommand(Security2Command.MessageEncapsulation) @expectedCCResponse( getCCResponseForMessageEncapsulation, @@ -1083,363 +1318,377 @@ export type MulticastContext = ) export class Security2CCMessageEncapsulation extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | Security2CCMessageEncapsulationOptions, + options: WithAddress, ) { super(options); - // Make sure that we can send/receive secure commands - this.securityManager = this.assertSecurity(options); - - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - // Check the sequence number to avoid duplicates - this._sequenceNumber = this.payload[0]; - const sendingNodeId = this.nodeId as number; - - // Ensure the node has a security class - const securityClass = options.context.getHighestSecurityClass( - sendingNodeId, - ); - validatePayload.withReason("No security class granted")( - securityClass !== SecurityClass.None, + if (!options.encapsulated && !options.extensions?.length) { + throw new ZWaveError( + "Security S2 encapsulation requires an encapsulated CC and/or extensions", + ZWaveErrorCodes.Argument_Invalid, ); + } - const hasExtensions = !!(this.payload[1] & 0b1); - const hasEncryptedExtensions = !!(this.payload[1] & 0b10); + this.sequenceNumber = options.sequenceNumber; - let offset = 2; - this.extensions = []; - let mustDiscardCommand = false; + this.securityClass = options.securityClass; + if (options.encapsulated) { + this.encapsulated = options.encapsulated; + options.encapsulated.encapsulatingCC = this as any; + } - const parseExtensions = (buffer: Buffer, wasEncrypted: boolean) => { - while (true) { - if (buffer.length < offset + 2) { - // An S2 extension was expected, but the buffer is too short - mustDiscardCommand = true; - return; - } + this.verifyDelivery = options.verifyDelivery !== false; - // The length field could be too large, which would cause part of the actual ciphertext - // to be ignored. Try to avoid this for known extensions by checking the actual and expected length. - const { actual: actualLength, expected: expectedLength } = - Security2Extension - .getExtensionLength( - buffer.subarray(offset), - ); - - // Parse the extension using the expected length if possible - const extensionLength = expectedLength ?? actualLength; - if (extensionLength < 2) { - // An S2 extension was expected, but the length is too short - mustDiscardCommand = true; - return; - } else if ( - extensionLength - > buffer.length - - offset - - (wasEncrypted - ? 0 - : SECURITY_S2_AUTH_TAG_LENGTH) - ) { - // The supposed length is longer than the space the extensions may occupy - mustDiscardCommand = true; - return; - } + this.extensions = options.extensions ?? []; + if ( + typeof this.nodeId !== "number" + && !this.extensions.some((e) => e instanceof MGRPExtension) + ) { + throw new ZWaveError( + "Multicast Security S2 encapsulation requires the MGRP extension", + ZWaveErrorCodes.Security2CC_MissingExtension, + ); + } + } - const extensionData = buffer.subarray( - offset, - offset + extensionLength, - ); - offset += extensionLength; - - const ext = Security2Extension.from(extensionData); - - switch (validateS2Extension(ext, wasEncrypted)) { - case ValidateS2ExtensionResult.OK: - if ( - expectedLength != undefined - && actualLength !== expectedLength - ) { - // The extension length field does not match, ignore the extension - } else { - this.extensions.push(ext); - } - break; - case ValidateS2ExtensionResult.DiscardExtension: - // Do nothing - break; - case ValidateS2ExtensionResult.DiscardCommand: - mustDiscardCommand = true; - break; - } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCMessageEncapsulation { + const securityManager = assertSecurityRX(ctx); - // Check if that was the last extension - if (!ext.moreToFollow) break; - } - }; - if (hasExtensions) parseExtensions(this.payload, false); + validatePayload(raw.payload.length >= 2); + // Check the sequence number to avoid duplicates + const sequenceNumber: number | undefined = raw.payload[0]; - const ctx = ((): MulticastContext => { - const multicastGroupId = this.getMulticastGroupId(); - if ( - options.context.frameType === "multicast" - || options.context.frameType === "broadcast" - ) { - if (multicastGroupId == undefined) { - validatePayload.fail( - "Multicast frames without MGRP extension", - ); - } - return { - isMulticast: true, - groupId: multicastGroupId, - }; - } else { - return { isMulticast: false, groupId: multicastGroupId }; - } - })(); - - // If a command is to be discarded before decryption, - // we still need to increment the SPAN or MPAN state - if (mustDiscardCommand) { - if (ctx.isMulticast) { - this.securityManager.nextPeerMPAN( - sendingNodeId, - ctx.groupId, - ); - } else { - this.securityManager.nextNonce(sendingNodeId); - } - validatePayload.fail( - "Invalid S2 extension", - ); - } + // Ensure the node has a security class + validatePayload.withReason("No security class granted")( + ctx.getHighestSecurityClass( + ctx.sourceNodeId, + ) !== SecurityClass.None, + ); - let prevSequenceNumber: number | undefined; - let mpanState: - | ReturnType - | undefined; - if (ctx.isMulticast) { - mpanState = this.securityManager.getPeerMPAN( - sendingNodeId, - ctx.groupId, - ); - } else { - // Don't accept duplicate Singlecast commands - prevSequenceNumber = validateSequenceNumber.call( - this, - this.securityManager, - this._sequenceNumber, - ); + const hasExtensions = !!(raw.payload[1] & 0b1); + const hasEncryptedExtensions = !!(raw.payload[1] & 0b10); - // When a node receives a singlecast message after a multicast group was marked out of sync, - // it must forget about the group. - if (ctx.groupId == undefined) { - this.securityManager.resetOutOfSyncMPANs( - sendingNodeId, - ); + let offset = 2; + const extensions: Security2Extension[] = []; + let mustDiscardCommand = false; + + const parseExtensions = (buffer: Buffer, wasEncrypted: boolean) => { + while (true) { + if (buffer.length < offset + 2) { + // An S2 extension was expected, but the buffer is too short + mustDiscardCommand = true; + return; } - } - - const unencryptedPayload = this.payload.subarray(0, offset); - const ciphertext = this.payload.subarray( - offset, - -SECURITY_S2_AUTH_TAG_LENGTH, - ); - const authTag = this.payload.subarray(-SECURITY_S2_AUTH_TAG_LENGTH); - this.authTag = authTag; - - const messageLength = super.computeEncapsulationOverhead() - + this.payload.length; - const authData = getAuthenticationData( - sendingNodeId, - this.getDestinationIDRX(options.context.ownNodeId), - options.context.homeId, - messageLength, - unencryptedPayload, - ); + // The length field could be too large, which would cause part of the actual ciphertext + // to be ignored. Try to avoid this for known extensions by checking the actual and expected length. + const { actual: actualLength, expected: expectedLength } = + Security2Extension + .getExtensionLength( + buffer.subarray(offset), + ); - let decrypt: () => DecryptionResult; - if (ctx.isMulticast) { - // For incoming multicast commands, make sure we have an MPAN - if (mpanState?.type !== MPANState.MPAN) { - // If we don't, mark the MPAN as out of sync, so we can respond accordingly on the singlecast followup - this.securityManager.storePeerMPAN( - sendingNodeId, - ctx.groupId, - { type: MPANState.OutOfSync }, - ); - failNoMPAN(); + // Parse the extension using the expected length if possible + const extensionLength = expectedLength ?? actualLength; + if (extensionLength < 2) { + // An S2 extension was expected, but the length is too short + mustDiscardCommand = true; + return; + } else if ( + extensionLength + > buffer.length + - offset + - (wasEncrypted + ? 0 + : SECURITY_S2_AUTH_TAG_LENGTH) + ) { + // The supposed length is longer than the space the extensions may occupy + mustDiscardCommand = true; + return; } - decrypt = () => - this.decryptMulticast( - sendingNodeId, - ctx.groupId, - ciphertext, - authData, - authTag, - ); - } else { - // Decrypt payload and verify integrity - const spanState = this.securityManager.getSPANState( - sendingNodeId, + const extensionData = buffer.subarray( + offset, + offset + extensionLength, ); - - // If we are not able to establish an SPAN yet, fail the decryption - if (spanState.type === SPANState.None) { - failNoSPAN(); - } else if (spanState.type === SPANState.RemoteEI) { - // TODO: The specs are not clear how to handle this case - // For now, do the same as if we didn't have any EI - failNoSPAN(); + offset += extensionLength; + + const ext = Security2Extension.from(extensionData); + + switch (validateS2Extension(ext, wasEncrypted)) { + case ValidateS2ExtensionResult.OK: + if ( + expectedLength != undefined + && actualLength !== expectedLength + ) { + // The extension length field does not match, ignore the extension + } else { + extensions.push(ext); + } + break; + case ValidateS2ExtensionResult.DiscardExtension: + // Do nothing + break; + case ValidateS2ExtensionResult.DiscardCommand: + mustDiscardCommand = true; + break; } - decrypt = () => - this.decryptSinglecast( - options.context, - sendingNodeId, - prevSequenceNumber!, - ciphertext, - authData, - authTag, - spanState, - ); - } - - let plaintext: Buffer | undefined; - let authOK = false; - let key: Buffer | undefined; - let iv: Buffer | undefined; - let decryptionSecurityClass: SecurityClass | undefined; - - // If the Receiver is unable to authenticate the singlecast message with the current SPAN, - // the Receiver SHOULD try decrypting the message with one or more of the following SPAN values, - // stopping when decryption is successful or the maximum number of iterations is reached. - - // If the Receiver is unable to decrypt the S2 MC frame with the current MPAN, the Receiver MAY try - // decrypting the frame with one or more of the subsequent MPAN values, stopping when decryption is - // successful or the maximum number of iterations is reached. - const decryptAttempts = ctx.isMulticast - ? MAX_DECRYPT_ATTEMPTS_MULTICAST - : ctx.groupId != undefined - ? MAX_DECRYPT_ATTEMPTS_SC_FOLLOWUP - : MAX_DECRYPT_ATTEMPTS_SINGLECAST; - - for (let i = 0; i < decryptAttempts; i++) { - ({ - plaintext, - authOK, - key, - iv, - securityClass: decryptionSecurityClass, - } = decrypt()); - if (!!authOK && !!plaintext) break; - // No need to try further SPANs if we just got the sender's EI - if (!!this.getSenderEI()) break; + // Check if that was the last extension + if (!ext.moreToFollow) break; } + }; + if (hasExtensions) parseExtensions(raw.payload, false); - // If authentication fails, do so with an error code that instructs the - // applHost to tell the node we have no nonce - if (!authOK || !plaintext) { - if (ctx.isMulticast) { - // Mark the MPAN as out of sync - this.securityManager.storePeerMPAN( - sendingNodeId, - ctx.groupId, - { type: MPANState.OutOfSync }, - ); - validatePayload.fail( - ZWaveErrorCodes.Security2CC_CannotDecodeMulticast, - ); - } else { + const mcctx = ((): MulticastContext => { + const multicastGroupId = getMulticastGroupId(extensions); + if ( + ctx.frameType === "multicast" || ctx.frameType === "broadcast" + ) { + if (multicastGroupId == undefined) { validatePayload.fail( - ZWaveErrorCodes.Security2CC_CannotDecode, + "Multicast frames without MGRP extension", ); } - } else if (!ctx.isMulticast && ctx.groupId != undefined) { - // After reception of a singlecast followup, the MPAN state must be increased - this.securityManager.tryIncrementPeerMPAN( - sendingNodeId, - ctx.groupId, - ); - } - - // Remember which security class was used to decrypt this message, so we can discard it later - this.securityClass = decryptionSecurityClass; - - offset = 0; - if (hasEncryptedExtensions) parseExtensions(plaintext, true); - - // Before we can continue, check if the command must be discarded - if (mustDiscardCommand) { - validatePayload.fail("Invalid extension"); + return { + isMulticast: true, + groupId: multicastGroupId, + }; + } else { + return { isMulticast: false, groupId: multicastGroupId }; } - - // If the MPAN extension was received, store the MPAN - if (!ctx.isMulticast) { - const mpanExtension = this.getMPANExtension(); - if (mpanExtension) { - this.securityManager.storePeerMPAN( - sendingNodeId, - mpanExtension.groupId, - { - type: MPANState.MPAN, - currentMPAN: mpanExtension.innerMPANState, - }, - ); - } + })(); + + // If a command is to be discarded before decryption, + // we still need to increment the SPAN or MPAN state + if (mustDiscardCommand) { + if (mcctx.isMulticast) { + securityManager.nextPeerMPAN( + ctx.sourceNodeId, + mcctx.groupId, + ); + } else { + securityManager.nextNonce(ctx.sourceNodeId); } + validatePayload.fail( + "Invalid S2 extension", + ); + } - // Not every S2 message includes an encapsulated CC - const decryptedCCBytes = plaintext.subarray(offset); - if (decryptedCCBytes.length > 0) { - // make sure this contains a complete CC command that's worth splitting - validatePayload(decryptedCCBytes.length >= 2); - // and deserialize the CC - this.encapsulated = CommandClass.from({ - data: decryptedCCBytes, - fromEncapsulation: true, - encapCC: this, - context: options.context, - }); - } - this.plaintext = decryptedCCBytes; - this.key = key; - this.iv = iv; + let prevSequenceNumber: number | undefined; + let mpanState: + | ReturnType + | undefined; + if (mcctx.isMulticast) { + mpanState = securityManager.getPeerMPAN( + ctx.sourceNodeId, + mcctx.groupId, + ); } else { - if (!options.encapsulated && !options.extensions?.length) { - throw new ZWaveError( - "Security S2 encapsulation requires an encapsulated CC and/or extensions", - ZWaveErrorCodes.Argument_Invalid, + // Don't accept duplicate Singlecast commands + prevSequenceNumber = validateSequenceNumber( + securityManager, + ctx.sourceNodeId, + sequenceNumber, + ); + + // When a node receives a singlecast message after a multicast group was marked out of sync, + // it must forget about the group. + if (mcctx.groupId == undefined) { + securityManager.resetOutOfSyncMPANs( + ctx.sourceNodeId, ); } + } - this.securityClass = options.securityClass; - if (options.encapsulated) { - this.encapsulated = options.encapsulated; - options.encapsulated.encapsulatingCC = this as any; - } + const unencryptedPayload = raw.payload.subarray(0, offset); + const ciphertext = raw.payload.subarray( + offset, + -SECURITY_S2_AUTH_TAG_LENGTH, + ); + const authTag = raw.payload.subarray(-SECURITY_S2_AUTH_TAG_LENGTH); + const messageLength = + 2 /* CommandClass.computeEncapsulationOverhead() */ + + raw.payload.length; - this.verifyDelivery = options.verifyDelivery !== false; + const authData = getAuthenticationData( + ctx.sourceNodeId, + getDestinationIDRX(ctx, extensions), + ctx.homeId, + messageLength, + unencryptedPayload, + ); - this.extensions = options.extensions ?? []; - if ( - typeof this.nodeId !== "number" - && !this.extensions.some((e) => e instanceof MGRPExtension) - ) { - throw new ZWaveError( - "Multicast Security S2 encapsulation requires the MGRP extension", - ZWaveErrorCodes.Security2CC_MissingExtension, + let decrypt: () => DecryptionResult; + if (mcctx.isMulticast) { + // For incoming multicast commands, make sure we have an MPAN + if (mpanState?.type !== MPANState.MPAN) { + // If we don't, mark the MPAN as out of sync, so we can respond accordingly on the singlecast followup + securityManager.storePeerMPAN( + ctx.sourceNodeId, + mcctx.groupId, + { type: MPANState.OutOfSync }, + ); + failNoMPAN(); + } + + decrypt = () => + decryptMulticast( + ctx.sourceNodeId, + securityManager, + mcctx.groupId, + ciphertext, + authData, + authTag, ); + } else { + // Decrypt payload and verify integrity + const spanState = securityManager.getSPANState( + ctx.sourceNodeId, + ); + + // If we are not able to establish an SPAN yet, fail the decryption + if (spanState.type === SPANState.None) { + failNoSPAN(); + } else if (spanState.type === SPANState.RemoteEI) { + // TODO: The specs are not clear how to handle this case + // For now, do the same as if we didn't have any EI + failNoSPAN(); } + + decrypt = () => + decryptSinglecast( + ctx, + securityManager, + ctx.sourceNodeId, + sequenceNumber, + prevSequenceNumber!, + ciphertext, + authData, + authTag, + spanState, + extensions, + ); } + + let plaintext: Buffer | undefined; + let authOK = false; + let key: Buffer | undefined; + let iv: Buffer | undefined; + let decryptionSecurityClass: SecurityClass | undefined; + + // If the Receiver is unable to authenticate the singlecast message with the current SPAN, + // the Receiver SHOULD try decrypting the message with one or more of the following SPAN values, + // stopping when decryption is successful or the maximum number of iterations is reached. + + // If the Receiver is unable to decrypt the S2 MC frame with the current MPAN, the Receiver MAY try + // decrypting the frame with one or more of the subsequent MPAN values, stopping when decryption is + // successful or the maximum number of iterations is reached. + const decryptAttempts = mcctx.isMulticast + ? MAX_DECRYPT_ATTEMPTS_MULTICAST + : mcctx.groupId != undefined + ? MAX_DECRYPT_ATTEMPTS_SC_FOLLOWUP + : MAX_DECRYPT_ATTEMPTS_SINGLECAST; + + for (let i = 0; i < decryptAttempts; i++) { + ({ + plaintext, + authOK, + key, + iv, + securityClass: decryptionSecurityClass, + } = decrypt()); + if (!!authOK && !!plaintext) break; + // No need to try further SPANs if we just got the sender's EI + if (!!getSenderEI(extensions)) break; + } + + // If authentication fails, do so with an error code that instructs the + // applHost to tell the node we have no nonce + if (!authOK || !plaintext) { + if (mcctx.isMulticast) { + // Mark the MPAN as out of sync + securityManager.storePeerMPAN( + ctx.sourceNodeId, + mcctx.groupId, + { type: MPANState.OutOfSync }, + ); + validatePayload.fail( + ZWaveErrorCodes.Security2CC_CannotDecodeMulticast, + ); + } else { + validatePayload.fail( + ZWaveErrorCodes.Security2CC_CannotDecode, + ); + } + } else if (!mcctx.isMulticast && mcctx.groupId != undefined) { + // After reception of a singlecast followup, the MPAN state must be increased + securityManager.tryIncrementPeerMPAN( + ctx.sourceNodeId, + mcctx.groupId, + ); + } + + // Remember which security class was used to decrypt this message, so we can discard it later + const securityClass: SecurityClass | undefined = + decryptionSecurityClass; + + offset = 0; + if (hasEncryptedExtensions) parseExtensions(plaintext, true); + + // Before we can continue, check if the command must be discarded + if (mustDiscardCommand) { + validatePayload.fail("Invalid extension"); + } + + // If the MPAN extension was received, store the MPAN + if (!mcctx.isMulticast) { + const mpanExtension = extensions.find((e) => + e instanceof MPANExtension + ); + if (mpanExtension) { + securityManager.storePeerMPAN( + ctx.sourceNodeId, + mpanExtension.groupId, + { + type: MPANState.MPAN, + currentMPAN: mpanExtension.innerMPANState, + }, + ); + } + } + + // Not every S2 message includes an encapsulated CC + const decryptedCCBytes = plaintext.subarray(offset); + let encapsulated: CommandClass | undefined; + if (decryptedCCBytes.length > 0) { + // make sure this contains a complete CC command that's worth splitting + validatePayload(decryptedCCBytes.length >= 2); + // and deserialize the CC + encapsulated = CommandClass.parse(decryptedCCBytes, ctx); + } + + const ret = new Security2CCMessageEncapsulation({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + securityClass, + extensions, + encapsulated, + }); + + // Remember for debugging purposes + ret.key = key; + ret.iv = iv; + ret.authData = authData; + ret.authTag = authTag; + ret.plaintext = decryptedCCBytes; + + return ret; } - private securityManager: SecurityManager2; public readonly securityClass?: SecurityClass; // Only used for testing/debugging purposes @@ -1452,26 +1701,25 @@ export class Security2CCMessageEncapsulation extends Security2CC { public readonly verifyDelivery: boolean = true; - private _sequenceNumber: number | undefined; - /** - * Return the sequence number of this command. - * - * **WARNING:** If the sequence number hasn't been set before, this will create a new one. - * When sending messages, this should only happen immediately before serializing. - */ - public get sequenceNumber(): number { - if (this._sequenceNumber == undefined) { + public sequenceNumber: number | undefined; + private ensureSequenceNumber( + securityManager: SecurityManager2, + ): asserts this is this & { + sequenceNumber: number; + } { + if (this.sequenceNumber == undefined) { if (this.isSinglecast()) { - this._sequenceNumber = this.securityManager - .nextSequenceNumber(this.nodeId); - } else { - const groupId = this.getDestinationIDTX(); - return this.securityManager.nextMulticastSequenceNumber( - groupId, + this.sequenceNumber = securityManager.nextSequenceNumber( + this.nodeId, ); + } else { + const groupId = getDestinationIDTX.call(this); + this.sequenceNumber = securityManager + .nextMulticastSequenceNumber( + groupId, + ); } } - return this._sequenceNumber; } public encapsulated?: CommandClass; @@ -1479,50 +1727,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { public override prepareRetransmission(): void { super.prepareRetransmission(); - this._sequenceNumber = undefined; - } - - private getDestinationIDTX(): number { - if (this.isSinglecast()) return this.nodeId; - - const ret = this.getMulticastGroupId(); - if (ret == undefined) { - throw new ZWaveError( - "Multicast Security S2 encapsulation requires the MGRP extension", - ZWaveErrorCodes.Security2CC_MissingExtension, - ); - } - return ret; - } - - private getDestinationIDRX(ownNodeId: number): number { - if (this.isSinglecast()) return ownNodeId; - - const ret = this.getMulticastGroupId(); - if (ret == undefined) { - throw new ZWaveError( - "Multicast Security S2 encapsulation requires the MGRP extension", - ZWaveErrorCodes.Security2CC_MissingExtension, - ); - } - return ret; - } - - private getMGRPExtension(): MGRPExtension | undefined { - return this.extensions.find( - (e) => e instanceof MGRPExtension, - ); - } - - public getMulticastGroupId(): number | undefined { - const mgrpExtension = this.getMGRPExtension(); - return mgrpExtension?.groupId; - } - - private getMPANExtension(): MPANExtension | undefined { - return this.extensions.find( - (e) => e instanceof MPANExtension, - ); + this.sequenceNumber = undefined; } public hasMOSExtension(): boolean { @@ -1531,17 +1736,22 @@ export class Security2CCMessageEncapsulation extends Security2CC { /** Returns the Sender's Entropy Input if this command contains an SPAN extension */ public getSenderEI(): Buffer | undefined { - const spanExtension = this.extensions.find( - (e) => e instanceof SPANExtension, - ); - return spanExtension?.senderEI; + return getSenderEI(this.extensions); + } + + /** Returns the multicast group ID if this command contains an MGRP extension */ + public getMulticastGroupId(): number | undefined { + return getMulticastGroupId(this.extensions); } - private maybeAddSPANExtension(ctx: CCEncodingContext): void { + private maybeAddSPANExtension( + ctx: CCEncodingContext, + securityManager: SecurityManager2, + ): void { if (!this.isSinglecast()) return; const receiverNodeId: number = this.nodeId; - const spanState = this.securityManager.getSPANState( + const spanState = securityManager.getSPANState( receiverNodeId, ); if ( @@ -1556,7 +1766,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { } else if (spanState.type === SPANState.RemoteEI) { // We have the receiver's EI, generate our input and send it over // With both, we can create an SPAN - const senderEI = this.securityManager.generateNonce( + const senderEI = securityManager.generateNonce( undefined, ); const receiverEI = spanState.receiverEI; @@ -1565,9 +1775,9 @@ export class Security2CCMessageEncapsulation extends Security2CC { // specific command specifies a security class if ( this.securityClass == undefined - && this.securityManager.tempKeys.has(receiverNodeId) + && securityManager.tempKeys.has(receiverNodeId) ) { - this.securityManager.initializeTempSPAN( + securityManager.initializeTempSPAN( receiverNodeId, senderEI, receiverEI, @@ -1582,7 +1792,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { ZWaveErrorCodes.Security2CC_NoSPAN, ); } - this.securityManager.initializeSPAN( + securityManager.initializeSPAN( receiverNodeId, securityClass, senderEI, @@ -1604,8 +1814,11 @@ export class Security2CCMessageEncapsulation extends Security2CC { } public serialize(ctx: CCEncodingContext): Buffer { + const securityManager = assertSecurityTX(ctx, this.nodeId); + this.ensureSequenceNumber(securityManager); + // Include Sender EI in the command if we only have the receiver's EI - this.maybeAddSPANExtension(ctx); + this.maybeAddSPANExtension(ctx, securityManager); const unencryptedExtensions = this.extensions.filter( (e) => !e.isEncrypted(), @@ -1634,7 +1847,9 @@ export class Security2CCMessageEncapsulation extends Security2CC { ]); // Generate the authentication data for CCM encryption - const destinationTag = this.getDestinationIDTX(); + const destinationTag = getDestinationIDTX.call( + this as Security2CCMessageEncapsulation, + ); const messageLength = this.computeEncapsulationOverhead() + serializedCC.length; const authData = getAuthenticationData( @@ -1652,18 +1867,18 @@ export class Security2CCMessageEncapsulation extends Security2CC { // Singlecast: // Generate a nonce for encryption, and remember it to attempt decryption // of potential in-flight messages from the target node. - iv = this.securityManager.nextNonce(this.nodeId, true); + iv = securityManager.nextNonce(this.nodeId, true); const { keyCCM } = // Prefer the overridden security class if it was given this.securityClass != undefined - ? this.securityManager.getKeysForSecurityClass( + ? securityManager.getKeysForSecurityClass( this.securityClass, ) - : this.securityManager.getKeysForNode(this.nodeId); + : securityManager.getKeysForNode(this.nodeId); key = keyCCM; } else { // Multicast: - const keyAndIV = this.securityManager.getMulticastKeyAndIV( + const keyAndIV = securityManager.getMulticastKeyAndIV( destinationTag, ); key = keyAndIV.key; @@ -1713,7 +1928,7 @@ export class Security2CCMessageEncapsulation extends Security2CC { public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { const message: MessageRecord = { - "sequence number": this.sequenceNumber, + "sequence number": this.sequenceNumber ?? "(not set)", }; if (this.extensions.length > 0) { message.extensions = this.extensions @@ -1744,24 +1959,11 @@ export class Security2CCMessageEncapsulation extends Security2CC { } } - if (this.isSinglecast()) { - // TODO: This is ugly, we should probably do this in the constructor or so - let securityClass = this.securityClass; - if (securityClass == undefined) { - const spanState = this.securityManager.getSPANState( - this.nodeId, - ); - if (spanState.type === SPANState.SPAN) { - securityClass = spanState.securityClass; - } - } - - if (securityClass != undefined) { - message["security class"] = getEnumMemberName( - SecurityClass, - securityClass, - ); - } + if (this.securityClass != undefined) { + message["security class"] = getEnumMemberName( + SecurityClass, + this.securityClass, + ); } return { @@ -1769,205 +1971,12 @@ export class Security2CCMessageEncapsulation extends Security2CC { message, }; } - - private decryptSinglecast( - ctx: CCParsingContext, - sendingNodeId: number, - prevSequenceNumber: number, - ciphertext: Buffer, - authData: Buffer, - authTag: Buffer, - spanState: SPANTableEntry & { - type: SPANState.SPAN | SPANState.LocalEI; - }, - ): DecryptionResult { - const decryptWithNonce = (nonce: Buffer) => { - const { keyCCM: key } = this.securityManager.getKeysForNode( - sendingNodeId, - ); - - const iv = nonce; - return { - key, - iv, - ...decryptAES128CCM(key, iv, ciphertext, authData, authTag), - }; - }; - const getNonceAndDecrypt = () => { - const iv = this.securityManager.nextNonce(sendingNodeId); - return decryptWithNonce(iv); - }; - - if (spanState.type === SPANState.SPAN) { - // There SHOULD be a shared SPAN between both parties. But experience has shown that both could have - // sent a command at roughly the same time, using the same SPAN for encryption. - // To avoid a nasty desync and both nodes trying to resync at the same time, causing message loss, - // we accept commands encrypted with the previous SPAN under very specific circumstances: - if ( - // The previous SPAN is still known, i.e. the node didn't send another command that was successfully decrypted - !!spanState.currentSPAN - // it is still valid - && spanState.currentSPAN.expires > highResTimestamp() - // The received command is exactly the next, expected one - && prevSequenceNumber != undefined - && this["_sequenceNumber"] === ((prevSequenceNumber + 1) & 0xff) - // And in case of a mock-based test, do this only on the controller - && !ctx.__internalIsMockNode - ) { - const nonce = spanState.currentSPAN.nonce; - spanState.currentSPAN = undefined; - - // If we could decrypt this way, we're done... - const result = decryptWithNonce(nonce); - if (result.authOK) { - return { - ...result, - securityClass: spanState.securityClass, - }; - } - // ...otherwise, we need to try the normal way - } else { - // forgetting the current SPAN shouldn't be necessary but better be safe than sorry - spanState.currentSPAN = undefined; - } - - // This can only happen if the security class is known - return { - ...getNonceAndDecrypt(), - securityClass: spanState.securityClass, - }; - } else if (spanState.type === SPANState.LocalEI) { - // We've sent the other our receiver's EI and received its sender's EI, - // meaning we can now establish an SPAN - const senderEI = this.getSenderEI(); - if (!senderEI) failNoSPAN(); - const receiverEI = spanState.receiverEI; - - // How we do this depends on whether we know the security class of the other node - const isBootstrappingNode = this.securityManager.tempKeys.has( - sendingNodeId, - ); - if (isBootstrappingNode) { - // We're currently bootstrapping the node, it might be using a temporary key - this.securityManager.initializeTempSPAN( - sendingNodeId, - senderEI, - receiverEI, - ); - - const ret = getNonceAndDecrypt(); - // Decryption with the temporary key worked - if (ret.authOK) { - return { - ...ret, - securityClass: SecurityClass.Temporary, - }; - } - - // Reset the SPAN state and try with the recently granted security class - this.securityManager.setSPANState( - sendingNodeId, - spanState, - ); - } - - // When ending up here, one of two situations has occured: - // a) We've taken over an existing network and do not know the node's security class - // b) We know the security class, but we're about to establish a new SPAN. This may happen at a lower - // security class than the one the node normally uses, e.g. when we're being queried for securely - // supported CCs. - // In both cases, we should simply try decoding with multiple security classes, starting from the highest one. - // If this fails, we restore the previous (partial) SPAN state. - - // Try all security classes where we do not definitely know that it was not granted - // While bootstrapping a node, we consider the key that is being exchanged (including S0) to be the highest. No need to look at others - const possibleSecurityClasses = isBootstrappingNode - ? [ctx.getHighestSecurityClass(sendingNodeId)!] - : securityClassOrder.filter( - (s) => - ctx.hasSecurityClass(sendingNodeId, s) - !== false, - ); - - for (const secClass of possibleSecurityClasses) { - // Skip security classes we don't have keys for - if ( - !this.securityManager.hasKeysForSecurityClass( - secClass, - ) - ) { - continue; - } - - // Initialize an SPAN with that security class - this.securityManager.initializeSPAN( - sendingNodeId, - secClass, - senderEI, - receiverEI, - ); - const ret = getNonceAndDecrypt(); - - // It worked, return the result - if (ret.authOK) { - // Also if we weren't sure before, we now know that the security class is granted - if ( - ctx.hasSecurityClass(sendingNodeId, secClass) - === undefined - ) { - ctx.setSecurityClass(sendingNodeId, secClass, true); - } - return { - ...ret, - securityClass: secClass, - }; - } else { - // Reset the SPAN state and try with the next security class - this.securityManager.setSPANState( - sendingNodeId, - spanState, - ); - } - } - } - - // Nothing worked, fail the decryption - return { - plaintext: Buffer.from([]), - authOK: false, - securityClass: undefined, - }; - } - - private decryptMulticast( - sendingNodeId: number, - groupId: number, - ciphertext: Buffer, - authData: Buffer, - authTag: Buffer, - ): DecryptionResult { - const iv = this.securityManager.nextPeerMPAN( - sendingNodeId, - groupId, - ); - const { keyCCM: key } = this.securityManager.getKeysForNode( - sendingNodeId, - ); - return { - key, - iv, - ...decryptAES128CCM(key, iv, ciphertext, authData, authTag), - // The security class is irrelevant when decrypting multicast commands - securityClass: undefined, - }; - } } // @publicAPI export type Security2CCNonceReportOptions = & { - ownNodeId: number; - securityManagers: SecurityManagers; + sequenceNumber?: number; } & ( | { @@ -1985,64 +1994,77 @@ export type Security2CCNonceReportOptions = @CCCommand(Security2Command.NonceReport) export class Security2CCNonceReport extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & Security2CCNonceReportOptions), + options: WithAddress, ) { super(options); + this.SOS = options.SOS; + this.MOS = options.MOS; + this.sequenceNumber = options.sequenceNumber; + if (options.SOS) this.receiverEI = options.receiverEI; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCNonceReport { // Make sure that we can send/receive secure commands - this.securityManager = this.assertSecurity(options); - - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this._sequenceNumber = this.payload[0]; - // Don't accept duplicate commands - validateSequenceNumber.call( - this, - this.securityManager, - this._sequenceNumber, - ); + const securityManager = assertSecurityRX(ctx); - this.MOS = !!(this.payload[1] & 0b10); - this.SOS = !!(this.payload[1] & 0b1); - validatePayload(this.MOS || this.SOS); + validatePayload(raw.payload.length >= 2); + const sequenceNumber = raw.payload[0]; - if (this.SOS) { - // If the SOS flag is set, the REI field MUST be included in the command - validatePayload(this.payload.length >= 18); - this.receiverEI = this.payload.subarray(2, 18); + // Don't accept duplicate commands + validateSequenceNumber( + securityManager, + ctx.sourceNodeId, + sequenceNumber, + ); + const MOS = !!(raw.payload[1] & 0b10); + const SOS = !!(raw.payload[1] & 0b1); + + if (SOS) { + // If the SOS flag is set, the REI field MUST be included in the command + validatePayload(raw.payload.length >= 18); + const receiverEI = raw.payload.subarray(2, 18); + + // In that case we also need to store it, so the next sent command + // can use it for encryption + securityManager.storeRemoteEI( + ctx.sourceNodeId, + receiverEI, + ); - // In that case we also need to store it, so the next sent command - // can use it for encryption - this.securityManager.storeRemoteEI( - this.nodeId as number, - this.receiverEI, - ); - } + return new Security2CCNonceReport({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + MOS, + SOS, + receiverEI, + }); + } else if (MOS) { + return new Security2CCNonceReport({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + MOS, + SOS: false, + }); } else { - this.SOS = options.SOS; - this.MOS = options.MOS; - if (options.SOS) this.receiverEI = options.receiverEI; + validatePayload.fail("Either MOS or SOS must be set"); } } - private securityManager!: SecurityManager2; - private _sequenceNumber: number | undefined; - /** - * Return the sequence number of this command. - * - * **WARNING:** If the sequence number hasn't been set before, this will create a new one. - * When sending messages, this should only happen immediately before serializing. - */ - public get sequenceNumber(): number { - if (this._sequenceNumber == undefined) { - this._sequenceNumber = this.securityManager - .nextSequenceNumber( - this.nodeId as number, - ); + public sequenceNumber: number | undefined; + private ensureSequenceNumber( + securityManager: SecurityManager2, + ): asserts this is this & { + sequenceNumber: number; + } { + if (this.sequenceNumber == undefined) { + this.sequenceNumber = securityManager.nextSequenceNumber( + this.nodeId as number, + ); } - return this._sequenceNumber; } public readonly SOS: boolean; @@ -2050,6 +2072,9 @@ export class Security2CCNonceReport extends Security2CC { public readonly receiverEI?: Buffer; public serialize(ctx: CCEncodingContext): Buffer { + const securityManager = assertSecurityTX(ctx, this.nodeId); + this.ensureSequenceNumber(securityManager); + this.payload = Buffer.from([ this.sequenceNumber, (this.MOS ? 0b10 : 0) + (this.SOS ? 0b1 : 0), @@ -2062,7 +2087,7 @@ export class Security2CCNonceReport extends Security2CC { public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { const message: MessageRecord = { - "sequence number": this.sequenceNumber, + "sequence number": this.sequenceNumber ?? "(not set)", SOS: this.SOS, MOS: this.MOS, }; @@ -2078,8 +2103,7 @@ export class Security2CCNonceReport extends Security2CC { // @publicAPI export interface Security2CCNonceGetOptions { - ownNodeId: number; - securityManagers: Readonly; + sequenceNumber?: number; } @CCCommand(Security2Command.NonceGet) @@ -2089,48 +2113,48 @@ export class Security2CCNonceGet extends Security2CC { // 250 ms before receiving the Security 2 Nonce Report Command. public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & Security2CCNonceGetOptions), + options: WithAddress, ) { super(options); + this.sequenceNumber = options.sequenceNumber; + } - // Make sure that we can send/receive secure commands - this.securityManager = this.assertSecurity(options); - - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this._sequenceNumber = this.payload[0]; - // Don't accept duplicate commands - validateSequenceNumber.call( - this, - this.securityManager, - this._sequenceNumber, - ); - } else { - // No options here - } + public static from(raw: CCRaw, ctx: CCParsingContext): Security2CCNonceGet { + const securityManager = assertSecurityRX(ctx); + + validatePayload(raw.payload.length >= 1); + const sequenceNumber = raw.payload[0]; + + // Don't accept duplicate commands + validateSequenceNumber( + securityManager, + ctx.sourceNodeId, + sequenceNumber, + ); + + return new Security2CCNonceGet({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + }); } - private securityManager!: SecurityManager2; - private _sequenceNumber: number | undefined; - /** - * Return the sequence number of this command. - * - * **WARNING:** If the sequence number hasn't been set before, this will create a new one. - * When sending messages, this should only happen immediately before serializing. - */ - public get sequenceNumber(): number { - if (this._sequenceNumber == undefined) { - this._sequenceNumber = this.securityManager - .nextSequenceNumber( - this.nodeId as number, - ); + public sequenceNumber: number | undefined; + private ensureSequenceNumber( + securityManager: SecurityManager2, + ): asserts this is this & { + sequenceNumber: number; + } { + if (this.sequenceNumber == undefined) { + this.sequenceNumber = securityManager.nextSequenceNumber( + this.nodeId as number, + ); } - return this._sequenceNumber; } public serialize(ctx: CCEncodingContext): Buffer { + const securityManager = assertSecurityTX(ctx, this.nodeId); + this.ensureSequenceNumber(securityManager); + this.payload = Buffer.from([this.sequenceNumber]); return super.serialize(ctx); } @@ -2138,7 +2162,9 @@ export class Security2CCNonceGet extends Security2CC { public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { return { ...super.toLogEntry(ctx), - message: { "sequence number": this.sequenceNumber }, + message: { + "sequence number": this.sequenceNumber ?? "(not set)", + }, }; } } @@ -2156,38 +2182,51 @@ export interface Security2CCKEXReportOptions { @CCCommand(Security2Command.KEXReport) export class Security2CCKEXReport extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & Security2CCKEXReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.requestCSA = !!(this.payload[0] & 0b10); - this.echo = !!(this.payload[0] & 0b1); - // Remember the reserved bits for the echo - this._reserved = this.payload[0] & 0b1111_1100; - // The bit mask starts at 0, but bit 0 is not used - this.supportedKEXSchemes = parseBitMask( - this.payload.subarray(1, 2), - 0, - ).filter((s) => s !== 0); - this.supportedECDHProfiles = parseBitMask( - this.payload.subarray(2, 3), - ECDHProfiles.Curve25519, - ); - this.requestedKeys = parseBitMask( - this.payload.subarray(3, 4), - SecurityClass.S2_Unauthenticated, - ); - } else { - this.requestCSA = options.requestCSA; - this.echo = options.echo; - this._reserved = options._reserved ?? 0; - this.supportedKEXSchemes = options.supportedKEXSchemes; - this.supportedECDHProfiles = options.supportedECDHProfiles; - this.requestedKeys = options.requestedKeys; - } + this.requestCSA = options.requestCSA; + this.echo = options.echo; + this._reserved = options._reserved ?? 0; + this.supportedKEXSchemes = options.supportedKEXSchemes; + this.supportedECDHProfiles = options.supportedECDHProfiles; + this.requestedKeys = options.requestedKeys; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCKEXReport { + validatePayload(raw.payload.length >= 4); + const requestCSA = !!(raw.payload[0] & 0b10); + const echo = !!(raw.payload[0] & 0b1); + + // Remember the reserved bits for the echo + const _reserved = raw.payload[0] & 0b1111_1100; + + // The bit mask starts at 0, but bit 0 is not used + const supportedKEXSchemes: KEXSchemes[] = parseBitMask( + raw.payload.subarray(1, 2), + 0, + ).filter((s) => s !== 0); + const supportedECDHProfiles: ECDHProfiles[] = parseBitMask( + raw.payload.subarray(2, 3), + ECDHProfiles.Curve25519, + ); + const requestedKeys: SecurityClass[] = parseBitMask( + raw.payload.subarray(3, 4), + SecurityClass.S2_Unauthenticated, + ); + + return new Security2CCKEXReport({ + nodeId: ctx.sourceNodeId, + requestCSA, + echo, + _reserved, + supportedKEXSchemes, + supportedECDHProfiles, + requestedKeys, + }); } public readonly _reserved: number; @@ -2280,43 +2319,50 @@ function testExpectedResponseForKEXSet( @expectedCCResponse(getExpectedResponseForKEXSet, testExpectedResponseForKEXSet) export class Security2CCKEXSet extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & Security2CCKEXSetOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this._reserved = this.payload[0] & 0b1111_1100; - this.permitCSA = !!(this.payload[0] & 0b10); - this.echo = !!(this.payload[0] & 0b1); - // The bit mask starts at 0, but bit 0 is not used - const selectedKEXSchemes = parseBitMask( - this.payload.subarray(1, 2), - 0, - ).filter((s) => s !== 0); - validatePayload(selectedKEXSchemes.length === 1); - this.selectedKEXScheme = selectedKEXSchemes[0]; - - const selectedECDHProfiles = parseBitMask( - this.payload.subarray(2, 3), - ECDHProfiles.Curve25519, - ); - validatePayload(selectedECDHProfiles.length === 1); - this.selectedECDHProfile = selectedECDHProfiles[0]; + this.permitCSA = options.permitCSA; + this.echo = options.echo; + this._reserved = options._reserved ?? 0; + this.selectedKEXScheme = options.selectedKEXScheme; + this.selectedECDHProfile = options.selectedECDHProfile; + this.grantedKeys = options.grantedKeys; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): Security2CCKEXSet { + validatePayload(raw.payload.length >= 4); + const _reserved = raw.payload[0] & 0b1111_1100; + const permitCSA = !!(raw.payload[0] & 0b10); + const echo = !!(raw.payload[0] & 0b1); + + // The bit mask starts at 0, but bit 0 is not used + const selectedKEXSchemes = parseBitMask( + raw.payload.subarray(1, 2), + 0, + ).filter((s) => s !== 0); + validatePayload(selectedKEXSchemes.length === 1); + const selectedKEXScheme: KEXSchemes = selectedKEXSchemes[0]; + const selectedECDHProfiles = parseBitMask( + raw.payload.subarray(2, 3), + ECDHProfiles.Curve25519, + ); + validatePayload(selectedECDHProfiles.length === 1); + const selectedECDHProfile: ECDHProfiles = selectedECDHProfiles[0]; + const grantedKeys: SecurityClass[] = parseBitMask( + raw.payload.subarray(3, 4), + SecurityClass.S2_Unauthenticated, + ); - this.grantedKeys = parseBitMask( - this.payload.subarray(3, 4), - SecurityClass.S2_Unauthenticated, - ); - } else { - this.permitCSA = options.permitCSA; - this.echo = options.echo; - this._reserved = options._reserved ?? 0; - this.selectedKEXScheme = options.selectedKEXScheme; - this.selectedECDHProfile = options.selectedECDHProfile; - this.grantedKeys = options.grantedKeys; - } + return new Security2CCKEXSet({ + nodeId: ctx.sourceNodeId, + _reserved, + permitCSA, + echo, + selectedKEXScheme, + selectedECDHProfile, + grantedKeys, + }); } public readonly _reserved: number; @@ -2372,22 +2418,27 @@ export class Security2CCKEXSet extends Security2CC { } // @publicAPI -export interface Security2CCKEXFailOptions extends CCCommandOptions { +export interface Security2CCKEXFailOptions { failType: KEXFailType; } @CCCommand(Security2Command.KEXFail) export class Security2CCKEXFail extends Security2CC { public constructor( - options: CommandClassDeserializationOptions | Security2CCKEXFailOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.failType = this.payload[0]; - } else { - this.failType = options.failType; - } + this.failType = options.failType; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): Security2CCKEXFail { + validatePayload(raw.payload.length >= 1); + const failType: KEXFailType = raw.payload[0]; + + return new Security2CCKEXFail({ + nodeId: ctx.sourceNodeId, + failType, + }); } public failType: KEXFailType; @@ -2406,7 +2457,7 @@ export class Security2CCKEXFail extends Security2CC { } // @publicAPI -export interface Security2CCPublicKeyReportOptions extends CCCommandOptions { +export interface Security2CCPublicKeyReportOptions { includingNode: boolean; publicKey: Buffer; } @@ -2414,19 +2465,26 @@ export interface Security2CCPublicKeyReportOptions extends CCCommandOptions { @CCCommand(Security2Command.PublicKeyReport) export class Security2CCPublicKeyReport extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | Security2CCPublicKeyReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 17); - this.includingNode = !!(this.payload[0] & 0b1); - this.publicKey = this.payload.subarray(1); - } else { - this.includingNode = options.includingNode; - this.publicKey = options.publicKey; - } + this.includingNode = options.includingNode; + this.publicKey = options.publicKey; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCPublicKeyReport { + validatePayload(raw.payload.length >= 17); + const includingNode = !!(raw.payload[0] & 0b1); + const publicKey: Buffer = raw.payload.subarray(1); + + return new Security2CCPublicKeyReport({ + nodeId: ctx.sourceNodeId, + includingNode, + publicKey, + }); } public includingNode: boolean; @@ -2452,7 +2510,7 @@ export class Security2CCPublicKeyReport extends Security2CC { } // @publicAPI -export interface Security2CCNetworkKeyReportOptions extends CCCommandOptions { +export interface Security2CCNetworkKeyReportOptions { grantedKey: SecurityClass; networkKey: Buffer; } @@ -2460,19 +2518,29 @@ export interface Security2CCNetworkKeyReportOptions extends CCCommandOptions { @CCCommand(Security2Command.NetworkKeyReport) export class Security2CCNetworkKeyReport extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | Security2CCNetworkKeyReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 17); - this.grantedKey = bitMaskToSecurityClass(this.payload, 0); - this.networkKey = this.payload.subarray(1, 17); - } else { - this.grantedKey = options.grantedKey; - this.networkKey = options.networkKey; - } + this.grantedKey = options.grantedKey; + this.networkKey = options.networkKey; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCNetworkKeyReport { + validatePayload(raw.payload.length >= 17); + const grantedKey: SecurityClass = bitMaskToSecurityClass( + raw.payload, + 0, + ); + const networkKey = raw.payload.subarray(1, 17); + + return new Security2CCNetworkKeyReport({ + nodeId: ctx.sourceNodeId, + grantedKey, + networkKey, + }); } public grantedKey: SecurityClass; @@ -2502,7 +2570,7 @@ export class Security2CCNetworkKeyReport extends Security2CC { } // @publicAPI -export interface Security2CCNetworkKeyGetOptions extends CCCommandOptions { +export interface Security2CCNetworkKeyGetOptions { requestedKey: SecurityClass; } @@ -2511,17 +2579,26 @@ export interface Security2CCNetworkKeyGetOptions extends CCCommandOptions { // FIXME: maybe use the dynamic @expectedCCResponse instead? export class Security2CCNetworkKeyGet extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | Security2CCNetworkKeyGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.requestedKey = bitMaskToSecurityClass(this.payload, 0); - } else { - this.requestedKey = options.requestedKey; - } + this.requestedKey = options.requestedKey; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCNetworkKeyGet { + validatePayload(raw.payload.length >= 1); + const requestedKey: SecurityClass = bitMaskToSecurityClass( + raw.payload, + 0, + ); + + return new Security2CCNetworkKeyGet({ + nodeId: ctx.sourceNodeId, + requestedKey, + }); } public requestedKey: SecurityClass; @@ -2548,7 +2625,7 @@ export class Security2CCNetworkKeyGet extends Security2CC { export class Security2CCNetworkKeyVerify extends Security2CC {} // @publicAPI -export interface Security2CCTransferEndOptions extends CCCommandOptions { +export interface Security2CCTransferEndOptions { keyVerified: boolean; keyRequestComplete: boolean; } @@ -2556,19 +2633,26 @@ export interface Security2CCTransferEndOptions extends CCCommandOptions { @CCCommand(Security2Command.TransferEnd) export class Security2CCTransferEnd extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | Security2CCTransferEndOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.keyVerified = !!(this.payload[0] & 0b10); - this.keyRequestComplete = !!(this.payload[0] & 0b1); - } else { - this.keyVerified = options.keyVerified; - this.keyRequestComplete = options.keyRequestComplete; - } + this.keyVerified = options.keyVerified; + this.keyRequestComplete = options.keyRequestComplete; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCTransferEnd { + validatePayload(raw.payload.length >= 1); + const keyVerified = !!(raw.payload[0] & 0b10); + const keyRequestComplete = !!(raw.payload[0] & 0b1); + + return new Security2CCTransferEnd({ + nodeId: ctx.sourceNodeId, + keyVerified, + keyRequestComplete, + }); } public keyVerified: boolean; @@ -2593,30 +2677,34 @@ export class Security2CCTransferEnd extends Security2CC { } // @publicAPI -export interface Security2CCCommandsSupportedReportOptions - extends CCCommandOptions -{ +export interface Security2CCCommandsSupportedReportOptions { supportedCCs: CommandClasses[]; } @CCCommand(Security2Command.CommandsSupportedReport) export class Security2CCCommandsSupportedReport extends Security2CC { public constructor( - options: - | CommandClassDeserializationOptions - | Security2CCCommandsSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - const CCs = parseCCList(this.payload); - // SDS13783: A sending node MAY terminate the list of supported command classes with the - // COMMAND_CLASS_MARK command class identifier. - // A receiving node MUST stop parsing the list of supported command classes if it detects the - // COMMAND_CLASS_MARK command class identifier in the Security 2 Commands Supported Report - this.supportedCCs = CCs.supportedCCs; - } else { - this.supportedCCs = options.supportedCCs; - } + this.supportedCCs = options.supportedCCs; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): Security2CCCommandsSupportedReport { + const CCs = parseCCList(raw.payload); + // SDS13783: A sending node MAY terminate the list of supported command classes with the + // COMMAND_CLASS_MARK command class identifier. + // A receiving node MUST stop parsing the list of supported command classes if it detects the + // COMMAND_CLASS_MARK command class identifier in the Security 2 Commands Supported Report + const supportedCCs = CCs.supportedCCs; + + return new Security2CCCommandsSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedCCs, + }); } public readonly supportedCCs: CommandClasses[]; diff --git a/packages/cc/src/cc/SecurityCC.ts b/packages/cc/src/cc/SecurityCC.ts index d569657858db..22063a81c72c 100644 --- a/packages/cc/src/cc/SecurityCC.ts +++ b/packages/cc/src/cc/SecurityCC.ts @@ -7,6 +7,7 @@ import { SecurityClass, type SecurityManager, TransmitOptions, + type WithAddress, ZWaveError, ZWaveErrorCodes, computeMAC, @@ -31,11 +32,9 @@ import { wait } from "alcalzone-shared/async"; import { randomBytes } from "node:crypto"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -80,6 +79,42 @@ function throwNoNonce(reason?: string): never { const HALF_NONCE_SIZE = 8; +function assertSecurityRX( + ctx: CCParsingContext, +): asserts ctx is CCParsingContext & { securityManager: SecurityManager } { + if (!ctx.ownNodeId) { + throw new ZWaveError( + `Secure commands (S0) can only be decoded when the controller's node id is known!`, + ZWaveErrorCodes.Driver_NotReady, + ); + } + + if (!ctx.securityManager) { + throw new ZWaveError( + `Secure commands (S0) can only be decoded when the security manager is set up!`, + ZWaveErrorCodes.Driver_NoSecurity, + ); + } +} + +function assertSecurityTX( + ctx: CCEncodingContext, +): asserts ctx is CCEncodingContext & { securityManager: SecurityManager } { + if (!ctx.ownNodeId) { + throw new ZWaveError( + `Secure commands (S0) can only be sent when the controller's node id is known!`, + ZWaveErrorCodes.Driver_NotReady, + ); + } + + if (!ctx.securityManager) { + throw new ZWaveError( + `Secure commands (S0) can only be sent when the security manager is set up!`, + ZWaveErrorCodes.Driver_NoSecurity, + ); + } +} + // TODO: Ignore commands if received via multicast // Encapsulation CCs are used internally and too frequently that we @@ -115,8 +150,6 @@ export class SecurityCCAPI extends PhysicalCCAPI { : SecurityCCCommandEncapsulation )({ nodeId: this.endpoint.nodeId, - ownNodeId: this.host.ownNodeId, - securityManager: this.host.securityManager!, encapsulated, }); await this.host.sendCommand(cc, this.commandOptions); @@ -130,7 +163,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCNonceGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -180,7 +213,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCNonceReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, nonce, }); @@ -215,7 +248,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCSchemeGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); await this.host.sendCommand(cc, this.commandOptions); // There is only one scheme, so we hardcode it @@ -230,14 +263,12 @@ export class SecurityCCAPI extends PhysicalCCAPI { let cc: CommandClass = new SecurityCCSchemeReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); if (encapsulated) { cc = new SecurityCCCommandEncapsulation({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, - ownNodeId: this.host.ownNodeId, - securityManager: this.host.securityManager!, + endpointIndex: this.endpoint.index, encapsulated: cc, }); } @@ -252,7 +283,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCSchemeInherit({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); await this.host.sendCommand(cc, this.commandOptions); // There is only one scheme, so we don't return anything here @@ -266,14 +297,12 @@ export class SecurityCCAPI extends PhysicalCCAPI { const keySet = new SecurityCCNetworkKeySet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, networkKey, }); const cc = new SecurityCCCommandEncapsulation({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, - ownNodeId: this.host.ownNodeId, - securityManager: this.host.securityManager!, + endpointIndex: this.endpoint.index, encapsulated: keySet, alternativeNetworkKey: Buffer.alloc(16, 0), }); @@ -288,7 +317,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCNetworkKeyVerify({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); await this.host.sendCommand(cc, this.commandOptions); } @@ -302,7 +331,7 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCCommandsSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< SecurityCCCommandsSupportedReport @@ -326,9 +355,10 @@ export class SecurityCCAPI extends PhysicalCCAPI { const cc = new SecurityCCCommandsSupportedReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, supportedCCs, controlledCCs, + reportsToFollow: 0, }); await this.host.sendCommand(cc, this.commandOptions); } @@ -341,42 +371,6 @@ export class SecurityCC extends CommandClass { // Force singlecast for the Security CC declare nodeId: number; - protected assertSecurity( - options: - | (CCCommandOptions & { - ownNodeId: number; - securityManager: SecurityManager; - }) - | CommandClassDeserializationOptions, - ): SecurityManager { - const verb = gotDeserializationOptions(options) ? "decoded" : "sent"; - const ownNodeId = gotDeserializationOptions(options) - ? options.context.ownNodeId - : options.ownNodeId; - if (!ownNodeId) { - throw new ZWaveError( - `Secure commands (S0) can only be ${verb} when the controller's node id is known!`, - ZWaveErrorCodes.Driver_NotReady, - ); - } - - let ret: SecurityManager | undefined; - if (gotDeserializationOptions(options)) { - ret = options.context.securityManager; - } else { - ret = options.securityManager; - } - - if (!ret) { - throw new ZWaveError( - `Secure commands (S0) can only be ${verb} when the security manager is set up!`, - ZWaveErrorCodes.Driver_NoSecurity, - ); - } - - return ret; - } - public async interview( ctx: InterviewContext, ): Promise { @@ -542,8 +536,6 @@ export class SecurityCC extends CommandClass { // TODO: When to return a SecurityCCCommandEncapsulationNonceGet? const ret = new SecurityCCCommandEncapsulation({ nodeId: cc.nodeId, - ownNodeId, - securityManager, encapsulated: cc, }); @@ -556,32 +548,37 @@ export class SecurityCC extends CommandClass { } } -interface SecurityCCNonceReportOptions extends CCCommandOptions { +interface SecurityCCNonceReportOptions { nonce: Buffer; } @CCCommand(SecurityCommand.NonceReport) export class SecurityCCNonceReport extends SecurityCC { constructor( - options: - | CommandClassDeserializationOptions - | SecurityCCNonceReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload.withReason("Invalid nonce length")( - this.payload.length === HALF_NONCE_SIZE, + if (options.nonce.length !== HALF_NONCE_SIZE) { + throw new ZWaveError( + `Nonce must have length ${HALF_NONCE_SIZE}!`, + ZWaveErrorCodes.Argument_Invalid, ); - this.nonce = this.payload; - } else { - if (options.nonce.length !== HALF_NONCE_SIZE) { - throw new ZWaveError( - `Nonce must have length ${HALF_NONCE_SIZE}!`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.nonce = options.nonce; } + this.nonce = options.nonce; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SecurityCCNonceReport { + validatePayload.withReason("Invalid nonce length")( + raw.payload.length === HALF_NONCE_SIZE, + ); + + return new SecurityCCNonceReport({ + nodeId: ctx.sourceNodeId, + nonce: raw.payload, + }); } public nonce: Buffer; @@ -604,14 +601,18 @@ export class SecurityCCNonceReport extends SecurityCC { export class SecurityCCNonceGet extends SecurityCC {} // @publicAPI -export interface SecurityCCCommandEncapsulationOptions - extends CCCommandOptions -{ - ownNodeId: number; - securityManager: SecurityManager; - encapsulated: CommandClass; - alternativeNetworkKey?: Buffer; -} +export type SecurityCCCommandEncapsulationOptions = + & { + alternativeNetworkKey?: Buffer; + } + & ({ + encapsulated: CommandClass; + } | { + decryptedCCBytes: Buffer; + sequenced: boolean; + secondFrame: boolean; + sequenceCounter: number; + }); function getCCResponseForCommandEncapsulation( sent: SecurityCCCommandEncapsulation, @@ -628,88 +629,97 @@ function getCCResponseForCommandEncapsulation( ) export class SecurityCCCommandEncapsulation extends SecurityCC { public constructor( - options: - | CommandClassDeserializationOptions - | SecurityCCCommandEncapsulationOptions, + options: WithAddress, ) { super(options); - this.securityManager = this.assertSecurity(options); + if ("encapsulated" in options) { + this.encapsulated = options.encapsulated; + this.encapsulated.encapsulatingCC = this as any; + } else { + this.decryptedCCBytes = options.decryptedCCBytes; + this.sequenced = options.sequenced; + this.secondFrame = options.secondFrame; + this.sequenceCounter = options.sequenceCounter; + } + this.alternativeNetworkKey = options.alternativeNetworkKey; + } - if (gotDeserializationOptions(options)) { - // HALF_NONCE_SIZE bytes iv, 1 byte frame control, at least 1 CC byte, 1 byte nonce id, 8 bytes auth code - validatePayload( - this.payload.length >= HALF_NONCE_SIZE + 1 + 1 + 1 + 8, - ); - const iv = this.payload.subarray(0, HALF_NONCE_SIZE); - const encryptedPayload = this.payload.subarray(HALF_NONCE_SIZE, -9); - const nonceId = this.payload.at(-9)!; - const authCode = this.payload.subarray(-8); - - // Retrieve the used nonce from the nonce store - const nonce = this.securityManager.getNonce(nonceId); - // Only accept the message if the nonce hasn't expired - validatePayload.withReason( + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SecurityCCCommandEncapsulation { + assertSecurityRX(ctx); + + // HALF_NONCE_SIZE bytes iv, 1 byte frame control, at least 1 CC byte, 1 byte nonce id, 8 bytes auth code + validatePayload( + raw.payload.length >= HALF_NONCE_SIZE + 1 + 1 + 1 + 8, + ); + const iv = raw.payload.subarray(0, HALF_NONCE_SIZE); + const encryptedPayload = raw.payload.subarray(HALF_NONCE_SIZE, -9); + const nonceId = raw.payload.at(-9)!; + const authCode = raw.payload.subarray(-8); + + // Retrieve the used nonce from the nonce store + const nonce = ctx.securityManager.getNonce(nonceId); + // Only accept the message if the nonce hasn't expired + if (!nonce) { + validatePayload.fail( `Nonce ${ num2hex( nonceId, ) } expired, cannot decode security encapsulated command.`, - )(!!nonce); - // and mark the nonce as used - this.securityManager.deleteNonce(nonceId); - - this.authKey = this.securityManager.authKey; - this.encryptionKey = this.securityManager.encryptionKey; - - // Validate the encrypted data - const authData = getAuthenticationData( - iv, - nonce!, - this.ccCommand, - this.nodeId, - options.context.ownNodeId, - encryptedPayload, ); - const expectedAuthCode = computeMAC(authData, this.authKey); - // Only accept messages with a correct auth code - validatePayload.withReason( - "Invalid auth code, won't accept security encapsulated command.", - )(authCode.equals(expectedAuthCode)); - - // Decrypt the encapsulated CC - const frameControlAndDecryptedCC = decryptAES128OFB( - encryptedPayload, - this.encryptionKey, - Buffer.concat([iv, nonce!]), - ); - const frameControl = frameControlAndDecryptedCC[0]; - this.sequenceCounter = frameControl & 0b1111; - this.sequenced = !!(frameControl & 0b1_0000); - this.secondFrame = !!(frameControl & 0b10_0000); + } + // and mark the nonce as used + ctx.securityManager.deleteNonce(nonceId); - this.decryptedCCBytes = frameControlAndDecryptedCC.subarray(1); + // Validate the encrypted data + const authData = getAuthenticationData( + iv, + nonce, + SecurityCommand.CommandEncapsulation, + ctx.sourceNodeId, + ctx.ownNodeId, + encryptedPayload, + ); + const expectedAuthCode = computeMAC( + authData, + ctx.securityManager.authKey, + ); + // Only accept messages with a correct auth code + validatePayload.withReason( + "Invalid auth code, won't accept security encapsulated command.", + )(authCode.equals(expectedAuthCode)); + + // Decrypt the encapsulated CC + const frameControlAndDecryptedCC = decryptAES128OFB( + encryptedPayload, + ctx.securityManager.encryptionKey, + Buffer.concat([iv, nonce]), + ); + const frameControl = frameControlAndDecryptedCC[0]; + const sequenceCounter = frameControl & 0b1111; + const sequenced = !!(frameControl & 0b1_0000); + const secondFrame = !!(frameControl & 0b10_0000); + const decryptedCCBytes: Buffer | undefined = frameControlAndDecryptedCC + .subarray(1); - // Remember for debugging purposes - this.authData = authData; - this.authCode = authCode; - this.iv = iv; - } else { - this.encapsulated = options.encapsulated; - options.encapsulated.encapsulatingCC = this as any; - if (options.alternativeNetworkKey) { - this.authKey = generateAuthKey(options.alternativeNetworkKey); - this.encryptionKey = generateEncryptionKey( - options.alternativeNetworkKey, - ); - } else { - this.authKey = this.securityManager.authKey; - this.encryptionKey = this.securityManager.encryptionKey; - } - } - } + const ret = new SecurityCCCommandEncapsulation({ + nodeId: ctx.sourceNodeId, + sequenceCounter, + sequenced, + secondFrame, + decryptedCCBytes, + }); + + ret.authData = authData; + ret.authCode = authCode; + ret.iv = iv; - private securityManager: SecurityManager; + return ret; + } private sequenced: boolean | undefined; private secondFrame: boolean | undefined; @@ -718,12 +728,10 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { private decryptedCCBytes: Buffer | undefined; public encapsulated!: CommandClass; - private authKey: Buffer; - private encryptionKey: Buffer; + private alternativeNetworkKey?: Buffer; public get nonceId(): number | undefined { - if (!this.nonce) return undefined; - return this.securityManager.getNonceId(this.nonce); + return this.nonce?.[0]; } public nonce: Buffer | undefined; @@ -763,12 +771,8 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { // make sure this contains a complete CC command that's worth splitting validatePayload(this.decryptedCCBytes.length >= 2); // and deserialize the CC - this.encapsulated = CommandClass.from({ - data: this.decryptedCCBytes, - fromEncapsulation: true, - encapCC: this, - context: ctx, - }); + this.encapsulated = CommandClass.parse(this.decryptedCCBytes, ctx); + this.encapsulated.encapsulatingCC = this as any; } public serialize(ctx: CCEncodingContext): Buffer { @@ -776,6 +780,19 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { if (this.nonce.length !== HALF_NONCE_SIZE) { throwNoNonce("Invalid nonce size"); } + assertSecurityTX(ctx); + + let authKey: Buffer; + let encryptionKey: Buffer; + if (this.alternativeNetworkKey) { + authKey = generateAuthKey(this.alternativeNetworkKey); + encryptionKey = generateEncryptionKey( + this.alternativeNetworkKey, + ); + } else { + authKey = ctx.securityManager.authKey; + encryptionKey = ctx.securityManager.encryptionKey; + } const serializedCC = this.encapsulated.serialize(ctx); const plaintext = Buffer.concat([ @@ -785,7 +802,7 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { // Encrypt the payload const senderNonce = randomBytes(HALF_NONCE_SIZE); const iv = Buffer.concat([senderNonce, this.nonce]); - const ciphertext = encryptAES128OFB(plaintext, this.encryptionKey, iv); + const ciphertext = encryptAES128OFB(plaintext, encryptionKey, iv); // And generate the auth code const authData = getAuthenticationData( senderNonce, @@ -795,7 +812,7 @@ export class SecurityCCCommandEncapsulation extends SecurityCC { this.nodeId, ciphertext, ); - const authCode = computeMAC(authData, this.authKey); + const authCode = computeMAC(authData, authKey); // Remember for debugging purposes this.iv = iv; @@ -868,14 +885,15 @@ export class SecurityCCCommandEncapsulationNonceGet @CCCommand(SecurityCommand.SchemeReport) export class SecurityCCSchemeReport extends SecurityCC { - public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, - ) { - super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - // The including controller MUST NOT perform any validation of the Supported Security Schemes byte - } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SecurityCCSchemeReport { + validatePayload(raw.payload.length >= 1); + // The including controller MUST NOT perform any validation of the Supported Security Schemes byte + return new SecurityCCSchemeReport({ + nodeId: ctx.sourceNodeId, + }); } public serialize(ctx: CCEncodingContext): Buffer { @@ -896,13 +914,6 @@ export class SecurityCCSchemeReport extends SecurityCC { @CCCommand(SecurityCommand.SchemeGet) @expectedCCResponse(SecurityCCSchemeReport) export class SecurityCCSchemeGet extends SecurityCC { - public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, - ) { - super(options); - // Don't care, we won't get sent this and we have no options - } - public serialize(ctx: CCEncodingContext): Buffer { // Since it is unlikely that any more schemes will be added to S0, we hardcode the default scheme here (bit 0 = 0) this.payload = Buffer.from([0]); @@ -921,13 +932,6 @@ export class SecurityCCSchemeGet extends SecurityCC { @CCCommand(SecurityCommand.SchemeInherit) @expectedCCResponse(SecurityCCSchemeReport) export class SecurityCCSchemeInherit extends SecurityCC { - public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, - ) { - super(options); - // Don't care, we won't get sent this and we have no options - } - public serialize(ctx: CCEncodingContext): Buffer { // Since it is unlikely that any more schemes will be added to S0, we hardcode the default scheme here (bit 0 = 0) this.payload = Buffer.from([0]); @@ -947,7 +951,7 @@ export class SecurityCCSchemeInherit extends SecurityCC { export class SecurityCCNetworkKeyVerify extends SecurityCC {} // @publicAPI -export interface SecurityCCNetworkKeySetOptions extends CCCommandOptions { +export interface SecurityCCNetworkKeySetOptions { networkKey: Buffer; } @@ -955,23 +959,29 @@ export interface SecurityCCNetworkKeySetOptions extends CCCommandOptions { @expectedCCResponse(SecurityCCNetworkKeyVerify) export class SecurityCCNetworkKeySet extends SecurityCC { public constructor( - options: - | CommandClassDeserializationOptions - | SecurityCCNetworkKeySetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 16); - this.networkKey = this.payload.subarray(0, 16); - } else { - if (options.networkKey.length !== 16) { - throw new ZWaveError( - `The network key must have length 16!`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.networkKey = options.networkKey; + if (options.networkKey.length !== 16) { + throw new ZWaveError( + `The network key must have length 16!`, + ZWaveErrorCodes.Argument_Invalid, + ); } + this.networkKey = options.networkKey; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SecurityCCNetworkKeySet { + validatePayload(raw.payload.length >= 16); + const networkKey: Buffer = raw.payload.subarray(0, 16); + + return new SecurityCCNetworkKeySet({ + nodeId: ctx.sourceNodeId, + networkKey, + }); } public networkKey: Buffer; @@ -989,9 +999,8 @@ export class SecurityCCNetworkKeySet extends SecurityCC { } // @publicAPI -export interface SecurityCCCommandsSupportedReportOptions - extends CCCommandOptions -{ +export interface SecurityCCCommandsSupportedReportOptions { + reportsToFollow?: number; supportedCCs: CommandClasses[]; controlledCCs: CommandClasses[]; } @@ -999,36 +1008,38 @@ export interface SecurityCCCommandsSupportedReportOptions @CCCommand(SecurityCommand.CommandsSupportedReport) export class SecurityCCCommandsSupportedReport extends SecurityCC { public constructor( - options: - | CommandClassDeserializationOptions - | SecurityCCCommandsSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.reportsToFollow = this.payload[0]; - const list = parseCCList(this.payload.subarray(1)); - this.supportedCCs = list.supportedCCs; - this.controlledCCs = list.controlledCCs; - } else { - this.supportedCCs = options.supportedCCs; - this.controlledCCs = options.controlledCCs; - // TODO: properly split the CCs into multiple reports - this.reportsToFollow = 0; - } + this.supportedCCs = options.supportedCCs; + this.controlledCCs = options.controlledCCs; + // TODO: properly split the CCs into multiple reports + this.reportsToFollow = options.reportsToFollow ?? 0; } - public readonly reportsToFollow: number; - - public serialize(ctx: CCEncodingContext): Buffer { - this.payload = Buffer.concat([ - Buffer.from([this.reportsToFollow]), - encodeCCList(this.supportedCCs, this.controlledCCs), - ]); - return super.serialize(ctx); + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SecurityCCCommandsSupportedReport { + validatePayload(raw.payload.length >= 1); + const reportsToFollow = raw.payload[0]; + const list = parseCCList(raw.payload.subarray(1)); + const supportedCCs: CommandClasses[] = list.supportedCCs; + const controlledCCs: CommandClasses[] = list.controlledCCs; + + return new SecurityCCCommandsSupportedReport({ + nodeId: ctx.sourceNodeId, + reportsToFollow, + supportedCCs, + controlledCCs, + }); } + public reportsToFollow: number; + public supportedCCs: CommandClasses[]; + public controlledCCs: CommandClasses[]; + public getPartialCCSessionId(): Record | undefined { // Nothing special we can distinguish sessions with return {}; @@ -1038,9 +1049,6 @@ export class SecurityCCCommandsSupportedReport extends SecurityCC { return this.reportsToFollow > 0; } - public supportedCCs: CommandClasses[]; - public controlledCCs: CommandClasses[]; - public mergePartialCCs( partials: SecurityCCCommandsSupportedReport[], ): void { @@ -1053,6 +1061,14 @@ export class SecurityCCCommandsSupportedReport extends SecurityCC { .reduce((prev, cur) => prev.concat(...cur), []); } + public serialize(ctx: CCEncodingContext): Buffer { + this.payload = Buffer.concat([ + Buffer.from([this.reportsToFollow]), + encodeCCList(this.supportedCCs, this.controlledCCs), + ]); + return super.serialize(ctx); + } + public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { return { ...super.toLogEntry(ctx), diff --git a/packages/cc/src/cc/SoundSwitchCC.ts b/packages/cc/src/cc/SoundSwitchCC.ts index 8dda10b755c8..f382abe19903 100644 --- a/packages/cc/src/cc/SoundSwitchCC.ts +++ b/packages/cc/src/cc/SoundSwitchCC.ts @@ -6,12 +6,17 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { clamp } from "alcalzone-shared/math"; @@ -25,12 +30,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, type CCResponsePredicate, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -118,7 +121,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCTonesNumberGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< SoundSwitchCCTonesNumberReport @@ -139,7 +142,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCToneInfoGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, toneId, }); const response = await this.host.sendCommand< @@ -163,7 +166,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCConfigurationSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, defaultToneId, defaultVolume, }); @@ -179,7 +182,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCConfigurationGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< SoundSwitchCCConfigurationReport @@ -211,7 +214,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCTonePlaySet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, toneId, volume, }); @@ -226,7 +229,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCTonePlaySet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, toneId: 0x00, volume: 0x00, }); @@ -242,7 +245,7 @@ export class SoundSwitchCCAPI extends CCAPI { const cc = new SoundSwitchCCTonePlayGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< SoundSwitchCCTonePlayReport @@ -467,26 +470,30 @@ duration: ${info.duration} seconds`; } // @publicAPI -export interface SoundSwitchCCTonesNumberReportOptions - extends CCCommandOptions -{ +export interface SoundSwitchCCTonesNumberReportOptions { toneCount: number; } @CCCommand(SoundSwitchCommand.TonesNumberReport) export class SoundSwitchCCTonesNumberReport extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | SoundSwitchCCTonesNumberReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.toneCount = this.payload[0]; - } else { - this.toneCount = options.toneCount; - } + this.toneCount = options.toneCount; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCTonesNumberReport { + validatePayload(raw.payload.length >= 1); + const toneCount = raw.payload[0]; + + return new SoundSwitchCCTonesNumberReport({ + nodeId: ctx.sourceNodeId, + toneCount, + }); } public toneCount: number; @@ -509,7 +516,7 @@ export class SoundSwitchCCTonesNumberReport extends SoundSwitchCC { export class SoundSwitchCCTonesNumberGet extends SoundSwitchCC {} // @publicAPI -export interface SoundSwitchCCToneInfoReportOptions extends CCCommandOptions { +export interface SoundSwitchCCToneInfoReportOptions { toneId: number; duration: number; name: string; @@ -518,25 +525,34 @@ export interface SoundSwitchCCToneInfoReportOptions extends CCCommandOptions { @CCCommand(SoundSwitchCommand.ToneInfoReport) export class SoundSwitchCCToneInfoReport extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | SoundSwitchCCToneInfoReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.toneId = this.payload[0]; - this.duration = this.payload.readUInt16BE(1); - const nameLength = this.payload[3]; - validatePayload(this.payload.length >= 4 + nameLength); - this.name = this.payload.subarray(4, 4 + nameLength).toString( - "utf8", - ); - } else { - this.toneId = options.toneId; - this.duration = options.duration; - this.name = options.name; - } + this.toneId = options.toneId; + this.duration = options.duration; + this.name = options.name; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCToneInfoReport { + validatePayload(raw.payload.length >= 4); + const toneId = raw.payload[0]; + const duration = raw.payload.readUInt16BE(1); + const nameLength = raw.payload[3]; + + validatePayload(raw.payload.length >= 4 + nameLength); + const name = raw.payload.subarray(4, 4 + nameLength).toString( + "utf8", + ); + + return new SoundSwitchCCToneInfoReport({ + nodeId: ctx.sourceNodeId, + toneId, + duration, + name, + }); } public readonly toneId: number; @@ -572,7 +588,7 @@ const testResponseForSoundSwitchToneInfoGet: CCResponsePredicate< }; // @publicAPI -export interface SoundSwitchCCToneInfoGetOptions extends CCCommandOptions { +export interface SoundSwitchCCToneInfoGetOptions { toneId: number; } @@ -583,17 +599,23 @@ export interface SoundSwitchCCToneInfoGetOptions extends CCCommandOptions { ) export class SoundSwitchCCToneInfoGet extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | SoundSwitchCCToneInfoGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.toneId = this.payload[0]; - } else { - this.toneId = options.toneId; - } + this.toneId = options.toneId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCToneInfoGet { + validatePayload(raw.payload.length >= 1); + const toneId = raw.payload[0]; + + return new SoundSwitchCCToneInfoGet({ + nodeId: ctx.sourceNodeId, + toneId, + }); } public toneId: number; @@ -612,7 +634,7 @@ export class SoundSwitchCCToneInfoGet extends SoundSwitchCC { } // @publicAPI -export interface SoundSwitchCCConfigurationSetOptions extends CCCommandOptions { +export interface SoundSwitchCCConfigurationSetOptions { defaultVolume: number; defaultToneId: number; } @@ -621,19 +643,26 @@ export interface SoundSwitchCCConfigurationSetOptions extends CCCommandOptions { @useSupervision() export class SoundSwitchCCConfigurationSet extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | SoundSwitchCCConfigurationSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.defaultVolume = this.payload[0]; - this.defaultToneId = this.payload[1]; - } else { - this.defaultVolume = options.defaultVolume; - this.defaultToneId = options.defaultToneId; - } + this.defaultVolume = options.defaultVolume; + this.defaultToneId = options.defaultToneId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCConfigurationSet { + validatePayload(raw.payload.length >= 2); + const defaultVolume = raw.payload[0]; + const defaultToneId = raw.payload[1]; + + return new SoundSwitchCCConfigurationSet({ + nodeId: ctx.sourceNodeId, + defaultVolume, + defaultToneId, + }); } public defaultVolume: number; @@ -664,19 +693,26 @@ export interface SoundSwitchCCConfigurationReportOptions { @CCCommand(SoundSwitchCommand.ConfigurationReport) export class SoundSwitchCCConfigurationReport extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & SoundSwitchCCConfigurationReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.defaultVolume = clamp(this.payload[0], 0, 100); - this.defaultToneId = this.payload[1]; - } else { - this.defaultVolume = options.defaultVolume; - this.defaultToneId = options.defaultToneId; - } + this.defaultVolume = options.defaultVolume; + this.defaultToneId = options.defaultToneId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCConfigurationReport { + validatePayload(raw.payload.length >= 2); + const defaultVolume = clamp(raw.payload[0], 0, 100); + const defaultToneId = raw.payload[1]; + + return new SoundSwitchCCConfigurationReport({ + nodeId: ctx.sourceNodeId, + defaultVolume, + defaultToneId, + }); } @ccValue(SoundSwitchCCValues.defaultVolume) @@ -716,21 +752,29 @@ export interface SoundSwitchCCTonePlaySetOptions { @useSupervision() export class SoundSwitchCCTonePlaySet extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & SoundSwitchCCTonePlaySetOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.toneId = this.payload[0]; - if (this.toneId !== 0 && this.payload.length >= 2) { - this.volume = this.payload[1]; - } - } else { - this.toneId = options.toneId; - this.volume = options.volume; + this.toneId = options.toneId; + this.volume = options.volume; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCTonePlaySet { + validatePayload(raw.payload.length >= 1); + const toneId = raw.payload[0]; + let volume: number | undefined; + if (toneId !== 0 && raw.payload.length >= 2) { + volume = raw.payload[1]; } + + return new SoundSwitchCCTonePlaySet({ + nodeId: ctx.sourceNodeId, + toneId, + volume, + }); } public toneId: ToneId | number; @@ -765,21 +809,30 @@ export interface SoundSwitchCCTonePlayReportOptions { @CCCommand(SoundSwitchCommand.TonePlayReport) export class SoundSwitchCCTonePlayReport extends SoundSwitchCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & SoundSwitchCCTonePlayReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.toneId = this.payload[0]; - if (this.toneId !== 0 && this.payload.length >= 2) { - this.volume = this.payload[1]; - } - } else { - this.toneId = options.toneId; - this.volume = options.volume; + this.toneId = options.toneId; + this.volume = options.volume; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): SoundSwitchCCTonePlayReport { + validatePayload(raw.payload.length >= 1); + const toneId = raw.payload[0]; + + let volume: number | undefined; + if (toneId !== 0 && raw.payload.length >= 2) { + volume = raw.payload[1]; } + + return new SoundSwitchCCTonePlayReport({ + nodeId: ctx.sourceNodeId, + toneId, + volume, + }); } @ccValue(SoundSwitchCCValues.toneId) diff --git a/packages/cc/src/cc/SupervisionCC.ts b/packages/cc/src/cc/SupervisionCC.ts index 4faea6a5614f..b9c1343c95a3 100644 --- a/packages/cc/src/cc/SupervisionCC.ts +++ b/packages/cc/src/cc/SupervisionCC.ts @@ -14,6 +14,7 @@ import { SupervisionStatus, type SupportsCC, TransmitOptions, + type WithAddress, ZWaveError, ZWaveErrorCodes, isTransmissionError, @@ -21,17 +22,13 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetNode, GetValueDB, } from "@zwave-js/host/safe"; import { getEnumMemberName } from "@zwave-js/shared/safe"; import { PhysicalCCAPI } from "../lib/API"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { API, CCCommand, @@ -93,7 +90,7 @@ export class SupervisionCCAPI extends PhysicalCCAPI { } = options; const cc = new SupervisionCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...cmdOptions, }); @@ -157,7 +154,7 @@ export class SupervisionCC extends CommandClass { const ret = new SupervisionCCGet({ nodeId: cc.nodeId, // Supervision CC is wrapped inside MultiChannel CCs, so the endpoint must be copied - endpoint: cc.endpointIndex, + endpointIndex: cc.endpointIndex, encapsulated: cc, sessionId, requestStatusUpdates, @@ -291,29 +288,47 @@ export type SupervisionCCReportOptions = @CCCommand(SupervisionCommand.Report) export class SupervisionCCReport extends SupervisionCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & SupervisionCCReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.moreUpdatesFollow = !!(this.payload[0] & 0b1_0_000000); - this.requestWakeUpOnDemand = !!(this.payload[0] & 0b0_1_000000); - this.sessionId = this.payload[0] & 0b111111; - this.status = this.payload[1]; - this.duration = Duration.parseReport(this.payload[2]); + this.moreUpdatesFollow = options.moreUpdatesFollow; + this.requestWakeUpOnDemand = !!options.requestWakeUpOnDemand; + this.sessionId = options.sessionId; + this.status = options.status; + if (options.status === SupervisionStatus.Working) { + this.duration = options.duration; } else { - this.moreUpdatesFollow = options.moreUpdatesFollow; - this.requestWakeUpOnDemand = !!options.requestWakeUpOnDemand; - this.sessionId = options.sessionId; - this.status = options.status; - if (options.status === SupervisionStatus.Working) { - this.duration = options.duration; - } else { - this.duration = new Duration(0, "seconds"); - } + this.duration = new Duration(0, "seconds"); + } + } + + public static from(raw: CCRaw, ctx: CCParsingContext): SupervisionCCReport { + validatePayload(raw.payload.length >= 3); + const moreUpdatesFollow = !!(raw.payload[0] & 0b1_0_000000); + const requestWakeUpOnDemand = !!(raw.payload[0] & 0b0_1_000000); + const sessionId = raw.payload[0] & 0b111111; + const status: SupervisionStatus = raw.payload[1]; + + if (status === SupervisionStatus.Working) { + const duration = Duration.parseReport(raw.payload[2]) + ?? new Duration(0, "seconds"); + return new SupervisionCCReport({ + nodeId: ctx.sourceNodeId, + moreUpdatesFollow, + requestWakeUpOnDemand, + sessionId, + status, + duration, + }); + } else { + return new SupervisionCCReport({ + nodeId: ctx.sourceNodeId, + moreUpdatesFollow, + requestWakeUpOnDemand, + sessionId, + status, + }); } } @@ -372,7 +387,7 @@ export class SupervisionCCReport extends SupervisionCC { } // @publicAPI -export interface SupervisionCCGetOptions extends CCCommandOptions { +export interface SupervisionCCGetOptions { requestStatusUpdates: boolean; encapsulated: CommandClass; sessionId: number; @@ -389,27 +404,29 @@ function testResponseForSupervisionCCGet( @expectedCCResponse(SupervisionCCReport, testResponseForSupervisionCCGet) export class SupervisionCCGet extends SupervisionCC { public constructor( - options: CommandClassDeserializationOptions | SupervisionCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.requestStatusUpdates = !!(this.payload[0] & 0b1_0_000000); - this.sessionId = this.payload[0] & 0b111111; - - this.encapsulated = CommandClass.from({ - data: this.payload.subarray(2), - fromEncapsulation: true, - encapCC: this, - origin: options.origin, - context: options.context, - }); - } else { - this.sessionId = options.sessionId; - this.requestStatusUpdates = options.requestStatusUpdates; - this.encapsulated = options.encapsulated; - options.encapsulated.encapsulatingCC = this as any; - } + this.sessionId = options.sessionId; + this.requestStatusUpdates = options.requestStatusUpdates; + this.encapsulated = options.encapsulated; + // Supervision is inside MultiChannel CCs, so the endpoint must be copied + this.encapsulated.endpointIndex = this.endpointIndex; + this.encapsulated.encapsulatingCC = this as any; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): SupervisionCCGet { + validatePayload(raw.payload.length >= 3); + const requestStatusUpdates = !!(raw.payload[0] & 0b1_0_000000); + const sessionId = raw.payload[0] & 0b111111; + + const encapsulated = CommandClass.parse(raw.payload.subarray(2), ctx); + return new SupervisionCCGet({ + nodeId: ctx.sourceNodeId, + requestStatusUpdates, + sessionId, + encapsulated, + }); } public requestStatusUpdates: boolean; diff --git a/packages/cc/src/cc/ThermostatFanModeCC.ts b/packages/cc/src/cc/ThermostatFanModeCC.ts index 0bace1013cbe..c2e9e307760b 100644 --- a/packages/cc/src/cc/ThermostatFanModeCC.ts +++ b/packages/cc/src/cc/ThermostatFanModeCC.ts @@ -6,6 +6,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, enumValuesToMetadataStates, @@ -13,7 +14,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -26,13 +31,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -176,7 +179,7 @@ export class ThermostatFanModeCCAPI extends CCAPI { const cc = new ThermostatFanModeCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatFanModeCCReport @@ -201,7 +204,7 @@ export class ThermostatFanModeCCAPI extends CCAPI { const cc = new ThermostatFanModeCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, mode, off, }); @@ -218,7 +221,7 @@ export class ThermostatFanModeCCAPI extends CCAPI { const cc = new ThermostatFanModeCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatFanModeCCSupportedReport @@ -332,30 +335,35 @@ export class ThermostatFanModeCC extends CommandClass { } // @publicAPI -export type ThermostatFanModeCCSetOptions = CCCommandOptions & { +export interface ThermostatFanModeCCSetOptions { mode: ThermostatFanMode; off?: boolean; -}; +} @CCCommand(ThermostatFanModeCommand.Set) @useSupervision() export class ThermostatFanModeCCSet extends ThermostatFanModeCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatFanModeCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.mode = options.mode; - this.off = options.off; - } + this.mode = options.mode; + this.off = options.off; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): ThermostatFanModeCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ThermostatFanModeCCSet({ + // nodeId: ctx.sourceNodeId, + // }); } public mode: ThermostatFanMode; @@ -380,18 +388,38 @@ export class ThermostatFanModeCCSet extends ThermostatFanModeCC { } } +// @publicAPI +export interface ThermostatFanModeCCReportOptions { + mode: ThermostatFanMode; + off?: boolean; +} + @CCCommand(ThermostatFanModeCommand.Report) export class ThermostatFanModeCCReport extends ThermostatFanModeCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.mode = this.payload[0] & 0b1111; + // TODO: Check implementation: + this.mode = options.mode; + this.off = options.off; + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatFanModeCCReport { + validatePayload(raw.payload.length >= 1); + const mode: ThermostatFanMode = raw.payload[0] & 0b1111; // V3+ - this.off = !!(this.payload[0] & 0b1000_0000); + const off = !!(raw.payload[0] & 0b1000_0000); + + return new ThermostatFanModeCCReport({ + nodeId: ctx.sourceNodeId, + mode, + off, + }); } @ccValue(ThermostatFanModeCCValues.fanMode) @@ -418,16 +446,35 @@ export class ThermostatFanModeCCReport extends ThermostatFanModeCC { @expectedCCResponse(ThermostatFanModeCCReport) export class ThermostatFanModeCCGet extends ThermostatFanModeCC {} +// @publicAPI +export interface ThermostatFanModeCCSupportedReportOptions { + supportedModes: ThermostatFanMode[]; +} + @CCCommand(ThermostatFanModeCommand.SupportedReport) export class ThermostatFanModeCCSupportedReport extends ThermostatFanModeCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - this.supportedModes = parseBitMask( - this.payload, + + // TODO: Check implementation: + this.supportedModes = options.supportedModes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatFanModeCCSupportedReport { + const supportedModes: ThermostatFanMode[] = parseBitMask( + raw.payload, ThermostatFanMode["Auto low"], ); + + return new ThermostatFanModeCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedModes, + }); } public persistValues(ctx: PersistValuesContext): boolean { diff --git a/packages/cc/src/cc/ThermostatFanStateCC.ts b/packages/cc/src/cc/ThermostatFanStateCC.ts index 51edac9af02f..52eccf068a2d 100644 --- a/packages/cc/src/cc/ThermostatFanStateCC.ts +++ b/packages/cc/src/cc/ThermostatFanStateCC.ts @@ -5,10 +5,11 @@ import { MessagePriority, type MessageRecord, ValueMetadata, + type WithAddress, enumValuesToMetadataStates, validatePayload, } from "@zwave-js/core/safe"; -import type { GetValueDB } from "@zwave-js/host/safe"; +import type { CCParsingContext, GetValueDB } from "@zwave-js/host/safe"; import { getEnumMemberName } from "@zwave-js/shared/safe"; import { CCAPI, @@ -17,8 +18,8 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, } from "../lib/CommandClass"; @@ -81,7 +82,7 @@ export class ThermostatFanStateCCAPI extends CCAPI { const cc = new ThermostatFanStateCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatFanStateCCReport @@ -149,15 +150,33 @@ export class ThermostatFanStateCC extends CommandClass { } } +// @publicAPI +export interface ThermostatFanStateCCReportOptions { + state: ThermostatFanState; +} + @CCCommand(ThermostatFanStateCommand.Report) export class ThermostatFanStateCCReport extends ThermostatFanStateCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length == 1); - this.state = this.payload[0] & 0b1111; + // TODO: Check implementation: + this.state = options.state; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatFanStateCCReport { + validatePayload(raw.payload.length == 1); + const state: ThermostatFanState = raw.payload[0] & 0b1111; + + return new ThermostatFanStateCCReport({ + nodeId: ctx.sourceNodeId, + state, + }); } @ccValue(ThermostatFanStateCCValues.fanState) diff --git a/packages/cc/src/cc/ThermostatModeCC.ts b/packages/cc/src/cc/ThermostatModeCC.ts index bea74df217b5..5a7a15185c45 100644 --- a/packages/cc/src/cc/ThermostatModeCC.ts +++ b/packages/cc/src/cc/ThermostatModeCC.ts @@ -6,6 +6,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeBitMask, @@ -14,7 +15,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { buffer2hex, getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -27,13 +32,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -132,7 +135,7 @@ export class ThermostatModeCCAPI extends CCAPI { const cc = new ThermostatModeCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatModeCCReport @@ -182,7 +185,7 @@ export class ThermostatModeCCAPI extends CCAPI { const cc = new ThermostatModeCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, mode, manufacturerData: manufacturerData as any, }); @@ -199,7 +202,7 @@ export class ThermostatModeCCAPI extends CCAPI { const cc = new ThermostatModeCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatModeCCSupportedReport @@ -306,48 +309,54 @@ export class ThermostatModeCC extends CommandClass { // @publicAPI export type ThermostatModeCCSetOptions = - & CCCommandOptions - & ( - | { - mode: Exclude< - ThermostatMode, - (typeof ThermostatMode)["Manufacturer specific"] - >; - } - | { - mode: (typeof ThermostatMode)["Manufacturer specific"]; - manufacturerData: Buffer; - } - ); + | { + mode: Exclude< + ThermostatMode, + (typeof ThermostatMode)["Manufacturer specific"] + >; + } + | { + mode: (typeof ThermostatMode)["Manufacturer specific"]; + manufacturerData: Buffer; + }; @CCCommand(ThermostatModeCommand.Set) @useSupervision() export class ThermostatModeCCSet extends ThermostatModeCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatModeCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const manufacturerDataLength = (this.payload[0] >>> 5) & 0b111; - this.mode = this.payload[0] & 0b11111; - if (manufacturerDataLength > 0) { - validatePayload( - this.payload.length >= 1 + manufacturerDataLength, - ); - this.manufacturerData = this.payload.subarray( - 1, - 1 + manufacturerDataLength, - ); - } - } else { - this.mode = options.mode; - if ("manufacturerData" in options) { - this.manufacturerData = options.manufacturerData; - } + this.mode = options.mode; + if ("manufacturerData" in options) { + this.manufacturerData = options.manufacturerData; + } + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ThermostatModeCCSet { + validatePayload(raw.payload.length >= 1); + const mode: ThermostatMode = raw.payload[0] & 0b11111; + if (mode !== ThermostatMode["Manufacturer specific"]) { + return new ThermostatModeCCSet({ + nodeId: ctx.sourceNodeId, + mode, + }); } + + const manufacturerDataLength = (raw.payload[0] >>> 5) & 0b111; + validatePayload( + raw.payload.length >= 1 + manufacturerDataLength, + ); + const manufacturerData = raw.payload.subarray( + 1, + 1 + manufacturerDataLength, + ); + + return new ThermostatModeCCSet({ + nodeId: ctx.sourceNodeId, + mode, + manufacturerData, + }); } public mode: ThermostatMode; @@ -385,49 +394,58 @@ export class ThermostatModeCCSet extends ThermostatModeCC { // @publicAPI export type ThermostatModeCCReportOptions = - & CCCommandOptions - & ( - | { - mode: Exclude< - ThermostatMode, - (typeof ThermostatMode)["Manufacturer specific"] - >; - manufacturerData?: undefined; - } - | { - mode: (typeof ThermostatMode)["Manufacturer specific"]; - manufacturerData?: Buffer; - } - ); + | { + mode: Exclude< + ThermostatMode, + (typeof ThermostatMode)["Manufacturer specific"] + >; + manufacturerData?: undefined; + } + | { + mode: (typeof ThermostatMode)["Manufacturer specific"]; + manufacturerData?: Buffer; + }; @CCCommand(ThermostatModeCommand.Report) export class ThermostatModeCCReport extends ThermostatModeCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatModeCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.mode = this.payload[0] & 0b11111; + this.mode = options.mode; + this.manufacturerData = options.manufacturerData; + } - // V3+ - const manufacturerDataLength = this.payload[0] >>> 5; - if (manufacturerDataLength > 0) { - validatePayload( - this.payload.length >= 1 + manufacturerDataLength, - ); - this.manufacturerData = this.payload.subarray( - 1, - 1 + manufacturerDataLength, - ); - } - } else { - this.mode = options.mode; - this.manufacturerData = options.manufacturerData; + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatModeCCReport { + validatePayload(raw.payload.length >= 1); + const mode: ThermostatMode = raw.payload[0] & 0b11111; + + if (mode !== ThermostatMode["Manufacturer specific"]) { + return new ThermostatModeCCReport({ + nodeId: ctx.sourceNodeId, + mode, + }); } + + // V3+ + const manufacturerDataLength = raw.payload[0] >>> 5; + validatePayload( + raw.payload.length >= 1 + manufacturerDataLength, + ); + const manufacturerData = raw.payload.subarray( + 1, + 1 + manufacturerDataLength, + ); + + return new ThermostatModeCCReport({ + nodeId: ctx.sourceNodeId, + mode, + manufacturerData, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -507,28 +525,32 @@ export class ThermostatModeCCReport extends ThermostatModeCC { export class ThermostatModeCCGet extends ThermostatModeCC {} // @publicAPI -export interface ThermostatModeCCSupportedReportOptions - extends CCCommandOptions -{ +export interface ThermostatModeCCSupportedReportOptions { supportedModes: ThermostatMode[]; } @CCCommand(ThermostatModeCommand.SupportedReport) export class ThermostatModeCCSupportedReport extends ThermostatModeCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatModeCCSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - this.supportedModes = parseBitMask( - this.payload, - ThermostatMode.Off, - ); - } else { - this.supportedModes = options.supportedModes; - } + this.supportedModes = options.supportedModes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatModeCCSupportedReport { + const supportedModes: ThermostatMode[] = parseBitMask( + raw.payload, + ThermostatMode.Off, + ); + + return new ThermostatModeCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedModes, + }); } public persistValues(ctx: PersistValuesContext): boolean { diff --git a/packages/cc/src/cc/ThermostatOperatingStateCC.ts b/packages/cc/src/cc/ThermostatOperatingStateCC.ts index e863afe1d701..3987e436fedf 100644 --- a/packages/cc/src/cc/ThermostatOperatingStateCC.ts +++ b/packages/cc/src/cc/ThermostatOperatingStateCC.ts @@ -1,4 +1,4 @@ -import type { MessageOrCCLogEntry } from "@zwave-js/core/safe"; +import type { MessageOrCCLogEntry, WithAddress } from "@zwave-js/core/safe"; import { CommandClasses, type MaybeNotKnown, @@ -7,7 +7,7 @@ import { enumValuesToMetadataStates, validatePayload, } from "@zwave-js/core/safe"; -import type { GetValueDB } from "@zwave-js/host/safe"; +import type { CCParsingContext, GetValueDB } from "@zwave-js/host/safe"; import { getEnumMemberName } from "@zwave-js/shared/safe"; import { CCAPI, @@ -17,8 +17,8 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, } from "../lib/CommandClass"; @@ -87,7 +87,7 @@ export class ThermostatOperatingStateCCAPI extends PhysicalCCAPI { const cc = new ThermostatOperatingStateCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatOperatingStateCCReport @@ -158,17 +158,35 @@ export class ThermostatOperatingStateCC extends CommandClass { } } +// @publicAPI +export interface ThermostatOperatingStateCCReportOptions { + state: ThermostatOperatingState; +} + @CCCommand(ThermostatOperatingStateCommand.Report) export class ThermostatOperatingStateCCReport extends ThermostatOperatingStateCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - this.state = this.payload[0]; + // TODO: Check implementation: + this.state = options.state; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatOperatingStateCCReport { + validatePayload(raw.payload.length >= 1); + const state: ThermostatOperatingState = raw.payload[0]; + + return new ThermostatOperatingStateCCReport({ + nodeId: ctx.sourceNodeId, + state, + }); } @ccValue(ThermostatOperatingStateCCValues.operatingState) diff --git a/packages/cc/src/cc/ThermostatSetbackCC.ts b/packages/cc/src/cc/ThermostatSetbackCC.ts index 788ffe7dc6c3..fab9ddada914 100644 --- a/packages/cc/src/cc/ThermostatSetbackCC.ts +++ b/packages/cc/src/cc/ThermostatSetbackCC.ts @@ -4,9 +4,14 @@ import { type MessageOrCCLogEntry, MessagePriority, type SupervisionResult, + type WithAddress, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -16,12 +21,10 @@ import { throwUnsupportedProperty, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -77,7 +80,7 @@ export class ThermostatSetbackCCAPI extends CCAPI { const cc = new ThermostatSetbackCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatSetbackCCReport @@ -102,7 +105,7 @@ export class ThermostatSetbackCCAPI extends CCAPI { const cc = new ThermostatSetbackCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setbackType, setbackState, }); @@ -166,7 +169,7 @@ setback state: ${setbackResp.setbackState}`; } // @publicAPI -export interface ThermostatSetbackCCSetOptions extends CCCommandOptions { +export interface ThermostatSetbackCCSetOptions { setbackType: SetbackType; setbackState: SetbackState; } @@ -175,22 +178,30 @@ export interface ThermostatSetbackCCSetOptions extends CCCommandOptions { @useSupervision() export class ThermostatSetbackCCSet extends ThermostatSetbackCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetbackCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.setbackType = this.payload[0] & 0b11; - // If we receive an unknown setback state, return the raw value - const rawSetbackState = this.payload.readInt8(1); - this.setbackState = decodeSetbackState(rawSetbackState) - || rawSetbackState; - } else { - this.setbackType = options.setbackType; - this.setbackState = options.setbackState; - } + this.setbackType = options.setbackType; + this.setbackState = options.setbackState; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetbackCCSet { + validatePayload(raw.payload.length >= 2); + const setbackType: SetbackType = raw.payload[0] & 0b11; + + // If we receive an unknown setback state, return the raw value + const rawSetbackState = raw.payload.readInt8(1); + const setbackState: SetbackState = decodeSetbackState(rawSetbackState) + || rawSetbackState; + + return new ThermostatSetbackCCSet({ + nodeId: ctx.sourceNodeId, + setbackType, + setbackState, + }); } public setbackType: SetbackType; @@ -230,23 +241,31 @@ export interface ThermostatSetbackCCReportOptions { @CCCommand(ThermostatSetbackCommand.Report) export class ThermostatSetbackCCReport extends ThermostatSetbackCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & ThermostatSetbackCCReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.setbackType = this.payload[0] & 0b11; - // If we receive an unknown setback state, return the raw value - const rawSetbackState = this.payload.readInt8(1); - this.setbackState = decodeSetbackState(rawSetbackState) - || rawSetbackState; - } else { - this.setbackType = options.setbackType; - this.setbackState = options.setbackState; - } + this.setbackType = options.setbackType; + this.setbackState = options.setbackState; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetbackCCReport { + validatePayload(raw.payload.length >= 2); + const setbackType: SetbackType = raw.payload[0] & 0b11; + + // If we receive an unknown setback state, return the raw value + const rawSetbackState = raw.payload.readInt8(1); + const setbackState: SetbackState = decodeSetbackState(rawSetbackState) + || rawSetbackState; + + return new ThermostatSetbackCCReport({ + nodeId: ctx.sourceNodeId, + setbackType, + setbackState, + }); } public readonly setbackType: SetbackType; diff --git a/packages/cc/src/cc/ThermostatSetpointCC.ts b/packages/cc/src/cc/ThermostatSetpointCC.ts index ee12fdf6559d..3c39c854c610 100644 --- a/packages/cc/src/cc/ThermostatSetpointCC.ts +++ b/packages/cc/src/cc/ThermostatSetpointCC.ts @@ -7,6 +7,7 @@ import { type SupervisionResult, ValueMetadata, type ValueMetadataNumeric, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeBitMask, @@ -18,7 +19,11 @@ import { supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -31,13 +36,11 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -220,7 +223,7 @@ export class ThermostatSetpointCCAPI extends CCAPI { const cc = new ThermostatSetpointCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, }); const response = await this.host.sendCommand< @@ -253,7 +256,7 @@ export class ThermostatSetpointCCAPI extends CCAPI { const cc = new ThermostatSetpointCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, value, scale, @@ -271,7 +274,7 @@ export class ThermostatSetpointCCAPI extends CCAPI { const cc = new ThermostatSetpointCCCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, setpointType, }); const response = await this.host.sendCommand< @@ -305,7 +308,7 @@ export class ThermostatSetpointCCAPI extends CCAPI { const cc = new ThermostatSetpointCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< ThermostatSetpointCCSupportedReport @@ -540,7 +543,7 @@ maximum value: ${setpointCaps.maxValue} ${maxValueUnit}`; } // @publicAPI -export interface ThermostatSetpointCCSetOptions extends CCCommandOptions { +export interface ThermostatSetpointCCSetOptions { setpointType: ThermostatSetpointType; value: number; scale: number; @@ -550,25 +553,32 @@ export interface ThermostatSetpointCCSetOptions extends CCCommandOptions { @useSupervision() export class ThermostatSetpointCCSet extends ThermostatSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetpointCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.setpointType = this.payload[0] & 0b1111; - // parseFloatWithScale does its own validation - const { value, scale } = parseFloatWithScale( - this.payload.subarray(1), - ); - this.value = value; - this.scale = scale; - } else { - this.setpointType = options.setpointType; - this.value = options.value; - this.scale = options.scale; - } + this.setpointType = options.setpointType; + this.value = options.value; + this.scale = options.scale; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetpointCCSet { + validatePayload(raw.payload.length >= 1); + const setpointType: ThermostatSetpointType = raw.payload[0] & 0b1111; + + // parseFloatWithScale does its own validation + const { value, scale } = parseFloatWithScale( + raw.payload.subarray(1), + ); + + return new ThermostatSetpointCCSet({ + nodeId: ctx.sourceNodeId, + setpointType, + value, + scale, + }); } public setpointType: ThermostatSetpointType; @@ -602,7 +612,7 @@ export class ThermostatSetpointCCSet extends ThermostatSetpointCC { } // @publicAPI -export interface ThermostatSetpointCCReportOptions extends CCCommandOptions { +export interface ThermostatSetpointCCReportOptions { type: ThermostatSetpointType; value: number; scale: number; @@ -611,33 +621,43 @@ export interface ThermostatSetpointCCReportOptions extends CCCommandOptions { @CCCommand(ThermostatSetpointCommand.Report) export class ThermostatSetpointCCReport extends ThermostatSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetpointCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.type = this.payload[0] & 0b1111; - if (this.type === 0) { - // Not supported - this.value = 0; - this.scale = 0; - return; - } + this.type = options.type; + this.value = options.value; + this.scale = options.scale; + } - // parseFloatWithScale does its own validation - const { value, scale } = parseFloatWithScale( - this.payload.subarray(1), - ); - this.value = value; - this.scale = scale; - } else { - this.type = options.type; - this.value = options.value; - this.scale = options.scale; + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetpointCCReport { + validatePayload(raw.payload.length >= 1); + const type: ThermostatSetpointType = raw.payload[0] & 0b1111; + + if (type === 0) { + // Not supported + return new ThermostatSetpointCCReport({ + nodeId: ctx.sourceNodeId, + type, + value: 0, + scale: 0, + }); } + + // parseFloatWithScale does its own validation + const { value, scale } = parseFloatWithScale( + raw.payload.subarray(1), + ); + + return new ThermostatSetpointCCReport({ + nodeId: ctx.sourceNodeId, + type, + value, + scale, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -705,7 +725,7 @@ function testResponseForThermostatSetpointGet( } // @publicAPI -export interface ThermostatSetpointCCGetOptions extends CCCommandOptions { +export interface ThermostatSetpointCCGetOptions { setpointType: ThermostatSetpointType; } @@ -716,17 +736,23 @@ export interface ThermostatSetpointCCGetOptions extends CCCommandOptions { ) export class ThermostatSetpointCCGet extends ThermostatSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetpointCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.setpointType = this.payload[0] & 0b1111; - } else { - this.setpointType = options.setpointType; - } + this.setpointType = options.setpointType; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetpointCCGet { + validatePayload(raw.payload.length >= 1); + const setpointType: ThermostatSetpointType = raw.payload[0] & 0b1111; + + return new ThermostatSetpointCCGet({ + nodeId: ctx.sourceNodeId, + setpointType, + }); } public setpointType: ThermostatSetpointType; @@ -750,9 +776,7 @@ export class ThermostatSetpointCCGet extends ThermostatSetpointCC { } // @publicAPI -export interface ThermostatSetpointCCCapabilitiesReportOptions - extends CCCommandOptions -{ +export interface ThermostatSetpointCCCapabilitiesReportOptions { type: ThermostatSetpointType; minValue: number; minValueScale: number; @@ -765,31 +789,41 @@ export class ThermostatSetpointCCCapabilitiesReport extends ThermostatSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetpointCCCapabilitiesReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.type = this.payload[0]; - let bytesRead: number; - // parseFloatWithScale does its own validation - ({ - value: this.minValue, - scale: this.minValueScale, - bytesRead, - } = parseFloatWithScale(this.payload.subarray(1))); - ({ value: this.maxValue, scale: this.maxValueScale } = - parseFloatWithScale(this.payload.subarray(1 + bytesRead))); - } else { - this.type = options.type; - this.minValue = options.minValue; - this.minValueScale = options.minValueScale; - this.maxValue = options.maxValue; - this.maxValueScale = options.maxValueScale; - } + this.type = options.type; + this.minValue = options.minValue; + this.minValueScale = options.minValueScale; + this.maxValue = options.maxValue; + this.maxValueScale = options.maxValueScale; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetpointCCCapabilitiesReport { + validatePayload(raw.payload.length >= 1); + const type: ThermostatSetpointType = raw.payload[0]; + // parseFloatWithScale does its own validation + const { + value: minValue, + scale: minValueScale, + bytesRead, + } = parseFloatWithScale(raw.payload.subarray(1)); + const { value: maxValue, scale: maxValueScale } = parseFloatWithScale( + raw.payload.subarray(1 + bytesRead), + ); + + return new ThermostatSetpointCCCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + type, + minValue, + minValueScale, + maxValue, + maxValueScale, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -839,9 +873,7 @@ export class ThermostatSetpointCCCapabilitiesReport } // @publicAPI -export interface ThermostatSetpointCCCapabilitiesGetOptions - extends CCCommandOptions -{ +export interface ThermostatSetpointCCCapabilitiesGetOptions { setpointType: ThermostatSetpointType; } @@ -849,17 +881,23 @@ export interface ThermostatSetpointCCCapabilitiesGetOptions @expectedCCResponse(ThermostatSetpointCCCapabilitiesReport) export class ThermostatSetpointCCCapabilitiesGet extends ThermostatSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetpointCCCapabilitiesGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.setpointType = this.payload[0] & 0b1111; - } else { - this.setpointType = options.setpointType; - } + this.setpointType = options.setpointType; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetpointCCCapabilitiesGet { + validatePayload(raw.payload.length >= 1); + const setpointType: ThermostatSetpointType = raw.payload[0] & 0b1111; + + return new ThermostatSetpointCCCapabilitiesGet({ + nodeId: ctx.sourceNodeId, + setpointType, + }); } public setpointType: ThermostatSetpointType; @@ -883,42 +921,46 @@ export class ThermostatSetpointCCCapabilitiesGet extends ThermostatSetpointCC { } // @publicAPI -export interface ThermostatSetpointCCSupportedReportOptions - extends CCCommandOptions -{ +export interface ThermostatSetpointCCSupportedReportOptions { supportedSetpointTypes: ThermostatSetpointType[]; } @CCCommand(ThermostatSetpointCommand.SupportedReport) export class ThermostatSetpointCCSupportedReport extends ThermostatSetpointCC { public constructor( - options: - | CommandClassDeserializationOptions - | ThermostatSetpointCCSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const bitMask = this.payload; - const supported = parseBitMask( - bitMask, - ThermostatSetpointType["N/A"], + if (options.supportedSetpointTypes.length === 0) { + throw new ZWaveError( + `At least one setpoint type must be supported`, + ZWaveErrorCodes.Argument_Invalid, ); - // We use this command only when we are sure that bitmask interpretation A is used - // FIXME: Figure out if we can do this without the CC version - this.supportedSetpointTypes = supported.map( - (i) => thermostatSetpointTypeMap[i], - ); - } else { - if (options.supportedSetpointTypes.length === 0) { - throw new ZWaveError( - `At least one setpoint type must be supported`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.supportedSetpointTypes = options.supportedSetpointTypes; } + this.supportedSetpointTypes = options.supportedSetpointTypes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ThermostatSetpointCCSupportedReport { + validatePayload(raw.payload.length >= 1); + const bitMask = raw.payload; + const supported = parseBitMask( + bitMask, + ThermostatSetpointType["N/A"], + ); + // We use this command only when we are sure that bitmask interpretation A is used + // FIXME: Figure out if we can do this without the CC version + const supportedSetpointTypes: ThermostatSetpointType[] = supported.map( + (i) => thermostatSetpointTypeMap[i], + ); + + return new ThermostatSetpointCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedSetpointTypes, + }); } @ccValue(ThermostatSetpointCCValues.supportedSetpointTypes) diff --git a/packages/cc/src/cc/TimeCC.ts b/packages/cc/src/cc/TimeCC.ts index 5c72beb053e9..34da2eef957a 100644 --- a/packages/cc/src/cc/TimeCC.ts +++ b/packages/cc/src/cc/TimeCC.ts @@ -4,6 +4,7 @@ import { type MessageOrCCLogEntry, MessagePriority, type SupervisionResult, + type WithAddress, ZWaveError, ZWaveErrorCodes, formatDate, @@ -11,17 +12,19 @@ import { validatePayload, } from "@zwave-js/core"; import { type MaybeNotKnown } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { padStart } from "alcalzone-shared/strings"; import { CCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -61,7 +64,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCTimeGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -82,7 +85,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCTimeReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, hour, minute, second, @@ -96,7 +99,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCDateGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -117,7 +120,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCDateReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, year, month, day, @@ -133,7 +136,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCTimeOffsetSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, standardOffset: timezone.standardOffset, dstOffset: timezone.dstOffset, dstStart: timezone.startDate, @@ -147,7 +150,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCTimeOffsetGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< TimeCCTimeOffsetReport @@ -173,7 +176,7 @@ export class TimeCCAPI extends CCAPI { const cc = new TimeCCTimeOffsetReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, standardOffset: timezone.standardOffset, dstOffset: timezone.dstOffset, dstStart: timezone.startDate, @@ -225,7 +228,7 @@ export class TimeCC extends CommandClass { } // @publicAPI -export interface TimeCCTimeReportOptions extends CCCommandOptions { +export interface TimeCCTimeReportOptions { hour: number; minute: number; second: number; @@ -234,22 +237,35 @@ export interface TimeCCTimeReportOptions extends CCCommandOptions { @CCCommand(TimeCommand.TimeReport) export class TimeCCTimeReport extends TimeCC { public constructor( - options: CommandClassDeserializationOptions | TimeCCTimeReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.hour = this.payload[0] & 0b11111; - validatePayload(this.hour >= 0, this.hour <= 23); - this.minute = this.payload[1]; - validatePayload(this.minute >= 0, this.minute <= 59); - this.second = this.payload[2]; - validatePayload(this.second >= 0, this.second <= 59); - } else { - this.hour = options.hour; - this.minute = options.minute; - this.second = options.second; - } + this.hour = options.hour; + this.minute = options.minute; + this.second = options.second; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): TimeCCTimeReport { + validatePayload(raw.payload.length >= 3); + const hour = raw.payload[0] & 0b11111; + const minute = raw.payload[1]; + const second = raw.payload[2]; + + validatePayload( + hour >= 0, + hour <= 23, + minute >= 0, + minute <= 59, + second >= 0, + second <= 59, + ); + + return new TimeCCTimeReport({ + nodeId: ctx.sourceNodeId, + hour, + minute, + second, + }); } public hour: number; @@ -286,7 +302,7 @@ export class TimeCCTimeReport extends TimeCC { export class TimeCCTimeGet extends TimeCC {} // @publicAPI -export interface TimeCCDateReportOptions extends CCCommandOptions { +export interface TimeCCDateReportOptions { year: number; month: number; day: number; @@ -295,19 +311,26 @@ export interface TimeCCDateReportOptions extends CCCommandOptions { @CCCommand(TimeCommand.DateReport) export class TimeCCDateReport extends TimeCC { public constructor( - options: CommandClassDeserializationOptions | TimeCCDateReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.year = this.payload.readUInt16BE(0); - this.month = this.payload[2]; - this.day = this.payload[3]; - } else { - this.year = options.year; - this.month = options.month; - this.day = options.day; - } + this.year = options.year; + this.month = options.month; + this.day = options.day; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): TimeCCDateReport { + validatePayload(raw.payload.length >= 4); + const year = raw.payload.readUInt16BE(0); + const month = raw.payload[2]; + const day = raw.payload[3]; + + return new TimeCCDateReport({ + nodeId: ctx.sourceNodeId, + year, + month, + day, + }); } public year: number; @@ -347,7 +370,7 @@ export class TimeCCDateReport extends TimeCC { export class TimeCCDateGet extends TimeCC {} // @publicAPI -export interface TimeCCTimeOffsetSetOptions extends CCCommandOptions { +export interface TimeCCTimeOffsetSetOptions { standardOffset: number; dstOffset: number; dstStart: Date; @@ -358,23 +381,28 @@ export interface TimeCCTimeOffsetSetOptions extends CCCommandOptions { @useSupervision() export class TimeCCTimeOffsetSet extends TimeCC { public constructor( - options: - | CommandClassDeserializationOptions - | TimeCCTimeOffsetSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.standardOffset = options.standardOffset; - this.dstOffset = options.dstOffset; - this.dstStartDate = options.dstStart; - this.dstEndDate = options.dstEnd; - } + this.standardOffset = options.standardOffset; + this.dstOffset = options.dstOffset; + this.dstStartDate = options.dstStart; + this.dstEndDate = options.dstEnd; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): TimeCCTimeOffsetSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new TimeCCTimeOffsetSet({ + // nodeId: ctx.sourceNodeId, + // }); } public standardOffset: number; @@ -414,7 +442,7 @@ export class TimeCCTimeOffsetSet extends TimeCC { } // @publicAPI -export interface TimeCCTimeOffsetReportOptions extends CCCommandOptions { +export interface TimeCCTimeOffsetReportOptions { standardOffset: number; dstOffset: number; dstStart: Date; @@ -424,40 +452,46 @@ export interface TimeCCTimeOffsetReportOptions extends CCCommandOptions { @CCCommand(TimeCommand.TimeOffsetReport) export class TimeCCTimeOffsetReport extends TimeCC { public constructor( - options: - | CommandClassDeserializationOptions - | TimeCCTimeOffsetReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 9); - const { standardOffset, dstOffset } = parseTimezone(this.payload); - this.standardOffset = standardOffset; - this.dstOffset = dstOffset; - - const currentYear = new Date().getUTCFullYear(); - this.dstStartDate = new Date( - Date.UTC( - currentYear, - this.payload[3] - 1, - this.payload[4], - this.payload[5], - ), - ); - this.dstEndDate = new Date( - Date.UTC( - currentYear, - this.payload[6] - 1, - this.payload[7], - this.payload[8], - ), - ); - } else { - this.standardOffset = options.standardOffset; - this.dstOffset = options.dstOffset; - this.dstStartDate = options.dstStart; - this.dstEndDate = options.dstEnd; - } + this.standardOffset = options.standardOffset; + this.dstOffset = options.dstOffset; + this.dstStartDate = options.dstStart; + this.dstEndDate = options.dstEnd; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TimeCCTimeOffsetReport { + validatePayload(raw.payload.length >= 9); + const { standardOffset, dstOffset } = parseTimezone(raw.payload); + const currentYear = new Date().getUTCFullYear(); + const dstStartDate: Date = new Date( + Date.UTC( + currentYear, + raw.payload[3] - 1, + raw.payload[4], + raw.payload[5], + ), + ); + const dstEndDate: Date = new Date( + Date.UTC( + currentYear, + raw.payload[6] - 1, + raw.payload[7], + raw.payload[8], + ), + ); + + return new TimeCCTimeOffsetReport({ + nodeId: ctx.sourceNodeId, + standardOffset, + dstOffset, + dstStart: dstStartDate, + dstEnd: dstEndDate, + }); } public standardOffset: number; diff --git a/packages/cc/src/cc/TimeParametersCC.ts b/packages/cc/src/cc/TimeParametersCC.ts index 7d17582e4f6a..bef39dba2b50 100644 --- a/packages/cc/src/cc/TimeParametersCC.ts +++ b/packages/cc/src/cc/TimeParametersCC.ts @@ -4,6 +4,7 @@ import { MessagePriority, type SupervisionResult, ValueMetadata, + type WithAddress, formatDate, validatePayload, } from "@zwave-js/core"; @@ -15,6 +16,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetValueDB, } from "@zwave-js/host/safe"; @@ -29,12 +31,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -179,7 +179,7 @@ export class TimeParametersCCAPI extends CCAPI { const cc = new TimeParametersCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< TimeParametersCCReport @@ -209,7 +209,7 @@ export class TimeParametersCCAPI extends CCAPI { const cc = new TimeParametersCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, dateAndTime, useLocalTime, }); @@ -255,26 +255,45 @@ export class TimeParametersCC extends CommandClass { } } +// @publicAPI +export interface TimeParametersCCReportOptions { + dateAndTime: Date; +} + @CCCommand(TimeParametersCommand.Report) export class TimeParametersCCReport extends TimeParametersCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 7); + + // TODO: Check implementation: + this.dateAndTime = options.dateAndTime; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TimeParametersCCReport { + validatePayload(raw.payload.length >= 7); const dateSegments = { - year: this.payload.readUInt16BE(0), - month: this.payload[2], - day: this.payload[3], - hour: this.payload[4], - minute: this.payload[5], - second: this.payload[6], + year: raw.payload.readUInt16BE(0), + month: raw.payload[2], + day: raw.payload[3], + hour: raw.payload[4], + minute: raw.payload[5], + second: raw.payload[6], }; - this._dateAndTime = segmentsToDate( + const dateAndTime: Date = segmentsToDate( dateSegments, // Assume we can use UTC and correct this assumption in persistValues false, ); + + return new TimeParametersCCReport({ + nodeId: ctx.sourceNodeId, + dateAndTime, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -283,17 +302,14 @@ export class TimeParametersCCReport extends TimeParametersCC { if (local) { // The initial assumption was incorrect, re-interpret the time const segments = dateToSegments(this.dateAndTime, false); - this._dateAndTime = segmentsToDate(segments, local); + this.dateAndTime = segmentsToDate(segments, local); } return super.persistValues(ctx); } - private _dateAndTime: Date; @ccValue(TimeParametersCCValues.dateAndTime) - public get dateAndTime(): Date { - return this._dateAndTime; - } + public dateAndTime: Date; public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { return { @@ -313,7 +329,7 @@ export class TimeParametersCCReport extends TimeParametersCC { export class TimeParametersCCGet extends TimeParametersCC {} // @publicAPI -export interface TimeParametersCCSetOptions extends CCCommandOptions { +export interface TimeParametersCCSetOptions { dateAndTime: Date; useLocalTime?: boolean; } @@ -322,37 +338,40 @@ export interface TimeParametersCCSetOptions extends CCCommandOptions { @useSupervision() export class TimeParametersCCSet extends TimeParametersCC { public constructor( - options: - | CommandClassDeserializationOptions - | TimeParametersCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 7); - const dateSegments = { - year: this.payload.readUInt16BE(0), - month: this.payload[2], - day: this.payload[3], - hour: this.payload[4], - minute: this.payload[5], - second: this.payload[6], - }; - validatePayload( - dateSegments.month >= 1 && dateSegments.month <= 12, - dateSegments.day >= 1 && dateSegments.day <= 31, - dateSegments.hour >= 0 && dateSegments.hour <= 23, - dateSegments.minute >= 0 && dateSegments.minute <= 59, - dateSegments.second >= 0 && dateSegments.second <= 59, - ); - this.dateAndTime = segmentsToDate( - dateSegments, - // Assume we can use UTC and correct this assumption in persistValues - false, - ); - } else { - this.dateAndTime = options.dateAndTime; - this.useLocalTime = options.useLocalTime; - } + this.dateAndTime = options.dateAndTime; + this.useLocalTime = options.useLocalTime; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): TimeParametersCCSet { + validatePayload(raw.payload.length >= 7); + const dateSegments = { + year: raw.payload.readUInt16BE(0), + month: raw.payload[2], + day: raw.payload[3], + hour: raw.payload[4], + minute: raw.payload[5], + second: raw.payload[6], + }; + validatePayload( + dateSegments.month >= 1 && dateSegments.month <= 12, + dateSegments.day >= 1 && dateSegments.day <= 31, + dateSegments.hour >= 0 && dateSegments.hour <= 23, + dateSegments.minute >= 0 && dateSegments.minute <= 59, + dateSegments.second >= 0 && dateSegments.second <= 59, + ); + const dateAndTime = segmentsToDate( + dateSegments, + // Assume we can use UTC and correct this assumption in persistValues + false, + ); + + return new TimeParametersCCSet({ + nodeId: ctx.sourceNodeId, + dateAndTime, + }); } public persistValues(ctx: PersistValuesContext): boolean { diff --git a/packages/cc/src/cc/TransportServiceCC.ts b/packages/cc/src/cc/TransportServiceCC.ts index fbabeca8c79d..92f7b8f9cbcd 100644 --- a/packages/cc/src/cc/TransportServiceCC.ts +++ b/packages/cc/src/cc/TransportServiceCC.ts @@ -3,6 +3,7 @@ import { CommandClasses, type MessageOrCCLogEntry, type SinglecastCC, + type WithAddress, ZWaveError, ZWaveErrorCodes, validatePayload, @@ -14,11 +15,9 @@ import type { } from "@zwave-js/host/safe"; import { buffer2hex } from "@zwave-js/shared/safe"; import { - type CCCommandOptions, + type CCRaw, type CCResponseRole, CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, } from "../lib/CommandClass"; import { CCCommand, @@ -56,39 +55,10 @@ export class TransportServiceCC extends CommandClass { declare ccCommand: TransportServiceCommand; declare nodeId: number; - - // Override the default helper method - public static getCCCommand(data: Buffer): number | undefined { - const originalCCCommand = super.getCCCommand(data)!; - // Transport Service only uses the higher 5 bits for the command - return originalCCCommand & 0b11111_000; - } - - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected deserialize(data: Buffer) { - const ret = super.deserialize(data) as { - ccId: CommandClasses; - ccCommand: number; - payload: Buffer; - }; - // Transport Service re-uses the lower 3 bits of the ccCommand as payload - ret.payload = Buffer.concat([ - Buffer.from([ret.ccCommand & 0b111]), - ret.payload, - ]); - return ret; - } - - /** Encapsulates a command that should be sent in multiple segments */ - public static encapsulate(_cc: CommandClass): TransportServiceCC { - throw new Error("not implemented"); - } } // @publicAPI -export interface TransportServiceCCFirstSegmentOptions - extends CCCommandOptions -{ +export interface TransportServiceCCFirstSegmentOptions { datagramSize: number; sessionId: number; headerExtension?: Buffer | undefined; @@ -113,52 +83,62 @@ export function isTransportServiceEncapsulation( // @expectedCCResponse(TransportServiceCCReport) export class TransportServiceCCFirstSegment extends TransportServiceCC { public constructor( - options: - | CommandClassDeserializationOptions - | TransportServiceCCFirstSegmentOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // Deserialization has already split the datagram size from the ccCommand. - // Therefore we have one more payload byte + this.datagramSize = options.datagramSize; + this.sessionId = options.sessionId; + this.headerExtension = options.headerExtension; + this.partialDatagram = options.partialDatagram; + } - validatePayload(this.payload.length >= 6); // 2 bytes dgram size, 1 byte sessid/ext, 1+ bytes payload, 2 bytes checksum + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TransportServiceCCFirstSegment { + // Deserialization has already split the datagram size from the ccCommand. + // Therefore we have one more payload byte - // Verify the CRC - const headerBuffer = Buffer.from([ - this.ccId, - this.ccCommand | this.payload[0], - ]); - const ccBuffer = this.payload.subarray(1, -2); - let expectedCRC = CRC16_CCITT(headerBuffer); - expectedCRC = CRC16_CCITT(ccBuffer, expectedCRC); - const actualCRC = this.payload.readUInt16BE( - this.payload.length - 2, - ); - validatePayload(expectedCRC === actualCRC); - - this.datagramSize = this.payload.readUInt16BE(0); - this.sessionId = this.payload[2] >>> 4; - let payloadOffset = 3; - - // If there is a header extension, read it - const hasHeaderExtension = !!(this.payload[2] & 0b1000); - if (hasHeaderExtension) { - const extLength = this.payload[3]; - this.headerExtension = this.payload.subarray(4, 4 + extLength); - payloadOffset += 1 + extLength; - } + validatePayload(raw.payload.length >= 6); // 2 bytes dgram size, 1 byte sessid/ext, 1+ bytes payload, 2 bytes checksum - this.partialDatagram = this.payload.subarray(payloadOffset, -2); - // A node supporting the Transport Service Command Class, version 2 - // MUST NOT send Transport Service segments with the Payload field longer than 39 bytes. - validatePayload(this.partialDatagram.length <= MAX_SEGMENT_SIZE); - } else { - this.datagramSize = options.datagramSize; - this.sessionId = options.sessionId; - this.headerExtension = options.headerExtension; - this.partialDatagram = options.partialDatagram; + // Verify the CRC + const headerBuffer = Buffer.from([ + CommandClasses["Transport Service"], + TransportServiceCommand.FirstSegment | raw.payload[0], + ]); + const ccBuffer = raw.payload.subarray(1, -2); + let expectedCRC = CRC16_CCITT(headerBuffer); + expectedCRC = CRC16_CCITT(ccBuffer, expectedCRC); + const actualCRC = raw.payload.readUInt16BE( + raw.payload.length - 2, + ); + validatePayload(expectedCRC === actualCRC); + const datagramSize = raw.payload.readUInt16BE(0); + const sessionId = raw.payload[2] >>> 4; + let payloadOffset = 3; + + // If there is a header extension, read it + const hasHeaderExtension = !!(raw.payload[2] & 0b1000); + let headerExtension: Buffer | undefined; + + if (hasHeaderExtension) { + const extLength = raw.payload[3]; + headerExtension = raw.payload.subarray(4, 4 + extLength); + payloadOffset += 1 + extLength; } + const partialDatagram: Buffer = raw.payload.subarray(payloadOffset, -2); + + // A node supporting the Transport Service Command Class, version 2 + // MUST NOT send Transport Service segments with the Payload field longer than 39 bytes. + validatePayload(partialDatagram.length <= MAX_SEGMENT_SIZE); + + return new TransportServiceCCFirstSegment({ + nodeId: ctx.sourceNodeId, + datagramSize, + sessionId, + headerExtension, + partialDatagram, + }); } public datagramSize: number; @@ -245,55 +225,66 @@ export interface TransportServiceCCSubsequentSegmentOptions // @expectedCCResponse(TransportServiceCCReport) export class TransportServiceCCSubsequentSegment extends TransportServiceCC { public constructor( - options: - | CommandClassDeserializationOptions - | TransportServiceCCSubsequentSegmentOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // Deserialization has already split the datagram size from the ccCommand. - // Therefore we have one more payload byte + this.datagramSize = options.datagramSize; + this.datagramOffset = options.datagramOffset; + this.sessionId = options.sessionId; + this.headerExtension = options.headerExtension; + this.partialDatagram = options.partialDatagram; + } - validatePayload(this.payload.length >= 7); // 2 bytes dgram size, 1 byte sessid/ext/offset, 1 byte offset, 1+ bytes payload, 2 bytes checksum + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TransportServiceCCSubsequentSegment { + // Deserialization has already split the datagram size from the ccCommand. + // Therefore we have one more payload byte - // Verify the CRC - const headerBuffer = Buffer.from([ - this.ccId, - this.ccCommand | this.payload[0], - ]); - const ccBuffer = this.payload.subarray(1, -2); - let expectedCRC = CRC16_CCITT(headerBuffer); - expectedCRC = CRC16_CCITT(ccBuffer, expectedCRC); - const actualCRC = this.payload.readUInt16BE( - this.payload.length - 2, - ); - validatePayload(expectedCRC === actualCRC); - - this.datagramSize = this.payload.readUInt16BE(0); - this.sessionId = this.payload[2] >>> 4; - this.datagramOffset = ((this.payload[2] & 0b111) << 8) - + this.payload[3]; - let payloadOffset = 4; - - // If there is a header extension, read it - const hasHeaderExtension = !!(this.payload[2] & 0b1000); - if (hasHeaderExtension) { - const extLength = this.payload[4]; - this.headerExtension = this.payload.subarray(5, 5 + extLength); - payloadOffset += 1 + extLength; - } + validatePayload(raw.payload.length >= 7); // 2 bytes dgram size, 1 byte sessid/ext/offset, 1 byte offset, 1+ bytes payload, 2 bytes checksum - this.partialDatagram = this.payload.subarray(payloadOffset, -2); - // A node supporting the Transport Service Command Class, version 2 - // MUST NOT send Transport Service segments with the Payload field longer than 39 bytes. - validatePayload(this.partialDatagram.length <= MAX_SEGMENT_SIZE); - } else { - this.datagramSize = options.datagramSize; - this.datagramOffset = options.datagramOffset; - this.sessionId = options.sessionId; - this.headerExtension = options.headerExtension; - this.partialDatagram = options.partialDatagram; + // Verify the CRC + const headerBuffer = Buffer.from([ + CommandClasses["Transport Service"], + TransportServiceCommand.SubsequentSegment | raw.payload[0], + ]); + const ccBuffer = raw.payload.subarray(1, -2); + let expectedCRC = CRC16_CCITT(headerBuffer); + expectedCRC = CRC16_CCITT(ccBuffer, expectedCRC); + const actualCRC = raw.payload.readUInt16BE( + raw.payload.length - 2, + ); + validatePayload(expectedCRC === actualCRC); + const datagramSize = raw.payload.readUInt16BE(0); + const sessionId = raw.payload[2] >>> 4; + const datagramOffset = ((raw.payload[2] & 0b111) << 8) + + raw.payload[3]; + let payloadOffset = 4; + + // If there is a header extension, read it + const hasHeaderExtension = !!(raw.payload[2] & 0b1000); + let headerExtension: Buffer | undefined; + + if (hasHeaderExtension) { + const extLength = raw.payload[4]; + headerExtension = raw.payload.subarray(5, 5 + extLength); + payloadOffset += 1 + extLength; } + const partialDatagram: Buffer = raw.payload.subarray(payloadOffset, -2); + + // A node supporting the Transport Service Command Class, version 2 + // MUST NOT send Transport Service segments with the Payload field longer than 39 bytes. + validatePayload(partialDatagram.length <= MAX_SEGMENT_SIZE); + + return new TransportServiceCCSubsequentSegment({ + nodeId: ctx.sourceNodeId, + datagramSize, + sessionId, + datagramOffset, + headerExtension, + partialDatagram, + }); } public datagramSize: number; @@ -365,12 +356,8 @@ export class TransportServiceCCSubsequentSegment extends TransportServiceCC { } // and deserialize the CC - this._encapsulated = CommandClass.from({ - data: datagram, - fromEncapsulation: true, - encapCC: this, - context: ctx, - }); + this._encapsulated = CommandClass.parse(datagram, ctx); + this._encapsulated.encapsulatingCC = this as any; } public serialize(ctx: CCEncodingContext): Buffer { @@ -437,9 +424,7 @@ export class TransportServiceCCSubsequentSegment extends TransportServiceCC { } // @publicAPI -export interface TransportServiceCCSegmentRequestOptions - extends CCCommandOptions -{ +export interface TransportServiceCCSegmentRequestOptions { sessionId: number; datagramOffset: number; } @@ -463,20 +448,27 @@ function testResponseForSegmentRequest( @expectedCCResponse(TransportServiceCC, testResponseForSegmentRequest) export class TransportServiceCCSegmentRequest extends TransportServiceCC { public constructor( - options: - | CommandClassDeserializationOptions - | TransportServiceCCSegmentRequestOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.sessionId = this.payload[1] >>> 4; - this.datagramOffset = ((this.payload[1] & 0b111) << 8) - + this.payload[2]; - } else { - this.sessionId = options.sessionId; - this.datagramOffset = options.datagramOffset; - } + this.sessionId = options.sessionId; + this.datagramOffset = options.datagramOffset; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TransportServiceCCSegmentRequest { + validatePayload(raw.payload.length >= 3); + const sessionId = raw.payload[1] >>> 4; + const datagramOffset = ((raw.payload[1] & 0b111) << 8) + + raw.payload[2]; + + return new TransportServiceCCSegmentRequest({ + nodeId: ctx.sourceNodeId, + sessionId, + datagramOffset, + }); } public sessionId: number; @@ -503,26 +495,30 @@ export class TransportServiceCCSegmentRequest extends TransportServiceCC { } // @publicAPI -export interface TransportServiceCCSegmentCompleteOptions - extends CCCommandOptions -{ +export interface TransportServiceCCSegmentCompleteOptions { sessionId: number; } @CCCommand(TransportServiceCommand.SegmentComplete) export class TransportServiceCCSegmentComplete extends TransportServiceCC { public constructor( - options: - | CommandClassDeserializationOptions - | TransportServiceCCSegmentCompleteOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.sessionId = this.payload[1] >>> 4; - } else { - this.sessionId = options.sessionId; - } + this.sessionId = options.sessionId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TransportServiceCCSegmentComplete { + validatePayload(raw.payload.length >= 2); + const sessionId = raw.payload[1] >>> 4; + + return new TransportServiceCCSegmentComplete({ + nodeId: ctx.sourceNodeId, + sessionId, + }); } public sessionId: number; @@ -541,24 +537,30 @@ export class TransportServiceCCSegmentComplete extends TransportServiceCC { } // @publicAPI -export interface TransportServiceCCSegmentWaitOptions extends CCCommandOptions { +export interface TransportServiceCCSegmentWaitOptions { pendingSegments: number; } @CCCommand(TransportServiceCommand.SegmentWait) export class TransportServiceCCSegmentWait extends TransportServiceCC { public constructor( - options: - | CommandClassDeserializationOptions - | TransportServiceCCSegmentWaitOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.pendingSegments = this.payload[1]; - } else { - this.pendingSegments = options.pendingSegments; - } + this.pendingSegments = options.pendingSegments; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): TransportServiceCCSegmentWait { + validatePayload(raw.payload.length >= 2); + const pendingSegments = raw.payload[1]; + + return new TransportServiceCCSegmentWait({ + nodeId: ctx.sourceNodeId, + pendingSegments, + }); } public pendingSegments: number; diff --git a/packages/cc/src/cc/UserCodeCC.ts b/packages/cc/src/cc/UserCodeCC.ts index 3569d7a22224..8ee665a9ee0b 100644 --- a/packages/cc/src/cc/UserCodeCC.ts +++ b/packages/cc/src/cc/UserCodeCC.ts @@ -7,6 +7,7 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, encodeBitMask, @@ -17,6 +18,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetSupportedCCVersion, GetValueDB, } from "@zwave-js/host/safe"; @@ -41,14 +43,12 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, getEffectiveCCVersion, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -452,7 +452,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCUsersNumberGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< UserCodeCCUsersNumberReport @@ -485,7 +485,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCExtendedUserCodeGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, userId, reportMore: multiple, }); @@ -510,7 +510,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, userId, }); const response = await this.host.sendCommand( @@ -550,7 +550,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, userId, userIdStatus, userCode, @@ -661,7 +661,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { } const cc = new UserCodeCCExtendedUserCodeSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, userCodes: codes, }); return this.host.sendCommand(cc, this.commandOptions); @@ -695,7 +695,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, userId, userIdStatus: UserIDStatus.Available, }); @@ -712,7 +712,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< UserCodeCCCapabilitiesReport @@ -742,7 +742,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCKeypadModeGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< UserCodeCCKeypadModeReport @@ -786,7 +786,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCKeypadModeSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, keypadMode, }); @@ -801,7 +801,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCAdminCodeGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< UserCodeCCAdminCodeReport @@ -854,7 +854,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCAdminCodeSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, adminCode, }); @@ -869,7 +869,7 @@ export class UserCodeCCAPI extends PhysicalCCAPI { const cc = new UserCodeCCUserCodeChecksumGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< UserCodeCCUserCodeChecksumReport @@ -1234,60 +1234,70 @@ export type UserCodeCCSetOptions = @useSupervision() export class UserCodeCCSet extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & UserCodeCCSetOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.userIdStatus = this.payload[1]; - if ( - this.userIdStatus !== UserIDStatus.Available - && this.userIdStatus !== UserIDStatus.StatusNotAvailable - ) { - this.userCode = this.payload.subarray(2); - } else { - this.userCode = Buffer.alloc(4, 0x00); - } - } else { - this.userId = options.userId; - this.userIdStatus = options.userIdStatus; + this.userId = options.userId; + this.userIdStatus = options.userIdStatus; - // Validate options - if (this.userId < 0) { - throw new ZWaveError( - `${this.constructor.name}: The user ID must be between greater than 0.`, - ZWaveErrorCodes.Argument_Invalid, - ); - } else if ( - this.userId === 0 - && this.userIdStatus !== UserIDStatus.Available - ) { + // Validate options + if (this.userId < 0) { + throw new ZWaveError( + `${this.constructor.name}: The user ID must be between greater than 0.`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if ( + this.userId === 0 + && this.userIdStatus !== UserIDStatus.Available + ) { + throw new ZWaveError( + `${this.constructor.name}: User ID 0 may only be used to clear all user codes`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (this.userIdStatus === UserIDStatus.Available) { + this.userCode = "\0".repeat(4); + } else { + this.userCode = options.userCode!; + // Specs say ASCII 0-9, manufacturers don't care :) + if (this.userCode.length < 4 || this.userCode.length > 10) { throw new ZWaveError( - `${this.constructor.name}: User ID 0 may only be used to clear all user codes`, + `${this.constructor.name}: The user code must have a length of 4 to 10 ${ + typeof this.userCode === "string" + ? "characters" + : "bytes" + }`, ZWaveErrorCodes.Argument_Invalid, ); - } else if (this.userIdStatus === UserIDStatus.Available) { - this.userCode = "\0".repeat(4); - } else { - this.userCode = options.userCode!; - // Specs say ASCII 0-9, manufacturers don't care :) - if (this.userCode.length < 4 || this.userCode.length > 10) { - throw new ZWaveError( - `${this.constructor.name}: The user code must have a length of 4 to 10 ${ - typeof this.userCode === "string" - ? "characters" - : "bytes" - }`, - ZWaveErrorCodes.Argument_Invalid, - ); - } } } } + public static from(raw: CCRaw, ctx: CCParsingContext): UserCodeCCSet { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const userIdStatus: UserIDStatus = raw.payload[1]; + if (userIdStatus === UserIDStatus.StatusNotAvailable) { + validatePayload.fail("Invalid user ID status"); + } + + if (userIdStatus === UserIDStatus.Available) { + return new UserCodeCCSet({ + nodeId: ctx.sourceNodeId, + userId, + userIdStatus, + }); + } + + const userCode = raw.payload.subarray(2); + + return new UserCodeCCSet({ + nodeId: ctx.sourceNodeId, + userId, + userIdStatus, + userCode, + }); + } + public userId: number; public userIdStatus: UserIDStatus; public userCode: string | Buffer; @@ -1315,7 +1325,7 @@ export class UserCodeCCSet extends UserCodeCC { } // @publicAPI -export interface UserCodeCCReportOptions extends CCCommandOptions { +export interface UserCodeCCReportOptions { userId: number; userIdStatus: UserIDStatus; userCode?: string | Buffer; @@ -1326,48 +1336,56 @@ export class UserCodeCCReport extends UserCodeCC implements NotificationEventPayload { public constructor( - options: CommandClassDeserializationOptions | UserCodeCCReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userId = this.payload[0]; - this.userIdStatus = this.payload[1]; + this.userId = options.userId; + this.userIdStatus = options.userIdStatus; + this.userCode = options.userCode ?? ""; + } - if ( - this.payload.length === 2 - && (this.userIdStatus === UserIDStatus.Available - || this.userIdStatus === UserIDStatus.StatusNotAvailable) - ) { - // The user code is not set or not available and this report contains no user code - this.userCode = ""; - } else { - // The specs require the user code to be at least 4 digits - validatePayload(this.payload.length >= 6); + public static from(raw: CCRaw, ctx: CCParsingContext): UserCodeCCReport { + validatePayload(raw.payload.length >= 2); + const userId = raw.payload[0]; + const userIdStatus: UserIDStatus = raw.payload[1]; + let userCode: string | Buffer; - let userCodeBuffer = this.payload.subarray(2); - // Specs say infer user code from payload length, manufacturers send zero-padded strings - while (userCodeBuffer.at(-1) === 0) { - userCodeBuffer = userCodeBuffer.subarray(0, -1); - } - // Specs say ASCII 0-9, manufacturers don't care :) - // Thus we check if the code is printable using ASCII, if not keep it as a Buffer - const userCodeString = userCodeBuffer.toString("utf8"); - if (isPrintableASCII(userCodeString)) { - this.userCode = userCodeString; - } else if (isPrintableASCIIWithWhitespace(userCodeString)) { - // Ignore leading and trailing whitespace in V1 reports if the rest is ASCII - this.userCode = userCodeString.trim(); - } else { - this.userCode = userCodeBuffer; - } - } + if ( + raw.payload.length === 2 + && (userIdStatus === UserIDStatus.Available + || userIdStatus === UserIDStatus.StatusNotAvailable) + ) { + // The user code is not set or not available and this report contains no user code + userCode = ""; } else { - this.userId = options.userId; - this.userIdStatus = options.userIdStatus; - this.userCode = options.userCode ?? ""; + // The specs require the user code to be at least 4 digits + validatePayload(raw.payload.length >= 6); + + let userCodeBuffer = raw.payload.subarray(2); + // Specs say infer user code from payload length, manufacturers send zero-padded strings + while (userCodeBuffer.at(-1) === 0) { + userCodeBuffer = userCodeBuffer.subarray(0, -1); + } + // Specs say ASCII 0-9, manufacturers don't care :) + // Thus we check if the code is printable using ASCII, if not keep it as a Buffer + const userCodeString = userCodeBuffer.toString("utf8"); + if (isPrintableASCII(userCodeString)) { + userCode = userCodeString; + } else if (isPrintableASCIIWithWhitespace(userCodeString)) { + // Ignore leading and trailing whitespace in V1 reports if the rest is ASCII + userCode = userCodeString.trim(); + } else { + userCode = userCodeBuffer; + } } + + return new UserCodeCCReport({ + nodeId: ctx.sourceNodeId, + userId, + userIdStatus, + userCode, + }); } public readonly userId: number; @@ -1420,7 +1438,7 @@ export class UserCodeCCReport extends UserCodeCC } // @publicAPI -export interface UserCodeCCGetOptions extends CCCommandOptions { +export interface UserCodeCCGetOptions { userId: number; } @@ -1428,15 +1446,20 @@ export interface UserCodeCCGetOptions extends CCCommandOptions { @expectedCCResponse(UserCodeCCReport) export class UserCodeCCGet extends UserCodeCC { public constructor( - options: CommandClassDeserializationOptions | UserCodeCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.userId = this.payload[0]; - } else { - this.userId = options.userId; - } + this.userId = options.userId; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): UserCodeCCGet { + validatePayload(raw.payload.length >= 1); + const userId = raw.payload[0]; + + return new UserCodeCCGet({ + nodeId: ctx.sourceNodeId, + userId, + }); } public userId: number; @@ -1455,31 +1478,40 @@ export class UserCodeCCGet extends UserCodeCC { } // @publicAPI -export interface UserCodeCCUsersNumberReportOptions extends CCCommandOptions { +export interface UserCodeCCUsersNumberReportOptions { supportedUsers: number; } @CCCommand(UserCodeCommand.UsersNumberReport) export class UserCodeCCUsersNumberReport extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCUsersNumberReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - if (this.payload.length >= 3) { - // V2+ - this.supportedUsers = this.payload.readUInt16BE(1); - } else { - // V1 - this.supportedUsers = this.payload[0]; - } + this.supportedUsers = options.supportedUsers; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCUsersNumberReport { + validatePayload(raw.payload.length >= 1); + + let supportedUsers; + + if (raw.payload.length >= 3) { + // V2+ + supportedUsers = raw.payload.readUInt16BE(1); } else { - this.supportedUsers = options.supportedUsers; + // V1 + supportedUsers = raw.payload[0]; } + + return new UserCodeCCUsersNumberReport({ + nodeId: ctx.sourceNodeId, + supportedUsers, + }); } @ccValue(UserCodeCCValues.supportedUsers) @@ -1506,7 +1538,7 @@ export class UserCodeCCUsersNumberReport extends UserCodeCC { export class UserCodeCCUsersNumberGet extends UserCodeCC {} // @publicAPI -export interface UserCodeCCCapabilitiesReportOptions extends CCCommandOptions { +export interface UserCodeCCCapabilitiesReportOptions { supportsAdminCode: boolean; supportsAdminCodeDeactivation: boolean; supportsUserCodeChecksum: boolean; @@ -1520,79 +1552,92 @@ export interface UserCodeCCCapabilitiesReportOptions extends CCCommandOptions { @CCCommand(UserCodeCommand.CapabilitiesReport) export class UserCodeCCCapabilitiesReport extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCCapabilitiesReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - let offset = 0; + this.supportsAdminCode = options.supportsAdminCode; + this.supportsAdminCodeDeactivation = + options.supportsAdminCodeDeactivation; + this.supportsUserCodeChecksum = options.supportsUserCodeChecksum; + this.supportsMultipleUserCodeReport = + options.supportsMultipleUserCodeReport; + this.supportsMultipleUserCodeSet = options.supportsMultipleUserCodeSet; + this.supportedUserIDStatuses = options.supportedUserIDStatuses; + this.supportedKeypadModes = options.supportedKeypadModes; + this.supportedASCIIChars = options.supportedASCIIChars; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCCapabilitiesReport { + let offset = 0; + + validatePayload(raw.payload.length >= offset + 1); + const supportsAdminCode = !!(raw.payload[offset] & 0b100_00000); + const supportsAdminCodeDeactivation = !!( + raw.payload[offset] & 0b010_00000 + ); + const statusBitMaskLength = raw.payload[offset] & 0b000_11111; + offset += 1; - validatePayload(this.payload.length >= offset + 1); - this.supportsAdminCode = !!(this.payload[offset] & 0b100_00000); - this.supportsAdminCodeDeactivation = !!( - this.payload[offset] & 0b010_00000 - ); - const statusBitMaskLength = this.payload[offset] & 0b000_11111; - offset += 1; + validatePayload( + raw.payload.length >= offset + statusBitMaskLength + 1, + ); + const supportedUserIDStatuses: UserIDStatus[] = parseBitMask( + raw.payload.subarray(offset, offset + statusBitMaskLength), + UserIDStatus.Available, + ); - validatePayload( - this.payload.length >= offset + statusBitMaskLength + 1, - ); - this.supportedUserIDStatuses = parseBitMask( - this.payload.subarray(offset, offset + statusBitMaskLength), - UserIDStatus.Available, - ); - offset += statusBitMaskLength; + offset += statusBitMaskLength; + const supportsUserCodeChecksum = !!( + raw.payload[offset] & 0b100_00000 + ); + const supportsMultipleUserCodeReport = !!( + raw.payload[offset] & 0b010_00000 + ); + const supportsMultipleUserCodeSet = !!( + raw.payload[offset] & 0b001_00000 + ); + const keypadModesBitMaskLength = raw.payload[offset] & 0b000_11111; + offset += 1; - this.supportsUserCodeChecksum = !!( - this.payload[offset] & 0b100_00000 - ); - this.supportsMultipleUserCodeReport = !!( - this.payload[offset] & 0b010_00000 - ); - this.supportsMultipleUserCodeSet = !!( - this.payload[offset] & 0b001_00000 - ); - const keypadModesBitMaskLength = this.payload[offset] & 0b000_11111; - offset += 1; + validatePayload( + raw.payload.length >= offset + keypadModesBitMaskLength + 1, + ); + const supportedKeypadModes: KeypadMode[] = parseBitMask( + raw.payload.subarray( + offset, + offset + keypadModesBitMaskLength, + ), + KeypadMode.Normal, + ); - validatePayload( - this.payload.length >= offset + keypadModesBitMaskLength + 1, - ); - this.supportedKeypadModes = parseBitMask( - this.payload.subarray( - offset, - offset + keypadModesBitMaskLength, - ), - KeypadMode.Normal, - ); - offset += keypadModesBitMaskLength; + offset += keypadModesBitMaskLength; - const keysBitMaskLength = this.payload[offset] & 0b000_11111; - offset += 1; + const keysBitMaskLength = raw.payload[offset] & 0b000_11111; + offset += 1; - validatePayload(this.payload.length >= offset + keysBitMaskLength); - this.supportedASCIIChars = Buffer.from( - parseBitMask( - this.payload.subarray(offset, offset + keysBitMaskLength), - 0, - ), - ).toString("ascii"); - } else { - this.supportsAdminCode = options.supportsAdminCode; - this.supportsAdminCodeDeactivation = - options.supportsAdminCodeDeactivation; - this.supportsUserCodeChecksum = options.supportsUserCodeChecksum; - this.supportsMultipleUserCodeReport = - options.supportsMultipleUserCodeReport; - this.supportsMultipleUserCodeSet = - options.supportsMultipleUserCodeSet; - this.supportedUserIDStatuses = options.supportedUserIDStatuses; - this.supportedKeypadModes = options.supportedKeypadModes; - this.supportedASCIIChars = options.supportedASCIIChars; - } + validatePayload(raw.payload.length >= offset + keysBitMaskLength); + const supportedASCIIChars = Buffer.from( + parseBitMask( + raw.payload.subarray(offset, offset + keysBitMaskLength), + 0, + ), + ).toString("ascii"); + + return new UserCodeCCCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + supportsAdminCode, + supportsAdminCodeDeactivation, + supportedUserIDStatuses, + supportsUserCodeChecksum, + supportsMultipleUserCodeReport, + supportsMultipleUserCodeSet, + supportedKeypadModes, + supportedASCIIChars, + }); } @ccValue(UserCodeCCValues.supportsAdminCode) @@ -1688,7 +1733,7 @@ export class UserCodeCCCapabilitiesReport extends UserCodeCC { export class UserCodeCCCapabilitiesGet extends UserCodeCC {} // @publicAPI -export interface UserCodeCCKeypadModeSetOptions extends CCCommandOptions { +export interface UserCodeCCKeypadModeSetOptions { keypadMode: KeypadMode; } @@ -1696,17 +1741,23 @@ export interface UserCodeCCKeypadModeSetOptions extends CCCommandOptions { @useSupervision() export class UserCodeCCKeypadModeSet extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCKeypadModeSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.keypadMode = this.payload[0]; - } else { - this.keypadMode = options.keypadMode; - } + this.keypadMode = options.keypadMode; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCKeypadModeSet { + validatePayload(raw.payload.length >= 1); + const keypadMode: KeypadMode = raw.payload[0]; + + return new UserCodeCCKeypadModeSet({ + nodeId: ctx.sourceNodeId, + keypadMode, + }); } public keypadMode: KeypadMode; @@ -1725,24 +1776,30 @@ export class UserCodeCCKeypadModeSet extends UserCodeCC { } // @publicAPI -export interface UserCodeCCKeypadModeReportOptions extends CCCommandOptions { +export interface UserCodeCCKeypadModeReportOptions { keypadMode: KeypadMode; } @CCCommand(UserCodeCommand.KeypadModeReport) export class UserCodeCCKeypadModeReport extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCKeypadModeReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.keypadMode = this.payload[0]; - } else { - this.keypadMode = options.keypadMode; - } + this.keypadMode = options.keypadMode; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCKeypadModeReport { + validatePayload(raw.payload.length >= 1); + const keypadMode: KeypadMode = raw.payload[0]; + + return new UserCodeCCKeypadModeReport({ + nodeId: ctx.sourceNodeId, + keypadMode, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -1789,7 +1846,7 @@ export class UserCodeCCKeypadModeReport extends UserCodeCC { export class UserCodeCCKeypadModeGet extends UserCodeCC {} // @publicAPI -export interface UserCodeCCAdminCodeSetOptions extends CCCommandOptions { +export interface UserCodeCCAdminCodeSetOptions { adminCode: string; } @@ -1797,21 +1854,27 @@ export interface UserCodeCCAdminCodeSetOptions extends CCCommandOptions { @useSupervision() export class UserCodeCCAdminCodeSet extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCAdminCodeSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const codeLength = this.payload[0] & 0b1111; - validatePayload(this.payload.length >= 1 + codeLength); - this.adminCode = this.payload - .subarray(1, 1 + codeLength) - .toString("ascii"); - } else { - this.adminCode = options.adminCode; - } + this.adminCode = options.adminCode; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCAdminCodeSet { + validatePayload(raw.payload.length >= 1); + const codeLength = raw.payload[0] & 0b1111; + validatePayload(raw.payload.length >= 1 + codeLength); + const adminCode = raw.payload + .subarray(1, 1 + codeLength) + .toString("ascii"); + + return new UserCodeCCAdminCodeSet({ + nodeId: ctx.sourceNodeId, + adminCode, + }); } public adminCode: string; @@ -1833,28 +1896,34 @@ export class UserCodeCCAdminCodeSet extends UserCodeCC { } // @publicAPI -export interface UserCodeCCAdminCodeReportOptions extends CCCommandOptions { +export interface UserCodeCCAdminCodeReportOptions { adminCode: string; } @CCCommand(UserCodeCommand.AdminCodeReport) export class UserCodeCCAdminCodeReport extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCAdminCodeReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const codeLength = this.payload[0] & 0b1111; - validatePayload(this.payload.length >= 1 + codeLength); - this.adminCode = this.payload - .subarray(1, 1 + codeLength) - .toString("ascii"); - } else { - this.adminCode = options.adminCode; - } + this.adminCode = options.adminCode; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCAdminCodeReport { + validatePayload(raw.payload.length >= 1); + const codeLength = raw.payload[0] & 0b1111; + validatePayload(raw.payload.length >= 1 + codeLength); + const adminCode = raw.payload + .subarray(1, 1 + codeLength) + .toString("ascii"); + + return new UserCodeCCAdminCodeReport({ + nodeId: ctx.sourceNodeId, + adminCode, + }); } @ccValue(UserCodeCCValues.adminCode) @@ -1881,26 +1950,30 @@ export class UserCodeCCAdminCodeReport extends UserCodeCC { export class UserCodeCCAdminCodeGet extends UserCodeCC {} // @publicAPI -export interface UserCodeCCUserCodeChecksumReportOptions - extends CCCommandOptions -{ +export interface UserCodeCCUserCodeChecksumReportOptions { userCodeChecksum: number; } @CCCommand(UserCodeCommand.UserCodeChecksumReport) export class UserCodeCCUserCodeChecksumReport extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCUserCodeChecksumReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.userCodeChecksum = this.payload.readUInt16BE(0); - } else { - this.userCodeChecksum = options.userCodeChecksum; - } + this.userCodeChecksum = options.userCodeChecksum; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCUserCodeChecksumReport { + validatePayload(raw.payload.length >= 2); + const userCodeChecksum = raw.payload.readUInt16BE(0); + + return new UserCodeCCUserCodeChecksumReport({ + nodeId: ctx.sourceNodeId, + userCodeChecksum, + }); } @ccValue(UserCodeCCValues.userCodeChecksum) @@ -1925,7 +1998,7 @@ export class UserCodeCCUserCodeChecksumReport extends UserCodeCC { export class UserCodeCCUserCodeChecksumGet extends UserCodeCC {} // @publicAPI -export interface UserCodeCCExtendedUserCodeSetOptions extends CCCommandOptions { +export interface UserCodeCCExtendedUserCodeSetOptions { userCodes: UserCodeCCSetOptions[]; } @@ -1939,20 +2012,25 @@ export interface UserCode { @useSupervision() export class UserCodeCCExtendedUserCodeSet extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCExtendedUserCodeSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.userCodes = options.userCodes; - } + this.userCodes = options.userCodes; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): UserCodeCCExtendedUserCodeSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new UserCodeCCExtendedUserCodeSet({ + // nodeId: ctx.sourceNodeId, + // }); } public userCodes: UserCodeCCSetOptions[]; @@ -1996,28 +2074,48 @@ export class UserCodeCCExtendedUserCodeSet extends UserCodeCC { } } +// @publicAPI +export interface UserCodeCCExtendedUserCodeReportOptions { + userCodes: UserCode[]; + nextUserId: number; +} + @CCCommand(UserCodeCommand.ExtendedUserCodeReport) export class UserCodeCCExtendedUserCodeReport extends UserCodeCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 1); - const numCodes = this.payload[0]; + + // TODO: Check implementation: + this.userCodes = options.userCodes; + this.nextUserId = options.nextUserId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): UserCodeCCExtendedUserCodeReport { + validatePayload(raw.payload.length >= 1); + const numCodes = raw.payload[0]; let offset = 1; const userCodes: UserCode[] = []; // parse each user code for (let i = 0; i < numCodes; i++) { const { code, bytesRead } = parseExtendedUserCode( - this.payload.subarray(offset), + raw.payload.subarray(offset), ); userCodes.push(code); offset += bytesRead; } - this.userCodes = userCodes; + validatePayload(raw.payload.length >= offset + 2); + const nextUserId = raw.payload.readUInt16BE(offset); - validatePayload(this.payload.length >= offset + 2); - this.nextUserId = this.payload.readUInt16BE(offset); + return new UserCodeCCExtendedUserCodeReport({ + nodeId: ctx.sourceNodeId, + userCodes, + nextUserId, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -2056,7 +2154,7 @@ export class UserCodeCCExtendedUserCodeReport extends UserCodeCC { } // @publicAPI -export interface UserCodeCCExtendedUserCodeGetOptions extends CCCommandOptions { +export interface UserCodeCCExtendedUserCodeGetOptions { userId: number; reportMore?: boolean; } @@ -2065,21 +2163,26 @@ export interface UserCodeCCExtendedUserCodeGetOptions extends CCCommandOptions { @expectedCCResponse(UserCodeCCExtendedUserCodeReport) export class UserCodeCCExtendedUserCodeGet extends UserCodeCC { public constructor( - options: - | CommandClassDeserializationOptions - | UserCodeCCExtendedUserCodeGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.userId = options.userId; - this.reportMore = !!options.reportMore; - } + this.userId = options.userId; + this.reportMore = !!options.reportMore; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): UserCodeCCExtendedUserCodeGet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new UserCodeCCExtendedUserCodeGet({ + // nodeId: ctx.sourceNodeId, + // }); } public userId: number; diff --git a/packages/cc/src/cc/VersionCC.ts b/packages/cc/src/cc/VersionCC.ts index bda8e5f7b67e..13c0ac0a251b 100644 --- a/packages/cc/src/cc/VersionCC.ts +++ b/packages/cc/src/cc/VersionCC.ts @@ -6,6 +6,7 @@ import { type MessageRecord, SecurityClass, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, ZWaveLibraryTypes, @@ -15,16 +16,18 @@ import { securityClassOrder, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -242,7 +245,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -264,7 +267,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -281,7 +284,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCCommandClassGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, requestedCC, }); const response = await this.host.sendCommand< @@ -322,7 +325,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCCommandClassReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, requestedCC, ccVersion, }); @@ -338,7 +341,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< VersionCCCapabilitiesReport @@ -359,7 +362,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCCapabilitiesReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, // At this time, we do not support responding to Z-Wave Software Get supportsZWaveSoftwareGet: false, }); @@ -375,7 +378,7 @@ export class VersionCCAPI extends PhysicalCCAPI { const cc = new VersionCCZWaveSoftwareGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< VersionCCZWaveSoftwareReport @@ -648,57 +651,63 @@ export interface VersionCCReportOptions { @CCCommand(VersionCommand.Report) export class VersionCCReport extends VersionCC { public constructor( - options: - | CommandClassDeserializationOptions - | (VersionCCReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 5); - this.libraryType = this.payload[0]; - this.protocolVersion = `${this.payload[1]}.${this.payload[2]}`; - this.firmwareVersions = [`${this.payload[3]}.${this.payload[4]}`]; - if (this.payload.length >= 7) { - // V2+ - this.hardwareVersion = this.payload[5]; - const additionalFirmwares = this.payload[6]; - validatePayload( - this.payload.length >= 7 + 2 * additionalFirmwares, - ); - for (let i = 0; i < additionalFirmwares; i++) { - this.firmwareVersions.push( - `${this.payload[7 + 2 * i]}.${ - this.payload[7 + 2 * i + 1] - }`, - ); - } - } - } else { - if (!/^\d+\.\d+(\.\d+)?$/.test(options.protocolVersion)) { - throw new ZWaveError( - `protocolVersion must be a string in the format "major.minor" or "major.minor.patch", received "${options.protocolVersion}"`, - ZWaveErrorCodes.Argument_Invalid, - ); - } else if ( - !options.firmwareVersions.every((fw) => - /^\d+\.\d+(\.\d+)?$/.test(fw) - ) - ) { - throw new ZWaveError( - `firmwareVersions must be an array of strings in the format "major.minor" or "major.minor.patch", received "${ - JSON.stringify( - options.firmwareVersions, - ) - }"`, - ZWaveErrorCodes.Argument_Invalid, + if (!/^\d+\.\d+(\.\d+)?$/.test(options.protocolVersion)) { + throw new ZWaveError( + `protocolVersion must be a string in the format "major.minor" or "major.minor.patch", received "${options.protocolVersion}"`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if ( + !options.firmwareVersions.every((fw) => + /^\d+\.\d+(\.\d+)?$/.test(fw) + ) + ) { + throw new ZWaveError( + `firmwareVersions must be an array of strings in the format "major.minor" or "major.minor.patch", received "${ + JSON.stringify( + options.firmwareVersions, + ) + }"`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.libraryType = options.libraryType; + this.protocolVersion = options.protocolVersion; + this.firmwareVersions = options.firmwareVersions; + this.hardwareVersion = options.hardwareVersion; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): VersionCCReport { + validatePayload(raw.payload.length >= 5); + const libraryType: ZWaveLibraryTypes = raw.payload[0]; + const protocolVersion = `${raw.payload[1]}.${raw.payload[2]}`; + const firmwareVersions = [`${raw.payload[3]}.${raw.payload[4]}`]; + + let hardwareVersion: number | undefined; + if (raw.payload.length >= 7) { + // V2+ + hardwareVersion = raw.payload[5]; + const additionalFirmwares = raw.payload[6]; + validatePayload( + raw.payload.length >= 7 + 2 * additionalFirmwares, + ); + for (let i = 0; i < additionalFirmwares; i++) { + firmwareVersions.push( + `${raw.payload[7 + 2 * i]}.${raw.payload[7 + 2 * i + 1]}`, ); } - this.libraryType = options.libraryType; - this.protocolVersion = options.protocolVersion; - this.firmwareVersions = options.firmwareVersions; - this.hardwareVersion = options.hardwareVersion; } + + return new VersionCCReport({ + nodeId: ctx.sourceNodeId, + libraryType, + protocolVersion, + firmwareVersions, + hardwareVersion, + }); } @ccValue(VersionCCValues.libraryType) @@ -769,7 +778,7 @@ export class VersionCCReport extends VersionCC { export class VersionCCGet extends VersionCC {} // @publicAPI -export interface VersionCCCommandClassReportOptions extends CCCommandOptions { +export interface VersionCCCommandClassReportOptions { requestedCC: CommandClasses; ccVersion: number; } @@ -777,19 +786,26 @@ export interface VersionCCCommandClassReportOptions extends CCCommandOptions { @CCCommand(VersionCommand.CommandClassReport) export class VersionCCCommandClassReport extends VersionCC { public constructor( - options: - | VersionCCCommandClassReportOptions - | CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.requestedCC = this.payload[0]; - this.ccVersion = this.payload[1]; - } else { - this.requestedCC = options.requestedCC; - this.ccVersion = options.ccVersion; - } + this.requestedCC = options.requestedCC; + this.ccVersion = options.ccVersion; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): VersionCCCommandClassReport { + validatePayload(raw.payload.length >= 2); + const requestedCC: CommandClasses = raw.payload[0]; + const ccVersion = raw.payload[1]; + + return new VersionCCCommandClassReport({ + nodeId: ctx.sourceNodeId, + requestedCC, + ccVersion, + }); } public ccVersion: number; @@ -812,7 +828,7 @@ export class VersionCCCommandClassReport extends VersionCC { } // @publicAPI -export interface VersionCCCommandClassGetOptions extends CCCommandOptions { +export interface VersionCCCommandClassGetOptions { requestedCC: CommandClasses; } @@ -831,17 +847,23 @@ function testResponseForVersionCommandClassGet( ) export class VersionCCCommandClassGet extends VersionCC { public constructor( - options: - | CommandClassDeserializationOptions - | VersionCCCommandClassGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.requestedCC = this.payload[0]; - } else { - this.requestedCC = options.requestedCC; - } + this.requestedCC = options.requestedCC; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): VersionCCCommandClassGet { + validatePayload(raw.payload.length >= 1); + const requestedCC: CommandClasses = raw.payload[0]; + + return new VersionCCCommandClassGet({ + nodeId: ctx.sourceNodeId, + requestedCC, + }); } public requestedCC: CommandClasses; @@ -867,19 +889,25 @@ export interface VersionCCCapabilitiesReportOptions { @CCCommand(VersionCommand.CapabilitiesReport) export class VersionCCCapabilitiesReport extends VersionCC { public constructor( - options: - | CommandClassDeserializationOptions - | (VersionCCCapabilitiesReportOptions & CCCommandOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const capabilities = this.payload[0]; - this.supportsZWaveSoftwareGet = !!(capabilities & 0b100); - } else { - this.supportsZWaveSoftwareGet = options.supportsZWaveSoftwareGet; - } + this.supportsZWaveSoftwareGet = options.supportsZWaveSoftwareGet; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): VersionCCCapabilitiesReport { + validatePayload(raw.payload.length >= 1); + const capabilities = raw.payload[0]; + const supportsZWaveSoftwareGet = !!(capabilities & 0b100); + + return new VersionCCCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + supportsZWaveSoftwareGet, + }); } @ccValue(VersionCCValues.supportsZWaveSoftwareGet) @@ -907,41 +935,92 @@ export class VersionCCCapabilitiesReport extends VersionCC { @expectedCCResponse(VersionCCCapabilitiesReport) export class VersionCCCapabilitiesGet extends VersionCC {} +// @publicAPI +export interface VersionCCZWaveSoftwareReportOptions { + sdkVersion: string; + applicationFrameworkAPIVersion: string; + applicationFrameworkBuildNumber: number; + hostInterfaceVersion: string; + hostInterfaceBuildNumber: number; + zWaveProtocolVersion: string; + zWaveProtocolBuildNumber: number; + applicationVersion: string; + applicationBuildNumber: number; +} + @CCCommand(VersionCommand.ZWaveSoftwareReport) export class VersionCCZWaveSoftwareReport extends VersionCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 23); - this.sdkVersion = parseVersion(this.payload); - this.applicationFrameworkAPIVersion = parseVersion( - this.payload.subarray(3), + // TODO: Check implementation: + this.sdkVersion = options.sdkVersion; + this.applicationFrameworkAPIVersion = + options.applicationFrameworkAPIVersion; + this.applicationFrameworkBuildNumber = + options.applicationFrameworkBuildNumber; + this.hostInterfaceVersion = options.hostInterfaceVersion; + this.hostInterfaceBuildNumber = options.hostInterfaceBuildNumber; + this.zWaveProtocolVersion = options.zWaveProtocolVersion; + this.zWaveProtocolBuildNumber = options.zWaveProtocolBuildNumber; + this.applicationVersion = options.applicationVersion; + this.applicationBuildNumber = options.applicationBuildNumber; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): VersionCCZWaveSoftwareReport { + validatePayload(raw.payload.length >= 23); + const sdkVersion = parseVersion(raw.payload); + const applicationFrameworkAPIVersion = parseVersion( + raw.payload.subarray(3), ); - if (this.applicationFrameworkAPIVersion !== "unused") { - this.applicationFrameworkBuildNumber = this.payload.readUInt16BE(6); + let applicationFrameworkBuildNumber; + if (applicationFrameworkAPIVersion !== "unused") { + applicationFrameworkBuildNumber = raw.payload.readUInt16BE(6); } else { - this.applicationFrameworkBuildNumber = 0; + applicationFrameworkBuildNumber = 0; } - this.hostInterfaceVersion = parseVersion(this.payload.subarray(8)); - if (this.hostInterfaceVersion !== "unused") { - this.hostInterfaceBuildNumber = this.payload.readUInt16BE(11); + + const hostInterfaceVersion = parseVersion(raw.payload.subarray(8)); + let hostInterfaceBuildNumber; + if (hostInterfaceVersion !== "unused") { + hostInterfaceBuildNumber = raw.payload.readUInt16BE(11); } else { - this.hostInterfaceBuildNumber = 0; + hostInterfaceBuildNumber = 0; } - this.zWaveProtocolVersion = parseVersion(this.payload.subarray(13)); - if (this.zWaveProtocolVersion !== "unused") { - this.zWaveProtocolBuildNumber = this.payload.readUInt16BE(16); + + const zWaveProtocolVersion = parseVersion(raw.payload.subarray(13)); + let zWaveProtocolBuildNumber; + if (zWaveProtocolVersion !== "unused") { + zWaveProtocolBuildNumber = raw.payload.readUInt16BE(16); } else { - this.zWaveProtocolBuildNumber = 0; + zWaveProtocolBuildNumber = 0; } - this.applicationVersion = parseVersion(this.payload.subarray(18)); - if (this.applicationVersion !== "unused") { - this.applicationBuildNumber = this.payload.readUInt16BE(21); + + const applicationVersion = parseVersion(raw.payload.subarray(18)); + let applicationBuildNumber; + if (applicationVersion !== "unused") { + applicationBuildNumber = raw.payload.readUInt16BE(21); } else { - this.applicationBuildNumber = 0; + applicationBuildNumber = 0; } + + return new VersionCCZWaveSoftwareReport({ + nodeId: ctx.sourceNodeId, + sdkVersion, + applicationFrameworkAPIVersion, + applicationFrameworkBuildNumber, + hostInterfaceVersion, + hostInterfaceBuildNumber, + zWaveProtocolVersion, + zWaveProtocolBuildNumber, + applicationVersion, + applicationBuildNumber, + }); } @ccValue(VersionCCValues.sdkVersion) diff --git a/packages/cc/src/cc/WakeUpCC.ts b/packages/cc/src/cc/WakeUpCC.ts index 1fe8a52c93c3..d209b426df5a 100644 --- a/packages/cc/src/cc/WakeUpCC.ts +++ b/packages/cc/src/cc/WakeUpCC.ts @@ -6,10 +6,15 @@ import { type SupervisionResult, TransmitOptions, ValueMetadata, + type WithAddress, supervisedCommandSucceeded, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { clamp } from "alcalzone-shared/math"; @@ -23,12 +28,10 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, type PersistValuesContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -134,7 +137,7 @@ export class WakeUpCCAPI extends CCAPI { const cc = new WakeUpCCIntervalGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< WakeUpCCIntervalReport @@ -156,7 +159,7 @@ export class WakeUpCCAPI extends CCAPI { const cc = new WakeUpCCIntervalCapabilitiesGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< WakeUpCCIntervalCapabilitiesReport @@ -184,7 +187,7 @@ export class WakeUpCCAPI extends CCAPI { const cc = new WakeUpCCIntervalSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, wakeUpInterval, controllerNodeId, }); @@ -199,7 +202,7 @@ export class WakeUpCCAPI extends CCAPI { const cc = new WakeUpCCNoMoreInformation({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); await this.host.sendCommand(cc, { ...this.commandOptions, @@ -351,7 +354,7 @@ controller node: ${wakeupResp.controllerNodeId}`; } // @publicAPI -export interface WakeUpCCIntervalSetOptions extends CCCommandOptions { +export interface WakeUpCCIntervalSetOptions { wakeUpInterval: number; controllerNodeId: number; } @@ -360,19 +363,23 @@ export interface WakeUpCCIntervalSetOptions extends CCCommandOptions { @useSupervision() export class WakeUpCCIntervalSet extends WakeUpCC { public constructor( - options: - | CommandClassDeserializationOptions - | WakeUpCCIntervalSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 4); - this.wakeUpInterval = this.payload.readUIntBE(0, 3); - this.controllerNodeId = this.payload[3]; - } else { - this.wakeUpInterval = options.wakeUpInterval; - this.controllerNodeId = options.controllerNodeId; - } + this.wakeUpInterval = options.wakeUpInterval; + this.controllerNodeId = options.controllerNodeId; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): WakeUpCCIntervalSet { + validatePayload(raw.payload.length >= 4); + const wakeUpInterval = raw.payload.readUIntBE(0, 3); + const controllerNodeId = raw.payload[3]; + + return new WakeUpCCIntervalSet({ + nodeId: ctx.sourceNodeId, + wakeUpInterval, + controllerNodeId, + }); } public wakeUpInterval: number; @@ -400,16 +407,37 @@ export class WakeUpCCIntervalSet extends WakeUpCC { } } +// @publicAPI +export interface WakeUpCCIntervalReportOptions { + wakeUpInterval: number; + controllerNodeId: number; +} + @CCCommand(WakeUpCommand.IntervalReport) export class WakeUpCCIntervalReport extends WakeUpCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - this.wakeUpInterval = this.payload.readUIntBE(0, 3); - this.controllerNodeId = this.payload[3]; + // TODO: Check implementation: + this.wakeUpInterval = options.wakeUpInterval; + this.controllerNodeId = options.controllerNodeId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): WakeUpCCIntervalReport { + validatePayload(raw.payload.length >= 4); + const wakeUpInterval = raw.payload.readUIntBE(0, 3); + const controllerNodeId = raw.payload[3]; + + return new WakeUpCCIntervalReport({ + nodeId: ctx.sourceNodeId, + wakeUpInterval, + controllerNodeId, + }); } @ccValue(WakeUpCCValues.wakeUpInterval) @@ -439,25 +467,53 @@ export class WakeUpCCWakeUpNotification extends WakeUpCC {} @CCCommand(WakeUpCommand.NoMoreInformation) export class WakeUpCCNoMoreInformation extends WakeUpCC {} +// @publicAPI +export interface WakeUpCCIntervalCapabilitiesReportOptions { + minWakeUpInterval: number; + maxWakeUpInterval: number; + defaultWakeUpInterval: number; + wakeUpIntervalSteps: number; + wakeUpOnDemandSupported: boolean; +} + @CCCommand(WakeUpCommand.IntervalCapabilitiesReport) export class WakeUpCCIntervalCapabilitiesReport extends WakeUpCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 12); - this.minWakeUpInterval = this.payload.readUIntBE(0, 3); - this.maxWakeUpInterval = this.payload.readUIntBE(3, 3); - this.defaultWakeUpInterval = this.payload.readUIntBE(6, 3); - this.wakeUpIntervalSteps = this.payload.readUIntBE(9, 3); + // TODO: Check implementation: + this.minWakeUpInterval = options.minWakeUpInterval; + this.maxWakeUpInterval = options.maxWakeUpInterval; + this.defaultWakeUpInterval = options.defaultWakeUpInterval; + this.wakeUpIntervalSteps = options.wakeUpIntervalSteps; + this.wakeUpOnDemandSupported = options.wakeUpOnDemandSupported; + } - if (this.payload.length >= 13) { + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): WakeUpCCIntervalCapabilitiesReport { + validatePayload(raw.payload.length >= 12); + const minWakeUpInterval = raw.payload.readUIntBE(0, 3); + const maxWakeUpInterval = raw.payload.readUIntBE(3, 3); + const defaultWakeUpInterval = raw.payload.readUIntBE(6, 3); + const wakeUpIntervalSteps = raw.payload.readUIntBE(9, 3); + let wakeUpOnDemandSupported = false; + if (raw.payload.length >= 13) { // V3+ - this.wakeUpOnDemandSupported = !!(this.payload[12] & 0b1); - } else { - this.wakeUpOnDemandSupported = false; + wakeUpOnDemandSupported = !!(raw.payload[12] & 0b1); } + + return new WakeUpCCIntervalCapabilitiesReport({ + nodeId: ctx.sourceNodeId, + minWakeUpInterval, + maxWakeUpInterval, + defaultWakeUpInterval, + wakeUpIntervalSteps, + wakeUpOnDemandSupported, + }); } public persistValues(ctx: PersistValuesContext): boolean { diff --git a/packages/cc/src/cc/WindowCoveringCC.ts b/packages/cc/src/cc/WindowCoveringCC.ts index 85eba7531edd..eb5e026b001c 100644 --- a/packages/cc/src/cc/WindowCoveringCC.ts +++ b/packages/cc/src/cc/WindowCoveringCC.ts @@ -6,12 +6,17 @@ import { type MessageRecord, type SupervisionResult, ValueMetadata, + type WithAddress, encodeBitMask, parseBitMask, validatePayload, } from "@zwave-js/core"; import { type MaybeNotKnown } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host"; import { getEnumMemberName, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { @@ -28,11 +33,9 @@ import { throwWrongValueType, } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -472,7 +475,7 @@ export class WindowCoveringCCAPI extends CCAPI { const cc = new WindowCoveringCCSupportedGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< WindowCoveringCCSupportedReport @@ -493,7 +496,7 @@ export class WindowCoveringCCAPI extends CCAPI { const cc = new WindowCoveringCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); const response = await this.host.sendCommand< @@ -522,7 +525,7 @@ export class WindowCoveringCCAPI extends CCAPI { const cc = new WindowCoveringCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, targetValues, duration, }); @@ -543,7 +546,7 @@ export class WindowCoveringCCAPI extends CCAPI { const cc = new WindowCoveringCCStartLevelChange({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, direction, duration, @@ -563,7 +566,7 @@ export class WindowCoveringCCAPI extends CCAPI { const cc = new WindowCoveringCCStopLevelChange({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, parameter, }); @@ -677,34 +680,37 @@ ${ } // @publicAPI -export interface WindowCoveringCCSupportedReportOptions - extends CCCommandOptions -{ +export interface WindowCoveringCCSupportedReportOptions { supportedParameters: readonly WindowCoveringParameter[]; } @CCCommand(WindowCoveringCommand.SupportedReport) export class WindowCoveringCCSupportedReport extends WindowCoveringCC { public constructor( - options: - | CommandClassDeserializationOptions - | WindowCoveringCCSupportedReportOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); + this.supportedParameters = options.supportedParameters; + } - const numBitmaskBytes = this.payload[0] & 0b1111; - validatePayload(this.payload.length >= 1 + numBitmaskBytes); - const bitmask = this.payload.subarray(1, 1 + numBitmaskBytes); + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): WindowCoveringCCSupportedReport { + validatePayload(raw.payload.length >= 1); + + const numBitmaskBytes = raw.payload[0] & 0b1111; + validatePayload(raw.payload.length >= 1 + numBitmaskBytes); + const bitmask = raw.payload.subarray(1, 1 + numBitmaskBytes); + const supportedParameters: WindowCoveringParameter[] = parseBitMask( + bitmask, + WindowCoveringParameter["Outbound Left (no position)"], + ); - this.supportedParameters = parseBitMask( - bitmask, - WindowCoveringParameter["Outbound Left (no position)"], - ); - } else { - this.supportedParameters = options.supportedParameters; - } + return new WindowCoveringCCSupportedReport({ + nodeId: ctx.sourceNodeId, + supportedParameters, + }); } @ccValue(WindowCoveringCCValues.supportedParameters) @@ -750,18 +756,46 @@ export class WindowCoveringCCSupportedReport extends WindowCoveringCC { @expectedCCResponse(WindowCoveringCCSupportedReport) export class WindowCoveringCCSupportedGet extends WindowCoveringCC {} +// @publicAPI +export interface WindowCoveringCCReportOptions { + parameter: WindowCoveringParameter; + currentValue: number; + targetValue: number; + duration: Duration; +} + @CCCommand(WindowCoveringCommand.Report) export class WindowCoveringCCReport extends WindowCoveringCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); - validatePayload(this.payload.length >= 4); - this.parameter = this.payload[0]; - this.currentValue = this.payload[1]; - this.targetValue = this.payload[2]; - this.duration = Duration.parseReport(this.payload[3]) + + // TODO: Check implementation: + this.parameter = options.parameter; + this.currentValue = options.currentValue; + this.targetValue = options.targetValue; + this.duration = options.duration; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): WindowCoveringCCReport { + validatePayload(raw.payload.length >= 4); + const parameter: WindowCoveringParameter = raw.payload[0]; + const currentValue = raw.payload[1]; + const targetValue = raw.payload[2]; + const duration = Duration.parseReport(raw.payload[3]) ?? Duration.unknown(); + + return new WindowCoveringCCReport({ + nodeId: ctx.sourceNodeId, + parameter, + currentValue, + targetValue, + duration, + }); } public readonly parameter: WindowCoveringParameter; @@ -799,7 +833,7 @@ export class WindowCoveringCCReport extends WindowCoveringCC { } // @publicAPI -export interface WindowCoveringCCGetOptions extends CCCommandOptions { +export interface WindowCoveringCCGetOptions { parameter: WindowCoveringParameter; } @@ -814,17 +848,20 @@ function testResponseForWindowCoveringGet( @expectedCCResponse(WindowCoveringCCReport, testResponseForWindowCoveringGet) export class WindowCoveringCCGet extends WindowCoveringCC { public constructor( - options: - | CommandClassDeserializationOptions - | WindowCoveringCCGetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.parameter = this.payload[0]; - } else { - this.parameter = options.parameter; - } + this.parameter = options.parameter; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): WindowCoveringCCGet { + validatePayload(raw.payload.length >= 1); + const parameter: WindowCoveringParameter = raw.payload[0]; + + return new WindowCoveringCCGet({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: WindowCoveringParameter; @@ -848,7 +885,7 @@ export class WindowCoveringCCGet extends WindowCoveringCC { } // @publicAPI -export interface WindowCoveringCCSetOptions extends CCCommandOptions { +export interface WindowCoveringCCSetOptions { targetValues: { parameter: WindowCoveringParameter; value: number; @@ -860,33 +897,41 @@ export interface WindowCoveringCCSetOptions extends CCCommandOptions { @useSupervision() export class WindowCoveringCCSet extends WindowCoveringCC { public constructor( - options: - | CommandClassDeserializationOptions - | WindowCoveringCCSetOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const numEntries = this.payload[0] & 0b11111; - - validatePayload(this.payload.length >= 1 + numEntries * 2); - this.targetValues = []; - for (let i = 0; i < numEntries; i++) { - const offset = 1 + i * 2; - this.targetValues.push({ - parameter: this.payload[offset], - value: this.payload[offset + 1], - }); - } - if (this.payload.length >= 2 + numEntries * 2) { - this.duration = Duration.parseSet( - this.payload[1 + numEntries * 2], - ); - } - } else { - this.targetValues = options.targetValues; - this.duration = Duration.from(options.duration); + this.targetValues = options.targetValues; + this.duration = Duration.from(options.duration); + } + + public static from(raw: CCRaw, ctx: CCParsingContext): WindowCoveringCCSet { + validatePayload(raw.payload.length >= 1); + const numEntries = raw.payload[0] & 0b11111; + + validatePayload(raw.payload.length >= 1 + numEntries * 2); + const targetValues: WindowCoveringCCSetOptions["targetValues"] = []; + + for (let i = 0; i < numEntries; i++) { + const offset = 1 + i * 2; + targetValues.push({ + parameter: raw.payload[offset], + value: raw.payload[offset + 1], + }); + } + + let duration: Duration | undefined; + + if (raw.payload.length >= 2 + numEntries * 2) { + duration = Duration.parseSet( + raw.payload[1 + numEntries * 2], + ); } + + return new WindowCoveringCCSet({ + nodeId: ctx.sourceNodeId, + targetValues, + duration, + }); } public targetValues: { @@ -930,9 +975,7 @@ export class WindowCoveringCCSet extends WindowCoveringCC { } // @publicAPI -export interface WindowCoveringCCStartLevelChangeOptions - extends CCCommandOptions -{ +export interface WindowCoveringCCStartLevelChangeOptions { parameter: WindowCoveringParameter; direction: keyof typeof LevelChangeDirection; duration?: Duration | string; @@ -942,23 +985,35 @@ export interface WindowCoveringCCStartLevelChangeOptions @useSupervision() export class WindowCoveringCCStartLevelChange extends WindowCoveringCC { public constructor( - options: - | CommandClassDeserializationOptions - | WindowCoveringCCStartLevelChangeOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.direction = !!(this.payload[0] & 0b0100_0000) ? "down" : "up"; - this.parameter = this.payload[1]; - if (this.payload.length >= 3) { - this.duration = Duration.parseSet(this.payload[2]); - } - } else { - this.parameter = options.parameter; - this.direction = options.direction; - this.duration = Duration.from(options.duration); + this.parameter = options.parameter; + this.direction = options.direction; + this.duration = Duration.from(options.duration); + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): WindowCoveringCCStartLevelChange { + validatePayload(raw.payload.length >= 2); + const direction = !!(raw.payload[0] & 0b0100_0000) + ? "down" + : "up"; + const parameter: WindowCoveringParameter = raw.payload[1]; + let duration: Duration | undefined; + + if (raw.payload.length >= 3) { + duration = Duration.parseSet(raw.payload[2]); } + + return new WindowCoveringCCStartLevelChange({ + nodeId: ctx.sourceNodeId, + direction, + parameter, + duration, + }); } public parameter: WindowCoveringParameter; @@ -993,9 +1048,7 @@ export class WindowCoveringCCStartLevelChange extends WindowCoveringCC { } // @publicAPI -export interface WindowCoveringCCStopLevelChangeOptions - extends CCCommandOptions -{ +export interface WindowCoveringCCStopLevelChangeOptions { parameter: WindowCoveringParameter; } @@ -1003,17 +1056,23 @@ export interface WindowCoveringCCStopLevelChangeOptions @useSupervision() export class WindowCoveringCCStopLevelChange extends WindowCoveringCC { public constructor( - options: - | CommandClassDeserializationOptions - | WindowCoveringCCStopLevelChangeOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.parameter = this.payload[0]; - } else { - this.parameter = options.parameter; - } + this.parameter = options.parameter; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): WindowCoveringCCStopLevelChange { + validatePayload(raw.payload.length >= 1); + const parameter: WindowCoveringParameter = raw.payload[0]; + + return new WindowCoveringCCStopLevelChange({ + nodeId: ctx.sourceNodeId, + parameter, + }); } public parameter: WindowCoveringParameter; diff --git a/packages/cc/src/cc/ZWavePlusCC.ts b/packages/cc/src/cc/ZWavePlusCC.ts index 71eb1db7a881..d8bf67d13daf 100644 --- a/packages/cc/src/cc/ZWavePlusCC.ts +++ b/packages/cc/src/cc/ZWavePlusCC.ts @@ -3,18 +3,21 @@ import { type MaybeNotKnown, type MessageOrCCLogEntry, MessagePriority, + type WithAddress, validatePayload, } from "@zwave-js/core/safe"; -import type { CCEncodingContext, GetValueDB } from "@zwave-js/host/safe"; +import type { + CCEncodingContext, + CCParsingContext, + GetValueDB, +} from "@zwave-js/host/safe"; import { getEnumMemberName, num2hex, pick } from "@zwave-js/shared/safe"; import { validateArgs } from "@zwave-js/transformers"; import { CCAPI, PhysicalCCAPI } from "../lib/API"; import { - type CCCommandOptions, + type CCRaw, CommandClass, - type CommandClassDeserializationOptions, type InterviewContext, - gotDeserializationOptions, } from "../lib/CommandClass"; import { API, @@ -82,7 +85,7 @@ export class ZWavePlusCCAPI extends PhysicalCCAPI { const cc = new ZWavePlusCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand( cc, @@ -105,7 +108,7 @@ export class ZWavePlusCCAPI extends PhysicalCCAPI { const cc = new ZWavePlusCCReport({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, ...options, }); await this.host.sendCommand(cc, this.commandOptions); @@ -175,25 +178,32 @@ export interface ZWavePlusCCReportOptions { @CCCommand(ZWavePlusCommand.Report) export class ZWavePlusCCReport extends ZWavePlusCC { public constructor( - options: - | CommandClassDeserializationOptions - | (CCCommandOptions & ZWavePlusCCReportOptions), + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 7); - this.zwavePlusVersion = this.payload[0]; - this.roleType = this.payload[1]; - this.nodeType = this.payload[2]; - this.installerIcon = this.payload.readUInt16BE(3); - this.userIcon = this.payload.readUInt16BE(5); - } else { - this.zwavePlusVersion = options.zwavePlusVersion; - this.roleType = options.roleType; - this.nodeType = options.nodeType; - this.installerIcon = options.installerIcon; - this.userIcon = options.userIcon; - } + this.zwavePlusVersion = options.zwavePlusVersion; + this.roleType = options.roleType; + this.nodeType = options.nodeType; + this.installerIcon = options.installerIcon; + this.userIcon = options.userIcon; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ZWavePlusCCReport { + validatePayload(raw.payload.length >= 7); + const zwavePlusVersion = raw.payload[0]; + const roleType: ZWavePlusRoleType = raw.payload[1]; + const nodeType: ZWavePlusNodeType = raw.payload[2]; + const installerIcon = raw.payload.readUInt16BE(3); + const userIcon = raw.payload.readUInt16BE(5); + + return new ZWavePlusCCReport({ + nodeId: ctx.sourceNodeId, + zwavePlusVersion, + roleType, + nodeType, + installerIcon, + userIcon, + }); } @ccValue(ZWavePlusCCValues.zwavePlusVersion) diff --git a/packages/cc/src/cc/ZWaveProtocolCC.ts b/packages/cc/src/cc/ZWaveProtocolCC.ts index cd45e039a9ae..f9b1d4a378a5 100644 --- a/packages/cc/src/cc/ZWaveProtocolCC.ts +++ b/packages/cc/src/cc/ZWaveProtocolCC.ts @@ -9,6 +9,7 @@ import { type NodeProtocolInfoAndDeviceClass, type NodeType, type ProtocolVersion, + type WithAddress, ZWaveDataRate, ZWaveError, ZWaveErrorCodes, @@ -20,13 +21,8 @@ import { parseNodeProtocolInfoAndDeviceClass, validatePayload, } from "@zwave-js/core"; -import type { CCEncodingContext } from "@zwave-js/host"; -import { - type CCCommandOptions, - CommandClass, - type CommandClassDeserializationOptions, - gotDeserializationOptions, -} from "../lib/CommandClass"; +import type { CCEncodingContext, CCParsingContext } from "@zwave-js/host"; +import { type CCRaw, CommandClass } from "../lib/CommandClass"; import { CCCommand, commandClass, @@ -69,8 +65,9 @@ export class ZWaveProtocolCC extends CommandClass { } // @publicAPI +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface ZWaveProtocolCCNodeInformationFrameOptions - extends CCCommandOptions, NodeInformationFrame + extends NodeInformationFrame {} @CCCommand(ZWaveProtocolCommand.NodeInformationFrame) @@ -78,32 +75,35 @@ export class ZWaveProtocolCCNodeInformationFrame extends ZWaveProtocolCC implements NodeInformationFrame { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCNodeInformationFrameOptions, + options: WithAddress, ) { super(options); - let nif: NodeInformationFrame; - if (gotDeserializationOptions(options)) { - nif = parseNodeInformationFrame(this.payload); - } else { - nif = options; - } + this.basicDeviceClass = options.basicDeviceClass; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + this.isListening = options.isListening; + this.isFrequentListening = options.isFrequentListening; + this.isRouting = options.isRouting; + this.supportedDataRates = options.supportedDataRates; + this.protocolVersion = options.protocolVersion; + this.optionalFunctionality = options.optionalFunctionality; + this.nodeType = options.nodeType; + this.supportsSecurity = options.supportsSecurity; + this.supportsBeaming = options.supportsBeaming; + this.supportedCCs = options.supportedCCs; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCNodeInformationFrame { + const nif = parseNodeInformationFrame(raw.payload); - 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; + return new ZWaveProtocolCCNodeInformationFrame({ + nodeId: ctx.sourceNodeId, + ...nif, + }); } public basicDeviceClass: BasicDeviceClass; @@ -133,7 +133,7 @@ export class ZWaveProtocolCCRequestNodeInformationFrame {} // @publicAPI -export interface ZWaveProtocolCCAssignIDsOptions extends CCCommandOptions { +export interface ZWaveProtocolCCAssignIDsOptions { assignedNodeId: number; homeId: number; } @@ -141,19 +141,26 @@ export interface ZWaveProtocolCCAssignIDsOptions extends CCCommandOptions { @CCCommand(ZWaveProtocolCommand.AssignIDs) export class ZWaveProtocolCCAssignIDs extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCAssignIDsOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 5); - this.assignedNodeId = this.payload[0]; - this.homeId = this.payload.readUInt32BE(1); - } else { - this.assignedNodeId = options.assignedNodeId; - this.homeId = options.homeId; - } + this.assignedNodeId = options.assignedNodeId; + this.homeId = options.homeId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCAssignIDs { + validatePayload(raw.payload.length >= 5); + const assignedNodeId = raw.payload[0]; + const homeId = raw.payload.readUInt32BE(1); + + return new ZWaveProtocolCCAssignIDs({ + nodeId: ctx.sourceNodeId, + assignedNodeId, + homeId, + }); } public assignedNodeId: number; @@ -168,9 +175,7 @@ export class ZWaveProtocolCCAssignIDs extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCFindNodesInRangeOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCFindNodesInRangeOptions { candidateNodeIds: number[]; wakeUpTime: WakeUpTime; dataRate?: ZWaveDataRate; @@ -179,45 +184,55 @@ export interface ZWaveProtocolCCFindNodesInRangeOptions @CCCommand(ZWaveProtocolCommand.FindNodesInRange) export class ZWaveProtocolCCFindNodesInRange extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCFindNodesInRangeOptions, + options: WithAddress, ) { super(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.subarray(1, 1 + bitmaskLength), - ); + this.candidateNodeIds = options.candidateNodeIds; + this.wakeUpTime = options.wakeUpTime; + this.dataRate = options.dataRate ?? ZWaveDataRate["9k6"]; + } - const rest = this.payload.subarray(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 { - validatePayload.fail("Invalid payload length"); - } - } else if (rest.length >= 1) { - this.wakeUpTime = parseWakeUpTime(rest[0]); - this.dataRate = ZWaveDataRate["9k6"]; + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCFindNodesInRange { + validatePayload(raw.payload.length >= 1); + const speedPresent = raw.payload[0] & 0b1000_0000; + const bitmaskLength = raw.payload[0] & 0b0001_1111; + + validatePayload(raw.payload.length >= 1 + bitmaskLength); + const candidateNodeIds = parseBitMask( + raw.payload.subarray(1, 1 + bitmaskLength), + ); + const rest = raw.payload.subarray(1 + bitmaskLength); + + let dataRate: ZWaveDataRate; + let wakeUpTime: WakeUpTime; + if (speedPresent) { + validatePayload(rest.length >= 1); + if (rest.length === 1) { + dataRate = rest[0] & 0b111; + wakeUpTime = WakeUpTime.None; + } else if (rest.length === 2) { + wakeUpTime = parseWakeUpTime(rest[0]); + dataRate = rest[1] & 0b111; } else { - this.wakeUpTime = WakeUpTime.None; - this.dataRate = ZWaveDataRate["9k6"]; + validatePayload.fail("Invalid payload length"); } + } else if (rest.length >= 1) { + wakeUpTime = parseWakeUpTime(rest[0]); + dataRate = ZWaveDataRate["9k6"]; } else { - this.candidateNodeIds = options.candidateNodeIds; - this.wakeUpTime = options.wakeUpTime; - this.dataRate = options.dataRate ?? ZWaveDataRate["9k6"]; + wakeUpTime = WakeUpTime.None; + dataRate = ZWaveDataRate["9k6"]; } + + return new ZWaveProtocolCCFindNodesInRange({ + nodeId: ctx.sourceNodeId, + candidateNodeIds, + dataRate, + wakeUpTime, + }); } public candidateNodeIds: number[]; @@ -237,7 +252,7 @@ export class ZWaveProtocolCCFindNodesInRange extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCRangeInfoOptions extends CCCommandOptions { +export interface ZWaveProtocolCCRangeInfoOptions { neighborNodeIds: number[]; wakeUpTime?: WakeUpTime; } @@ -245,28 +260,37 @@ export interface ZWaveProtocolCCRangeInfoOptions extends CCCommandOptions { @CCCommand(ZWaveProtocolCommand.RangeInfo) export class ZWaveProtocolCCRangeInfo extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCRangeInfoOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const bitmaskLength = this.payload[0] & 0b0001_1111; + this.neighborNodeIds = options.neighborNodeIds; + this.wakeUpTime = options.wakeUpTime; + } - validatePayload(this.payload.length >= 1 + bitmaskLength); - this.neighborNodeIds = parseBitMask( - this.payload.subarray(1, 1 + bitmaskLength), + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCRangeInfo { + validatePayload(raw.payload.length >= 1); + const bitmaskLength = raw.payload[0] & 0b0001_1111; + + validatePayload(raw.payload.length >= 1 + bitmaskLength); + const neighborNodeIds = parseBitMask( + raw.payload.subarray(1, 1 + bitmaskLength), + ); + + let wakeUpTime: WakeUpTime | undefined; + if (raw.payload.length >= 2 + bitmaskLength) { + wakeUpTime = parseWakeUpTime( + raw.payload[1 + bitmaskLength], ); - if (this.payload.length >= 2 + bitmaskLength) { - this.wakeUpTime = parseWakeUpTime( - this.payload[1 + bitmaskLength], - ); - } - } else { - this.neighborNodeIds = options.neighborNodeIds; - this.wakeUpTime = options.wakeUpTime; } + + return new ZWaveProtocolCCRangeInfo({ + nodeId: ctx.sourceNodeId, + neighborNodeIds, + wakeUpTime, + }); } public neighborNodeIds: number[]; @@ -290,26 +314,30 @@ export class ZWaveProtocolCCRangeInfo extends ZWaveProtocolCC { export class ZWaveProtocolCCGetNodesInRange extends ZWaveProtocolCC {} // @publicAPI -export interface ZWaveProtocolCCCommandCompleteOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCCommandCompleteOptions { sequenceNumber: number; } @CCCommand(ZWaveProtocolCommand.CommandComplete) export class ZWaveProtocolCCCommandComplete extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCCommandCompleteOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.sequenceNumber = this.payload[0]; - } else { - this.sequenceNumber = options.sequenceNumber; - } + this.sequenceNumber = options.sequenceNumber; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCCommandComplete { + validatePayload(raw.payload.length >= 1); + const sequenceNumber = raw.payload[0]; + + return new ZWaveProtocolCCCommandComplete({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + }); } public sequenceNumber: number; @@ -321,9 +349,7 @@ export class ZWaveProtocolCCCommandComplete extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCTransferPresentationOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCTransferPresentationOptions { supportsNWI: boolean; includeNode: boolean; excludeNode: boolean; @@ -332,28 +358,36 @@ export interface ZWaveProtocolCCTransferPresentationOptions @CCCommand(ZWaveProtocolCommand.TransferPresentation) export class ZWaveProtocolCCTransferPresentation extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCTransferPresentationOptions, + options: WithAddress, ) { super(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; + 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 static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCTransferPresentation { + validatePayload(raw.payload.length >= 1); + const option = raw.payload[0]; + const supportsNWI = !!(option & 0b0001); + const excludeNode = !!(option & 0b0010); + const includeNode = !!(option & 0b0100); + + return new ZWaveProtocolCCTransferPresentation({ + nodeId: ctx.sourceNodeId, + supportsNWI, + excludeNode, + includeNode, + }); } public supportsNWI: boolean; @@ -372,7 +406,7 @@ export class ZWaveProtocolCCTransferPresentation extends ZWaveProtocolCC { // @publicAPI export interface ZWaveProtocolCCTransferNodeInformationOptions - extends CCCommandOptions, NodeProtocolInfoAndDeviceClass + extends NodeProtocolInfoAndDeviceClass { sequenceNumber: number; sourceNodeId: number; @@ -383,38 +417,45 @@ export class ZWaveProtocolCCTransferNodeInformation extends ZWaveProtocolCC implements NodeProtocolInfoAndDeviceClass { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCTransferNodeInformationOptions, + options: WithAddress, ) { super(options); - let info: NodeProtocolInfoAndDeviceClass; - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.sequenceNumber = this.payload[0]; - this.sourceNodeId = this.payload[1]; - info = parseNodeProtocolInfoAndDeviceClass( - this.payload.subarray(2), - ).info; - } else { - this.sequenceNumber = options.sequenceNumber; - this.sourceNodeId = options.sourceNodeId; - info = options; - } + this.sequenceNumber = options.sequenceNumber; + this.sourceNodeId = options.sourceNodeId; + + this.basicDeviceClass = options.basicDeviceClass; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + this.isListening = options.isListening; + this.isFrequentListening = options.isFrequentListening; + this.isRouting = options.isRouting; + this.supportedDataRates = options.supportedDataRates; + this.protocolVersion = options.protocolVersion; + this.optionalFunctionality = options.optionalFunctionality; + this.nodeType = options.nodeType; + this.supportsSecurity = options.supportsSecurity; + this.supportsBeaming = options.supportsBeaming; + } - 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 static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCTransferNodeInformation { + validatePayload(raw.payload.length >= 2); + const sequenceNumber = raw.payload[0]; + const sourceNodeId = raw.payload[1]; + + const { info } = parseNodeProtocolInfoAndDeviceClass( + raw.payload.subarray(2), + ); + + return new ZWaveProtocolCCTransferNodeInformation({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + sourceNodeId, + ...info, + }); } public sequenceNumber: number; @@ -442,9 +483,7 @@ export class ZWaveProtocolCCTransferNodeInformation extends ZWaveProtocolCC } // @publicAPI -export interface ZWaveProtocolCCTransferRangeInformationOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCTransferRangeInformationOptions { sequenceNumber: number; testedNodeId: number; neighborNodeIds: number[]; @@ -453,25 +492,34 @@ export interface ZWaveProtocolCCTransferRangeInformationOptions @CCCommand(ZWaveProtocolCommand.TransferRangeInformation) export class ZWaveProtocolCCTransferRangeInformation extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCTransferRangeInformationOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 3); - this.sequenceNumber = this.payload[0]; - this.testedNodeId = this.payload[1]; - const bitmaskLength = this.payload[2]; - validatePayload(this.payload.length >= 3 + bitmaskLength); - this.neighborNodeIds = parseBitMask( - this.payload.subarray(3, 3 + bitmaskLength), - ); - } else { - this.sequenceNumber = options.sequenceNumber; - this.testedNodeId = options.testedNodeId; - this.neighborNodeIds = options.neighborNodeIds; - } + this.sequenceNumber = options.sequenceNumber; + this.testedNodeId = options.testedNodeId; + this.neighborNodeIds = options.neighborNodeIds; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCTransferRangeInformation { + validatePayload(raw.payload.length >= 3); + const sequenceNumber = raw.payload[0]; + const testedNodeId = raw.payload[1]; + const bitmaskLength = raw.payload[2]; + + validatePayload(raw.payload.length >= 3 + bitmaskLength); + const neighborNodeIds = parseBitMask( + raw.payload.subarray(3, 3 + bitmaskLength), + ); + + return new ZWaveProtocolCCTransferRangeInformation({ + nodeId: ctx.sourceNodeId, + sequenceNumber, + testedNodeId, + neighborNodeIds, + }); } public sequenceNumber: number; @@ -493,24 +541,30 @@ export class ZWaveProtocolCCTransferRangeInformation extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCTransferEndOptions extends CCCommandOptions { +export interface ZWaveProtocolCCTransferEndOptions { status: NetworkTransferStatus; } @CCCommand(ZWaveProtocolCommand.TransferEnd) export class ZWaveProtocolCCTransferEnd extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCTransferEndOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.status = this.payload[0]; - } else { - this.status = options.status; - } + this.status = options.status; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCTransferEnd { + validatePayload(raw.payload.length >= 1); + const status: NetworkTransferStatus = raw.payload[0]; + + return new ZWaveProtocolCCTransferEnd({ + nodeId: ctx.sourceNodeId, + status, + }); } public status: NetworkTransferStatus; @@ -522,9 +576,7 @@ export class ZWaveProtocolCCTransferEnd extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCAssignReturnRouteOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCAssignReturnRouteOptions { destinationNodeId: number; routeIndex: number; repeaters: number[]; @@ -535,36 +587,46 @@ export interface ZWaveProtocolCCAssignReturnRouteOptions @CCCommand(ZWaveProtocolCommand.AssignReturnRoute) export class ZWaveProtocolCCAssignReturnRoute extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCAssignReturnRouteOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 7); - this.destinationNodeId = this.payload[0]; - this.routeIndex = this.payload[1] >>> 4; - const numRepeaters = this.payload[1] & 0b1111; - this.repeaters = [...this.payload.subarray(2, 2 + numRepeaters)]; - const speedAndWakeup = this.payload[2 + numRepeaters]; - this.destinationSpeed = bitmask2DataRate( - (speedAndWakeup >>> 3) & 0b111, + if (options.repeaters.length > MAX_REPEATERS) { + throw new ZWaveError( + `${this.constructor.name}: too many repeaters`, + ZWaveErrorCodes.Argument_Invalid, ); - 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.destinationNodeId = options.destinationNodeId; - this.routeIndex = options.routeIndex; - this.repeaters = options.repeaters; - this.destinationWakeUp = options.destinationWakeUp; - this.destinationSpeed = options.destinationSpeed; } + + this.destinationNodeId = options.destinationNodeId; + this.routeIndex = options.routeIndex; + this.repeaters = options.repeaters; + this.destinationWakeUp = options.destinationWakeUp; + this.destinationSpeed = options.destinationSpeed; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCAssignReturnRoute { + validatePayload(raw.payload.length >= 7); + const destinationNodeId = raw.payload[0]; + const routeIndex = raw.payload[1] >>> 4; + const numRepeaters = raw.payload[1] & 0b1111; + const repeaters = [...raw.payload.subarray(2, 2 + numRepeaters)]; + const speedAndWakeup = raw.payload[2 + numRepeaters]; + const destinationSpeed = bitmask2DataRate( + (speedAndWakeup >>> 3) & 0b111, + ); + const destinationWakeUp: WakeUpTime = (speedAndWakeup >>> 1) & 0b11; + + return new ZWaveProtocolCCAssignReturnRoute({ + nodeId: ctx.sourceNodeId, + destinationNodeId, + routeIndex, + repeaters, + destinationSpeed, + destinationWakeUp, + }); } public destinationNodeId: number; @@ -589,7 +651,7 @@ export class ZWaveProtocolCCAssignReturnRoute extends ZWaveProtocolCC { // @publicAPI export interface ZWaveProtocolCCNewNodeRegisteredOptions - extends CCCommandOptions, NodeInformationFrame + extends NodeInformationFrame { newNodeId: number; } @@ -599,35 +661,40 @@ export class ZWaveProtocolCCNewNodeRegistered extends ZWaveProtocolCC implements NodeInformationFrame { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCNewNodeRegisteredOptions, + options: WithAddress, ) { super(options); - let nif: NodeInformationFrame; - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.newNodeId = this.payload[0]; - nif = parseNodeInformationFrame(this.payload.subarray(1)); - } else { - this.newNodeId = options.newNodeId; - nif = options; - } + this.newNodeId = options.newNodeId; + this.basicDeviceClass = options.basicDeviceClass; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + this.isListening = options.isListening; + this.isFrequentListening = options.isFrequentListening; + this.isRouting = options.isRouting; + this.supportedDataRates = options.supportedDataRates; + this.protocolVersion = options.protocolVersion; + this.optionalFunctionality = options.optionalFunctionality; + this.nodeType = options.nodeType; + this.supportsSecurity = options.supportsSecurity; + this.supportsBeaming = options.supportsBeaming; + this.supportedCCs = options.supportedCCs; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCNewNodeRegistered { + validatePayload(raw.payload.length >= 1); + const newNodeId = raw.payload[0]; + + const nif = parseNodeInformationFrame(raw.payload.subarray(1)); - 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; + return new ZWaveProtocolCCNewNodeRegistered({ + nodeId: ctx.sourceNodeId, + newNodeId, + ...nif, + }); } public newNodeId: number; @@ -655,9 +722,7 @@ export class ZWaveProtocolCCNewNodeRegistered extends ZWaveProtocolCC } // @publicAPI -export interface ZWaveProtocolCCNewRangeRegisteredOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCNewRangeRegisteredOptions { testedNodeId: number; neighborNodeIds: number[]; } @@ -665,22 +730,29 @@ export interface ZWaveProtocolCCNewRangeRegisteredOptions @CCCommand(ZWaveProtocolCommand.NewRangeRegistered) export class ZWaveProtocolCCNewRangeRegistered extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCNewRangeRegisteredOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.testedNodeId = this.payload[0]; - const numNeighbors = this.payload[1]; - this.neighborNodeIds = [ - ...this.payload.subarray(2, 2 + numNeighbors), - ]; - } else { - this.testedNodeId = options.testedNodeId; - this.neighborNodeIds = options.neighborNodeIds; - } + this.testedNodeId = options.testedNodeId; + this.neighborNodeIds = options.neighborNodeIds; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCNewRangeRegistered { + validatePayload(raw.payload.length >= 2); + const testedNodeId = raw.payload[0]; + const numNeighbors = raw.payload[1]; + const neighborNodeIds = [ + ...raw.payload.subarray(2, 2 + numNeighbors), + ]; + + return new ZWaveProtocolCCNewRangeRegistered({ + nodeId: ctx.sourceNodeId, + testedNodeId, + neighborNodeIds, + }); } public testedNodeId: number; @@ -697,9 +769,7 @@ export class ZWaveProtocolCCNewRangeRegistered extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCTransferNewPrimaryControllerCompleteOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCTransferNewPrimaryControllerCompleteOptions { genericDeviceClass: number; } @@ -708,17 +778,25 @@ export class ZWaveProtocolCCTransferNewPrimaryControllerComplete extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCTransferNewPrimaryControllerCompleteOptions, + options: WithAddress< + ZWaveProtocolCCTransferNewPrimaryControllerCompleteOptions + >, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.genericDeviceClass = this.payload[0]; - } else { - this.genericDeviceClass = options.genericDeviceClass; - } + this.genericDeviceClass = options.genericDeviceClass; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCTransferNewPrimaryControllerComplete { + validatePayload(raw.payload.length >= 1); + const genericDeviceClass = raw.payload[0]; + + return new ZWaveProtocolCCTransferNewPrimaryControllerComplete({ + nodeId: ctx.sourceNodeId, + genericDeviceClass, + }); } public genericDeviceClass: number; @@ -735,7 +813,7 @@ export class ZWaveProtocolCCAutomaticControllerUpdateStart {} // @publicAPI -export interface ZWaveProtocolCCSUCNodeIDOptions extends CCCommandOptions { +export interface ZWaveProtocolCCSUCNodeIDOptions { sucNodeId: number; isSIS: boolean; } @@ -743,20 +821,27 @@ export interface ZWaveProtocolCCSUCNodeIDOptions extends CCCommandOptions { @CCCommand(ZWaveProtocolCommand.SUCNodeID) export class ZWaveProtocolCCSUCNodeID extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCSUCNodeIDOptions, + options: WithAddress, ) { super(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; - } + this.sucNodeId = options.sucNodeId; + this.isSIS = options.isSIS; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCSUCNodeID { + validatePayload(raw.payload.length >= 1); + const sucNodeId = raw.payload[0]; + const capabilities = raw.payload[1] ?? 0; + const isSIS = !!(capabilities & 0b1); + + return new ZWaveProtocolCCSUCNodeID({ + nodeId: ctx.sourceNodeId, + sucNodeId, + isSIS, + }); } public sucNodeId: number; @@ -769,26 +854,32 @@ export class ZWaveProtocolCCSUCNodeID extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCSetSUCOptions extends CCCommandOptions { +export interface ZWaveProtocolCCSetSUCOptions { enableSIS: boolean; } @CCCommand(ZWaveProtocolCommand.SetSUC) export class ZWaveProtocolCCSetSUC extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCSetSUCOptions, + options: WithAddress, ) { super(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; - } + this.enableSIS = options.enableSIS; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCSetSUC { + validatePayload(raw.payload.length >= 2); + // Byte 0 must be 0x01 or ignored + const capabilities = raw.payload[1] ?? 0; + const enableSIS = !!(capabilities & 0b1); + + return new ZWaveProtocolCCSetSUC({ + nodeId: ctx.sourceNodeId, + enableSIS, + }); } public enableSIS: boolean; @@ -800,7 +891,7 @@ export class ZWaveProtocolCCSetSUC extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCSetSUCAckOptions extends CCCommandOptions { +export interface ZWaveProtocolCCSetSUCAckOptions { accepted: boolean; isSIS: boolean; } @@ -808,20 +899,27 @@ export interface ZWaveProtocolCCSetSUCAckOptions extends CCCommandOptions { @CCCommand(ZWaveProtocolCommand.SetSUCAck) export class ZWaveProtocolCCSetSUCAck extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCSetSUCAckOptions, + options: WithAddress, ) { super(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; - } + this.accepted = options.accepted; + this.isSIS = options.isSIS; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCSetSUCAck { + validatePayload(raw.payload.length >= 2); + const accepted = raw.payload[0] === 0x01; + const capabilities = raw.payload[1] ?? 0; + const isSIS = !!(capabilities & 0b1); + + return new ZWaveProtocolCCSetSUCAck({ + nodeId: ctx.sourceNodeId, + accepted, + isSIS, + }); } public accepted: boolean; @@ -842,34 +940,38 @@ export class ZWaveProtocolCCAssignSUCReturnRoute {} // @publicAPI -export interface ZWaveProtocolCCStaticRouteRequestOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCStaticRouteRequestOptions { nodeIds: number[]; } @CCCommand(ZWaveProtocolCommand.StaticRouteRequest) export class ZWaveProtocolCCStaticRouteRequest extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCStaticRouteRequestOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 5); - this.nodeIds = [...this.payload.subarray(0, 5)].filter( - (id) => id > 0 && id <= MAX_NODES, + 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, ); - } 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; } + this.nodeIds = options.nodeIds; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCStaticRouteRequest { + validatePayload(raw.payload.length >= 5); + const nodeIds = [...raw.payload.subarray(0, 5)].filter( + (id) => id > 0 && id <= MAX_NODES, + ); + + return new ZWaveProtocolCCStaticRouteRequest({ + nodeId: ctx.sourceNodeId, + nodeIds, + }); } public nodeIds: number[]; @@ -884,24 +986,27 @@ export class ZWaveProtocolCCStaticRouteRequest extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCLostOptions extends CCCommandOptions { +export interface ZWaveProtocolCCLostOptions { lostNodeId: number; } @CCCommand(ZWaveProtocolCommand.Lost) export class ZWaveProtocolCCLost extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCLostOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.lostNodeId = this.payload[0]; - } else { - this.lostNodeId = options.lostNodeId; - } + this.lostNodeId = options.lostNodeId; + } + + public static from(raw: CCRaw, ctx: CCParsingContext): ZWaveProtocolCCLost { + validatePayload(raw.payload.length >= 1); + const lostNodeId = raw.payload[0]; + + return new ZWaveProtocolCCLost({ + nodeId: ctx.sourceNodeId, + lostNodeId, + }); } public lostNodeId: number; @@ -913,27 +1018,33 @@ export class ZWaveProtocolCCLost extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCAcceptLostOptions extends CCCommandOptions { +export interface ZWaveProtocolCCAcceptLostOptions { accepted: boolean; } @CCCommand(ZWaveProtocolCommand.AcceptLost) export class ZWaveProtocolCCAcceptLost extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCAcceptLostOptions, + options: WithAddress, ) { super(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; - } + this.accepted = options.accepted; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCAcceptLost { + validatePayload(raw.payload.length >= 1); + validatePayload( + raw.payload[0] === 0x04 || raw.payload[0] === 0x05, + ); + const accepted = raw.payload[0] === 0x05; + + return new ZWaveProtocolCCAcceptLost({ + nodeId: ctx.sourceNodeId, + accepted, + }); } public accepted: boolean; @@ -945,53 +1056,61 @@ export class ZWaveProtocolCCAcceptLost extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCNOPPowerOptions extends CCCommandOptions { +export interface ZWaveProtocolCCNOPPowerOptions { powerDampening: number; } @CCCommand(ZWaveProtocolCommand.NOPPower) export class ZWaveProtocolCCNOPPower extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCNOPPowerOptions, + options: WithAddress, ) { super(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 { - validatePayload.fail("Invalid payload length!"); - } + 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; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCNOPPower { + let powerDampening; + + if (raw.payload.length >= 2) { + // Ignore byte 0 + powerDampening = raw.payload[1]; + } else if (raw.payload.length === 1) { + powerDampening = [ + 0xf0, + 0xc8, + 0xa7, + 0x91, + 0x77, + 0x67, + 0x60, + 0x46, + 0x38, + 0x35, + 0x32, + 0x30, + 0x24, + 0x22, + 0x20, + ].indexOf(raw.payload[0]); + if (powerDampening === -1) powerDampening = 0; } 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; + validatePayload.fail("Invalid payload length!"); } + + return new ZWaveProtocolCCNOPPower({ + nodeId: ctx.sourceNodeId, + powerDampening, + }); } // Power dampening in (negative) dBm. A value of 2 means -2 dBm. @@ -1004,28 +1123,34 @@ export class ZWaveProtocolCCNOPPower extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCReservedIDsOptions extends CCCommandOptions { +export interface ZWaveProtocolCCReservedIDsOptions { reservedNodeIDs: number[]; } @CCCommand(ZWaveProtocolCommand.ReservedIDs) export class ZWaveProtocolCCReservedIDs extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCReservedIDsOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - const numNodeIDs = this.payload[0]; - validatePayload(this.payload.length >= 1 + numNodeIDs); - this.reservedNodeIDs = [ - ...this.payload.subarray(1, 1 + numNodeIDs), - ]; - } else { - this.reservedNodeIDs = options.reservedNodeIDs; - } + this.reservedNodeIDs = options.reservedNodeIDs; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCReservedIDs { + validatePayload(raw.payload.length >= 1); + const numNodeIDs = raw.payload[0]; + validatePayload(raw.payload.length >= 1 + numNodeIDs); + const reservedNodeIDs = [ + ...raw.payload.subarray(1, 1 + numNodeIDs), + ]; + + return new ZWaveProtocolCCReservedIDs({ + nodeId: ctx.sourceNodeId, + reservedNodeIDs, + }); } public reservedNodeIDs: number[]; @@ -1040,7 +1165,7 @@ export class ZWaveProtocolCCReservedIDs extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCReserveNodeIDsOptions extends CCCommandOptions { +export interface ZWaveProtocolCCReserveNodeIDsOptions { numNodeIDs: number; } @@ -1048,17 +1173,23 @@ export interface ZWaveProtocolCCReserveNodeIDsOptions extends CCCommandOptions { @expectedCCResponse(ZWaveProtocolCCReservedIDs) export class ZWaveProtocolCCReserveNodeIDs extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCReserveNodeIDsOptions, + options: WithAddress, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 1); - this.numNodeIDs = this.payload[0]; - } else { - this.numNodeIDs = options.numNodeIDs; - } + this.numNodeIDs = options.numNodeIDs; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCReserveNodeIDs { + validatePayload(raw.payload.length >= 1); + const numNodeIDs = raw.payload[0]; + + return new ZWaveProtocolCCReserveNodeIDs({ + nodeId: ctx.sourceNodeId, + numNodeIDs, + }); } public numNodeIDs: number; @@ -1070,9 +1201,7 @@ export class ZWaveProtocolCCReserveNodeIDs extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCNodesExistReplyOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCNodesExistReplyOptions { nodeMaskType: number; nodeListUpdated: boolean; } @@ -1080,19 +1209,26 @@ export interface ZWaveProtocolCCNodesExistReplyOptions @CCCommand(ZWaveProtocolCommand.NodesExistReply) export class ZWaveProtocolCCNodesExistReply extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCNodesExistReplyOptions, + options: WithAddress, ) { super(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; - } + this.nodeMaskType = options.nodeMaskType; + this.nodeListUpdated = options.nodeListUpdated; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCNodesExistReply { + validatePayload(raw.payload.length >= 2); + const nodeMaskType = raw.payload[0]; + const nodeListUpdated = raw.payload[1] === 0x01; + + return new ZWaveProtocolCCNodesExistReply({ + nodeId: ctx.sourceNodeId, + nodeMaskType, + nodeListUpdated, + }); } public nodeMaskType: number; @@ -1115,7 +1251,7 @@ function testResponseForZWaveProtocolNodesExist( } // @publicAPI -export interface ZWaveProtocolCCNodesExistOptions extends CCCommandOptions { +export interface ZWaveProtocolCCNodesExistOptions { nodeMaskType: number; nodeIDs: number[]; } @@ -1127,21 +1263,28 @@ export interface ZWaveProtocolCCNodesExistOptions extends CCCommandOptions { ) export class ZWaveProtocolCCNodesExist extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCNodesExistOptions, + options: WithAddress, ) { super(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.subarray(2, 2 + numNodeIDs)]; - } else { - this.nodeMaskType = options.nodeMaskType; - this.nodeIDs = options.nodeIDs; - } + this.nodeMaskType = options.nodeMaskType; + this.nodeIDs = options.nodeIDs; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCNodesExist { + validatePayload(raw.payload.length >= 2); + const nodeMaskType = raw.payload[0]; + const numNodeIDs = raw.payload[1]; + validatePayload(raw.payload.length >= 2 + numNodeIDs); + const nodeIDs = [...raw.payload.subarray(2, 2 + numNodeIDs)]; + + return new ZWaveProtocolCCNodesExist({ + nodeId: ctx.sourceNodeId, + nodeMaskType, + nodeIDs, + }); } public nodeMaskType: number; @@ -1158,7 +1301,7 @@ export class ZWaveProtocolCCNodesExist extends ZWaveProtocolCC { } // @publicAPI -export interface ZWaveProtocolCCSetNWIModeOptions extends CCCommandOptions { +export interface ZWaveProtocolCCSetNWIModeOptions { enabled: boolean; timeoutMinutes?: number; } @@ -1166,19 +1309,26 @@ export interface ZWaveProtocolCCSetNWIModeOptions extends CCCommandOptions { @CCCommand(ZWaveProtocolCommand.SetNWIMode) export class ZWaveProtocolCCSetNWIMode extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCSetNWIModeOptions, + options: WithAddress, ) { super(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; - } + this.enabled = options.enabled; + this.timeoutMinutes = options.timeoutMinutes; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCSetNWIMode { + validatePayload(raw.payload.length >= 2); + const enabled = raw.payload[0] === 0x01; + const timeoutMinutes: number | undefined = raw.payload[1] || undefined; + + return new ZWaveProtocolCCSetNWIMode({ + nodeId: ctx.sourceNodeId, + enabled, + timeoutMinutes, + }); } public enabled: boolean; @@ -1199,9 +1349,7 @@ export class ZWaveProtocolCCExcludeRequest {} // @publicAPI -export interface ZWaveProtocolCCAssignReturnRoutePriorityOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCAssignReturnRoutePriorityOptions { targetNodeId: number; routeNumber: number; } @@ -1209,19 +1357,26 @@ export interface ZWaveProtocolCCAssignReturnRoutePriorityOptions @CCCommand(ZWaveProtocolCommand.AssignReturnRoutePriority) export class ZWaveProtocolCCAssignReturnRoutePriority extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCAssignReturnRoutePriorityOptions, + options: WithAddress, ) { super(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; - } + this.targetNodeId = options.targetNodeId; + this.routeNumber = options.routeNumber; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCAssignReturnRoutePriority { + validatePayload(raw.payload.length >= 2); + const targetNodeId = raw.payload[0]; + const routeNumber = raw.payload[1]; + + return new ZWaveProtocolCCAssignReturnRoutePriority({ + nodeId: ctx.sourceNodeId, + targetNodeId, + routeNumber, + }); } public targetNodeId: number; @@ -1239,9 +1394,7 @@ export class ZWaveProtocolCCAssignSUCReturnRoutePriority {} // @publicAPI -export interface ZWaveProtocolCCSmartStartIncludedNodeInformationOptions - extends CCCommandOptions -{ +export interface ZWaveProtocolCCSmartStartIncludedNodeInformationOptions { nwiHomeId: Buffer; } @@ -1250,23 +1403,31 @@ export class ZWaveProtocolCCSmartStartIncludedNodeInformation extends ZWaveProtocolCC { public constructor( - options: - | CommandClassDeserializationOptions - | ZWaveProtocolCCSmartStartIncludedNodeInformationOptions, + options: WithAddress< + ZWaveProtocolCCSmartStartIncludedNodeInformationOptions + >, ) { super(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; + if (options.nwiHomeId.length !== 4) { + throw new ZWaveError( + `nwiHomeId must have length 4`, + ZWaveErrorCodes.Argument_Invalid, + ); } + this.nwiHomeId = options.nwiHomeId; + } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ZWaveProtocolCCSmartStartIncludedNodeInformation { + validatePayload(raw.payload.length >= 4); + const nwiHomeId: Buffer = raw.payload.subarray(0, 4); + + return new ZWaveProtocolCCSmartStartIncludedNodeInformation({ + nodeId: ctx.sourceNodeId, + nwiHomeId, + }); } public nwiHomeId: Buffer; diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index 11af0556c837..11d9de2ae3c1 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -1,7 +1,11 @@ // This file is auto-generated by maintenance/generateCCExports.ts // Do not edit it by hand or your changes will be lost! -export type { AlarmSensorCCGetOptions } from "./AlarmSensorCC"; +export type { + AlarmSensorCCGetOptions, + AlarmSensorCCReportOptions, + AlarmSensorCCSupportedReportOptions, +} from "./AlarmSensorCC"; export { AlarmSensorCC, AlarmSensorCCGet, @@ -13,7 +17,7 @@ export { export type { AssociationCCGetOptions, AssociationCCRemoveOptions, - AssociationCCReportSpecificOptions, + AssociationCCReportOptions, AssociationCCSetOptions, AssociationCCSpecificGroupReportOptions, AssociationCCSupportedGroupingsReportOptions, @@ -34,7 +38,7 @@ export type { AssociationGroupInfoCCCommandListGetOptions, AssociationGroupInfoCCCommandListReportOptions, AssociationGroupInfoCCInfoGetOptions, - AssociationGroupInfoCCInfoReportSpecificOptions, + AssociationGroupInfoCCInfoReportOptions, AssociationGroupInfoCCNameGetOptions, AssociationGroupInfoCCNameReportOptions, } from "./AssociationGroupInfoCC"; @@ -50,8 +54,11 @@ export { } from "./AssociationGroupInfoCC"; export type { BarrierOperatorCCEventSignalingGetOptions, + BarrierOperatorCCEventSignalingReportOptions, BarrierOperatorCCEventSignalingSetOptions, + BarrierOperatorCCReportOptions, BarrierOperatorCCSetOptions, + BarrierOperatorCCSignalingCapabilitiesReportOptions, } from "./BarrierOperatorCC"; export { BarrierOperatorCC, @@ -73,7 +80,10 @@ export { BasicCCSet, BasicCCValues, } from "./BasicCC"; -export type { BatteryCCReportOptions } from "./BatteryCC"; +export type { + BatteryCCHealthReportOptions, + BatteryCCReportOptions, +} from "./BatteryCC"; export { BatteryCC, BatteryCCGet, @@ -108,7 +118,12 @@ export { } from "./BinarySwitchCC"; export type { CRC16CCCommandEncapsulationOptions } from "./CRC16CC"; export { CRC16CC, CRC16CCCommandEncapsulation } from "./CRC16CC"; -export type { CentralSceneCCConfigurationSetOptions } from "./CentralSceneCC"; +export type { + CentralSceneCCConfigurationReportOptions, + CentralSceneCCConfigurationSetOptions, + CentralSceneCCNotificationOptions, + CentralSceneCCSupportedReportOptions, +} from "./CentralSceneCC"; export { CentralSceneCC, CentralSceneCCConfigurationGet, @@ -120,8 +135,11 @@ export { CentralSceneCCValues, } from "./CentralSceneCC"; export type { + ClimateControlScheduleCCChangedReportOptions, ClimateControlScheduleCCGetOptions, + ClimateControlScheduleCCOverrideReportOptions, ClimateControlScheduleCCOverrideSetOptions, + ClimateControlScheduleCCReportOptions, ClimateControlScheduleCCSetOptions, } from "./ClimateControlScheduleCC"; export { @@ -136,7 +154,7 @@ export { ClimateControlScheduleCCSet, ClimateControlScheduleCCValues, } from "./ClimateControlScheduleCC"; -export type { ClockCCSetOptions } from "./ClockCC"; +export type { ClockCCReportOptions, ClockCCSetOptions } from "./ClockCC"; export { ClockCC, ClockCCGet, ClockCCReport, ClockCCSet } from "./ClockCC"; export type { ColorSwitchCCGetOptions, @@ -160,6 +178,7 @@ export { export type { ConfigurationCCAPISetOptions, ConfigurationCCBulkGetOptions, + ConfigurationCCBulkReportOptions, ConfigurationCCBulkSetOptions, ConfigurationCCGetOptions, ConfigurationCCInfoReportOptions, @@ -190,7 +209,10 @@ export { DeviceResetLocallyCCNotification, } from "./DeviceResetLocallyCC"; export type { + DoorLockCCCapabilitiesReportOptions, + DoorLockCCConfigurationReportOptions, DoorLockCCConfigurationSetOptions, + DoorLockCCOperationReportOptions, DoorLockCCOperationSetOptions, } from "./DoorLockCC"; export { @@ -205,7 +227,11 @@ export { DoorLockCCOperationSet, DoorLockCCValues, } from "./DoorLockCC"; -export type { DoorLockLoggingCCRecordGetOptions } from "./DoorLockLoggingCC"; +export type { + DoorLockLoggingCCRecordGetOptions, + DoorLockLoggingCCRecordReportOptions, + DoorLockLoggingCCRecordsSupportedReportOptions, +} from "./DoorLockLoggingCC"; export { DoorLockLoggingCC, DoorLockLoggingCCRecordGet, @@ -224,7 +250,13 @@ export { EnergyProductionCCReport, EnergyProductionCCValues, } from "./EnergyProductionCC"; -export type { EntryControlCCConfigurationSetOptions } from "./EntryControlCC"; +export type { + EntryControlCCConfigurationReportOptions, + EntryControlCCConfigurationSetOptions, + EntryControlCCEventSupportedReportOptions, + EntryControlCCKeySupportedReportOptions, + EntryControlCCNotificationOptions, +} from "./EntryControlCC"; export { EntryControlCC, EntryControlCCConfigurationGet, @@ -238,11 +270,16 @@ export { EntryControlCCValues, } from "./EntryControlCC"; export type { + FirmwareUpdateMetaDataCCActivationReportOptions, FirmwareUpdateMetaDataCCActivationSetOptions, + FirmwareUpdateMetaDataCCGetOptions, FirmwareUpdateMetaDataCCMetaDataReportOptions, FirmwareUpdateMetaDataCCPrepareGetOptions, + FirmwareUpdateMetaDataCCPrepareReportOptions, FirmwareUpdateMetaDataCCReportOptions, FirmwareUpdateMetaDataCCRequestGetOptions, + FirmwareUpdateMetaDataCCRequestReportOptions, + FirmwareUpdateMetaDataCCStatusReportOptions, } from "./FirmwareUpdateMetaDataCC"; export { FirmwareUpdateMetaDataCC, @@ -260,7 +297,11 @@ export { FirmwareUpdateMetaDataCCValues, } from "./FirmwareUpdateMetaDataCC"; export { HailCC } from "./HailCC"; -export type { HumidityControlModeCCSetOptions } from "./HumidityControlModeCC"; +export type { + HumidityControlModeCCReportOptions, + HumidityControlModeCCSetOptions, + HumidityControlModeCCSupportedReportOptions, +} from "./HumidityControlModeCC"; export { HumidityControlModeCC, HumidityControlModeCCGet, @@ -270,6 +311,7 @@ export { HumidityControlModeCCSupportedReport, HumidityControlModeCCValues, } from "./HumidityControlModeCC"; +export type { HumidityControlOperatingStateCCReportOptions } from "./HumidityControlOperatingStateCC"; export { HumidityControlOperatingStateCC, HumidityControlOperatingStateCCGet, @@ -278,9 +320,13 @@ export { } from "./HumidityControlOperatingStateCC"; export type { HumidityControlSetpointCCCapabilitiesGetOptions, + HumidityControlSetpointCCCapabilitiesReportOptions, HumidityControlSetpointCCGetOptions, + HumidityControlSetpointCCReportOptions, HumidityControlSetpointCCScaleSupportedGetOptions, + HumidityControlSetpointCCScaleSupportedReportOptions, HumidityControlSetpointCCSetOptions, + HumidityControlSetpointCCSupportedReportOptions, } from "./HumidityControlSetpointCC"; export { HumidityControlSetpointCC, @@ -308,10 +354,11 @@ export type { IndicatorCCDescriptionGetOptions, IndicatorCCDescriptionReportOptions, IndicatorCCGetOptions, - IndicatorCCReportSpecificOptions, + IndicatorCCReportOptions, IndicatorCCSetOptions, IndicatorCCSupportedGetOptions, IndicatorCCSupportedReportOptions, + IndicatorObject, } from "./IndicatorCC"; export { IndicatorCC, @@ -325,13 +372,19 @@ export { IndicatorCCValues, } from "./IndicatorCC"; export type { + IrrigationCCSystemConfigReportOptions, IrrigationCCSystemConfigSetOptions, + IrrigationCCSystemInfoReportOptions, IrrigationCCSystemShutoffOptions, + IrrigationCCSystemStatusReportOptions, IrrigationCCValveConfigGetOptions, + IrrigationCCValveConfigReportOptions, IrrigationCCValveConfigSetOptions, IrrigationCCValveInfoGetOptions, + IrrigationCCValveInfoReportOptions, IrrigationCCValveRunOptions, IrrigationCCValveTableGetOptions, + IrrigationCCValveTableReportOptions, IrrigationCCValveTableRunOptions, IrrigationCCValveTableSetOptions, } from "./IrrigationCC"; @@ -357,7 +410,10 @@ export { IrrigationCCValveTableRun, IrrigationCCValveTableSet, } from "./IrrigationCC"; -export type { LanguageCCSetOptions } from "./LanguageCC"; +export type { + LanguageCCReportOptions, + LanguageCCSetOptions, +} from "./LanguageCC"; export { LanguageCC, LanguageCCGet, @@ -365,7 +421,7 @@ export { LanguageCCSet, LanguageCCValues, } from "./LanguageCC"; -export type { LockCCSetOptions } from "./LockCC"; +export type { LockCCReportOptions, LockCCSetOptions } from "./LockCC"; export { LockCC, LockCCGet, @@ -377,6 +433,7 @@ export type { ManufacturerProprietaryCCOptions } from "./ManufacturerProprietary export { ManufacturerProprietaryCC } from "./ManufacturerProprietaryCC"; export type { ManufacturerSpecificCCDeviceSpecificGetOptions, + ManufacturerSpecificCCDeviceSpecificReportOptions, ManufacturerSpecificCCReportOptions, } from "./ManufacturerSpecificCC"; export { @@ -421,6 +478,7 @@ export { } from "./MultiChannelAssociationCC"; export type { MultiChannelCCAggregatedMembersGetOptions, + MultiChannelCCAggregatedMembersReportOptions, MultiChannelCCCapabilityGetOptions, MultiChannelCCCapabilityReportOptions, MultiChannelCCCommandEncapsulationOptions, @@ -429,6 +487,7 @@ export type { MultiChannelCCEndPointReportOptions, MultiChannelCCV1CommandEncapsulationOptions, MultiChannelCCV1GetOptions, + MultiChannelCCV1ReportOptions, } from "./MultiChannelCC"; export { MultiChannelCC, @@ -487,7 +546,9 @@ export { } from "./MultilevelSwitchCC"; export { NoOperationCC, messageIsPing } from "./NoOperationCC"; export type { + NodeNamingAndLocationCCLocationReportOptions, NodeNamingAndLocationCCLocationSetOptions, + NodeNamingAndLocationCCNameReportOptions, NodeNamingAndLocationCCNameSetOptions, } from "./NodeNamingCC"; export { @@ -535,8 +596,12 @@ export { PowerlevelCCTestNodeSet, } from "./PowerlevelCC"; export type { + ProtectionCCExclusiveControlReportOptions, ProtectionCCExclusiveControlSetOptions, + ProtectionCCReportOptions, ProtectionCCSetOptions, + ProtectionCCSupportedReportOptions, + ProtectionCCTimeoutReportOptions, ProtectionCCTimeoutSetOptions, } from "./ProtectionCC"; export { @@ -562,6 +627,7 @@ export { } from "./SceneActivationCC"; export type { SceneActuatorConfigurationCCGetOptions, + SceneActuatorConfigurationCCReportOptions, SceneActuatorConfigurationCCSetOptions, } from "./SceneActuatorConfigurationCC"; export { @@ -573,6 +639,7 @@ export { } from "./SceneActuatorConfigurationCC"; export type { SceneControllerConfigurationCCGetOptions, + SceneControllerConfigurationCCReportOptions, SceneControllerConfigurationCCSetOptions, } from "./SceneControllerConfigurationCC"; export { @@ -702,7 +769,11 @@ export { SupervisionCCReport, SupervisionCCValues, } from "./SupervisionCC"; -export type { ThermostatFanModeCCSetOptions } from "./ThermostatFanModeCC"; +export type { + ThermostatFanModeCCReportOptions, + ThermostatFanModeCCSetOptions, + ThermostatFanModeCCSupportedReportOptions, +} from "./ThermostatFanModeCC"; export { ThermostatFanModeCC, ThermostatFanModeCCGet, @@ -712,6 +783,7 @@ export { ThermostatFanModeCCSupportedReport, ThermostatFanModeCCValues, } from "./ThermostatFanModeCC"; +export type { ThermostatFanStateCCReportOptions } from "./ThermostatFanStateCC"; export { ThermostatFanStateCC, ThermostatFanStateCCGet, @@ -732,6 +804,7 @@ export { ThermostatModeCCSupportedReport, ThermostatModeCCValues, } from "./ThermostatModeCC"; +export type { ThermostatOperatingStateCCReportOptions } from "./ThermostatOperatingStateCC"; export { ThermostatOperatingStateCC, ThermostatOperatingStateCCGet, @@ -783,7 +856,10 @@ export { TimeCCTimeOffsetSet, TimeCCTimeReport, } from "./TimeCC"; -export type { TimeParametersCCSetOptions } from "./TimeParametersCC"; +export type { + TimeParametersCCReportOptions, + TimeParametersCCSetOptions, +} from "./TimeParametersCC"; export { TimeParametersCC, TimeParametersCCGet, @@ -813,6 +889,7 @@ export type { UserCodeCCAdminCodeSetOptions, UserCodeCCCapabilitiesReportOptions, UserCodeCCExtendedUserCodeGetOptions, + UserCodeCCExtendedUserCodeReportOptions, UserCodeCCExtendedUserCodeSetOptions, UserCodeCCGetOptions, UserCodeCCKeypadModeReportOptions, @@ -849,6 +926,7 @@ export type { VersionCCCommandClassGetOptions, VersionCCCommandClassReportOptions, VersionCCReportOptions, + VersionCCZWaveSoftwareReportOptions, } from "./VersionCC"; export { VersionCC, @@ -862,7 +940,11 @@ export { VersionCCZWaveSoftwareGet, VersionCCZWaveSoftwareReport, } from "./VersionCC"; -export type { WakeUpCCIntervalSetOptions } from "./WakeUpCC"; +export type { + WakeUpCCIntervalCapabilitiesReportOptions, + WakeUpCCIntervalReportOptions, + WakeUpCCIntervalSetOptions, +} from "./WakeUpCC"; export { WakeUpCC, WakeUpCCIntervalCapabilitiesGet, @@ -876,6 +958,7 @@ export { } from "./WakeUpCC"; export type { WindowCoveringCCGetOptions, + WindowCoveringCCReportOptions, WindowCoveringCCSetOptions, WindowCoveringCCStartLevelChangeOptions, WindowCoveringCCStopLevelChangeOptions, @@ -981,7 +1064,10 @@ export { manufacturerId, manufacturerProprietaryAPI, } from "./manufacturerProprietary/Decorators"; -export type { FibaroVenetianBlindCCSetOptions } from "./manufacturerProprietary/FibaroCC"; +export type { + FibaroVenetianBlindCCReportOptions, + FibaroVenetianBlindCCSetOptions, +} from "./manufacturerProprietary/FibaroCC"; export { FibaroCC, FibaroVenetianBlindCC, diff --git a/packages/cc/src/cc/manufacturerProprietary/FibaroCC.ts b/packages/cc/src/cc/manufacturerProprietary/FibaroCC.ts index ca3329ef440d..87d27ea9403f 100644 --- a/packages/cc/src/cc/manufacturerProprietary/FibaroCC.ts +++ b/packages/cc/src/cc/manufacturerProprietary/FibaroCC.ts @@ -5,6 +5,7 @@ import { type MessageRecord, type ValueID, ValueMetadata, + type WithAddress, ZWaveError, ZWaveErrorCodes, parseMaybeNumber, @@ -12,6 +13,7 @@ import { } from "@zwave-js/core/safe"; import type { CCEncodingContext, + CCParsingContext, GetDeviceConfig, GetValueDB, } from "@zwave-js/host/safe"; @@ -29,12 +31,11 @@ import { throwWrongValueType, } from "../../lib/API"; import { - type CCCommandOptions, - type CommandClassDeserializationOptions, + type CCRaw, + type CommandClassOptions, type InterviewContext, type PersistValuesContext, type RefreshValuesContext, - gotDeserializationOptions, } from "../../lib/CommandClass"; import { expectedCCResponse } from "../../lib/CommandClassDecorators"; import { @@ -116,7 +117,7 @@ export class FibaroCCAPI extends ManufacturerProprietaryCCAPI { public async fibaroVenetianBlindsGet() { const cc = new FibaroVenetianBlindCCGet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, }); const response = await this.host.sendCommand< FibaroVenetianBlindCCReport @@ -133,7 +134,7 @@ export class FibaroCCAPI extends ManufacturerProprietaryCCAPI { public async fibaroVenetianBlindsSetPosition(value: number): Promise { const cc = new FibaroVenetianBlindCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, position: value, }); await this.host.sendCommand(cc, this.commandOptions); @@ -143,7 +144,7 @@ export class FibaroCCAPI extends ManufacturerProprietaryCCAPI { public async fibaroVenetianBlindsSetTilt(value: number): Promise { const cc = new FibaroVenetianBlindCCSet({ nodeId: this.endpoint.nodeId, - endpoint: this.endpoint.index, + endpointIndex: this.endpoint.index, tilt: value, }); await this.host.sendCommand(cc, this.commandOptions); @@ -218,30 +219,33 @@ export class FibaroCCAPI extends ManufacturerProprietaryCCAPI { @manufacturerId(MANUFACTURERID_FIBARO) export class FibaroCC extends ManufacturerProprietaryCC { public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, + options: CommandClassOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - validatePayload(this.payload.length >= 2); - this.fibaroCCId = this.payload[0]; - this.fibaroCCCommand = this.payload[1]; - - const FibaroConstructor = getFibaroCCCommandConstructor( - this.fibaroCCId, - this.fibaroCCCommand, - ); - if ( - FibaroConstructor - && (new.target as any) !== FibaroConstructor - ) { - return new FibaroConstructor(options); - } - this.payload = this.payload.subarray(2); - } else { - this.fibaroCCId = getFibaroCCId(this); - this.fibaroCCCommand = getFibaroCCCommand(this); + this.fibaroCCId = getFibaroCCId(this); + this.fibaroCCCommand = getFibaroCCCommand(this); + } + + public static from(raw: CCRaw, ctx: CCParsingContext): FibaroCC { + validatePayload(raw.payload.length >= 2); + const fibaroCCId = raw.payload[0]; + const fibaroCCCommand = raw.payload[1]; + + const FibaroConstructor = getFibaroCCCommandConstructor( + fibaroCCId, + fibaroCCCommand, + ); + if (FibaroConstructor) { + return FibaroConstructor.from( + raw.withPayload(raw.payload.subarray(2)), + ctx, + ); } + + return new FibaroCC({ + nodeId: ctx.sourceNodeId, + }); } public fibaroCCId?: number; @@ -311,19 +315,10 @@ export class FibaroVenetianBlindCC extends FibaroCC { declare fibaroCCCommand: FibaroVenetianBlindCCCommand; public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, + options: CommandClassOptions, ) { super(options); this.fibaroCCId = FibaroCCIDs.VenetianBlind; - - if (gotDeserializationOptions(options)) { - if ( - this.fibaroCCCommand === FibaroVenetianBlindCCCommand.Report - && (new.target as any) !== FibaroVenetianBlindCCReport - ) { - return new FibaroVenetianBlindCCReport(options); - } - } } public async interview(ctx: InterviewContext): Promise { @@ -349,7 +344,7 @@ export class FibaroVenetianBlindCC extends FibaroCC { const resp = await ctx.sendCommand( new FibaroVenetianBlindCCGet({ nodeId: this.nodeId, - endpoint: this.endpointIndex, + endpointIndex: this.endpointIndex, }), ); if (resp) { @@ -366,40 +361,38 @@ tilt: ${resp.tilt}`; // @publicAPI export type FibaroVenetianBlindCCSetOptions = - & CCCommandOptions - & ( - | { - position: number; - } - | { - tilt: number; - } - | { - position: number; - tilt: number; - } - ); + | { + position: number; + } + | { + tilt: number; + } + | { + position: number; + tilt: number; + }; @fibaroCCCommand(FibaroVenetianBlindCCCommand.Set) export class FibaroVenetianBlindCCSet extends FibaroVenetianBlindCC { public constructor( - options: - | CommandClassDeserializationOptions - | FibaroVenetianBlindCCSetOptions, + options: WithAddress, ) { super(options); this.fibaroCCCommand = FibaroVenetianBlindCCCommand.Set; - if (Buffer.isBuffer(options)) { - // TODO: Deserialize payload - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - if ("position" in options) this.position = options.position; - if ("tilt" in options) this.tilt = options.tilt; - } + if ("position" in options) this.position = options.position; + if ("tilt" in options) this.tilt = options.tilt; + } + + public static from( + _raw: CCRaw, + _ctx: CCParsingContext, + ): FibaroVenetianBlindCCSet { + // TODO: Deserialize payload + throw new ZWaveError( + `${this.constructor.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); } public position: number | undefined; @@ -431,24 +424,47 @@ export class FibaroVenetianBlindCCSet extends FibaroVenetianBlindCC { } } +// @publicAPI +export interface FibaroVenetianBlindCCReportOptions { + position?: MaybeUnknown; + tilt?: MaybeUnknown; +} + @fibaroCCCommand(FibaroVenetianBlindCCCommand.Report) export class FibaroVenetianBlindCCReport extends FibaroVenetianBlindCC { public constructor( - options: CommandClassDeserializationOptions, + options: WithAddress, ) { super(options); this.fibaroCCCommand = FibaroVenetianBlindCCCommand.Report; - validatePayload(this.payload.length >= 3); + // TODO: Check implementation: + this.position = options.position; + this.tilt = options.tilt; + } + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FibaroVenetianBlindCCReport { + validatePayload(raw.payload.length >= 3); // When the node sends a report, payload[0] === 0b11. This is probably a // bit mask for position and tilt - if (!!(this.payload[0] & 0b10)) { - this._position = parseMaybeNumber(this.payload[1]); + let position: MaybeUnknown | undefined; + if (!!(raw.payload[0] & 0b10)) { + position = parseMaybeNumber(raw.payload[1]); } - if (!!(this.payload[0] & 0b01)) { - this._tilt = parseMaybeNumber(this.payload[2]); + + let tilt: MaybeUnknown | undefined; + if (!!(raw.payload[0] & 0b01)) { + tilt = parseMaybeNumber(raw.payload[2]); } + + return new FibaroVenetianBlindCCReport({ + nodeId: ctx.sourceNodeId, + position, + tilt, + }); } public persistValues(ctx: PersistValuesContext): boolean { @@ -479,15 +495,8 @@ export class FibaroVenetianBlindCCReport extends FibaroVenetianBlindCC { return true; } - private _position: MaybeUnknown | undefined; - public get position(): MaybeUnknown | undefined { - return this._position; - } - - private _tilt: MaybeUnknown | undefined; - public get tilt(): MaybeUnknown | undefined { - return this._tilt; - } + public position: MaybeUnknown | undefined; + public tilt: MaybeUnknown | undefined; public toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry { const message: MessageRecord = {}; @@ -508,9 +517,18 @@ export class FibaroVenetianBlindCCReport extends FibaroVenetianBlindCC { @expectedCCResponse(FibaroVenetianBlindCCReport) export class FibaroVenetianBlindCCGet extends FibaroVenetianBlindCC { public constructor( - options: CommandClassDeserializationOptions | CCCommandOptions, + options: CommandClassOptions, ) { super(options); this.fibaroCCCommand = FibaroVenetianBlindCCCommand.Get; } + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): FibaroVenetianBlindCCGet { + return new FibaroVenetianBlindCCGet({ + nodeId: ctx.sourceNodeId, + }); + } } diff --git a/packages/cc/src/lib/CommandClass.ts b/packages/cc/src/lib/CommandClass.ts index cc02852ffb5f..14bd7a835bb0 100644 --- a/packages/cc/src/lib/CommandClass.ts +++ b/packages/cc/src/lib/CommandClass.ts @@ -1,5 +1,6 @@ import { type BroadcastCC, + type CCAddress, type CCId, CommandClasses, type ControlsCC, @@ -41,10 +42,10 @@ import type { GetNode, GetSupportedCCVersion, GetValueDB, + HostIDs, LogNode, LookupManufacturer, } from "@zwave-js/host"; -import { MessageOrigin } from "@zwave-js/serial"; import { type JSONObject, buffer2hex, @@ -81,49 +82,12 @@ import { defaultCCValueOptions, } from "./Values"; -export type CommandClassDeserializationOptions = - & { - data: Buffer; - origin?: MessageOrigin; - context: CCParsingContext; - } - & ( - | { - fromEncapsulation?: false; - nodeId: number; - } - | { - fromEncapsulation: true; - encapCC: CommandClass; - } - ); - -export function gotDeserializationOptions( - options: CommandClassOptions, -): options is CommandClassDeserializationOptions { - return "data" in options && Buffer.isBuffer(options.data); -} - -export interface CCCommandOptions { - nodeId: number | MulticastDestination; - endpoint?: number; -} - -interface CommandClassCreationOptions extends CCCommandOptions { +export interface CommandClassOptions extends CCAddress { ccId?: number; // Used to overwrite the declared CC ID ccCommand?: number; // undefined = NoOp payload?: Buffer; - origin?: undefined; } -function gotCCCommandOptions(options: any): options is CCCommandOptions { - return typeof options.nodeId === "number" || isArray(options.nodeId); -} - -export type CommandClassOptions = - | CommandClassCreationOptions - | CommandClassDeserializationOptions; - // Defines the necessary traits an endpoint passed to a CC instance must have export type CCEndpoint = & EndpointId @@ -164,6 +128,7 @@ export type RefreshValuesContext = CCAPIHost< >; export type PersistValuesContext = + & HostIDs & GetValueDB & GetSupportedCCVersion & GetDeviceConfig @@ -191,75 +156,141 @@ export function getEffectiveCCVersion( || (defaultVersion ?? getImplementedVersion(cc.ccId)); } +export class CCRaw { + public constructor( + public ccId: CommandClasses, + public ccCommand: number | undefined, + public payload: Buffer, + ) {} + + public static parse(data: Buffer): CCRaw { + const { ccId, bytesRead: ccIdLength } = parseCCId(data); + // There are so few exceptions that we can handle them here manually + if (ccId === CommandClasses["No Operation"]) { + return new CCRaw(ccId, undefined, Buffer.allocUnsafe(0)); + } + let ccCommand: number | undefined = data[ccIdLength]; + let payload = data.subarray(ccIdLength + 1); + if (ccId === CommandClasses["Transport Service"]) { + // Transport Service only uses the higher 5 bits for the command + // and re-uses the lower 3 bits of the ccCommand as payload + payload = Buffer.concat([ + Buffer.from([ccCommand & 0b111]), + payload, + ]); + ccCommand = ccCommand & 0b11111_000; + } else if (ccId === CommandClasses["Manufacturer Proprietary"]) { + // ManufacturerProprietaryCC has no CC command, so the first + // payload byte is stored in ccCommand. + payload = Buffer.concat([ + Buffer.from([ccCommand]), + payload, + ]); + ccCommand = undefined; + } + + return new CCRaw(ccId, ccCommand, payload); + } + + public withPayload(payload: Buffer): CCRaw { + return new CCRaw(this.ccId, this.ccCommand, payload); + } + + public serialize(): Buffer { + const ccIdLength = this.ccId >= 0xf100 ? 2 : 1; + const data = Buffer.allocUnsafe(ccIdLength + 1 + this.payload.length); + data.writeUIntBE(this.ccId, 0, ccIdLength); + data[ccIdLength] = this.ccCommand ?? 0; + this.payload.copy(data, ccIdLength + 1); + return data; + } +} + // @publicAPI export class CommandClass implements CCId { // empty constructor to parse messages public constructor(options: CommandClassOptions) { - // Default to the root endpoint - Inherited classes may override this behavior - this.endpointIndex = - ("endpoint" in options ? options.endpoint : undefined) ?? 0; - - this.origin = options.origin - ?? (gotDeserializationOptions(options) - ? MessageOrigin.Controller - : MessageOrigin.Host); - - if (gotDeserializationOptions(options)) { - // For deserialized commands, try to invoke the correct subclass constructor - const CCConstructor = - getCCConstructor(CommandClass.getCommandClass(options.data)) - ?? CommandClass; - const ccId = CommandClass.getCommandClass(options.data); - const ccCommand = CCConstructor.getCCCommand(options.data); - if (ccCommand != undefined) { - const CommandConstructor = getCCCommandConstructor( - ccId, - ccCommand, - ); + const { + nodeId, + endpointIndex = 0, + ccId = getCommandClass(this), + ccCommand = getCCCommand(this), + payload = Buffer.allocUnsafe(0), + } = options; + + this.nodeId = nodeId; + this.endpointIndex = endpointIndex; + this.ccId = ccId; + this.ccCommand = ccCommand; + this.payload = payload; + } + + public static parse( + payload: Buffer, + ctx: CCParsingContext, + ): CommandClass { + const raw = CCRaw.parse(payload); + + // Find the correct subclass constructor to invoke + const CCConstructor = getCCConstructor(raw.ccId); + if (!CCConstructor) { + // None -> fall back to the default constructor + return CommandClass.from(raw, ctx); + } + + let CommandConstructor: CCConstructor | undefined; + if (raw.ccCommand != undefined) { + CommandConstructor = getCCCommandConstructor( + raw.ccId, + raw.ccCommand, + ); + } + // Not every CC has a constructor for its commands. In that case, + // call the CC constructor directly + try { + return (CommandConstructor ?? CCConstructor).from(raw, ctx); + } catch (e) { + // Indicate invalid payloads with a special CC type + if ( + isZWaveError(e) + && e.code === ZWaveErrorCodes.PacketFormat_InvalidPayload + ) { + const ccName = CommandConstructor?.name + ?? `${getCCName(raw.ccId)} CC`; + + // Preserve why the command was invalid + let reason: string | ZWaveErrorCodes | undefined; if ( - CommandConstructor - && (new.target as any) !== CommandConstructor + typeof e.context === "string" + || (typeof e.context === "number" + && ZWaveErrorCodes[e.context] != undefined) ) { - return new CommandConstructor(options); + reason = e.context; } - } - // If the constructor is correct or none was found, fall back to normal deserialization - if (options.fromEncapsulation) { - // Propagate the node ID and endpoint index from the encapsulating CC - this.nodeId = options.encapCC.nodeId; - if (!this.endpointIndex && options.encapCC.endpointIndex) { - this.endpointIndex = options.encapCC.endpointIndex; - } - // And remember which CC encapsulates this CC - this.encapsulatingCC = options.encapCC as any; - } else { - this.nodeId = options.nodeId; - } + const ret = new InvalidCC({ + nodeId: ctx.sourceNodeId, + ccId: raw.ccId, + ccCommand: raw.ccCommand, + ccName, + reason, + }); - this.frameType = options.context.frameType; - - ({ - ccId: this.ccId, - ccCommand: this.ccCommand, - payload: this.payload, - } = this.deserialize(options.data)); - } else if (gotCCCommandOptions(options)) { - const { - nodeId, - endpoint = 0, - ccId = getCommandClass(this), - ccCommand = getCCCommand(this), - payload = Buffer.allocUnsafe(0), - } = options; - this.nodeId = nodeId; - this.endpointIndex = endpoint; - this.ccId = ccId; - this.ccCommand = ccCommand; - this.payload = payload; + return ret; + } + throw e; } } + public static from(raw: CCRaw, ctx: CCParsingContext): CommandClass { + return new this({ + nodeId: ctx.sourceNodeId, + ccId: raw.ccId, + ccCommand: raw.ccCommand, + payload: raw.payload, + }); + } + /** This CC's identifier */ public ccId!: CommandClasses; public ccCommand?: number; @@ -276,8 +307,6 @@ export class CommandClass implements CCId { /** Which endpoint of the node this CC belongs to. 0 for the root device. */ public endpointIndex: number; - public origin: MessageOrigin; - /** * Which encapsulation CCs this CC is/was/should be encapsulated with. * @@ -332,29 +361,6 @@ export class CommandClass implements CCId { ); } - /** - * Deserializes a CC from a buffer that contains a serialized CC - */ - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - protected deserialize(data: Buffer) { - const ccId = CommandClass.getCommandClass(data); - const ccIdLength = this.isExtended() ? 2 : 1; - if (data.length > ccIdLength) { - // This is not a NoOp CC (contains command and payload) - const ccCommand = data[ccIdLength]; - const payload = data.subarray(ccIdLength + 1); - return { - ccId, - ccCommand, - payload, - }; - } else { - // NoOp CC (no command, no payload) - const payload = Buffer.allocUnsafe(0); - return { ccId, payload }; - } - } - /** * Serializes this CommandClass to be embedded in a message payload or another CC */ @@ -385,93 +391,6 @@ export class CommandClass implements CCId { // Do nothing by default } - /** Extracts the CC id from a buffer that contains a serialized CC */ - public static getCommandClass(data: Buffer): CommandClasses { - return parseCCId(data).ccId; - } - - /** Extracts the CC command from a buffer that contains a serialized CC */ - public static getCCCommand(data: Buffer): number | undefined { - if (data[0] === 0) return undefined; // NoOp - const isExtendedCC = data[0] >= 0xf1; - return isExtendedCC ? data[2] : data[1]; - } - - /** - * Retrieves the correct constructor for the CommandClass in the given Buffer. - * It is assumed that the buffer only contains the serialized CC. This throws if the CC is not implemented. - */ - public static getConstructor(ccData: Buffer): CCConstructor { - const cc = CommandClass.getCommandClass(ccData); - const ret = getCCConstructor(cc); - if (!ret) { - const ccName = getCCName(cc); - throw new ZWaveError( - `The command class ${ccName} is not implemented`, - ZWaveErrorCodes.CC_NotImplemented, - ); - } - return ret; - } - - /** - * Creates an instance of the CC that is serialized in the given buffer - */ - public static from( - options: CommandClassDeserializationOptions, - ): CommandClass { - // Fall back to unspecified command class in case we receive one that is not implemented - const ccId = CommandClass.getCommandClass(options.data); - const Constructor = getCCConstructor(ccId) ?? CommandClass; - - try { - return new Constructor(options); - } catch (e) { - // Indicate invalid payloads with a special CC type - if ( - isZWaveError(e) - && e.code === ZWaveErrorCodes.PacketFormat_InvalidPayload - ) { - const nodeId = options.fromEncapsulation - ? options.encapCC.nodeId - : options.nodeId; - let ccName: string | undefined; - const ccId = CommandClass.getCommandClass(options.data); - const ccCommand = CommandClass.getCCCommand(options.data); - if (ccCommand != undefined) { - ccName = getCCCommandConstructor(ccId, ccCommand)?.name; - } - // Fall back to the unspecified CC if the command cannot be determined - if (!ccName) { - ccName = `${getCCName(ccId)} CC`; - } - // Preserve why the command was invalid - let reason: string | ZWaveErrorCodes | undefined; - if ( - typeof e.context === "string" - || (typeof e.context === "number" - && ZWaveErrorCodes[e.context] != undefined) - ) { - reason = e.context; - } - - const ret = new InvalidCC({ - nodeId, - ccId, - ccName, - reason, - }); - - if (options.fromEncapsulation) { - ret.encapsulatingCC = options.encapCC as any; - } - - return ret; - } - throw e; - } - } - /** * Create an instance of the given CC without checking whether it is supported. * If the CC is implemented, this returns an instance of the given CC which is linked to the given endpoint. @@ -486,7 +405,7 @@ export class CommandClass implements CCId { if (Constructor) { return new Constructor({ nodeId: endpoint.nodeId, - endpoint: endpoint.index, + endpointIndex: endpoint.index, }) as T; } } @@ -1233,13 +1152,13 @@ export class CommandClass implements CCId { } } -export interface InvalidCCCreationOptions extends CommandClassCreationOptions { +export interface InvalidCCOptions extends CommandClassOptions { ccName: string; reason?: string | ZWaveErrorCodes; } export class InvalidCC extends CommandClass { - public constructor(options: InvalidCCCreationOptions) { + public constructor(options: InvalidCCOptions) { super(options); this._ccName = options.ccName; // Numeric reasons are used internally to communicate problems with a CC diff --git a/packages/core/src/traits/CommandClasses.ts b/packages/core/src/traits/CommandClasses.ts index 759567a2d7dc..924ab874dcdb 100644 --- a/packages/core/src/traits/CommandClasses.ts +++ b/packages/core/src/traits/CommandClasses.ts @@ -8,10 +8,16 @@ import type { NODE_ID_BROADCAST_LR, } from "../consts"; -/** A basic abstraction of a Z-Wave Command Class providing access to the relevant functionality */ -export interface CCId { +/** Identifies which node and/or endpoint a CC is addressed to */ +export interface CCAddress { nodeId: number | MulticastDestination; endpointIndex?: number; +} + +export type WithAddress = T & CCAddress; + +/** Uniquely identifies a CC and its address */ +export interface CCId extends CCAddress { ccId: CommandClasses; ccCommand?: number; } diff --git a/packages/eslint-plugin/src/rules/consistent-cc-classes.ts b/packages/eslint-plugin/src/rules/consistent-cc-classes.ts index 2db8230635ae..95ebbe95d3bc 100644 --- a/packages/eslint-plugin/src/rules/consistent-cc-classes.ts +++ b/packages/eslint-plugin/src/rules/consistent-cc-classes.ts @@ -45,6 +45,9 @@ function getRequiredInterviewCCsFromMethod( export const consistentCCClasses = ESLintUtils.RuleCreator.withoutDocs({ create(context) { let currentCCId: CommandClasses | undefined; + let isInCCCommand = false; + let ctor: TSESTree.MethodDefinition | undefined; + let hasFromImpl: boolean; return { // Look at class declarations ending with "CC" @@ -154,7 +157,23 @@ export const consistentCCClasses = ESLintUtils.RuleCreator.withoutDocs({ } }, MethodDefinition(node) { - // Only care about methods inside non-application CC classes, + if (isInCCCommand) { + if ( + node.key.type === AST_NODE_TYPES.Identifier + && node.key.name === "from" + ) { + hasFromImpl = true; + } + + if ( + node.key.type === AST_NODE_TYPES.Identifier + && node.key.name === "constructor" + ) { + ctor = node; + } + } + + // For the following, only care about methods inside non-application CC classes, // since only application CCs may depend on other application CCs if (!currentCCId || applicationCCs.includes(currentCCId)) { return; @@ -198,8 +217,64 @@ export const consistentCCClasses = ESLintUtils.RuleCreator.withoutDocs({ }); } }, - "ClassDeclaration:exit"(_node) { + + "ClassDeclaration:exit"(node) { + // Ensure each CC class with a custom constructor also has a from method + if (isInCCCommand && !!ctor && !hasFromImpl) { + const fix = (fixer: TSESLint.RuleFixer) => { + return fixer.insertTextAfter( + ctor!, + ` + + public static from( + raw: CCRaw, + ctx: CCParsingContext, + ): ${node.id!.name} { + // TODO: Deserialize payload + throw new ZWaveError( + \`\${this.constructor.name}: deserialization not implemented\`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + }`, + ); + }; + + context.report({ + node: ctor, + loc: ctor.key.loc, + messageId: "missing-from-impl", + suggest: [ + { + messageId: "suggest-impl-from", + fix, + }, + ], + }); + } + currentCCId = undefined; + isInCCCommand = false; + hasFromImpl = false; + ctor = undefined; + }, + + // ================================================================= + + // Ensure consistent implementation of CC commands + + // Look at class declarations containing, but not ending with "CC" + "ClassDeclaration[id.name=/.+CC.+/]"( + node: TSESTree.ClassDeclaration & { + id: TSESTree.Identifier; + }, + ) { + if ( + node.superClass?.type === AST_NODE_TYPES.Identifier + && node.superClass.name.endsWith("CC") + ) { + // TODO: Implement more rules, for now only look at constructor/from + isInCCCommand = true; + } }, // ================================================================= @@ -330,12 +405,15 @@ export const consistentCCClasses = ESLintUtils.RuleCreator.withoutDocs({ "Classes implementing a CC API must have a CC assigned using the `@API(...)` decorator", "missing-version-decorator": "Classes implementing a CC must be decorated with `@implementedVersion(...)`", + "missing-from-impl": + "CC implementations with a custom constructor must also override the `CommandClass.from(...)` method", "must-export": "Classes implementing a CC must be exported", "must-export-api": "Classes implementing a CC API must be exported", "must-inherit-ccapi": "Classes implementing a CC API MUST inherit from `CCAPI` or `PhysicalCCAPI`", "suggest-extend-ccapi": "Inherit from `CCAPI`", "suggest-extend-physicalccapi": "Inherit from `PhysicalCCAPI`", + "suggest-impl-from": "Override `CommandClass.from(...)`", "must-inherit-commandclass": "Classes implementing a CC MUST inherit from `CommandClass`", "required-ccs-failed": diff --git a/packages/eslint-plugin/src/rules/no-internal-cc-types.ts b/packages/eslint-plugin/src/rules/no-internal-cc-types.ts index 0a148b853903..9742416528a8 100644 --- a/packages/eslint-plugin/src/rules/no-internal-cc-types.ts +++ b/packages/eslint-plugin/src/rules/no-internal-cc-types.ts @@ -31,7 +31,6 @@ export const noInternalCCTypes = ESLintUtils.RuleCreator.withoutDocs({ | TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration, ) { - if (node.id.name === "BasicCCSetOptions") debugger; let fullNode: | TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration diff --git a/packages/maintenance/src/refactorCCParsing.01.ts b/packages/maintenance/src/refactorCCParsing.01.ts new file mode 100644 index 000000000000..2922a7118737 --- /dev/null +++ b/packages/maintenance/src/refactorCCParsing.01.ts @@ -0,0 +1,319 @@ +import fs from "node:fs/promises"; +import { + type Node, + Project, + SyntaxKind, + type TypeReferenceNode, + VariableDeclarationKind, +} from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/cc/tsconfig.json", + }); + // project.addSourceFilesAtPaths("packages/cc/src/cc/**/*CC.ts"); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getBaseNameWithoutExtension().endsWith("CC") + ); + for (const file of sourceFiles) { + // const filePath = path.relative(process.cwd(), file.getFilePath()); + + const ifaceExtends = file.getDescendantsOfKind( + SyntaxKind.InterfaceDeclaration, + ) + .map((iface) => + [ + iface, + iface.getExtends().filter((ext) => + ext.getText() === "CCCommandOptions" + ), + ] as const + ) + .filter(([, exts]) => exts.length > 0); + for (const [self, exts] of ifaceExtends) { + for (const ext of exts) { + self.removeExtends(ext); + } + } + + const ccImplementations = file.getDescendantsOfKind( + SyntaxKind.ClassDeclaration, + ).filter((cls) => { + const name = cls.getName(); + if (!name) return false; + if (!name.includes("CC")) return false; + if (name.endsWith("CC")) return false; + return true; + }); + const ctors = ccImplementations.map((cls) => { + const ctors = cls.getConstructors(); + if (ctors.length !== 1) return; + const ctor = ctors[0]; + + // Make sure we have exactly one parameter + const ctorParams = ctor.getParameters(); + if (ctorParams.length !== 1) return; + const ctorParam = ctorParams[0]; + + // with a union type where one is CommandClassDeserializationOptions + const types = ctorParam.getDescendantsOfKind( + SyntaxKind.TypeReference, + ); + let otherType: TypeReferenceNode | undefined; + if ( + types.length === 1 + && types[0].getText() === "CommandClassDeserializationOptions" + ) { + // No other type, need to implement the constructor too + // There is also no if statement + return [cls.getName(), ctor, undefined, undefined] as const; + } else if ( + types.length === 2 + && types.some((type) => + type.getText() === "CommandClassDeserializationOptions" + ) + ) { + // ABCOptions | CommandClassDeserializationOptions + otherType = types.find( + (type) => + type.getText() !== "CommandClassDeserializationOptions", + )!; + } else if ( + types.length === 3 + && types.some((type) => + type.getText() === "CommandClassDeserializationOptions" + ) + && types.some((type) => type.getText() === "CCCommandOptions") + ) { + // (ABCOptions & CCCommandOptions) | CommandClassDeserializationOptions + otherType = types.find( + (type) => + type.getText() !== "CommandClassDeserializationOptions" + && type.getText() !== "CCCommandOptions", + )!; + } else { + return; + } + + // Ensure the constructor contains + // if (gotDeserializationOptions(options)) { + + const ifStatement = ctor.getBody() + ?.getChildrenOfKind(SyntaxKind.IfStatement) + .filter((stmt) => !!stmt.getElseStatement()) + .find((stmt) => { + const expr = stmt.getExpression(); + if (!expr) return false; + return expr.getText() + === "gotDeserializationOptions(options)"; + }); + if (!ifStatement) return; + + return [cls.getName(), ctor, otherType, ifStatement] as const; + }).filter((ctor) => ctor != undefined); + + if (!ctors.length) continue; + + for (const [clsName, ctor, otherType, ifStatement] of ctors) { + // Update the constructor signature + if (otherType) { + ctor.getParameters()[0].setType( + otherType.getText() + " & CCCommandOptions", + ); + } else { + ctor.getParameters()[0].setType( + `${clsName}Options & CCCommandOptions`, + ); + } + + // Replace "this.payload" with just "payload" + const methodBody = ctor.getBody()!.asKind(SyntaxKind.Block)!; + methodBody + ?.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) + .filter((expr) => expr.getText() === "this.payload") + .forEach((expr) => { + expr.replaceWithText("payload"); + }); + + // Replace all other assignments with let declarations + const parseImplBlock = ifStatement + ? ifStatement.getThenStatement().asKind( + SyntaxKind.Block, + )! + : methodBody; + const assignments = parseImplBlock + .getDescendantsOfKind(SyntaxKind.BinaryExpression) + .map((be) => { + if ( + be.getOperatorToken().getKind() + !== SyntaxKind.EqualsToken + ) return; + const left = be.getLeft(); + if (!left.isKind(SyntaxKind.PropertyAccessExpression)) { + return; + } + if ( + left.getExpression().getKind() + !== SyntaxKind.ThisKeyword + ) return; + const stmt = be.getParentIfKind( + SyntaxKind.ExpressionStatement, + ); + if (!stmt) return; + const identifier = left.getName(); + if (identifier === "nodeId") return; + const value = be.getRight(); + return [stmt, left, identifier, value] as const; + }) + .filter((ass) => ass != undefined); + + const properties = new Map(); + for (const [expr, left, identifier, value] of assignments) { + if (!properties.has(identifier)) { + // Find the correct type to use + let valueType: string | undefined = value.getType() + .getText().replaceAll( + /import\(.*?\)\./g, + "", + ); + const prop = ctor.getParent().getProperty( + identifier, + ); + if (prop) { + valueType = prop.getType().getText().replaceAll( + /import\(.*?\)\./g, + "", + ); + } + valueType = valueType.replace(/^readonly /, ""); + // Avoid trivially inferred types + const typeIsTrivial = valueType === "number" + || valueType === "number[]"; + + if (expr.getParent() === parseImplBlock) { + // Top level, create a variable declaration + const index = expr.getChildIndex(); + parseImplBlock.insertVariableStatement(index + 1, { + declarationKind: VariableDeclarationKind.Let, + declarations: [{ + name: identifier, + type: typeIsTrivial ? undefined : valueType, + initializer: value.getFullText(), + }], + }); + expr.remove(); + } else { + // Not top level, create an assignment instead + left.replaceWithText(identifier); + // Find the position to create the declaration + let cur: Node = expr; + while (cur.getParent() !== parseImplBlock) { + cur = cur.getParent()!; + } + const index = cur.getChildIndex(); + parseImplBlock.insertVariableStatement(index, { + declarationKind: VariableDeclarationKind.Let, + declarations: [{ + name: identifier, + type: typeIsTrivial ? undefined : valueType, + }], + }); + } + + properties.set(identifier, valueType); + } else { + left.replaceWithText(identifier); + } + } + + // Add a new parse method after the constructor + const ctorIndex = ctor.getChildIndex(); + const method = ctor.getParent().insertMethod(ctorIndex + 1, { + name: "parse", + parameters: [{ + name: "payload", + type: "Buffer", + }, { + name: "options", + type: "CommandClassDeserializationOptions", + }], + isStatic: true, + statements: + // For parse/create constructors, take the then block + ifStatement + ? ifStatement.getThenStatement() + .getChildSyntaxList()! + .getFullText() + // else take the whole constructor without "super()" + : methodBody.getStatementsWithComments().filter((s) => + !s.getText().startsWith("super(") + ).map((s) => s.getText()), + returnType: clsName, + }).toggleModifier("public", true); + + // Instantiate the class at the end of the parse method + method.getFirstDescendantByKind(SyntaxKind.Block)!.addStatements(` +return new ${clsName}({ + nodeId: options.context.sourceNodeId, + ${[...properties.keys()].join(",\n")} +})`); + + if (ifStatement) { + // Replace the `if` block with its else block + ifStatement.replaceWithText( + ifStatement.getElseStatement()!.getChildSyntaxList()! + .getFullText().trimStart(), + ); + } else { + // preserve only the super() call + methodBody.getStatementsWithComments().slice(1).forEach( + (stmt) => { + stmt.remove(); + }, + ); + // And add a best-guess implementation for the constructor + methodBody.addStatements("\n\n// TODO: Check implementation:"); + methodBody.addStatements( + [...properties.keys()].map((id) => { + if (id.startsWith("_")) id = id.slice(1); + return `this.${id} = options.${id};`; + }).join("\n"), + ); + + // Also we probably need to define the options type + const klass = ctor.getParent(); + file.insertInterface(klass.getChildIndex(), { + leadingTrivia: "// @publicAPI\n", + name: `${clsName}Options`, + isExported: true, + properties: [...properties.keys()].map((id) => { + if (id.startsWith("_")) id = id.slice(1); + return { + name: id, + hasQuestionToken: properties.get(id)?.includes( + "undefined", + ), + type: properties.get(id)?.replace( + "| undefined", + "", + ), + }; + }), + }); + } + } + + await file.save(); + } +} + +void main().catch(async (e) => { + await fs.writeFile(`${e.filePath}.old`, e.oldText); + await fs.writeFile(`${e.filePath}.new`, e.newText); + console.error(`Error refactoring file ${e.filePath} + old text: ${e.filePath}.old + new text: ${e.filePath}.new`); + + process.exit(1); +}); diff --git a/packages/maintenance/src/refactorCCParsing.02.ts b/packages/maintenance/src/refactorCCParsing.02.ts new file mode 100644 index 000000000000..84808bb0a152 --- /dev/null +++ b/packages/maintenance/src/refactorCCParsing.02.ts @@ -0,0 +1,126 @@ +import fs from "node:fs/promises"; +import { Project, SyntaxKind } from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/cc/tsconfig.json", + }); + // project.addSourceFilesAtPaths("packages/cc/src/cc/**/*CC.ts"); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getBaseNameWithoutExtension().endsWith("CC") + ); + for (const file of sourceFiles) { + // const filePath = path.relative(process.cwd(), file.getFilePath()); + + const ccImplementations = file.getDescendantsOfKind( + SyntaxKind.ClassDeclaration, + ).filter((cls) => { + const name = cls.getName(); + if (!name) return false; + if (!name.includes("CC")) return false; + // if (name.endsWith("CC")) return false; + return true; + }); + + const parse = ccImplementations.map((cls) => { + const method = cls.getMethod("parse"); + if (!method) return; + if (!method.isStatic()) return; + + return method; + }).filter((m) => m != undefined); + + if (!parse.length) continue; + + // Add required imports + const hasRawImport = file.getImportDeclarations().some( + (decl) => + decl.getNamedImports().some((imp) => imp.getName() === "CCRaw"), + ); + if (!hasRawImport) { + const existing = file.getImportDeclaration((decl) => + decl.getModuleSpecifierValue().endsWith("/lib/CommandClass") + ); + if (!existing) { + file.addImportDeclaration({ + moduleSpecifier: "../lib/CommandClass", + namedImports: [{ + name: "CCRaw", + isTypeOnly: true, + }], + }); + } else { + existing.addNamedImport({ + name: "CCRaw", + isTypeOnly: true, + }); + } + } + + const hasCCParsingContextImport = file.getImportDeclarations().some( + (decl) => + decl.getNamedImports().some((imp) => + imp.getName() === "CCParsingContext" + ), + ); + if (!hasCCParsingContextImport) { + const existing = file.getImportDeclaration((decl) => + decl.getModuleSpecifierValue().startsWith("@zwave-js/host") + ); + if (!existing) { + file.addImportDeclaration({ + moduleSpecifier: "@zwave-js/host", + namedImports: [{ + name: "CCParsingContext", + isTypeOnly: true, + }], + }); + } else { + existing.addNamedImport({ + name: "CCParsingContext", + isTypeOnly: true, + }); + } + } + + for (const impl of parse) { + // Update the method signature + impl.rename("from"); + impl.getParameters().forEach((p) => p.remove()); + impl.addParameter({ + name: "raw", + type: "CCRaw", + }); + impl.addParameter({ + name: "ctx", + type: "CCParsingContext", + }); + + // Replace "payload" with "raw.payload" + const idents = impl.getDescendantsOfKind(SyntaxKind.Identifier) + .filter((id) => id.getText() === "payload"); + idents.forEach((id) => id.replaceWithText("raw.payload")); + + // Replace "options.context.sourceNodeId" with "ctx.sourceNodeId" + const exprs = impl.getDescendantsOfKind( + SyntaxKind.PropertyAccessExpression, + ).filter((expr) => + expr.getText() === "options.context.sourceNodeId" + ); + exprs.forEach((expr) => expr.replaceWithText("ctx.sourceNodeId")); + } + + await file.save(); + } +} + +void main().catch(async (e) => { + await fs.writeFile(`${e.filePath}.old`, e.oldText); + await fs.writeFile(`${e.filePath}.new`, e.newText); + console.error(`Error refactoring file ${e.filePath} + old text: ${e.filePath}.old + new text: ${e.filePath}.new`); + + process.exit(1); +}); diff --git a/packages/maintenance/src/refactorCCParsing.03.ts b/packages/maintenance/src/refactorCCParsing.03.ts new file mode 100644 index 000000000000..9d39a56b15eb --- /dev/null +++ b/packages/maintenance/src/refactorCCParsing.03.ts @@ -0,0 +1,79 @@ +import fs from "node:fs/promises"; +import { type IntersectionTypeNode, Project, SyntaxKind } from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/cc/tsconfig.json", + }); + // project.addSourceFilesAtPaths("packages/cc/src/cc/**/*CC.ts"); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getBaseNameWithoutExtension().endsWith("CC") + ); + for (const file of sourceFiles) { + // const filePath = path.relative(process.cwd(), file.getFilePath()); + + // Add required imports + const hasWithAddressImport = file.getImportDeclarations().some( + (decl) => + decl.getNamedImports().some((imp) => + imp.getName() === "WithAddress" + ), + ); + if (!hasWithAddressImport) { + const existing = file.getImportDeclaration((decl) => + decl.getModuleSpecifierValue().startsWith("@zwave-js/core") + ); + if (!existing) { + file.addImportDeclaration({ + moduleSpecifier: "@zwave-js/core", + namedImports: [{ + name: "WithAddress", + isTypeOnly: true, + }], + }); + } else { + existing.addNamedImport({ + name: "WithAddress", + isTypeOnly: true, + }); + } + } + + // Remove old imports + const oldImports = file.getImportDeclarations().map((decl) => + decl.getNamedImports().find( + (imp) => imp.getName() === "CCCommandOptions", + ) + ).filter((i) => i != undefined); + for (const imp of oldImports) { + imp.remove(); + } + + const oldTypes = file.getDescendantsOfKind(SyntaxKind.TypeReference) + .filter((ref) => ref.getText() === "CCCommandOptions") + .map((ref) => ref.getParentIfKind(SyntaxKind.IntersectionType)) + .filter((typ): typ is IntersectionTypeNode => + typ != undefined + && !!typ.getParent().isKind(SyntaxKind.Parameter) + && !!typ.getParent().getParent()?.isKind(SyntaxKind.Constructor) + ); + for (const type of oldTypes) { + const otherType = type.getText().replace("& CCCommandOptions", "") + .replace("CCCommandOptions & ", ""); + type.replaceWithText(`WithAddress<${otherType}>`); + } + + await file.save(); + } +} + +void main().catch(async (e) => { + await fs.writeFile(`${e.filePath}.old`, e.oldText); + await fs.writeFile(`${e.filePath}.new`, e.newText); + console.error(`Error refactoring file ${e.filePath} + old text: ${e.filePath}.old + new text: ${e.filePath}.new`); + + process.exit(1); +}); diff --git a/packages/maintenance/src/refactorCCParsing.04.ts b/packages/maintenance/src/refactorCCParsing.04.ts new file mode 100644 index 000000000000..58d91526bf2a --- /dev/null +++ b/packages/maintenance/src/refactorCCParsing.04.ts @@ -0,0 +1,69 @@ +import fs from "node:fs/promises"; +import { Project, SyntaxKind } from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/cc/tsconfig.json", + }); + // project.addSourceFilesAtPaths("packages/cc/src/cc/**/*CC.ts"); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getBaseNameWithoutExtension().endsWith("CC") + ); + for (const file of sourceFiles) { + // const filePath = path.relative(process.cwd(), file.getFilePath()); + + const emptyFromImpls = file.getDescendantsOfKind( + SyntaxKind.MethodDeclaration, + ) + .filter((m) => m.isStatic() && m.getName() === "from") + .filter((m) => { + const params = m.getParameters(); + if (params.length !== 2) return false; + if ( + params[0].getDescendantsOfKind(SyntaxKind.TypeReference)[0] + ?.getText() !== "CCRaw" + ) return false; + if ( + params[1].getDescendantsOfKind(SyntaxKind.TypeReference)[0] + ?.getText() !== "CCParsingContext" + ) { + return false; + } + return true; + }) + .filter((m) => { + const body = m.getBody()?.asKind(SyntaxKind.Block); + if (!body) return false; + const firstStmt = body.getStatements()[0]; + if (!firstStmt) return false; + if ( + firstStmt.isKind(SyntaxKind.ThrowStatement) + && firstStmt.getText().includes("ZWaveError") + ) { + return true; + } + return false; + }); + + if (emptyFromImpls.length === 0) continue; + + for (const impl of emptyFromImpls) { + for (const param of impl.getParameters()) { + param.rename("_" + param.getName()); + } + } + + await file.save(); + } +} + +void main().catch(async (e) => { + await fs.writeFile(`${e.filePath}.old`, e.oldText); + await fs.writeFile(`${e.filePath}.new`, e.newText); + console.error(`Error refactoring file ${e.filePath} + old text: ${e.filePath}.old + new text: ${e.filePath}.new`); + + process.exit(1); +}); diff --git a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts index 81c42e810498..08690ab0ac33 100644 --- a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts +++ b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts @@ -13,7 +13,6 @@ import { ZWaveErrorCodes, isZWaveError, } from "@zwave-js/core"; -import { MessageOrigin } from "@zwave-js/serial"; import { MOCK_FRAME_ACK_TIMEOUT, type MockController, @@ -91,16 +90,13 @@ function createLazySendDataPayload( ): () => CommandClass { return () => { try { - const cmd = CommandClass.from({ - nodeId: controller.ownNodeId, - data: msg.payload, - origin: MessageOrigin.Host, - context: { - sourceNodeId: node.id, - __internalIsMockNode: true, - ...node.encodingContext, - ...node.securityManagers, - }, + const cmd = CommandClass.parse(msg.payload, { + sourceNodeId: controller.ownNodeId, + __internalIsMockNode: true, + ...node.encodingContext, + ...node.securityManagers, + // The frame type is always singlecast because the controller sends it to the node + frameType: "singlecast", }); // Store the command because assertReceivedHostMessage needs it // @ts-expect-error diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index f62bc819f10b..4d15111de9b7 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -686,7 +686,23 @@ export class Driver extends TypedEventEmitter }, }; this.messageEncodingContext = { - ...this.messageParsingContext, + getHighestSecurityClass: (nodeId) => + this.getHighestSecurityClass(nodeId), + hasSecurityClass: (nodeId, securityClass) => + this.hasSecurityClass(nodeId, securityClass), + setSecurityClass: (nodeId, securityClass, granted) => + this.setSecurityClass(nodeId, securityClass, granted), + getDeviceConfig: (nodeId) => this.getDeviceConfig(nodeId), + // These are evaluated lazily, so we cannot spread messageParsingContext unfortunately + get securityManager() { + return self.securityManager; + }, + get securityManager2() { + return self.securityManager2; + }, + get securityManagerLR() { + return self.securityManagerLR; + }, getSupportedCCVersion: (cc, nodeId, endpointIndex) => this.getSupportedCCVersion(cc, nodeId, endpointIndex), }; diff --git a/packages/zwave-js/src/lib/driver/MessageGenerators.ts b/packages/zwave-js/src/lib/driver/MessageGenerators.ts index aafa8eceab71..e0f667a5c258 100644 --- a/packages/zwave-js/src/lib/driver/MessageGenerators.ts +++ b/packages/zwave-js/src/lib/driver/MessageGenerators.ts @@ -526,7 +526,7 @@ export const secureMessageGeneratorS0: MessageGeneratorImplementation = // No free nonce, request a new one const cc = new SecurityCCNonceGet({ nodeId: nodeId, - endpoint: msg.command.endpointIndex, + endpointIndex: msg.command.endpointIndex, }); const nonceResp = yield* sendCommandGenerator< SecurityCCNonceReport @@ -605,9 +605,7 @@ export const secureMessageGeneratorS2: MessageGeneratorImplementation = // No free nonce, request a new one const cc = new Security2CCNonceGet({ nodeId: nodeId, - ownNodeId: driver.ownNodeId, - endpoint: msg.command.endpointIndex, - securityManagers: driver, + endpointIndex: msg.command.endpointIndex, }); const nonceResp = yield* sendCommandGenerator< Security2CCNonceReport @@ -836,8 +834,6 @@ export const secureMessageGeneratorS2Multicast: MessageGeneratorImplementation = if (innerMPANState) { const cc = new Security2CCMessageEncapsulation({ nodeId, - ownNodeId: driver.ownNodeId, - securityManagers: driver, extensions: [ new MPANExtension({ groupId, diff --git a/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts b/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts index 3639dd80f3f0..ad6875a565b2 100644 --- a/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts +++ b/packages/zwave-js/src/lib/node/MockNodeBehaviors.ts @@ -87,7 +87,7 @@ const respondToVersionCCCommandClassGet: MockNodeBehavior = { const cc = new VersionCCCommandClassReport({ nodeId: self.id, - endpoint: "index" in endpoint ? endpoint.index : undefined, + endpointIndex: "index" in endpoint ? endpoint.index : undefined, requestedCC: receivedCC.requestedCC, ccVersion: version, }); diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index b23bf7b15cec..38a79be6ea48 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -4152,8 +4152,18 @@ protocol version: ${this.protocolVersion}`; } private handleDeviceResetLocallyNotification( - _cmd: DeviceResetLocallyCCNotification, + cmd: DeviceResetLocallyCCNotification, ): void { + if (cmd.endpointIndex !== 0) { + // The notification MUST be issued by the root device, otherwise it is likely a corrupted message + this.driver.controllerLog.logNode(this.id, { + message: + `Received reset locally notification from non-root endpoint - ignoring it...`, + direction: "inbound", + }); + return; + } + // Handling this command can take a few seconds and require communication with the node. // If it was received with Supervision, we need to acknowledge it immediately. Therefore // defer the handling half a second. diff --git a/packages/zwave-js/src/lib/node/mockCCBehaviors/MultiChannel.ts b/packages/zwave-js/src/lib/node/mockCCBehaviors/MultiChannel.ts index f968c22693aa..63c1c85fc34a 100644 --- a/packages/zwave-js/src/lib/node/mockCCBehaviors/MultiChannel.ts +++ b/packages/zwave-js/src/lib/node/mockCCBehaviors/MultiChannel.ts @@ -46,8 +46,8 @@ const encapsulateMultiChannelCC: MockNodeBehavior = { response.cc = new MultiChannelCCCommandEncapsulation({ nodeId: response.cc.nodeId, + endpointIndex: source, encapsulated: response.cc, - endpoint: source, destination, }); } diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts index be1f8c2cf027..3dd6b9393ce4 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts @@ -90,15 +90,14 @@ export class ApplicationCommandRequest extends Message offset += nodeIdBytes; // and a command class const commandLength = this.payload[offset++]; - this.command = CommandClass.from({ - data: this.payload.subarray(offset, offset + commandLength), - nodeId, - origin: options.origin, - context: { + this.command = CommandClass.parse( + this.payload.subarray(offset, offset + commandLength), + { sourceNodeId: nodeId, ...options.ctx, + frameType: this.frameType, }, - }) as SinglecastCC; + ) as SinglecastCC; } else { // TODO: This logic is unsound if (!options.command.isSinglecast()) { diff --git a/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts b/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts index 73baeb18babf..397d8a3fd8c3 100644 --- a/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts @@ -72,15 +72,14 @@ export class BridgeApplicationCommandRequest extends Message offset += srcNodeIdBytes; // Parse the CC const commandLength = this.payload[offset++]; - this.command = CommandClass.from({ - data: this.payload.subarray(offset, offset + commandLength), - nodeId: sourceNodeId, - origin: options.origin, - context: { + this.command = CommandClass.parse( + this.payload.subarray(offset, offset + commandLength), + { sourceNodeId, ...options.ctx, + frameType: this.frameType, }, - }) as SinglecastCC; + ) as SinglecastCC; offset += commandLength; // Read the correct target node id diff --git a/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts b/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts index 7023067ab38f..1c6d36f277a1 100644 --- a/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts @@ -104,14 +104,9 @@ export class SendDataRequest ); if (options.parseCCs !== false) { - this.command = CommandClass.from({ - nodeId, - data: this.payload, - origin: options.origin, - context: { - sourceNodeId: nodeId, - ...options.ctx, - }, + this.command = CommandClass.parse(this.payload, { + sourceNodeId: nodeId, + ...options.ctx, }) as SinglecastCC; } else { // Little hack for testing with a network mock. This will be parsed in the next step. @@ -396,16 +391,10 @@ export class SendDataMulticastRequest< this.payload = serializedCC; if (options.parseCCs !== false) { - this.command = CommandClass.from({ - nodeId: this._nodeIds[0], - data: this.payload, - origin: options.origin, - context: { - sourceNodeId: NODE_ID_BROADCAST, // FIXME: Unknown? - ...options.ctx, - }, + this.command = CommandClass.parse(this.payload, { + sourceNodeId: NODE_ID_BROADCAST, // FIXME: Unknown? + ...options.ctx, }) as MulticastCC; - this.command.nodeId = this._nodeIds; } else { // Little hack for testing with a network mock. This will be parsed in the next step. this.command = undefined as any; diff --git a/packages/zwave-js/src/lib/test/cc-specific/discardUnsupportedReports.test.ts b/packages/zwave-js/src/lib/test/cc-specific/discardUnsupportedReports.test.ts index cbd179e574c3..5acfc308b636 100644 --- a/packages/zwave-js/src/lib/test/cc-specific/discardUnsupportedReports.test.ts +++ b/packages/zwave-js/src/lib/test/cc-specific/discardUnsupportedReports.test.ts @@ -45,7 +45,7 @@ integrationTest( }); cc = new MultiChannelCCCommandEncapsulation({ nodeId: mockController.ownNodeId, - endpoint: 1, + endpointIndex: 1, destination: 0, encapsulated: cc, }); @@ -64,7 +64,7 @@ integrationTest( }); cc = new MultiChannelCCCommandEncapsulation({ nodeId: mockController.ownNodeId, - endpoint: 1, + endpointIndex: 1, destination: 0, encapsulated: cc, }); diff --git a/packages/zwave-js/src/lib/test/cc/AssociationCC.test.ts b/packages/zwave-js/src/lib/test/cc/AssociationCC.test.ts index 06a44672522f..dd74763c5d38 100644 --- a/packages/zwave-js/src/lib/test/cc/AssociationCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/AssociationCC.test.ts @@ -6,6 +6,7 @@ import { AssociationCCSupportedGroupingsGet, AssociationCCSupportedGroupingsReport, AssociationCommand, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses } from "@zwave-js/core"; import test from "ava"; @@ -38,11 +39,11 @@ test("the SupportedGroupingsReport command should be deserialized correctly", (t 7, // # of groups ]), ); - const cc = new AssociationCCSupportedGroupingsReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as AssociationCCSupportedGroupingsReport; + t.is(cc.constructor, AssociationCCSupportedGroupingsReport); t.is(cc.groupCount, 7); }); @@ -92,11 +93,11 @@ test("the Report command should be deserialized correctly", (t) => { 5, ]), ); - const cc = new AssociationCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as AssociationCCReport; + t.is(cc.constructor, AssociationCCReport); t.is(cc.groupId, 5); t.is(cc.maxNodes, 9); diff --git a/packages/zwave-js/src/lib/test/cc/AssociationGroupInfoCC.test.ts b/packages/zwave-js/src/lib/test/cc/AssociationGroupInfoCC.test.ts index 0bf177f9dda6..3e924a7af981 100644 --- a/packages/zwave-js/src/lib/test/cc/AssociationGroupInfoCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/AssociationGroupInfoCC.test.ts @@ -9,6 +9,7 @@ import { AssociationGroupInfoCommand, AssociationGroupInfoProfile, BasicCommand, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses } from "@zwave-js/core"; import test from "ava"; @@ -51,11 +52,11 @@ test("the NameReport command should be deserialized correctly", (t) => { 0x72, ]), ); - const cc = new AssociationGroupInfoCCNameReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as AssociationGroupInfoCCNameReport; + t.is(cc.constructor, AssociationGroupInfoCCNameReport); t.is(cc.groupId, 7); t.is(cc.name, "foobar"); @@ -138,11 +139,11 @@ test("the Info Report command should be deserialized correctly", (t) => { 0, ]), ); - const cc = new AssociationGroupInfoCCInfoReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as AssociationGroupInfoCCInfoReport; + t.is(cc.constructor, AssociationGroupInfoCCInfoReport); t.is(cc.groups.length, 2); t.is(cc.groups[0].groupId, 1); @@ -184,11 +185,11 @@ test("the CommandListReport command should be deserialized correctly", (t) => { 0x05, ]), ); - const cc = new AssociationGroupInfoCCCommandListReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as AssociationGroupInfoCCCommandListReport; + t.is(cc.constructor, AssociationGroupInfoCCCommandListReport); t.is(cc.groupId, 7); t.is(cc.commands.size, 2); @@ -203,11 +204,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new AssociationGroupInfoCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as AssociationGroupInfoCC; t.is(cc.constructor, AssociationGroupInfoCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/BasicCC.test.ts b/packages/zwave-js/src/lib/test/cc/BasicCC.test.ts index 2780b0928c66..84c702d7648d 100644 --- a/packages/zwave-js/src/lib/test/cc/BasicCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/BasicCC.test.ts @@ -5,6 +5,7 @@ import { BasicCCSet, type BasicCCValues, BasicCommand, + CommandClass, getCCValues, } from "@zwave-js/cc"; import { CommandClasses } from "@zwave-js/core"; @@ -34,6 +35,20 @@ test("the Get command should serialize correctly", (t) => { t.deepEqual(basicCC.serialize({} as any), expected); }); +test("the Get command should be deserialized correctly", (t) => { + const ccData = buildCCBuffer( + Buffer.from([ + BasicCommand.Get, // CC Command + ]), + ); + const basicCC = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as BasicCCGet; + t.is(basicCC.constructor, BasicCCGet); + t.is(basicCC.nodeId, 2); +}); + test("the Set command should serialize correctly", (t) => { const basicCC = new BasicCCSet({ nodeId: 2, @@ -55,11 +70,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 55, // current value ]), ); - const basicCC = new BasicCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const basicCC = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as BasicCCReport; + t.is(basicCC.constructor, BasicCCReport); t.is(basicCC.currentValue, 55); t.is(basicCC.targetValue, undefined); @@ -75,11 +90,11 @@ test("the Report command (v2) should be deserialized correctly", (t) => { 1, // duration ]), ); - const basicCC = new BasicCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const basicCC = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as BasicCCReport; + t.is(basicCC.constructor, BasicCCReport); t.is(basicCC.currentValue, 55); t.is(basicCC.targetValue, 66); @@ -91,15 +106,14 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const basicCC: any = new BasicCC({ - nodeId: 2, - data: serializedCC, - context: {} as any, - }); + const basicCC = CommandClass.parse( + serializedCC, + { sourceNodeId: 2 } as any, + ) as BasicCCReport; t.is(basicCC.constructor, BasicCC); }); -test.only("getDefinedValueIDs() should include the target value for all endpoints except the node itself", (t) => { +test("getDefinedValueIDs() should include the target value for all endpoints except the node itself", (t) => { // Repro for GH#377 const commandClasses: CreateTestNodeOptions["commandClasses"] = { [CommandClasses.Basic]: { @@ -133,7 +147,7 @@ test.only("getDefinedValueIDs() should include the target value for all endpoint test("BasicCCSet should expect no response", (t) => { const cc = new BasicCCSet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, targetValue: 7, }); t.false(cc.expectsCCResponse()); @@ -142,7 +156,7 @@ test("BasicCCSet should expect no response", (t) => { test("BasicCCSet => BasicCCReport = unexpected", (t) => { const ccRequest = new BasicCCSet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, targetValue: 7, }); const ccResponse = new BasicCCReport({ diff --git a/packages/zwave-js/src/lib/test/cc/BatteryCC.test.ts b/packages/zwave-js/src/lib/test/cc/BatteryCC.test.ts index 34694b45787f..d2efa97ee728 100644 --- a/packages/zwave-js/src/lib/test/cc/BatteryCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/BatteryCC.test.ts @@ -1,10 +1,11 @@ import { BatteryCC, BatteryCCGet, - type BatteryCCReport, + BatteryCCReport, BatteryChargingStatus, BatteryCommand, BatteryReplacementStatus, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses } from "@zwave-js/core"; import test from "ava"; @@ -24,11 +25,11 @@ test("the Report command (v1) should be deserialized correctly: when the battery BatteryCommand.Report, // CC Command 55, // current value ]); - const batteryCC = new BatteryCC({ - nodeId: 7, - data: ccData, - context: {} as any, - }) as BatteryCCReport; + const batteryCC = CommandClass.parse( + ccData, + { sourceNodeId: 7 } as any, + ) as BatteryCCReport; + t.is(batteryCC.constructor, BatteryCCReport); t.is(batteryCC.level, 55); t.false(batteryCC.isLow); @@ -40,11 +41,11 @@ test("the Report command (v1) should be deserialized correctly: when the battery BatteryCommand.Report, // CC Command 0xff, // current value ]); - const batteryCC = new BatteryCC({ - nodeId: 7, - data: ccData, - context: {} as any, - }) as BatteryCCReport; + const batteryCC = CommandClass.parse( + ccData, + { sourceNodeId: 7 } as any, + ) as BatteryCCReport; + t.is(batteryCC.constructor, BatteryCCReport); t.is(batteryCC.level, 0); t.true(batteryCC.isLow); @@ -58,11 +59,11 @@ test("the Report command (v2) should be deserialized correctly: all flags set", 0b00_1111_00, 1, // disconnected ]); - const batteryCC = new BatteryCC({ - nodeId: 7, - data: ccData, - context: {} as any, - }) as BatteryCCReport; + const batteryCC = CommandClass.parse( + ccData, + { sourceNodeId: 7 } as any, + ) as BatteryCCReport; + t.is(batteryCC.constructor, BatteryCCReport); t.true(batteryCC.rechargeable); t.true(batteryCC.backup); @@ -79,11 +80,11 @@ test("the Report command (v2) should be deserialized correctly: charging status" 0b10_000000, // Maintaining 0, ]); - const batteryCC = new BatteryCC({ - nodeId: 7, - data: ccData, - context: {} as any, - }) as BatteryCCReport; + const batteryCC = CommandClass.parse( + ccData, + { sourceNodeId: 7 } as any, + ) as BatteryCCReport; + t.is(batteryCC.constructor, BatteryCCReport); t.is(batteryCC.chargingStatus, BatteryChargingStatus.Maintaining); }); @@ -96,11 +97,11 @@ test("the Report command (v2) should be deserialized correctly: recharge or repl 0b11, // Maintaining 0, ]); - const batteryCC = new BatteryCC({ - nodeId: 7, - data: ccData, - context: {} as any, - }) as BatteryCCReport; + const batteryCC = CommandClass.parse( + ccData, + { sourceNodeId: 7 } as any, + ) as BatteryCCReport; + t.is(batteryCC.constructor, BatteryCCReport); t.is(batteryCC.rechargeOrReplace, BatteryReplacementStatus.Now); }); @@ -110,12 +111,11 @@ test("deserializing an unsupported command should return an unspecified version CommandClasses.Battery, // CC 255, // not a valid command ]); - const basicCC: any = new BatteryCC({ - nodeId: 7, - data: serializedCC, - context: {} as any, - }); - t.is(basicCC.constructor, BatteryCC); + const batteryCC = CommandClass.parse( + serializedCC, + { sourceNodeId: 7 } as any, + ) as BatteryCCReport; + t.is(batteryCC.constructor, BatteryCC); }); // describe.skip(`interview()`, () => { diff --git a/packages/zwave-js/src/lib/test/cc/BinarySensorCC.test.ts b/packages/zwave-js/src/lib/test/cc/BinarySensorCC.test.ts index 523931edfa9e..08320b741ea4 100644 --- a/packages/zwave-js/src/lib/test/cc/BinarySensorCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/BinarySensorCC.test.ts @@ -6,6 +6,7 @@ import { BinarySensorCCSupportedReport, BinarySensorCommand, BinarySensorType, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses } from "@zwave-js/core"; import test from "ava"; @@ -48,11 +49,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 0xff, // current value ]), ); - const cc = new BinarySensorCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as BinarySensorCCReport; + t.is(cc.constructor, BinarySensorCCReport); t.is(cc.value, true); }); @@ -65,11 +66,11 @@ test("the Report command (v2) should be deserialized correctly", (t) => { BinarySensorType.CO2, ]), ); - const cc = new BinarySensorCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as BinarySensorCCReport; + t.is(cc.constructor, BinarySensorCCReport); t.is(cc.value, false); t.is(cc.type, BinarySensorType.CO2); @@ -93,11 +94,11 @@ test("the SupportedReport command should be deserialized correctly", (t) => { 0b10, ]), ); - const cc = new BinarySensorCCSupportedReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as BinarySensorCCSupportedReport; + t.is(cc.constructor, BinarySensorCCSupportedReport); t.deepEqual(cc.supportedSensorTypes, [ BinarySensorType["General Purpose"], @@ -112,11 +113,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new BinarySensorCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as BinarySensorCC; t.is(cc.constructor, BinarySensorCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/BinarySwitchCC.test.ts b/packages/zwave-js/src/lib/test/cc/BinarySwitchCC.test.ts index 5e5450d8f262..755c4fa39760 100644 --- a/packages/zwave-js/src/lib/test/cc/BinarySwitchCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/BinarySwitchCC.test.ts @@ -4,6 +4,7 @@ import { BinarySwitchCCReport, BinarySwitchCCSet, BinarySwitchCommand, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses, Duration } from "@zwave-js/core"; import { type GetSupportedCCVersion } from "@zwave-js/host"; @@ -79,11 +80,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 0xff, // current value ]), ); - const cc = new BinarySwitchCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as BinarySwitchCCReport; + t.is(cc.constructor, BinarySwitchCCReport); t.is(cc.currentValue, true); t.is(cc.targetValue, undefined); @@ -99,11 +100,11 @@ test("the Report command (v2) should be deserialized correctly", (t) => { 1, // duration ]), ); - const cc = new BinarySwitchCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as BinarySwitchCCReport; + t.is(cc.constructor, BinarySwitchCCReport); t.is(cc.currentValue, true); t.is(cc.targetValue, false); @@ -115,11 +116,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new BinarySwitchCC({ - nodeId: 2, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 2 } as any, + ) as BinarySwitchCC; t.is(cc.constructor, BinarySwitchCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/CRC16CC.test.ts b/packages/zwave-js/src/lib/test/cc/CRC16CC.test.ts index 64cefd9110ef..027a74774a00 100644 --- a/packages/zwave-js/src/lib/test/cc/CRC16CC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/CRC16CC.test.ts @@ -37,11 +37,10 @@ test("serialization and deserialization should be compatible", (t) => { t.is(crc16.encapsulated, basicCCSet); const serialized = crc16.serialize({} as any); - const deserialized = CommandClass.from({ - nodeId: basicCCSet.nodeId as number, - data: serialized, - context: {} as any, - }); + const deserialized = CommandClass.parse( + serialized, + { sourceNodeId: basicCCSet.nodeId as number } as any, + ); t.is(deserialized.nodeId, basicCCSet.nodeId); const deserializedPayload = (deserialized as CRC16CCCommandEncapsulation) .encapsulated as BasicCCSet; @@ -61,10 +60,9 @@ test("deserializing a CC with a wrong checksum should result in an invalid CC", const serialized = crc16.serialize({} as any); serialized[serialized.length - 1] ^= 0xff; - const decoded = CommandClass.from({ - nodeId: basicCCSet.nodeId as number, - data: serialized, - context: {} as any, - }); - t.true(decoded instanceof InvalidCC); + const deserialized = CommandClass.parse( + serialized, + { sourceNodeId: basicCCSet.nodeId as number } as any, + ); + t.true(deserialized instanceof InvalidCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/CentralSceneCC.test.ts b/packages/zwave-js/src/lib/test/cc/CentralSceneCC.test.ts index 0aa07df4f406..3bdb279111b4 100644 --- a/packages/zwave-js/src/lib/test/cc/CentralSceneCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/CentralSceneCC.test.ts @@ -8,6 +8,7 @@ import { CentralSceneCCSupportedReport, CentralSceneCommand, CentralSceneKeys, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses } from "@zwave-js/core"; import test from "ava"; @@ -68,11 +69,11 @@ test("the ConfigurationReport command should be deserialized correctly", (t) => 0b1000_0000, ]), ); - const cc = new CentralSceneCCConfigurationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as CentralSceneCCConfigurationReport; + t.is(cc.constructor, CentralSceneCCConfigurationReport); t.is(cc.slowRefresh, true); }); @@ -101,11 +102,11 @@ test("the SupportedReport command should be deserialized correctly", (t) => { 0, ]), ); - const cc = new CentralSceneCCSupportedReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as CentralSceneCCSupportedReport; + t.is(cc.constructor, CentralSceneCCSupportedReport); t.is(cc.sceneCount, 2); t.true(cc.supportsSlowRefresh); @@ -124,11 +125,11 @@ test("the Notification command should be deserialized correctly", (t) => { 8, // scene number ]), ); - const cc = new CentralSceneCCNotification({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as CentralSceneCCNotification; + t.is(cc.constructor, CentralSceneCCNotification); t.is(cc.sequenceNumber, 7); // slow refresh is only evaluated if the attribute is KeyHeldDown @@ -146,11 +147,11 @@ test("the Notification command should be deserialized correctly (KeyHeldDown)", 8, // scene number ]), ); - const cc = new CentralSceneCCNotification({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as CentralSceneCCNotification; + t.is(cc.constructor, CentralSceneCCNotification); t.is(cc.sequenceNumber, 7); t.true(cc.slowRefresh); @@ -162,11 +163,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new CentralSceneCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as CentralSceneCC; t.is(cc.constructor, CentralSceneCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/ColorSwitchCC.test.ts b/packages/zwave-js/src/lib/test/cc/ColorSwitchCC.test.ts index 957a23f0fd1e..da917686d044 100644 --- a/packages/zwave-js/src/lib/test/cc/ColorSwitchCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/ColorSwitchCC.test.ts @@ -9,6 +9,7 @@ import { ColorSwitchCCSupportedGet, ColorSwitchCCSupportedReport, ColorSwitchCommand, + CommandClass, } from "@zwave-js/cc"; import { CommandClasses, @@ -48,11 +49,11 @@ test("the SupportedReport command should deserialize correctly", (t) => { 0b0000_0001, ]), ); - const cc = new ColorSwitchCCSupportedReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as ColorSwitchCCSupportedReport; + t.is(cc.constructor, ColorSwitchCCSupportedReport); t.deepEqual(cc.supportedColorComponents, [ ColorComponent["Warm White"], @@ -91,11 +92,11 @@ test("the Report command should deserialize correctly (version 1)", (t) => { 0b1111_1111, // value: 255 ]), ); - const cc = new ColorSwitchCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as ColorSwitchCCReport; + t.is(cc.constructor, ColorSwitchCCReport); t.is(cc.colorComponent, ColorComponent.Red); t.is(cc.currentValue, 255); @@ -113,11 +114,11 @@ test("the Report command should deserialize correctly (version 3)", (t) => { 0b0000_0001, // duration: 1 ]), ); - const cc = new ColorSwitchCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as ColorSwitchCCReport; + t.is(cc.constructor, ColorSwitchCCReport); t.is(cc.colorComponent, ColorComponent.Red); t.is(cc.currentValue, 128); diff --git a/packages/zwave-js/src/lib/test/cc/CommandClass.persistValues.test.ts b/packages/zwave-js/src/lib/test/cc/CommandClass.persistValues.test.ts index 83a03979d623..81ed93adca18 100644 --- a/packages/zwave-js/src/lib/test/cc/CommandClass.persistValues.test.ts +++ b/packages/zwave-js/src/lib/test/cc/CommandClass.persistValues.test.ts @@ -1,4 +1,4 @@ -import { CentralSceneCommand, CentralSceneKeys } from "@zwave-js/cc"; +import { CentralSceneKeys } from "@zwave-js/cc"; import { BasicCCSet } from "@zwave-js/cc/BasicCC"; import { CentralSceneCCNotification } from "@zwave-js/cc/CentralSceneCC"; import { CommandClasses } from "@zwave-js/core"; @@ -93,14 +93,9 @@ test(`persistValues() should not store values marked as "events" (non-stateful)` const cc = new CentralSceneCCNotification({ nodeId: node2.id, - data: Buffer.from([ - CommandClasses["Central Scene"], - CentralSceneCommand.Notification, - 1, // seq number - CentralSceneKeys.KeyPressed, - 1, // scene number - ]), - context: {} as any, + sequenceNumber: 1, + sceneNumber: 1, + keyAttribute: CentralSceneKeys.KeyPressed, }); // Central Scene should use the value notification event instead of added/updated diff --git a/packages/zwave-js/src/lib/test/cc/CommandClass.test.ts b/packages/zwave-js/src/lib/test/cc/CommandClass.test.ts index 034bf949489f..2da463646b1a 100644 --- a/packages/zwave-js/src/lib/test/cc/CommandClass.test.ts +++ b/packages/zwave-js/src/lib/test/cc/CommandClass.test.ts @@ -42,13 +42,12 @@ test(`creating and serializing should work for unspecified commands`, (t) => { ); }); -test("from() returns an un-specialized instance when receiving a non-implemented CC", (t) => { +test("parse() returns an un-specialized instance when receiving a non-implemented CC", (t) => { // This is a Node Provisioning CC. Change it when that CC is implemented - const cc = CommandClass.from({ - data: Buffer.from("78030100", "hex"), - nodeId: 5, - context: {} as any, - }); + const cc = CommandClass.parse( + Buffer.from("78030100", "hex"), + { sourceNodeId: 5 } as any, + ); t.is(cc.constructor, CommandClass); t.is(cc.nodeId, 5); t.is(cc.ccId, 0x78); @@ -56,14 +55,13 @@ test("from() returns an un-specialized instance when receiving a non-implemented t.deepEqual(cc.payload, Buffer.from([0x01, 0x00])); }); -test("from() does not throw when the CC is implemented", (t) => { +test("parse() does not throw when the CC is implemented", (t) => { t.notThrows(() => - CommandClass.from({ - // CRC-16 with BasicCC - data: Buffer.from("560120024d26", "hex"), - nodeId: 5, - context: {} as any, - }) + // CRC-16 with BasicCC + CommandClass.parse( + Buffer.from("560120024d26", "hex"), + { sourceNodeId: 5 } as any, + ) ); }); diff --git a/packages/zwave-js/src/lib/test/cc/DoorLockCC.test.ts b/packages/zwave-js/src/lib/test/cc/DoorLockCC.test.ts index ac5bf54758d2..3167154c3c41 100644 --- a/packages/zwave-js/src/lib/test/cc/DoorLockCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/DoorLockCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, DoorLockCCCapabilitiesGet, DoorLockCCCapabilitiesReport, DoorLockCCConfigurationGet, @@ -74,11 +75,11 @@ test("the OperationReport command (v1-v3) should be deserialized correctly", (t) 20, // timeout seconds ]), ); - const cc = new DoorLockCCOperationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCOperationReport; + t.is(cc.constructor, DoorLockCCOperationReport); t.is(cc.currentMode, DoorLockMode.InsideUnsecuredWithTimeout); t.deepEqual(cc.outsideHandlesCanOpenDoor, [false, false, false, true]); @@ -104,11 +105,11 @@ test("the OperationReport command (v4) should be deserialized correctly", (t) => 0x01, // 1 second left ]), ); - const cc = new DoorLockCCOperationReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as DoorLockCCOperationReport; + t.is(cc.constructor, DoorLockCCOperationReport); cc.persistValues(host); t.is(cc.currentMode, DoorLockMode.OutsideUnsecured); @@ -149,11 +150,11 @@ test("the ConfigurationReport command (v1-v3) should be deserialized correctly", 20, // timeout seconds ]), ); - const cc = new DoorLockCCConfigurationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCConfigurationReport; + t.is(cc.constructor, DoorLockCCConfigurationReport); t.is(cc.operationType, DoorLockOperationType.Timed); t.deepEqual(cc.outsideHandlesCanOpenDoorConfiguration, [ @@ -185,11 +186,11 @@ test("the ConfigurationReport command must ignore invalid timeouts (constant)", 20, // timeout seconds ]), ); - const cc = new DoorLockCCConfigurationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCConfigurationReport; + t.is(cc.constructor, DoorLockCCConfigurationReport); t.is(cc.lockTimeoutConfiguration, undefined); }); @@ -204,11 +205,11 @@ test("the ConfigurationReport command must ignore invalid timeouts (invalid minu 20, // timeout seconds ]), ); - const cc = new DoorLockCCConfigurationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCConfigurationReport; + t.is(cc.constructor, DoorLockCCConfigurationReport); t.is(cc.lockTimeoutConfiguration, undefined); }); @@ -223,11 +224,11 @@ test("the ConfigurationReport command must ignore invalid timeouts (invalid seco 0xff, // timeout seconds ]), ); - const cc = new DoorLockCCConfigurationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCConfigurationReport; + t.is(cc.constructor, DoorLockCCConfigurationReport); t.is(cc.lockTimeoutConfiguration, undefined); }); @@ -248,11 +249,11 @@ test("the ConfigurationReport command (v4) should be deserialized correctly", (t 0b01, // flags ]), ); - const cc = new DoorLockCCConfigurationReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCConfigurationReport; + t.is(cc.constructor, DoorLockCCConfigurationReport); t.is(cc.autoRelockTime, 0xff01); t.is(cc.holdAndReleaseTime, 0x0203); @@ -314,11 +315,11 @@ test("the CapabilitiesReport command should be deserialized correctly", (t) => { 0b1010, // feature flags ]), ); - const cc = new DoorLockCCCapabilitiesReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockCCCapabilitiesReport; + t.is(cc.constructor, DoorLockCCCapabilitiesReport); t.deepEqual(cc.supportedOperationTypes, [ DoorLockOperationType.Constant, diff --git a/packages/zwave-js/src/lib/test/cc/DoorLockLoggingCC.test.ts b/packages/zwave-js/src/lib/test/cc/DoorLockLoggingCC.test.ts index c8e234abb8c4..f6a783a63637 100644 --- a/packages/zwave-js/src/lib/test/cc/DoorLockLoggingCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/DoorLockLoggingCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, DoorLockLoggingCCRecordGet, DoorLockLoggingCCRecordReport, DoorLockLoggingCCRecordsSupportedGet, @@ -37,11 +38,11 @@ test("the RecordsCountReport command should be deserialized correctly", (t) => { 0x14, // max records supported (20) ]), ); - const cc = new DoorLockLoggingCCRecordsSupportedReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockLoggingCCRecordsSupportedReport; + t.is(cc.constructor, DoorLockLoggingCCRecordsSupportedReport); t.is(cc.recordsCount, 20); }); @@ -79,11 +80,11 @@ test("the RecordReport command should be deserialized correctly", (t) => { ]), ); - const cc = new DoorLockLoggingCCRecordReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as DoorLockLoggingCCRecordReport; + t.is(cc.constructor, DoorLockLoggingCCRecordReport); t.is(cc.recordNumber, 7); diff --git a/packages/zwave-js/src/lib/test/cc/EntryControlCC.test.ts b/packages/zwave-js/src/lib/test/cc/EntryControlCC.test.ts index 168677275714..6c2de3081d77 100644 --- a/packages/zwave-js/src/lib/test/cc/EntryControlCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/EntryControlCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, EntryControlCCConfigurationGet, EntryControlCCConfigurationReport, EntryControlCCConfigurationSet, @@ -42,11 +43,11 @@ test("the Notification command should deserialize correctly", (t) => { ]), ); - const cc = new EntryControlCCNotification({ - nodeId: 1, + const cc = CommandClass.parse( data, - context: {} as any, - }); + { sourceNodeId: 1 } as any, + ) as EntryControlCCNotification; + t.is(cc.constructor, EntryControlCCNotification); t.deepEqual(cc.sequenceNumber, 1); t.deepEqual(cc.dataType, EntryControlDataTypes.ASCII); @@ -91,11 +92,11 @@ test("the ConfigurationReport command should be deserialize correctly", (t) => { ]), ); - const cc = new EntryControlCCConfigurationReport({ - nodeId: 1, + const cc = CommandClass.parse( data, - context: {} as any, - }); + { sourceNodeId: 1 } as any, + ) as EntryControlCCConfigurationReport; + t.is(cc.constructor, EntryControlCCConfigurationReport); t.deepEqual(cc.keyCacheSize, 1); t.deepEqual(cc.keyCacheTimeout, 2); @@ -131,11 +132,11 @@ test("the EventSupportedReport command should be deserialize correctly", (t) => ]), ); - const cc = new EntryControlCCEventSupportedReport({ - nodeId: 1, + const cc = CommandClass.parse( data, - context: {} as any, - }); + { sourceNodeId: 1 } as any, + ) as EntryControlCCEventSupportedReport; + t.is(cc.constructor, EntryControlCCEventSupportedReport); t.deepEqual(cc.supportedDataTypes, [EntryControlDataTypes.ASCII]); t.deepEqual(cc.supportedEventTypes, [ @@ -169,11 +170,11 @@ test("the KeySupportedReport command should be deserialize correctly", (t) => { ]), ); - const cc = new EntryControlCCKeySupportedReport({ - nodeId: 1, + const cc = CommandClass.parse( data, - context: {} as any, - }); + { sourceNodeId: 1 } as any, + ) as EntryControlCCKeySupportedReport; + t.is(cc.constructor, EntryControlCCKeySupportedReport); t.deepEqual(cc.supportedKeys, [1, 3, 4, 6]); }); diff --git a/packages/zwave-js/src/lib/test/cc/FibaroCC.test.ts b/packages/zwave-js/src/lib/test/cc/FibaroCC.test.ts index 7d21f7330e97..bb8fe9bc9488 100644 --- a/packages/zwave-js/src/lib/test/cc/FibaroCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/FibaroCC.test.ts @@ -47,14 +47,13 @@ test("the Report command should be deserialized correctly", (t) => { 0x00, // Tilt ]), ); - const cc = CommandClass.from({ - nodeId: 2, - data: ccData, - context: {} as any, - }); - t.true(cc instanceof FibaroVenetianBlindCCReport); - t.is((cc as FibaroVenetianBlindCCReport).position, 0); - t.is((cc as FibaroVenetianBlindCCReport).tilt, 0); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as FibaroVenetianBlindCCReport; + t.is(cc.constructor, FibaroVenetianBlindCCReport); + t.is(cc.position, 0); + t.is(cc.tilt, 0); }); test("FibaroVenetianBlindCCSet should expect no response", (t) => { @@ -77,9 +76,8 @@ test("FibaroVenetianBlindCCSet => FibaroVenetianBlindCCReport = unexpected", (t) nodeId: 2, tilt: 7, }); - const ccResponse = new FibaroVenetianBlindCCReport({ - nodeId: 2, - data: buildCCBuffer( + const ccResponse = CommandClass.parse( + buildCCBuffer( Buffer.from([ FibaroVenetianBlindCCCommand.Report, 0x03, // with Tilt and Position @@ -87,8 +85,8 @@ test("FibaroVenetianBlindCCSet => FibaroVenetianBlindCCReport = unexpected", (t) 0x07, // Tilt ]), ), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as FibaroVenetianBlindCCReport; t.false(ccRequest.isExpectedCCResponse(ccResponse)); }); @@ -97,9 +95,8 @@ test("FibaroVenetianBlindCCGet => FibaroVenetianBlindCCReport = expected", (t) = const ccRequest = new FibaroVenetianBlindCCGet({ nodeId: 2, }); - const ccResponse = new FibaroVenetianBlindCCReport({ - nodeId: 2, - data: buildCCBuffer( + const ccResponse = CommandClass.parse( + buildCCBuffer( Buffer.from([ FibaroVenetianBlindCCCommand.Report, 0x03, // with Tilt and Position @@ -107,8 +104,8 @@ test("FibaroVenetianBlindCCGet => FibaroVenetianBlindCCReport = expected", (t) = 0x07, // Tilt ]), ), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as FibaroVenetianBlindCCReport; t.true(ccRequest.isExpectedCCResponse(ccResponse)); }); diff --git a/packages/zwave-js/src/lib/test/cc/HumidityControlModeCC.test.ts b/packages/zwave-js/src/lib/test/cc/HumidityControlModeCC.test.ts index fb39ca7a96c7..f859eb75dd44 100644 --- a/packages/zwave-js/src/lib/test/cc/HumidityControlModeCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/HumidityControlModeCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, HumidityControlMode, HumidityControlModeCCGet, HumidityControlModeCCReport, @@ -57,11 +58,11 @@ test("the Report command should be deserialized correctly", (t) => { HumidityControlMode.Auto, // current value ]), ); - const cc = new HumidityControlModeCCReport({ - nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlModeCCReport; + t.is(cc.constructor, HumidityControlModeCCReport); t.is(cc.mode, HumidityControlMode.Auto); }); @@ -73,11 +74,10 @@ test("the Report command should set the correct value", (t) => { HumidityControlMode.Auto, // current value ]), ); - const report = new HumidityControlModeCCReport({ - nodeId, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlModeCCReport; report.persistValues(host); const currentValue = host.getValueDB(nodeId).getValue({ @@ -94,11 +94,10 @@ test("the Report command should set the correct metadata", (t) => { HumidityControlMode.Auto, // current value ]), ); - const cc = new HumidityControlModeCCReport({ - nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlModeCCReport; cc.persistValues(host); const currentValueMeta = host @@ -130,11 +129,11 @@ test("the SupportedReport command should be deserialized correctly", (t) => { (1 << HumidityControlMode.Off) | (1 << HumidityControlMode.Auto), ]), ); - const cc = new HumidityControlModeCCSupportedReport({ - nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlModeCCSupportedReport; + t.is(cc.constructor, HumidityControlModeCCSupportedReport); t.deepEqual(cc.supportedModes, [ HumidityControlMode.Off, @@ -149,11 +148,10 @@ test("the SupportedReport command should set the correct metadata", (t) => { (1 << HumidityControlMode.Off) | (1 << HumidityControlMode.Auto), ]), ); - const cc = new HumidityControlModeCCSupportedReport({ - nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlModeCCSupportedReport; cc.persistValues(host); const currentValueMeta = host diff --git a/packages/zwave-js/src/lib/test/cc/HumidityControlOperatingStateCC.test.ts b/packages/zwave-js/src/lib/test/cc/HumidityControlOperatingStateCC.test.ts index 158b1926bf2f..ab4b15c30b97 100644 --- a/packages/zwave-js/src/lib/test/cc/HumidityControlOperatingStateCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/HumidityControlOperatingStateCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, HumidityControlOperatingState, HumidityControlOperatingStateCCGet, HumidityControlOperatingStateCCReport, @@ -35,11 +36,11 @@ test("the Report command should be deserialized correctly", (t) => { HumidityControlOperatingState.Humidifying, // state ]), ); - const cc = new HumidityControlOperatingStateCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as HumidityControlOperatingStateCCReport; + t.is(cc.constructor, HumidityControlOperatingStateCCReport); t.is(cc.state, HumidityControlOperatingState.Humidifying); }); diff --git a/packages/zwave-js/src/lib/test/cc/HumidityControlSetpointCC.test.ts b/packages/zwave-js/src/lib/test/cc/HumidityControlSetpointCC.test.ts index 4166dbda4fdc..56c0de1fe87d 100644 --- a/packages/zwave-js/src/lib/test/cc/HumidityControlSetpointCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/HumidityControlSetpointCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, HumidityControlSetpointCCCapabilitiesGet, HumidityControlSetpointCCCapabilitiesReport, HumidityControlSetpointCCGet, @@ -70,11 +71,11 @@ test("the Report command should be deserialized correctly", (t) => { encodeFloatWithScale(12, 1), ]), ); - const cc = new HumidityControlSetpointCCReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCReport; + t.is(cc.constructor, HumidityControlSetpointCCReport); t.deepEqual(cc.type, HumidityControlSetpointType.Humidifier); t.is(cc.scale, 1); @@ -96,11 +97,10 @@ test("the Report command should set the correct value", (t) => { encodeFloatWithScale(12, 1), ]), ); - const report = new HumidityControlSetpointCCReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCReport; report.persistValues(host); const currentValue = host.getValueDB(nodeId).getValue({ @@ -128,11 +128,10 @@ test("the Report command should set the correct metadata", (t) => { encodeFloatWithScale(12, 1), ]), ); - const report = new HumidityControlSetpointCCReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCReport; report.persistValues(host); const setpointMeta = host.getValueDB(nodeId).getMetadata({ @@ -168,11 +167,11 @@ test("the SupportedReport command should be deserialized correctly", (t) => { | (1 << HumidityControlSetpointType.Auto), ]), ); - const cc = new HumidityControlSetpointCCSupportedReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCSupportedReport; + t.is(cc.constructor, HumidityControlSetpointCCSupportedReport); t.deepEqual(cc.supportedSetpointTypes, [ HumidityControlSetpointType.Humidifier, @@ -188,11 +187,10 @@ test("the SupportedReport command should set the correct value", (t) => { | (1 << HumidityControlSetpointType.Auto), ]), ); - const report = new HumidityControlSetpointCCSupportedReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCSupportedReport; report.persistValues(host); const currentValue = host.getValueDB(nodeId).getValue({ @@ -226,11 +224,11 @@ test("the ScaleSupportedReport command should be deserialized correctly", (t) => 0b11, // percent + absolute ]), ); - const cc = new HumidityControlSetpointCCScaleSupportedReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCScaleSupportedReport; + t.is(cc.constructor, HumidityControlSetpointCCScaleSupportedReport); t.deepEqual(cc.supportedScales, [0, 1]); // new Scale(0, { @@ -269,11 +267,11 @@ test("the CapabilitiesReport command should be deserialized correctly", (t) => { encodeFloatWithScale(90, 1), ]), ); - const cc = new HumidityControlSetpointCCCapabilitiesReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCCapabilitiesReport; + t.is(cc.constructor, HumidityControlSetpointCCCapabilitiesReport); t.deepEqual(cc.type, HumidityControlSetpointType.Humidifier); t.deepEqual(cc.minValue, 10); @@ -293,11 +291,10 @@ test("the CapabilitiesReport command should set the correct metadata", (t) => { encodeFloatWithScale(90, 1), ]), ); - const report = new HumidityControlSetpointCCCapabilitiesReport({ - nodeId: nodeId, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: nodeId } as any, + ) as HumidityControlSetpointCCCapabilitiesReport; report.persistValues(host); const setpointMeta = host.getValueDB(nodeId).getMetadata({ diff --git a/packages/zwave-js/src/lib/test/cc/IndicatorCC.test.ts b/packages/zwave-js/src/lib/test/cc/IndicatorCC.test.ts index 05a6cd7f2f03..bd6e42323bdb 100644 --- a/packages/zwave-js/src/lib/test/cc/IndicatorCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/IndicatorCC.test.ts @@ -100,11 +100,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 55, // value ]), ); - const cc = new IndicatorCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as IndicatorCCReport; + t.is(cc.constructor, IndicatorCCReport); t.is(cc.indicator0Value, 55); t.is(cc.values, undefined); @@ -124,11 +124,11 @@ test("the Report command (v2) should be deserialized correctly", (t) => { 1, // value ]), ); - const cc = new IndicatorCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as IndicatorCCReport; + t.is(cc.constructor, IndicatorCCReport); // Boolean indicators are only interpreted during persistValues cc.persistValues(host); @@ -151,11 +151,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new IndicatorCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as IndicatorCC; t.is(cc.constructor, IndicatorCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/LanguageCC.test.ts b/packages/zwave-js/src/lib/test/cc/LanguageCC.test.ts index 6fd678841444..434a6309e9bd 100644 --- a/packages/zwave-js/src/lib/test/cc/LanguageCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/LanguageCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, LanguageCC, LanguageCCGet, LanguageCCReport, @@ -75,11 +76,11 @@ test("the Report command should be deserialized correctly (w/o country code)", ( 0x75, ]), ); - const cc = new LanguageCCReport({ - nodeId: 4, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 4 } as any, + ) as LanguageCCReport; + t.is(cc.constructor, LanguageCCReport); t.is(cc.language, "deu"); t.is(cc.country, undefined); @@ -98,11 +99,11 @@ test("the Report command should be deserialized correctly (w/ country code)", (t 0x45, ]), ); - const cc = new LanguageCCReport({ - nodeId: 4, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 4 } as any, + ) as LanguageCCReport; + t.is(cc.constructor, LanguageCCReport); t.is(cc.language, "deu"); t.is(cc.country, "DE"); @@ -112,11 +113,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new LanguageCC({ - nodeId: 4, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 4 } as any, + ) as LanguageCC; t.is(cc.constructor, LanguageCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/ManufacturerSpecificCC.test.ts b/packages/zwave-js/src/lib/test/cc/ManufacturerSpecificCC.test.ts index 66e1ba7dac7e..111562efa31e 100644 --- a/packages/zwave-js/src/lib/test/cc/ManufacturerSpecificCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/ManufacturerSpecificCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, ManufacturerSpecificCCGet, ManufacturerSpecificCCReport, ManufacturerSpecificCommand, @@ -37,11 +38,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 0x06, ]), ); - const cc = new ManufacturerSpecificCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as ManufacturerSpecificCCReport; + t.is(cc.constructor, ManufacturerSpecificCCReport); t.is(cc.manufacturerId, 0x0102); t.is(cc.productType, 0x0304); diff --git a/packages/zwave-js/src/lib/test/cc/MeterCC.test.ts b/packages/zwave-js/src/lib/test/cc/MeterCC.test.ts index a335be76bad0..14806e0a3181 100644 --- a/packages/zwave-js/src/lib/test/cc/MeterCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/MeterCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, MeterCC, MeterCCGet, MeterCCReport, @@ -146,11 +147,11 @@ test("the Report command (V1) should be deserialized correctly", (t) => { 55, // value ]), ); - const cc = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(cc.constructor, MeterCCReport); t.is(cc.type, 3); t.is(cc.scale, 2); @@ -171,11 +172,11 @@ test("the Report command (V2) should be deserialized correctly (no time delta)", 0, ]), ); - const cc = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(cc.constructor, MeterCCReport); t.is(cc.type, 3); t.is(cc.scale, 2); @@ -197,11 +198,11 @@ test("the Report command (V2) should be deserialized correctly (with time delta) 54, // previous value ]), ); - const cc = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(cc.constructor, MeterCCReport); t.is(cc.type, 3); t.is(cc.scale, 2); @@ -223,11 +224,11 @@ test("the Report command (V3) should be deserialized correctly", (t) => { 54, // previous value ]), ); - const cc = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(cc.constructor, MeterCCReport); t.is(cc.scale, 6); }); @@ -245,11 +246,11 @@ test("the Report command (V4) should be deserialized correctly", (t) => { 0b01, // Scale2 ]), ); - const cc = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(cc.constructor, MeterCCReport); t.is(cc.scale, 8); }); @@ -268,11 +269,11 @@ test("the Report command should validate that a known meter type is given", (t) ]), ); - const report = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(report.constructor, MeterCCReport); // Meter type 31 (does not exist) assertZWaveError(t, () => report.persistValues(host), { @@ -294,11 +295,11 @@ test("the Report command should validate that a known meter scale is given", (t) ]), ); - const report = new MeterCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const report = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCReport; + t.is(report.constructor, MeterCCReport); // Meter type 4, Scale 8 (does not exist) assertZWaveError(t, () => report.persistValues(host), { @@ -327,11 +328,11 @@ test("the SupportedReport command (V2/V3) should be deserialized correctly", (t) 0b01101110, // supported scales ]), ); - const cc = new MeterCCSupportedReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCSupportedReport; + t.is(cc.constructor, MeterCCSupportedReport); t.is(cc.type, 21); t.true(cc.supportsReset); @@ -350,11 +351,11 @@ test("the SupportedReport command (V4/V5) should be deserialized correctly", (t) 1, ]), ); - const cc = new MeterCCSupportedReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as MeterCCSupportedReport; + t.is(cc.constructor, MeterCCSupportedReport); t.is(cc.type, 21); t.true(cc.supportsReset); @@ -388,11 +389,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new MeterCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as MeterCC; t.is(cc.constructor, MeterCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/MultiChannelAssociationCC.test.ts b/packages/zwave-js/src/lib/test/cc/MultiChannelAssociationCC.test.ts index 12db92f59c6f..bef3e1dfb1c7 100644 --- a/packages/zwave-js/src/lib/test/cc/MultiChannelAssociationCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/MultiChannelAssociationCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, MultiChannelAssociationCCGet, MultiChannelAssociationCCRemove, MultiChannelAssociationCCReport, @@ -38,11 +39,11 @@ test("the SupportedGroupingsReport command should be deserialized correctly", (t 7, // # of groups ]), ); - const cc = new MultiChannelAssociationCCSupportedGroupingsReport({ - nodeId: 4, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 4 } as any, + ) as MultiChannelAssociationCCSupportedGroupingsReport; + t.is(cc.constructor, MultiChannelAssociationCCSupportedGroupingsReport); t.is(cc.groupCount, 7); }); @@ -160,11 +161,11 @@ test("the Report command should be deserialized correctly (node IDs only)", (t) 5, ]), ); - const cc = new MultiChannelAssociationCCReport({ - nodeId: 4, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 4 } as any, + ) as MultiChannelAssociationCCReport; + t.is(cc.constructor, MultiChannelAssociationCCReport); t.is(cc.groupId, 5); t.is(cc.maxNodes, 9); @@ -189,11 +190,11 @@ test("the Report command should be deserialized correctly (endpoint addresses on 0b11010111, ]), ); - const cc = new MultiChannelAssociationCCReport({ - nodeId: 4, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 4 } as any, + ) as MultiChannelAssociationCCReport; + t.is(cc.constructor, MultiChannelAssociationCCReport); t.deepEqual(cc.nodeIds, []); t.deepEqual(cc.endpoints, [ @@ -227,11 +228,11 @@ test("the Report command should be deserialized correctly (both options)", (t) = 0b11010111, ]), ); - const cc = new MultiChannelAssociationCCReport({ - nodeId: 4, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 4 } as any, + ) as MultiChannelAssociationCCReport; + t.is(cc.constructor, MultiChannelAssociationCCReport); t.deepEqual(cc.nodeIds, [1, 5, 9]); t.deepEqual(cc.endpoints, [ diff --git a/packages/zwave-js/src/lib/test/cc/MultiChannelCC.test.ts b/packages/zwave-js/src/lib/test/cc/MultiChannelCC.test.ts index 78053cc8e033..87db658c682e 100644 --- a/packages/zwave-js/src/lib/test/cc/MultiChannelCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/MultiChannelCC.test.ts @@ -1,4 +1,4 @@ -import type { CommandClass } from "@zwave-js/cc"; +import { CommandClass } from "@zwave-js/cc"; import { BasicCCGet, BasicCCReport, @@ -80,7 +80,7 @@ test("the CommandEncapsulation command should serialize correctly", (t) => { let cc: CommandClass = new BasicCCSet({ nodeId: 2, targetValue: 5, - endpoint: 7, + endpointIndex: 7, }); cc = MultiChannelCC.encapsulate(cc); const expected = buildCCBuffer( @@ -150,11 +150,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new MultiChannelCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as MultiChannelCC; t.is(cc.constructor, MultiChannelCC); }); @@ -188,7 +187,7 @@ test("MultiChannelCC/BasicCCGet should expect a response", (t) => { const ccRequest = MultiChannelCC.encapsulate( new BasicCCGet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); t.true(ccRequest.expectsCCResponse()); @@ -198,7 +197,7 @@ test("MultiChannelCC/BasicCCGet (multicast) should expect NO response", (t) => { const ccRequest = MultiChannelCC.encapsulate( new BasicCCGet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); // A multicast request never expects a response @@ -210,7 +209,7 @@ test("MultiChannelCC/BasicCCSet should expect NO response", (t) => { const ccRequest = MultiChannelCC.encapsulate( new BasicCCSet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, targetValue: 7, }), ); @@ -221,7 +220,7 @@ test("MultiChannelCC/BasicCCGet => MultiChannelCC/BasicCCReport = expected", (t) const ccRequest = MultiChannelCC.encapsulate( new BasicCCGet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); const ccResponse = MultiChannelCC.encapsulate( @@ -239,13 +238,13 @@ test("MultiChannelCC/BasicCCGet => MultiChannelCC/BasicCCGet = unexpected", (t) const ccRequest = MultiChannelCC.encapsulate( new BasicCCGet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); const ccResponse = MultiChannelCC.encapsulate( new BasicCCGet({ nodeId: ccRequest.nodeId, - endpoint: 2, + endpointIndex: 2, }), ); ccResponse.endpointIndex = 2; @@ -257,7 +256,7 @@ test("MultiChannelCC/BasicCCGet => MultiCommandCC/BasicCCReport = unexpected", ( const ccRequest = MultiChannelCC.encapsulate( new BasicCCGet({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); const ccResponse = MultiCommandCC.encapsulate([ diff --git a/packages/zwave-js/src/lib/test/cc/MultilevelSwitchCC.test.ts b/packages/zwave-js/src/lib/test/cc/MultilevelSwitchCC.test.ts index 2d5f30b129dd..753e22153ba5 100644 --- a/packages/zwave-js/src/lib/test/cc/MultilevelSwitchCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/MultilevelSwitchCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, MultilevelSwitchCC, MultilevelSwitchCCGet, MultilevelSwitchCCReport, @@ -81,11 +82,11 @@ test("the Report command (V1) should be deserialized correctly", (t) => { 55, // current value ]), ); - const cc = new MultilevelSwitchCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as MultilevelSwitchCCReport; + t.is(cc.constructor, MultilevelSwitchCCReport); t.is(cc.currentValue, 55); t.is(cc.targetValue, undefined); @@ -101,11 +102,11 @@ test("the Report command (v4) should be deserialized correctly", (t) => { 1, // duration ]), ); - const cc = new MultilevelSwitchCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as MultilevelSwitchCCReport; + t.is(cc.constructor, MultilevelSwitchCCReport); t.is(cc.currentValue, 55); t.is(cc.targetValue, 66); @@ -166,11 +167,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new MultilevelSwitchCC({ - nodeId: 2, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 2 } as any, + ) as MultilevelSwitchCC; t.is(cc.constructor, MultilevelSwitchCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/PowerlevelCC.test.ts b/packages/zwave-js/src/lib/test/cc/PowerlevelCC.test.ts index d847a7b75013..54222320b630 100644 --- a/packages/zwave-js/src/lib/test/cc/PowerlevelCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/PowerlevelCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, Powerlevel, PowerlevelCC, PowerlevelCCGet, @@ -83,11 +84,11 @@ test("the Report command should be deserialized correctly (NormalPower)", (t) => 50, // timeout (ignored because NormalPower) ]), ); - const cc = new PowerlevelCCReport({ - nodeId: 5, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 5 } as any, + ) as PowerlevelCCReport; + t.is(cc.constructor, PowerlevelCCReport); t.is(cc.powerlevel, Powerlevel["Normal Power"]); t.is(cc.timeout, undefined); // timeout does not apply to NormalPower @@ -101,11 +102,11 @@ test("the Report command should be deserialized correctly (custom power)", (t) = 50, // timeout (ignored because NormalPower) ]), ); - const cc = new PowerlevelCCReport({ - nodeId: 5, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 5 } as any, + ) as PowerlevelCCReport; + t.is(cc.constructor, PowerlevelCCReport); t.is(cc.powerlevel, Powerlevel["-3 dBm"]); t.is(cc.timeout, 50); // timeout does not apply to NormalPower @@ -115,10 +116,9 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new PowerlevelCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as PowerlevelCC; t.is(cc.constructor, PowerlevelCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/SceneActivationCC.test.ts b/packages/zwave-js/src/lib/test/cc/SceneActivationCC.test.ts index 4520223956af..d9eddc9e718b 100644 --- a/packages/zwave-js/src/lib/test/cc/SceneActivationCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/SceneActivationCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, SceneActivationCC, SceneActivationCCSet, SceneActivationCommand, @@ -54,11 +55,11 @@ test("the Set command should be deserialized correctly", (t) => { 0x00, // 0 seconds ]), ); - const cc = new SceneActivationCCSet({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as SceneActivationCCSet; + t.is(cc.constructor, SceneActivationCCSet); t.is(cc.sceneId, 15); t.deepEqual(cc.dimmingDuration, new Duration(0, "seconds")); @@ -68,11 +69,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new SceneActivationCC({ - nodeId: 2, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 2 } as any, + ) as SceneActivationCC; t.is(cc.constructor, SceneActivationCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/SceneActuatorConfigurationCC.test.ts b/packages/zwave-js/src/lib/test/cc/SceneActuatorConfigurationCC.test.ts index 7270aef3ee8a..ed40a7b1cb21 100644 --- a/packages/zwave-js/src/lib/test/cc/SceneActuatorConfigurationCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/SceneActuatorConfigurationCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, SceneActuatorConfigurationCC, SceneActuatorConfigurationCCGet, SceneActuatorConfigurationCCReport, @@ -78,11 +79,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 0x05, // dimmingDuration ]), ); - const cc = new SceneActuatorConfigurationCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as SceneActuatorConfigurationCCReport; + t.is(cc.constructor, SceneActuatorConfigurationCCReport); t.is(cc.sceneId, 55); t.is(cc.level, 0x50); @@ -93,10 +94,9 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new SceneActuatorConfigurationCC({ - nodeId: 2, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 2 } as any, + ) as SceneActuatorConfigurationCC; t.is(cc.constructor, SceneActuatorConfigurationCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/SceneControllerConfigurationCC.test.ts b/packages/zwave-js/src/lib/test/cc/SceneControllerConfigurationCC.test.ts index ff76ea6285ec..64966795cb65 100644 --- a/packages/zwave-js/src/lib/test/cc/SceneControllerConfigurationCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/SceneControllerConfigurationCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, SceneControllerConfigurationCC, SceneControllerConfigurationCCGet, SceneControllerConfigurationCCReport, @@ -76,11 +77,11 @@ test("the Report command (v1) should be deserialized correctly", (t) => { 0x05, // dimming duration ]), ); - const cc = new SceneControllerConfigurationCCReport({ - nodeId: 2, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 2 } as any, + ) as SceneControllerConfigurationCCReport; + t.is(cc.constructor, SceneControllerConfigurationCCReport); t.is(cc.groupId, 3); t.is(cc.sceneId, 240); @@ -91,10 +92,9 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new SceneControllerConfigurationCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as SceneControllerConfigurationCC; t.is(cc.constructor, SceneControllerConfigurationCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/ThermostatFanModeCC.test.ts b/packages/zwave-js/src/lib/test/cc/ThermostatFanModeCC.test.ts index 72cc8c72bef7..ea0b4e98dac1 100644 --- a/packages/zwave-js/src/lib/test/cc/ThermostatFanModeCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/ThermostatFanModeCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, ThermostatFanMode, ThermostatFanModeCCGet, ThermostatFanModeCCReport, @@ -64,11 +65,11 @@ test("the Report command should be deserialized correctly", (t) => { 0b1000_0010, // Off bit set to 1 and Auto high mode ]), ); - const cc = new ThermostatFanModeCCReport({ - nodeId: 5, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 5 } as any, + ) as ThermostatFanModeCCReport; + t.is(cc.constructor, ThermostatFanModeCCReport); t.is(cc.mode, ThermostatFanMode["Auto high"]); t.is(cc.off, true); diff --git a/packages/zwave-js/src/lib/test/cc/ThermostatFanStateCC.test.ts b/packages/zwave-js/src/lib/test/cc/ThermostatFanStateCC.test.ts index 03ff89fe8d98..1766069a644c 100644 --- a/packages/zwave-js/src/lib/test/cc/ThermostatFanStateCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/ThermostatFanStateCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, ThermostatFanState, ThermostatFanStateCC, ThermostatFanStateCCGet, @@ -34,11 +35,11 @@ test("the Report command (v1 - v2) should be deserialized correctly", (t) => { ThermostatFanState["Idle / off"], // state ]), ); - const cc = new ThermostatFanStateCCReport({ - nodeId: 1, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 1 } as any, + ) as ThermostatFanStateCCReport; + t.is(cc.constructor, ThermostatFanStateCCReport); t.is(cc.state, ThermostatFanState["Idle / off"]); }); @@ -47,11 +48,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new ThermostatFanStateCC({ - nodeId: 1, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 1 } as any, + ) as ThermostatFanStateCC; t.is(cc.constructor, ThermostatFanStateCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/TimeCC.test.ts b/packages/zwave-js/src/lib/test/cc/TimeCC.test.ts index 8635a5ec304e..d9f24ae44afa 100644 --- a/packages/zwave-js/src/lib/test/cc/TimeCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/TimeCC.test.ts @@ -1,4 +1,5 @@ import { + CommandClass, TimeCC, TimeCCDateGet, TimeCCDateReport, @@ -37,11 +38,11 @@ test("the TimeReport command should be deserialized correctly", (t) => { 59, ]), ); - const cc = new TimeCCTimeReport({ - nodeId: 8, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 8 } as any, + ) as TimeCCTimeReport; + t.is(cc.constructor, TimeCCTimeReport); t.is(cc.hour, 14); t.is(cc.minute, 23); @@ -68,11 +69,11 @@ test("the DateReport command should be deserialized correctly", (t) => { 17, ]), ); - const cc = new TimeCCDateReport({ - nodeId: 8, - data: ccData, - context: {} as any, - }); + const cc = CommandClass.parse( + ccData, + { sourceNodeId: 8 } as any, + ) as TimeCCDateReport; + t.is(cc.constructor, TimeCCDateReport); t.is(cc.year, 1989); t.is(cc.month, 10); @@ -83,11 +84,10 @@ test("deserializing an unsupported command should return an unspecified version const serializedCC = buildCCBuffer( Buffer.from([255]), // not a valid command ); - const cc: any = new TimeCC({ - nodeId: 8, - data: serializedCC, - context: {} as any, - }); + const cc = CommandClass.parse( + serializedCC, + { sourceNodeId: 8 } as any, + ) as TimeCC; t.is(cc.constructor, TimeCC); }); diff --git a/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts b/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts index 6de0e0e34cb0..ddb6d59d538c 100644 --- a/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts +++ b/packages/zwave-js/src/lib/test/cc/WakeUpCC.test.ts @@ -10,7 +10,7 @@ import { randomBytes } from "node:crypto"; test("WakeUpCCNoMoreInformation should expect no response", (t) => { const cc = new WakeUpCCNoMoreInformation({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }); t.false(cc.expectsCCResponse()); }); @@ -19,7 +19,7 @@ test("MultiChannelCC/WakeUpCCNoMoreInformation should expect NO response", (t) = const ccRequest = MultiChannelCC.encapsulate( new WakeUpCCNoMoreInformation({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); t.false(ccRequest.expectsCCResponse()); @@ -42,7 +42,7 @@ test("SecurityCC/WakeUpCCNoMoreInformation should expect NO response", (t) => { securityManager as any, new WakeUpCCNoMoreInformation({ nodeId: 2, - endpoint: 2, + endpointIndex: 2, }), ); t.false(ccRequest.expectsCCResponse()); diff --git a/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts b/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts index a59b1b455a24..3497bc264da4 100644 --- a/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/decodeLowerS2Keys.test.ts @@ -81,8 +81,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -115,9 +113,7 @@ integrationTest( }); let cc = new Security2CCMessageEncapsulation({ nodeId: mockController.ownNodeId, - ownNodeId: mockNode.id, encapsulated: innerCC, - securityManagers: mockNode.securityManagers, }); await mockNode.sendToController( @@ -159,9 +155,7 @@ integrationTest( cc = new Security2CCMessageEncapsulation({ nodeId: mockController.ownNodeId, - ownNodeId: mockNode.id, encapsulated: innerCC, - securityManagers: mockNode.securityManagers, }); await mockNode.sendToController( diff --git a/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts b/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts index 32612ed3beab..258ec82ee2a1 100644 --- a/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts @@ -77,8 +77,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -104,8 +102,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, diff --git a/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts b/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts index 45c276005024..f82c2d4da011 100644 --- a/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/secureNodeSecureEndpoint.test.ts @@ -128,8 +128,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -155,8 +153,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, diff --git a/packages/zwave-js/src/lib/test/driver/assemblePartialCCs.test.ts b/packages/zwave-js/src/lib/test/driver/assemblePartialCCs.test.ts index b63ee216c405..98f828d14a37 100644 --- a/packages/zwave-js/src/lib/test/driver/assemblePartialCCs.test.ts +++ b/packages/zwave-js/src/lib/test/driver/assemblePartialCCs.test.ts @@ -1,4 +1,5 @@ -import { AssociationCCReport } from "@zwave-js/cc/AssociationCC"; +import { CommandClass } from "@zwave-js/cc"; +import { type AssociationCCReport } from "@zwave-js/cc/AssociationCC"; import { BasicCCSet } from "@zwave-js/cc/BasicCC"; import { MultiCommandCCCommandEncapsulation } from "@zwave-js/cc/MultiCommandCC"; import { SecurityCCCommandEncapsulation } from "@zwave-js/cc/SecurityCC"; @@ -61,9 +62,8 @@ test.serial( "returns true when a partial CC is received that expects no more reports", (t) => { const { driver } = t.context; - const cc = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -73,8 +73,8 @@ test.serial( 2, 3, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ); const msg = new ApplicationCommandRequest({ command: cc, }); @@ -86,9 +86,8 @@ test.serial( "returns false when a partial CC is received that expects more reports", (t) => { const { driver } = t.context; - const cc = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -98,8 +97,8 @@ test.serial( 2, 3, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; const msg = new ApplicationCommandRequest({ command: cc, }); @@ -111,9 +110,8 @@ test.serial( "returns true when the final partial CC is received and merges its data", (t) => { const { driver } = t.context; - const cc1 = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc1 = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -123,11 +121,10 @@ test.serial( 2, 3, ]), - context: {} as any, - }); - const cc2 = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; + const cc2 = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -137,8 +134,8 @@ test.serial( 5, 6, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; const msg1 = new ApplicationCommandRequest({ command: cc1, }); @@ -175,8 +172,6 @@ test.serial("supports nested partial/non-partial CCs", (t) => { const cc1 = new BasicCCSet({ nodeId: 2, targetValue: 25 }); const cc = new SecurityCCCommandEncapsulation({ nodeId: 2, - ownNodeId: driver.ownNodeId, - securityManager: driver.securityManager!, encapsulated: {} as any, }); cc.encapsulated = undefined as any; @@ -191,8 +186,6 @@ test.serial("supports nested partial/partial CCs (part 1)", (t) => { const { driver } = t.context; const cc = new SecurityCCCommandEncapsulation({ nodeId: 2, - ownNodeId: driver.ownNodeId, - securityManager: driver.securityManager!, encapsulated: {} as any, }); cc.encapsulated = undefined as any; @@ -216,8 +209,6 @@ test.serial("supports nested partial/partial CCs (part 2)", (t) => { const { driver } = t.context; const cc = new SecurityCCCommandEncapsulation({ nodeId: 2, - ownNodeId: driver.ownNodeId, - securityManager: driver.securityManager!, encapsulated: {} as any, }); cc.encapsulated = undefined as any; @@ -241,9 +232,8 @@ test.serial( "returns false when a partial CC throws Deserialization_NotImplemented during merging", (t) => { const { driver } = t.context; - const cc = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -253,8 +243,8 @@ test.serial( 2, 3, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; cc.mergePartialCCs = () => { throw new ZWaveError( "not implemented", @@ -272,9 +262,8 @@ test.serial( "returns false when a partial CC throws CC_NotImplemented during merging", (t) => { const { driver } = t.context; - const cc = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -284,8 +273,8 @@ test.serial( 2, 3, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; cc.mergePartialCCs = () => { throw new ZWaveError( "not implemented", @@ -303,9 +292,8 @@ test.serial( "returns false when a partial CC throws PacketFormat_InvalidPayload during merging", (t) => { const { driver } = t.context; - const cc = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -315,8 +303,8 @@ test.serial( 2, 3, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; cc.mergePartialCCs = () => { throw new ZWaveError( "not implemented", @@ -332,9 +320,8 @@ test.serial( test.serial("passes other errors during merging through", (t) => { const { driver } = t.context; - const cc = new AssociationCCReport({ - nodeId: 2, - data: Buffer.from([ + const cc = CommandClass.parse( + Buffer.from([ CommandClasses.Association, AssociationCommand.Report, 1, @@ -344,8 +331,8 @@ test.serial("passes other errors during merging through", (t) => { 2, 3, ]), - context: {} as any, - }); + { sourceNodeId: 2 } as any, + ) as AssociationCCReport; cc.mergePartialCCs = () => { throw new ZWaveError("invalid", ZWaveErrorCodes.Argument_Invalid); }; diff --git a/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts b/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts index d8b586c642f7..aaab6a3edd2e 100644 --- a/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts +++ b/packages/zwave-js/src/lib/test/driver/ignoreCCVersion0ForKnownSupportedCCs.test.ts @@ -79,8 +79,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -106,8 +104,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -265,6 +261,7 @@ integrationTest( CommandClasses.Version, ], controlledCCs: [], + reportsToFollow: 0, }); const cc = SecurityCC.encapsulate( self.id, @@ -347,15 +344,15 @@ integrationTest( const parseS0CC: MockNodeBehavior = { handleCC(controller, self, receivedCC) { // We don't support sequenced commands here - if ( - receivedCC instanceof SecurityCCCommandEncapsulation - ) { - receivedCC.mergePartialCCs( - [], - {} as any, - ); + if (receivedCC instanceof SecurityCCCommandEncapsulation) { + receivedCC.mergePartialCCs([], { + sourceNodeId: controller.ownNodeId, + __internalIsMockNode: true, + ...self.encodingContext, + ...self.securityManagers, + }); } - + // This just decodes - we need to call further handlers return undefined; }, }; diff --git a/packages/zwave-js/src/lib/test/driver/nodeAsleepBlockNonceReport.test.ts b/packages/zwave-js/src/lib/test/driver/nodeAsleepBlockNonceReport.test.ts index c2ca706a514e..3cb19c10bc05 100644 --- a/packages/zwave-js/src/lib/test/driver/nodeAsleepBlockNonceReport.test.ts +++ b/packages/zwave-js/src/lib/test/driver/nodeAsleepBlockNonceReport.test.ts @@ -63,14 +63,13 @@ integrationTest( const parseS0CC: MockNodeBehavior = { handleCC(controller, self, receivedCC) { // We don't support sequenced commands here - if ( - receivedCC - instanceof SecurityCCCommandEncapsulation - ) { - receivedCC.mergePartialCCs( - [], - {} as any, - ); + if (receivedCC instanceof SecurityCCCommandEncapsulation) { + receivedCC.mergePartialCCs([], { + sourceNodeId: controller.ownNodeId, + __internalIsMockNode: true, + ...self.encodingContext, + ...self.securityManagers, + }); } // This just decodes - we need to call further handlers return undefined; diff --git a/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts b/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts index 2c51783170f9..f255ee318a9d 100644 --- a/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts +++ b/packages/zwave-js/src/lib/test/driver/s0AndS2Encapsulation.test.ts @@ -110,7 +110,12 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { handleCC(controller, self, receivedCC) { // We don't support sequenced commands here if (receivedCC instanceof SecurityCCCommandEncapsulation) { - receivedCC.mergePartialCCs([], {} as any); + receivedCC.mergePartialCCs([], { + sourceNodeId: controller.ownNodeId, + __internalIsMockNode: true, + ...self.encodingContext, + ...self.securityManagers, + }); } return undefined; }, @@ -126,8 +131,6 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -153,8 +156,6 @@ integrationTest("S0 commands are S0-encapsulated, even when S2 is supported", { ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, diff --git a/packages/zwave-js/src/lib/test/driver/s0Encapsulation.test.ts b/packages/zwave-js/src/lib/test/driver/s0Encapsulation.test.ts index 8d910d3b8988..f378109ff57b 100644 --- a/packages/zwave-js/src/lib/test/driver/s0Encapsulation.test.ts +++ b/packages/zwave-js/src/lib/test/driver/s0Encapsulation.test.ts @@ -92,6 +92,7 @@ integrationTest("Communication via Security S0 works", { nodeId: controller.ownNodeId, supportedCCs: [CommandClasses.Basic], controlledCCs: [], + reportsToFollow: 0, }); const cc = SecurityCC.encapsulate( self.id, @@ -169,7 +170,12 @@ integrationTest("Communication via Security S0 works", { handleCC(controller, self, receivedCC) { // We don't support sequenced commands here if (receivedCC instanceof SecurityCCCommandEncapsulation) { - receivedCC.mergePartialCCs([], {} as any); + receivedCC.mergePartialCCs([], { + sourceNodeId: controller.ownNodeId, + __internalIsMockNode: true, + ...self.encodingContext, + ...self.securityManagers, + }); } // This just decodes - we need to call further handlers return undefined; diff --git a/packages/zwave-js/src/lib/test/driver/s0EncapsulationTwoNodes.test.ts b/packages/zwave-js/src/lib/test/driver/s0EncapsulationTwoNodes.test.ts index 572a410358ec..4d18d128968c 100644 --- a/packages/zwave-js/src/lib/test/driver/s0EncapsulationTwoNodes.test.ts +++ b/packages/zwave-js/src/lib/test/driver/s0EncapsulationTwoNodes.test.ts @@ -117,6 +117,7 @@ integrationTest( nodeId: controller.ownNodeId, supportedCCs: [CommandClasses.Basic], controlledCCs: [], + reportsToFollow: 0, }); const cc = SecurityCC.encapsulate( self.id, @@ -206,10 +207,12 @@ integrationTest( if ( receivedCC instanceof SecurityCCCommandEncapsulation ) { - receivedCC.mergePartialCCs( - [], - {} as any, - ); + receivedCC.mergePartialCCs([], { + sourceNodeId: controller.ownNodeId, + __internalIsMockNode: true, + ...self.encodingContext, + ...self.securityManagers, + }); } // This just decodes - we need to call further handlers return undefined; diff --git a/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts b/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts index be1ff71d905b..623664cb10a3 100644 --- a/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts +++ b/packages/zwave-js/src/lib/test/driver/s2Collisions.test.ts @@ -89,8 +89,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -116,8 +114,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -261,8 +257,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -288,8 +282,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -400,8 +392,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, @@ -427,8 +417,6 @@ integrationTest( ); const cc = new Security2CCNonceReport({ nodeId: controller.ownNodeId, - ownNodeId: self.id, - securityManagers: self.securityManagers, SOS: true, MOS: false, receiverEI: nonce, diff --git a/packages/zwave-js/src/lib/test/node/Node.handleCommand.test.ts b/packages/zwave-js/src/lib/test/node/Node.handleCommand.test.ts index a356fdc2d349..d21207e9b0cf 100644 --- a/packages/zwave-js/src/lib/test/node/Node.handleCommand.test.ts +++ b/packages/zwave-js/src/lib/test/node/Node.handleCommand.test.ts @@ -1,6 +1,6 @@ -import { BinarySwitchCommand, EntryControlCommand } from "@zwave-js/cc"; +import { CommandClass, EntryControlCommand } from "@zwave-js/cc"; import { BinarySwitchCCReport } from "@zwave-js/cc/BinarySwitchCC"; -import { EntryControlCCNotification } from "@zwave-js/cc/EntryControlCC"; +import { type EntryControlCCNotification } from "@zwave-js/cc/EntryControlCC"; import { type CommandClassInfo, CommandClasses } from "@zwave-js/core"; import test from "ava"; import sinon from "sinon"; @@ -66,12 +66,7 @@ test.serial( // Handle a command for the root endpoint const command = new BinarySwitchCCReport({ nodeId: 2, - data: Buffer.from([ - CommandClasses["Binary Switch"], - BinarySwitchCommand.Report, - 0xff, - ]), - context: {} as any, + currentValue: true, }); await node.handleCommand(command); @@ -117,11 +112,10 @@ test.serial( Buffer.alloc(12, 0xff), ]); - const command = new EntryControlCCNotification({ - nodeId: node.id, - data: buf, - context: {} as any, - }); + const command = CommandClass.parse( + buf, + { sourceNodeId: node.id } as any, + ) as EntryControlCCNotification; await node.handleCommand(command); diff --git a/packages/zwave-js/src/lib/zniffer/Zniffer.ts b/packages/zwave-js/src/lib/zniffer/Zniffer.ts index f20dd64a0f1e..a7ed91dc2be2 100644 --- a/packages/zwave-js/src/lib/zniffer/Zniffer.ts +++ b/packages/zwave-js/src/lib/zniffer/Zniffer.ts @@ -566,11 +566,9 @@ supported frequencies: ${ // TODO: Support parsing multicast S2 frames try { - cc = CommandClass.from({ - data: mpdu.payload, - fromEncapsulation: false, - nodeId: mpdu.sourceNodeId, - context: { + cc = CommandClass.parse( + mpdu.payload, + { homeId: mpdu.homeId, ownNodeId: destNodeId, sourceNodeId: mpdu.sourceNodeId, @@ -579,7 +577,7 @@ supported frequencies: ${ securityManagerLR: destSecurityManagerLR, ...this.parsingContext, }, - }); + ); } catch (e: any) { // Ignore console.error(e.stack);