diff --git a/packages/cc/src/cc/LanguageCC.ts b/packages/cc/src/cc/LanguageCC.ts index f249af5767e7..87a9221149c6 100644 --- a/packages/cc/src/cc/LanguageCC.ts +++ b/packages/cc/src/cc/LanguageCC.ts @@ -254,14 +254,11 @@ export class LanguageCCReport extends LanguageCC { 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(raw.payload.length >= 3); const language = raw.payload.toString("ascii", 0, 3); let country: MaybeNotKnown; diff --git a/packages/cc/src/cc/NoOperationCC.ts b/packages/cc/src/cc/NoOperationCC.ts index 65ac9fa282fa..993252cb840a 100644 --- a/packages/cc/src/cc/NoOperationCC.ts +++ b/packages/cc/src/cc/NoOperationCC.ts @@ -1,5 +1,4 @@ import { CommandClasses, MessagePriority } from "@zwave-js/core/safe"; -import type { Message } from "@zwave-js/serial"; import { PhysicalCCAPI } from "../lib/API"; import { CommandClass } from "../lib/CommandClass"; import { @@ -7,7 +6,6 @@ import { commandClass, implementedVersion, } from "../lib/CommandClassDecorators"; -import { isCommandClassContainer } from "../lib/ICommandClassContainer"; // @noSetValueAPI This CC has no set-type commands // @noInterview There's nothing to interview here @@ -37,13 +35,3 @@ export class NoOperationCCAPI extends PhysicalCCAPI { export class NoOperationCC extends CommandClass { declare ccCommand: undefined; } - -/** - * @publicAPI - * Tests if a given message is a ping - */ -export function messageIsPing( - msg: T, -): msg is T & { command: NoOperationCC } { - return isCommandClassContainer(msg) && msg.command instanceof NoOperationCC; -} diff --git a/packages/cc/src/cc/NotificationCC.ts b/packages/cc/src/cc/NotificationCC.ts index 335a68e0cb78..7c761041fcaf 100644 --- a/packages/cc/src/cc/NotificationCC.ts +++ b/packages/cc/src/cc/NotificationCC.ts @@ -1318,6 +1318,7 @@ export class NotificationCCReport extends NotificationCC { // Convert CommandClass instances to a standardized object representation const cc = CommandClass.parse(this.eventParameters, { ...ctx, + frameType: "singlecast", sourceNodeId: this.nodeId as number, // Security encapsulation is handled outside of this CC, // so it is not needed here: diff --git a/packages/cc/src/cc/index.ts b/packages/cc/src/cc/index.ts index 11d9de2ae3c1..459dff05b456 100644 --- a/packages/cc/src/cc/index.ts +++ b/packages/cc/src/cc/index.ts @@ -544,7 +544,7 @@ export { MultilevelSwitchCCSupportedReport, MultilevelSwitchCCValues, } from "./MultilevelSwitchCC"; -export { NoOperationCC, messageIsPing } from "./NoOperationCC"; +export { NoOperationCC } from "./NoOperationCC"; export type { NodeNamingAndLocationCCLocationReportOptions, NodeNamingAndLocationCCLocationSetOptions, diff --git a/packages/cc/src/index.ts b/packages/cc/src/index.ts index 0338ed9f54b8..c87f1b0b4cc5 100644 --- a/packages/cc/src/index.ts +++ b/packages/cc/src/index.ts @@ -6,7 +6,6 @@ export * from "./lib/API"; export * from "./lib/CommandClass"; export * from "./lib/CommandClassDecorators"; export * from "./lib/EncapsulatingCommandClass"; -export * from "./lib/ICommandClassContainer"; export { MGRPExtension, MOSExtension, diff --git a/packages/cc/src/lib/CommandClass.ts b/packages/cc/src/lib/CommandClass.ts index 14bd7a835bb0..7ae4b2f9668b 100644 --- a/packages/cc/src/lib/CommandClass.ts +++ b/packages/cc/src/lib/CommandClass.ts @@ -71,10 +71,6 @@ import { isEncapsulatingCommandClass, isMultiEncapsulatingCommandClass, } from "./EncapsulatingCommandClass"; -import { - type ICommandClassContainer, - isCommandClassContainer, -} from "./ICommandClassContainer"; import { type CCValue, type DynamicCCValue, @@ -195,15 +191,6 @@ export class CCRaw { 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 @@ -226,10 +213,10 @@ export class CommandClass implements CCId { } public static parse( - payload: Buffer, + data: Buffer, ctx: CCParsingContext, ): CommandClass { - const raw = CCRaw.parse(payload); + const raw = CCRaw.parse(data); // Find the correct subclass constructor to invoke const CCConstructor = getCCConstructor(raw.ccId); @@ -1189,26 +1176,6 @@ export class InvalidCC extends CommandClass { } } -/** @publicAPI */ -export function assertValidCCs(container: ICommandClassContainer): void { - if (container.command instanceof InvalidCC) { - if (typeof container.command.reason === "number") { - throw new ZWaveError( - "The message payload failed validation!", - container.command.reason, - ); - } else { - throw new ZWaveError( - "The message payload is invalid!", - ZWaveErrorCodes.PacketFormat_InvalidPayload, - container.command.reason, - ); - } - } else if (isCommandClassContainer(container.command)) { - assertValidCCs(container.command); - } -} - export type CCConstructor = typeof CommandClass & { // I don't like the any, but we need it to support half-implemented CCs (e.g. report classes) new (options: any): T; diff --git a/packages/cc/src/lib/ICommandClassContainer.ts b/packages/cc/src/lib/ICommandClassContainer.ts index 2bdbf4ff8f82..e69de29bb2d1 100644 --- a/packages/cc/src/lib/ICommandClassContainer.ts +++ b/packages/cc/src/lib/ICommandClassContainer.ts @@ -1,14 +0,0 @@ -import { CommandClass } from "./CommandClass"; - -export interface ICommandClassContainer { - command: CommandClass; -} - -/** - * Tests if the given message contains a CC - */ -export function isCommandClassContainer( - msg: T | undefined, -): msg is T & ICommandClassContainer { - return (msg as any)?.command instanceof CommandClass; -} diff --git a/packages/core/src/consts/Transmission.ts b/packages/core/src/consts/Transmission.ts index 006dab8e9cab..ba8c07ac2ca8 100644 --- a/packages/core/src/consts/Transmission.ts +++ b/packages/core/src/consts/Transmission.ts @@ -163,8 +163,6 @@ export function routingSchemeToString(scheme: RoutingScheme): string { export interface TXReport { /** Transmission time in ticks (multiples of 10ms) */ txTicks: number; - /** Number of repeaters used in the route to the destination, 0 for direct range */ - numRepeaters: number; /** RSSI value of the acknowledgement frame */ ackRSSI?: RSSI; /** RSSI values of the incoming acknowledgement frame, measured by repeater 0...3 */ diff --git a/packages/host/src/ZWaveHost.ts b/packages/host/src/ZWaveHost.ts index ce10e851ffd9..ad60c0001502 100644 --- a/packages/host/src/ZWaveHost.ts +++ b/packages/host/src/ZWaveHost.ts @@ -23,16 +23,6 @@ export interface HostIDs { homeId: number; } -// FIXME: This should not be needed. Instead have the driver set callback IDs during sendMessage -/** Allows generating a new callback ID */ -export interface GetNextCallbackId { - /** - * Returns the next callback ID. Callback IDs are used to correlate requests - * to the controller/nodes with its response - */ - getNextCallbackId(): number; -} - /** Allows querying device configuration for a node */ export interface GetDeviceConfig { getDeviceConfig(nodeId: number): DeviceConfig | undefined; @@ -71,7 +61,7 @@ export interface CCParsingContext __internalIsMockNode?: boolean; /** If known, the frame type of the containing message */ - frameType?: FrameType; + frameType: FrameType; getHighestSecurityClass(nodeId: number): MaybeNotKnown; diff --git a/packages/maintenance/src/generateTypedDocs.ts b/packages/maintenance/src/generateTypedDocs.ts index ae4ec6d61661..46fb2d8d2474 100644 --- a/packages/maintenance/src/generateTypedDocs.ts +++ b/packages/maintenance/src/generateTypedDocs.ts @@ -359,9 +359,8 @@ async function processCCDocFile( if (!APIClass) return; const ccId = getCommandClassFromClassDeclaration( - // FIXME: there seems to be some discrepancy between ts-morph's bundled typescript and our typescript - file.compilerNode as any, - APIClass.compilerNode as any, + file.compilerNode, + APIClass.compilerNode, ); if (ccId == undefined) return; const ccName = getCCName(ccId); diff --git a/packages/maintenance/src/refactorMessageParsing.01.ts b/packages/maintenance/src/refactorMessageParsing.01.ts new file mode 100644 index 000000000000..db6d34e32f68 --- /dev/null +++ b/packages/maintenance/src/refactorMessageParsing.01.ts @@ -0,0 +1,430 @@ +import fs from "node:fs/promises"; +import { + type Node, + Project, + SyntaxKind, + VariableDeclarationKind, +} from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/zwave-js/tsconfig.json", + }); + // project.addSourceFilesAtPaths("packages/cc/src/cc/**/*CC.ts"); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getFilePath().includes("lib/serialapi/") + ); + for (const file of sourceFiles) { + // const filePath = path.relative(process.cwd(), file.getFilePath()); + + // Remove `extends MessageBaseOptions` + const ifaceExtends = file.getDescendantsOfKind( + SyntaxKind.InterfaceDeclaration, + ) + .map((iface) => + [ + iface, + iface.getExtends().filter((ext) => + ext.getText() === "MessageBaseOptions" + ), + ] as const + ) + .filter(([, exts]) => exts.length > 0); + for (const [self, exts] of ifaceExtends) { + for (const ext of exts) { + self.removeExtends(ext); + self.toggleModifier("export", true); + } + } + + const msgImplementations = file.getDescendantsOfKind( + SyntaxKind.ClassDeclaration, + ).filter((cls) => { + if (cls.getExtends()?.getText() === "Message") return true; + + const name = cls.getName(); + if (!name) return false; + // if (name.endsWith("Base")) return false; + return name.includes("Request") + || name.endsWith("Response") + || name.endsWith("TransmitReport") + || name.endsWith("StatusReport") + || name.endsWith("Callback"); + }); + const ctors = msgImplementations.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]; + + if (cls.getName()!.startsWith("SendDataMulticastRequest")) debugger; + + // with a union type where one is MessageDeserializationOptions + const types = ctorParam.getDescendantsOfKind( + SyntaxKind.TypeReference, + ).map((t) => t.getText()); + let otherType: string | undefined; + if ( + types.length === 1 + && types[0] === "MessageDeserializationOptions" + ) { + // 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 === 1 + && types[0] === "MessageOptions" + ) { + // Actually MessageBaseOptions | MessageDeserializationOptions + otherType = "MessageBaseOptions"; + } else if ( + types.length === 2 + && types.includes("MessageDeserializationOptions") + ) { + // ABCOptions | MessageDeserializationOptions + otherType = types.find( + (type) => type !== "MessageDeserializationOptions", + ); + } else if ( + types.length === 3 + && types.includes("MessageDeserializationOptions") + && types.some((generic) => + types.some((type) => type.includes(`<${generic}>`)) + ) + ) { + // ABCOptions | MessageDeserializationOptions + otherType = types.find( + (type) => + types.some((other) => type.includes(`<${other}>`)), + ); + } else if ( + types.length === 3 + && types.includes("MessageDeserializationOptions") + && types.includes("MessageBaseOptions") + ) { + // (ABCOptions & MessageBaseOptions) | MessageDeserializationOptions + otherType = types.find( + (type) => + type !== "MessageDeserializationOptions" + && type !== "MessageBaseOptions", + ); + } 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; + + // Add required imports + const hasRawImport = file.getImportDeclarations().some( + (decl) => + decl.getNamedImports().some((imp) => + imp.getName() === "MessageRaw" + ), + ); + if (!hasRawImport) { + const existing = file.getImportDeclaration((decl) => + decl.getModuleSpecifierValue().startsWith("@zwave-js/serial") + ); + if (!existing) { + file.addImportDeclaration({ + moduleSpecifier: "@zwave-js/serial", + namedImports: [{ + name: "MessageRaw", + isTypeOnly: true, + }], + }); + } else { + existing.addNamedImport({ + name: "MessageRaw", + isTypeOnly: true, + }); + } + } + + const hasContextImport = file.getImportDeclarations().some( + (decl) => + decl.getNamedImports().some((imp) => + imp.getName() === "MessageParsingContext" + ), + ); + if (!hasContextImport) { + const existing = file.getImportDeclaration((decl) => + decl.getModuleSpecifierValue().startsWith("@zwave-js/serial") + ); + if (!existing) { + file.addImportDeclaration({ + moduleSpecifier: "@zwave-js/serial", + namedImports: [{ + name: "MessageParsingContext", + isTypeOnly: true, + }], + }); + } else { + existing.addNamedImport({ + name: "MessageParsingContext", + isTypeOnly: true, + }); + } + } + + // Remove old imports + const oldImports = file.getImportDeclarations().flatMap((decl) => + decl.getNamedImports().filter( + (imp) => + imp.getName() === "MessageDeserializationOptions" + || imp.getName() === "gotDeserializationOptions", + ) + ); + for (const imp of oldImports) { + imp.remove(); + } + + for (const [clsName, ctor, otherType, ifStatement] of ctors) { + // Update the constructor signature + if (otherType === "MessageBaseOptions") { + ctor.getParameters()[0].setType( + "MessageBaseOptions", + ); + } else if (otherType) { + ctor.getParameters()[0].setType( + otherType + " & MessageBaseOptions", + ); + } else { + ctor.getParameters()[0].setType( + `${clsName}Options & MessageBaseOptions`, + ); + } + + // Replace "this.payload" with "raw.payload" + const methodBody = ctor.getBody()!.asKind(SyntaxKind.Block)!; + methodBody + ?.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression) + .filter((expr) => expr.getText() === "this.payload") + .forEach((expr) => { + expr.replaceWithText("raw.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 === "ownNodeId" + || identifier === "_ownNodeId" + ) 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); + } + } + + // Replace all occurences of this.xxx with just xxx + const thisAccesses = parseImplBlock.getDescendantsOfKind( + SyntaxKind.PropertyAccessExpression, + ) + .filter((expr) => + !!expr.getExpressionIfKind(SyntaxKind.ThisKeyword) + ); + for (const expr of thisAccesses) { + expr.replaceWithText(expr.getName()); + } + + // Replace options.ctx with ctx + const optionsDotCtx = parseImplBlock.getDescendantsOfKind( + SyntaxKind.PropertyAccessExpression, + ) + .filter((expr) => expr.getText() === "options.ctx"); + for (const expr of optionsDotCtx) { + expr.replaceWithText("ctx"); + } + + // Add a new parse method after the constructor + const ctorIndex = ctor.getChildIndex(); + const method = ctor.getParent().insertMethod(ctorIndex + 1, { + name: "from", + parameters: [{ + name: "raw", + type: "MessageRaw", + }, { + name: "ctx", + type: "MessageParsingContext", + }], + 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}({ + ${[...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(), { + 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/refactorMessageParsing.02.ts b/packages/maintenance/src/refactorMessageParsing.02.ts new file mode 100644 index 000000000000..1c7f9f159266 --- /dev/null +++ b/packages/maintenance/src/refactorMessageParsing.02.ts @@ -0,0 +1,68 @@ +import fs from "node:fs/promises"; +import { Project, SyntaxKind } from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/zwave-js/tsconfig.json", + }); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getFilePath().includes("lib/serialapi/") + ); + for (const file of sourceFiles) { + 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() !== "MessageRaw" + ) return false; + if ( + params[1].getDescendantsOfKind(SyntaxKind.TypeReference)[0] + ?.getText() !== "MessageParsingContext" + ) { + 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()) { + if (!param.getName().startsWith("_")) { + 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/maintenance/src/refactorMessageParsing.03.ts b/packages/maintenance/src/refactorMessageParsing.03.ts new file mode 100644 index 000000000000..c57b0b2041c2 --- /dev/null +++ b/packages/maintenance/src/refactorMessageParsing.03.ts @@ -0,0 +1,61 @@ +import fs from "node:fs/promises"; +import { Project, SyntaxKind } from "ts-morph"; + +async function main() { + const project = new Project({ + tsConfigFilePath: "packages/zwave-js/tsconfig.json", + }); + + const sourceFiles = project.getSourceFiles().filter((file) => + file.getFilePath().includes("lib/serialapi/") + ); + for (const file of sourceFiles) { + const fromImplsReturningSelf = file + .getDescendantsOfKind(SyntaxKind.MethodDeclaration) + .filter((m) => m.isStatic() && m.getName() === "from") + .map((m) => { + const clsName = m + .getParentIfKind(SyntaxKind.ClassDeclaration) + ?.getName(); + if (clsName === "AssignSUCReturnRouteRequest") debugger; + if (m.getReturnTypeNode()?.getText() === clsName) { + return [clsName, m] as const; + } + }) + .filter((m) => m != undefined); + + const returnSelfStmts = fromImplsReturningSelf.flatMap( + ([clsName, method]) => { + if (clsName === "AssignSUCReturnRouteRequest") debugger; + + return method + .getDescendantsOfKind(SyntaxKind.ReturnStatement) + .map((ret) => + ret.getExpressionIfKind(SyntaxKind.NewExpression) + ) + .filter((newexp) => + newexp?.getExpressionIfKind(SyntaxKind.Identifier) + ?.getText() === clsName + ) + .filter((n) => n != undefined); + }, + ); + if (returnSelfStmts.length === 0) continue; + + for (const ret of returnSelfStmts) { + ret.setExpression("this"); + } + + 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/serial/src/message/Message.test.ts b/packages/serial/src/message/Message.test.ts index 5eff3b02e893..9e2fd13bee88 100644 --- a/packages/serial/src/message/Message.test.ts +++ b/packages/serial/src/message/Message.test.ts @@ -39,7 +39,7 @@ test("should deserialize and serialize correctly", (t) => { ]), ]; for (const original of okayMessages) { - const parsed = new Message({ data: original, ctx: {} as any }); + const parsed = Message.parse(original, {} as any); t.deepEqual(parsed.serialize({} as any), original); } }); @@ -91,7 +91,7 @@ test("should throw the correct error when parsing a faulty message", (t) => { for (const [message, msg, code] of brokenMessages) { assertZWaveError( t, - () => new Message({ data: message, ctx: {} as any }), + () => Message.parse(message, {} as any), { messageMatches: msg, errorCode: code, @@ -100,154 +100,6 @@ test("should throw the correct error when parsing a faulty message", (t) => { } }); -test("isComplete() should work correctly", (t) => { - // actual messages from OZW - const okayMessages = [ - Buffer.from([ - 0x01, - 0x09, - 0x00, - 0x13, - 0x03, - 0x02, - 0x00, - 0x00, - 0x25, - 0x0b, - 0xca, - ]), - Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99]), - Buffer.from([0x01, 0x06, 0x00, 0x46, 0x0c, 0x0d, 0x32, 0x8c]), - Buffer.from([ - 0x01, - 0x0a, - 0x00, - 0x13, - 0x03, - 0x03, - 0x8e, - 0x02, - 0x04, - 0x25, - 0x40, - 0x0b, - ]), - ]; - for (const msg of okayMessages) { - t.is(Message.isComplete(msg), true); // `${msg.toString("hex")} should be detected as complete` - } - - // truncated messages - const truncatedMessages = [ - undefined, - Buffer.from([]), - Buffer.from([0x01]), - Buffer.from([0x01, 0x09]), - Buffer.from([0x01, 0x09, 0x00]), - Buffer.from([ - 0x01, - 0x09, - 0x00, - 0x13, - 0x03, - 0x02, - 0x00, - 0x00, - 0x25, - 0x0b, - ]), - ]; - for (const msg of truncatedMessages) { - t.is(Message.isComplete(msg), false); // `${msg ? msg.toString("hex") : "null"} should be detected as incomplete` - } - - // faulty but non-truncated messages should be detected as complete - const faultyMessages = [ - Buffer.from([ - 0x01, - 0x09, - 0x00, - 0x13, - 0x03, - 0x02, - 0x00, - 0x00, - 0x25, - 0x0b, - 0xca, - ]), - Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99]), - Buffer.from([0x01, 0x06, 0x00, 0x46, 0x0c, 0x0d, 0x32, 0x8c]), - Buffer.from([ - 0x01, - 0x0a, - 0x00, - 0x13, - 0x03, - 0x03, - 0x8e, - 0x02, - 0x04, - 0x25, - 0x40, - 0x0b, - ]), - ]; - for (const msg of faultyMessages) { - t.is(Message.isComplete(msg), true); // `${msg.toString("hex")} should be detected as complete` - } - - // actual messages from OZW, appended with some random data - const tooLongMessages = [ - Buffer.from([ - 0x01, - 0x09, - 0x00, - 0x13, - 0x03, - 0x02, - 0x00, - 0x00, - 0x25, - 0x0b, - 0xca, - 0x00, - ]), - Buffer.from([0x01, 0x05, 0x00, 0x47, 0x04, 0x20, 0x99, 0x01, 0x02]), - Buffer.from([ - 0x01, - 0x06, - 0x00, - 0x46, - 0x0c, - 0x0d, - 0x32, - 0x8c, - 0xab, - 0xcd, - 0xef, - ]), - Buffer.from([ - 0x01, - 0x0a, - 0x00, - 0x13, - 0x03, - 0x03, - 0x8e, - 0x02, - 0x04, - 0x25, - 0x40, - 0x0b, - 0x12, - ]), - ]; - for (const msg of tooLongMessages) { - t.is(Message.isComplete(msg), true); // `${msg.toString("hex")} should be detected as complete` - } -}); - test("toJSON() should return a semi-readable JSON representation", (t) => { const msg1 = new Message({ type: MessageType.Request, @@ -302,9 +154,12 @@ test("toJSON() should return a semi-readable JSON representation", (t) => { t.deepEqual(msg4.toJSON(), json4); }); -test("getConstructor() should return `Message` for an unknown packet type", (t) => { +test("Parsing a buffer with an unknown function type returns an unspecified `Message` instance", (t) => { const unknown = Buffer.from([0x01, 0x03, 0x00, 0x00, 0xfc]); - t.is(Message.getConstructor(unknown), Message); + t.is( + Message.parse(unknown, {} as any).constructor, + Message, + ); }); test(`the constructor should throw when no message type is specified`, (t) => { diff --git a/packages/serial/src/message/Message.ts b/packages/serial/src/message/Message.ts index 4bf25cbafcd7..e1f5bbfefafa 100644 --- a/packages/serial/src/message/Message.ts +++ b/packages/serial/src/message/Message.ts @@ -24,13 +24,9 @@ import { MessageHeaders } from "../MessageHeaders"; import { FunctionType, MessageType } from "./Constants"; import { isNodeQuery } from "./INodeQuery"; -export type MessageConstructor = new ( - options?: MessageOptions, -) => T; - -export type DeserializingMessageConstructor = new ( - options: MessageDeserializationOptions, -) => T; +export type MessageConstructor = typeof Message & { + new (options: MessageBaseOptions): T; +}; /** Where a serialized message originates from, to distinguish how certain messages need to be deserialized */ export enum MessageOrigin { @@ -38,53 +34,19 @@ export enum MessageOrigin { Host, } -export interface MessageParsingContext - extends Readonly, HostIDs, GetDeviceConfig -{ +export interface MessageParsingContext extends HostIDs, GetDeviceConfig { /** How many bytes a node ID occupies in serial API commands */ nodeIdType: NodeIDType; - - getHighestSecurityClass(nodeId: number): MaybeNotKnown; - - hasSecurityClass( - nodeId: number, - securityClass: SecurityClass, - ): MaybeNotKnown; - - setSecurityClass( - nodeId: number, - securityClass: SecurityClass, - granted: boolean, - ): void; -} - -export interface MessageDeserializationOptions { - data: Buffer; + sdkVersion: string | undefined; + requestStorage: Map> | undefined; origin?: MessageOrigin; - /** Whether CCs should be parsed immediately (only affects messages that contain CCs). Default: `true` */ - parseCCs?: boolean; - /** If known already, this contains the SDK version of the stick which can be used to interpret payloads differently */ - sdkVersion?: string; - /** Optional context used during deserialization */ - context?: unknown; - // FIXME: This is a terrible property name when context already exists - ctx: MessageParsingContext; -} - -/** - * Tests whether the given message constructor options contain a buffer for deserialization - */ -export function gotDeserializationOptions( - options: Record | undefined, -): options is MessageDeserializationOptions { - return options != undefined && Buffer.isBuffer(options.data); } export interface MessageBaseOptions { callbackId?: number; } -export interface MessageCreationOptions extends MessageBaseOptions { +export interface MessageOptions extends MessageBaseOptions { type?: MessageType; functionType?: FunctionType; expectedResponse?: FunctionType | typeof Message | ResponsePredicate; @@ -92,10 +54,6 @@ export interface MessageCreationOptions extends MessageBaseOptions { payload?: Buffer; } -export type MessageOptions = - | MessageCreationOptions - | MessageDeserializationOptions; - export interface MessageEncodingContext extends Readonly, @@ -120,87 +78,129 @@ export interface MessageEncodingContext ): void; } +/** Returns the number of bytes the first message in the buffer occupies */ +function getMessageLength(data: Buffer): number { + const remainingLength = data[1]; + return remainingLength + 2; +} + +export class MessageRaw { + public constructor( + public readonly type: MessageType, + public readonly functionType: FunctionType, + public readonly payload: Buffer, + ) {} + + public static parse(data: Buffer): MessageRaw { + // SOF, length, type, commandId and checksum must be present + if (!data.length || data.length < 5) { + throw new ZWaveError( + "Could not deserialize the message because it was truncated", + ZWaveErrorCodes.PacketFormat_Truncated, + ); + } + // the packet has to start with SOF + if (data[0] !== MessageHeaders.SOF) { + throw new ZWaveError( + "Could not deserialize the message because it does not start with SOF", + ZWaveErrorCodes.PacketFormat_Invalid, + ); + } + // check the length again, this time with the transmitted length + const messageLength = getMessageLength(data); + if (data.length < messageLength) { + throw new ZWaveError( + "Could not deserialize the message because it was truncated", + ZWaveErrorCodes.PacketFormat_Truncated, + ); + } + // check the checksum + const expectedChecksum = computeChecksum( + data.subarray(0, messageLength), + ); + if (data[messageLength - 1] !== expectedChecksum) { + throw new ZWaveError( + "Could not deserialize the message because the checksum didn't match", + ZWaveErrorCodes.PacketFormat_Checksum, + ); + } + + const type: MessageType = data[2]; + const functionType: FunctionType = data[3]; + const payloadLength = messageLength - 5; + const payload = data.subarray(4, 4 + payloadLength); + + return new MessageRaw(type, functionType, payload); + } + + public withPayload(payload: Buffer): MessageRaw { + return new MessageRaw(this.type, this.functionType, payload); + } +} + /** * Represents a Z-Wave message for communication with the serial interface */ export class Message { public constructor( - public readonly options: MessageOptions = {}, + options: MessageOptions = {}, ) { - // decide which implementation we follow - if (gotDeserializationOptions(options)) { - // #1: deserialize from payload - const payload = options.data; - - // SOF, length, type, commandId and checksum must be present - if (!payload.length || payload.length < 5) { - throw new ZWaveError( - "Could not deserialize the message because it was truncated", - ZWaveErrorCodes.PacketFormat_Truncated, - ); - } - // the packet has to start with SOF - if (payload[0] !== MessageHeaders.SOF) { - throw new ZWaveError( - "Could not deserialize the message because it does not start with SOF", - ZWaveErrorCodes.PacketFormat_Invalid, - ); - } - // check the length again, this time with the transmitted length - const messageLength = Message.getMessageLength(payload); - if (payload.length < messageLength) { - throw new ZWaveError( - "Could not deserialize the message because it was truncated", - ZWaveErrorCodes.PacketFormat_Truncated, - ); - } - // check the checksum - const expectedChecksum = computeChecksum( - payload.subarray(0, messageLength), + const { + // Try to determine the message type if none is given + type = getMessageType(this), + // Try to determine the function type if none is given + functionType = getFunctionType(this), + // Fall back to decorated response/callback types if none is given + expectedResponse = getExpectedResponse(this), + expectedCallback = getExpectedCallback(this), + payload = Buffer.allocUnsafe(0), + callbackId, + } = options; + + if (type == undefined) { + throw new ZWaveError( + "A message must have a given or predefined message type", + ZWaveErrorCodes.Argument_Invalid, ); - if (payload[messageLength - 1] !== expectedChecksum) { - throw new ZWaveError( - "Could not deserialize the message because the checksum didn't match", - ZWaveErrorCodes.PacketFormat_Checksum, - ); - } + } + if (functionType == undefined) { + throw new ZWaveError( + "A message must have a given or predefined function type", + ZWaveErrorCodes.Argument_Invalid, + ); + } - this.type = payload[2]; - this.functionType = payload[3]; - const payloadLength = messageLength - 5; - this.payload = payload.subarray(4, 4 + payloadLength); - } else { - // Try to determine the message type - if (options.type == undefined) options.type = getMessageType(this); - if (options.type == undefined) { - throw new ZWaveError( - "A message must have a given or predefined message type", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.type = options.type; + this.type = type; + this.functionType = functionType; + this.expectedResponse = expectedResponse; + this.expectedCallback = expectedCallback; + this.callbackId = callbackId; + this.payload = payload; + } - if (options.functionType == undefined) { - options.functionType = getFunctionType(this); - } - if (options.functionType == undefined) { - throw new ZWaveError( - "A message must have a given or predefined function type", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.functionType = options.functionType; + public static parse( + data: Buffer, + ctx: MessageParsingContext, + ): Message { + const raw = MessageRaw.parse(data); - // Fall back to decorated response/callback types if none is given - this.expectedResponse = options.expectedResponse - ?? getExpectedResponse(this); - this.expectedCallback = options.expectedCallback - ?? getExpectedCallback(this); + const Constructor = getMessageConstructor(raw.type, raw.functionType) + ?? Message; - this.callbackId = options.callbackId; + return Constructor.from(raw, ctx); + } - this.payload = options.payload || Buffer.allocUnsafe(0); - } + /** Creates an instance of the message that is serialized in the given buffer */ + public static from( + raw: MessageRaw, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ctx: MessageParsingContext, + ): Message { + return new this({ + type: raw.type, + functionType: raw.functionType, + payload: raw.payload, + }); } public type: MessageType; @@ -271,59 +271,6 @@ export class Message { return ret; } - /** Returns the number of bytes the first message in the buffer occupies */ - public static getMessageLength(data: Buffer): number { - const remainingLength = data[1]; - return remainingLength + 2; - } - - /** - * Checks if there's enough data in the buffer to deserialize - */ - public static isComplete(data?: Buffer): boolean { - if (!data || !data.length || data.length < 5) return false; // not yet - - const messageLength = Message.getMessageLength(data); - if (data.length < messageLength) return false; // not yet - - return true; // probably, but the checksum may be wrong - } - - /** - * Retrieves the correct constructor for the next message in the given Buffer. - * It is assumed that the buffer has been checked beforehand - */ - public static getConstructor(data: Buffer): MessageConstructor { - return getMessageConstructor(data[2], data[3]) || Message; - } - - /** Creates an instance of the message that is serialized in the given buffer */ - public static from( - options: MessageDeserializationOptions, - contextStore?: Map>, - ): Message { - const Constructor = Message.getConstructor(options.data); - - // Take the context out of the context store if it exists - if (contextStore) { - const functionType = getFunctionTypeStatic(Constructor)!; - if (contextStore.has(functionType)) { - options.context = contextStore.get(functionType)!; - contextStore.delete(functionType); - } - } - - const ret = new Constructor(options); - return ret; - } - - /** Returns the slice of data which represents the message payload */ - public static extractPayload(data: Buffer): Buffer { - const messageLength = Message.getMessageLength(data); - const payloadLength = messageLength - 5; - return data.subarray(4, 4 + payloadLength); - } - /** Generates a representation of this Message for the log */ public toLogEntry(): MessageOrCCLogEntry { const tags = [ diff --git a/packages/testing/src/MockController.ts b/packages/testing/src/MockController.ts index 7aef7607df75..85c7be3de05b 100644 --- a/packages/testing/src/MockController.ts +++ b/packages/testing/src/MockController.ts @@ -8,6 +8,7 @@ import { securityClassOrder, } from "@zwave-js/core"; import { + type FunctionType, Message, type MessageEncodingContext, MessageHeaders, @@ -69,6 +70,7 @@ export class MockController { }; const securityClasses = new Map>(); + const requestStorage = new Map>(); const self = this; this.encodingContext = { @@ -130,6 +132,9 @@ export class MockController { }; this.parsingContext = { ...this.encodingContext, + // FIXME: Take from the controller capabilities + sdkVersion: undefined, + requestStorage, }; void this.execute(); @@ -228,11 +233,9 @@ export class MockController { let msg: Message; try { - msg = Message.from({ - data, + msg = Message.parse(data, { + ...this.parsingContext, origin: MessageOrigin.Host, - parseCCs: false, - ctx: this.parsingContext, }); this._receivedHostMessages.push(msg); if (this.autoAckHostMessages) { diff --git a/packages/zwave-js/src/Driver.ts b/packages/zwave-js/src/Driver.ts index 061315774056..608781b8998c 100644 --- a/packages/zwave-js/src/Driver.ts +++ b/packages/zwave-js/src/Driver.ts @@ -14,3 +14,4 @@ export type { ZWaveOptions, } from "./lib/driver/ZWaveOptions"; export type { DriverLogContext } from "./lib/log/Driver"; +export * from "./lib/serialapi/utils"; diff --git a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts index 08690ab0ac33..54dd0ecbc6dd 100644 --- a/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts +++ b/packages/zwave-js/src/lib/controller/MockControllerBehaviors.ts @@ -90,7 +90,7 @@ function createLazySendDataPayload( ): () => CommandClass { return () => { try { - const cmd = CommandClass.parse(msg.payload, { + const cmd = CommandClass.parse(msg.serializedCC!, { sourceNodeId: controller.ownNodeId, __internalIsMockNode: true, ...node.encodingContext, @@ -394,7 +394,7 @@ const handleSendDataMulticast: MockControllerBehavior = { // We deferred parsing of the CC because it requires the node's host to do so. // Now we can do that. Also set the CC node ID to the controller's own node ID, // so CC knows it came from the controller's node ID. - const nodeIds = msg["_nodeIds"]!; + const nodeIds = msg.nodeIds; const ackPromises = nodeIds.map((nodeId) => { const node = controller.nodes.get(nodeId)!; diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index 4d15111de9b7..ab663925a391 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -3,13 +3,13 @@ import { type CCAPIHost, CRC16CC, CRC16CCCommandEncapsulation, - type CommandClass, + CommandClass, type FirmwareUpdateResult, - type ICommandClassContainer, type InterviewContext, InvalidCC, KEXFailType, MultiChannelCC, + NoOperationCC, type PersistValuesContext, type Powerlevel, type RefreshValuesContext, @@ -39,13 +39,10 @@ import { WakeUpCCNoMoreInformation, WakeUpCCValues, type ZWaveProtocolCC, - assertValidCCs, getImplementedVersion, - isCommandClassContainer, isEncapsulatingCommandClass, isMultiEncapsulatingCommandClass, isTransportServiceEncapsulation, - messageIsPing, } from "@zwave-js/cc"; import { ConfigManager, @@ -115,6 +112,7 @@ import { } from "@zwave-js/core"; import type { CCEncodingContext, + CCParsingContext, HostIDs, NodeSchedulePollOptions, ZWaveHostOptions, @@ -183,9 +181,7 @@ import { type ZWaveNotificationCallback, zWaveNodeEvents, } from "../node/_Types"; -import { ApplicationCommandRequest } from "../serialapi/application/ApplicationCommandRequest"; import { ApplicationUpdateRequest } from "../serialapi/application/ApplicationUpdateRequest"; -import { BridgeApplicationCommandRequest } from "../serialapi/application/BridgeApplicationCommandRequest"; import { SerialAPIStartedRequest, SerialAPIWakeUpReason, @@ -219,6 +215,13 @@ import { SendTestFrameRequest, SendTestFrameTransmitReport, } from "../serialapi/transport/SendTestFrameMessages"; +import { + type CommandRequest, + type ContainsCC, + containsCC, + containsSerializedCC, + isCommandRequest, +} from "../serialapi/utils"; import { reportMissingDeviceConfig } from "../telemetry/deviceConfig"; import { type AppInfo, @@ -584,6 +587,31 @@ const enum ControllerRecoveryPhase { JammedAfterReset, } +function messageIsPing( + msg: T, +): msg is T & ContainsCC { + return containsCC(msg) && msg.command instanceof NoOperationCC; +} + +function assertValidCCs(container: ContainsCC): void { + if (container.command instanceof InvalidCC) { + if (typeof container.command.reason === "number") { + throw new ZWaveError( + "The message payload failed validation!", + container.command.reason, + ); + } else { + throw new ZWaveError( + "The message payload is invalid!", + ZWaveErrorCodes.PacketFormat_InvalidPayload, + container.command.reason, + ); + } + } else if (containsCC(container.command)) { + assertValidCCs(container.command); + } +} + // Strongly type the event emitter events export interface DriverEventCallbacks extends PrefixedNodeEvents { "driver ready": () => void; @@ -667,24 +695,6 @@ export class Driver extends TypedEventEmitter }); const self = this; - 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), - get securityManager() { - return self.securityManager; - }, - get securityManager2() { - return self.securityManager2; - }, - get securityManagerLR() { - return self.securityManagerLR; - }, - }; this.messageEncodingContext = { getHighestSecurityClass: (nodeId) => this.getHighestSecurityClass(nodeId), @@ -741,18 +751,12 @@ export class Driver extends TypedEventEmitter /** The serial port instance */ private serial: ZWaveSerialPortBase | undefined; - private messageParsingContext: Omit< - MessageParsingContext, - keyof HostIDs | "nodeIdType" - >; private messageEncodingContext: Omit< MessageEncodingContext, keyof HostIDs | "nodeIdType" >; - private getCCEncodingContext(): MessageEncodingContext & CCEncodingContext { - // FIXME: The type system isn't helping here. We need the security managers to encode CCs - // but not for messages, yet those implicitly encode CCs + private getEncodingContext(): MessageEncodingContext & CCEncodingContext { return { ...this.messageEncodingContext, ownNodeId: this.controller.ownNodeId!, @@ -761,17 +765,28 @@ export class Driver extends TypedEventEmitter }; } - private getCCParsingContext() { - // FIXME: The type system isn't helping here. We need the security managers to decode CCs - // but not for messages, yet those implicitly decode CCs + private getMessageParsingContext(): MessageParsingContext { return { - ...this.messageParsingContext, + getDeviceConfig: (nodeId) => this.getDeviceConfig(nodeId), + sdkVersion: this._controller?.sdkVersion, + requestStorage: this._requestStorage, ownNodeId: this.controller.ownNodeId!, homeId: this.controller.homeId!, nodeIdType: this._controller?.nodeIdType ?? NodeIDType.Short, }; } + private getCCParsingContext(): Omit< + CCParsingContext, + "sourceNodeId" | "frameType" + > { + return { + ...this.messageEncodingContext, + ownNodeId: this.controller.ownNodeId!, + homeId: this.controller.homeId!, + }; + } + // We have multiple queues to achieve multiple "layers" of communication priority: // The default queue for most messages private queue: TransactionQueue; @@ -832,14 +847,14 @@ export class Driver extends TypedEventEmitter return this.nodeSessions.get(nodeId)!; } - private _requestContext: Map> = + private _requestStorage: Map> = new Map(); /** * @internal * Stores data from Serial API command requests to be used by their responses */ - public get requestContext(): Map> { - return this._requestContext; + public get requestStorage(): Map> { + return this._requestStorage; } public readonly cacheDir: string; @@ -3517,22 +3532,29 @@ export class Driver extends TypedEventEmitter try { // Parse the message while remembering potential decoding errors in embedded CCs // This way we can log the invalid CC contents - msg = Message.from({ - data, - sdkVersion: this._controller?.sdkVersion, - ctx: this.getCCParsingContext(), - }, this._requestContext); - if (isCommandClassContainer(msg)) { + msg = Message.parse(data, this.getMessageParsingContext()); + + // Parse embedded CCs + if (isCommandRequest(msg) && containsSerializedCC(msg)) { + msg.command = CommandClass.parse( + msg.serializedCC, + { + ...this.getCCParsingContext(), + sourceNodeId: msg.getNodeId()!, + frameType: msg.frameType, + }, + ); + // Whether successful or not, a message from a node should update last seen const node = this.tryGetNode(msg); if (node) node.lastSeen = new Date(); // Ensure there are no errors - assertValidCCs(msg); + assertValidCCs(msg as ContainsCC); } // And update statistics if (!!this._controller) { - if (isCommandClassContainer(msg)) { + if (containsCC(msg)) { this.tryGetNode(msg)?.incrementStatistics("commandsRX"); } else { this._controller.incrementStatistics("messagesRX"); @@ -3548,7 +3570,7 @@ export class Driver extends TypedEventEmitter const response = this.handleDecodeError(e, data, msg); if (response) await this.writeHeader(response); if (!!this._controller) { - if (isCommandClassContainer(msg)) { + if (containsCC(msg)) { this.tryGetNode(msg)?.incrementStatistics( "commandsDroppedRX", ); @@ -3616,12 +3638,12 @@ export class Driver extends TypedEventEmitter // If we receive a CC from a node while the controller is not ready yet, // we can't do anything with it, but logging it may assume that it can access the controller. // To prevent this problem, we just ignore CCs until the controller is ready - if (!this._controller && isCommandClassContainer(msg)) return; + if (!this._controller && containsCC(msg)) return; // If the message could be decoded, forward it to the send thread if (msg) { let wasMessageLogged = false; - if (isCommandClassContainer(msg)) { + if (isCommandRequest(msg) && containsCC(msg)) { // SecurityCCCommandEncapsulationNonceGet is two commands in one, but // we're not set up to handle things like this. Reply to the nonce get // and handle the encapsulation part normally @@ -3834,7 +3856,7 @@ export class Driver extends TypedEventEmitter } private mustReplyWithSecurityS2MOS( - msg: ApplicationCommandRequest | BridgeApplicationCommandRequest, + msg: ContainsCC & CommandRequest, ): boolean { // We're looking for a singlecast S2-encapsulated request if (msg.frameType !== "singlecast") return false; @@ -3870,7 +3892,7 @@ export class Driver extends TypedEventEmitter if ( (e.code === ZWaveErrorCodes.Security2CC_NoSPAN || e.code === ZWaveErrorCodes.Security2CC_CannotDecode) - && isCommandClassContainer(msg) + && containsCC(msg) ) { // Decoding the command failed because no SPAN has been established with the other node const nodeId = msg.getNodeId()!; @@ -3896,7 +3918,7 @@ export class Driver extends TypedEventEmitter // Ensure that we're not flooding the queue with unnecessary NonceReports const isS2NonceReport = (t: Transaction) => t.message.getNodeId() === nodeId - && isCommandClassContainer(t.message) + && containsCC(t.message) && t.message.command instanceof Security2CCNonceReport; const message: string = @@ -3955,9 +3977,7 @@ export class Driver extends TypedEventEmitter direction: "outbound", }); // Send the node our nonce, and use the chance to re-sync the MPAN if necessary - const s2MulticastOutOfSync = - (msg instanceof ApplicationCommandRequest - || msg instanceof BridgeApplicationCommandRequest) + const s2MulticastOutOfSync = isCommandRequest(msg) && this.mustReplyWithSecurityS2MOS(msg); node.commandClasses["Security 2"] @@ -3978,7 +3998,7 @@ export class Driver extends TypedEventEmitter } else if ( (e.code === ZWaveErrorCodes.Security2CC_NoMPAN || e.code === ZWaveErrorCodes.Security2CC_CannotDecodeMulticast) - && isCommandClassContainer(msg) + && containsCC(msg) ) { // Decoding the command failed because the MPAN used by the other node // is not known to us yet @@ -4458,7 +4478,7 @@ export class Driver extends TypedEventEmitter * Assembles partial CCs of in a message body. Returns `true` when the message is complete and can be handled further. * If the message expects another partial one, this returns `false`. */ - private assemblePartialCCs(msg: Message & ICommandClassContainer): boolean { + private assemblePartialCCs(msg: CommandRequest & ContainsCC): boolean { let command: CommandClass | undefined = msg.command; // We search for the every CC that provides us with a session ID // There might be newly-completed CCs that contain a partial CC, @@ -4484,8 +4504,9 @@ export class Driver extends TypedEventEmitter this.partialCCSessions.delete(partialSessionKey!); try { command.mergePartialCCs(session, { - sourceNodeId: msg.command.nodeId as number, ...this.getCCParsingContext(), + sourceNodeId: msg.command.nodeId as number, + frameType: msg.frameType, }); // Ensure there are no errors assertValidCCs(msg); @@ -5002,7 +5023,7 @@ ${handlers.length} left`, private async handleRequest(msg: Message): Promise { let handlers: RequestHandlerEntry[] | undefined; - if (isNodeQuery(msg) || isCommandClassContainer(msg)) { + if (isNodeQuery(msg) || containsCC(msg)) { const node = this.tryGetNode(msg); if (node) { // We have received an unsolicited message from a dead node, bring it back to life @@ -5021,8 +5042,10 @@ ${handlers.length} left`, } } - // It could also be that this is the node's response for a CC that we sent, but where the ACK is delayed - if (isCommandClassContainer(msg)) { + if (isCommandRequest(msg) && containsCC(msg)) { + const nodeId = msg.getNodeId()!; + + // It could also be that this is the node's response for a CC that we sent, but where the ACK is delayed const currentMessage = this.queue.currentTransaction ?.getCurrentMessage(); if ( @@ -5041,20 +5064,10 @@ ${handlers.length} left`, currentMessage.prematureNodeUpdate = msg; return; } - } - if (isCommandClassContainer(msg)) { // For further actions, we are only interested in the innermost CC this.unwrapCommands(msg); - } - // Otherwise go through the static handlers - if ( - msg instanceof ApplicationCommandRequest - || msg instanceof BridgeApplicationCommandRequest - ) { - // we handle ApplicationCommandRequests differently because they are handled by the nodes directly - const nodeId = msg.command.nodeId; // cannot handle ApplicationCommandRequests without a controller if (this._controller == undefined) { this.driverLog.print( @@ -5349,7 +5362,7 @@ ${handlers.length} left`, return cmd; } - public unwrapCommands(msg: Message & ICommandClassContainer): void { + public unwrapCommands(msg: Message & ContainsCC): void { // Unwrap encapsulating CCs until we get to the core while ( isEncapsulatingCommandClass(msg.command) @@ -5512,7 +5525,7 @@ ${handlers.length} left`, if (currentNormalMsg?.getNodeId() !== targetNode.id) { return false; } - if (!isCommandClassContainer(currentNormalMsg)) { + if (!containsCC(currentNormalMsg)) { return false; } @@ -5833,7 +5846,7 @@ ${handlers.length} left`, const machine = createSerialAPICommandMachine( msg, - msg.serialize(this.getCCEncodingContext()), + msg.serialize(this.getEncodingContext()), { sendData: (data) => this.writeSerial(data), sendDataAbort: () => this.abortSendData(), @@ -5971,7 +5984,7 @@ ${handlers.length} left`, this.ensureReady(); let node: ZWaveNode | undefined; - if (isNodeQuery(msg) || isCommandClassContainer(msg)) { + if (isNodeQuery(msg) || containsCC(msg)) { node = this.tryGetNode(msg); } @@ -6032,7 +6045,7 @@ ${handlers.length} left`, // Create the transaction const { generator, resultPromise } = createMessageGenerator( this, - this.getCCEncodingContext(), + this.getEncodingContext(), msg, (msg, _result) => { this.handleSerialAPICommandResult(msg, options, _result); @@ -6153,7 +6166,7 @@ ${handlers.length} left`, public createSendDataMessage( command: CommandClass, options: Omit = {}, - ): SendDataMessage { + ): SendDataMessage & ContainsCC { // Automatically encapsulate commands before sending if (options.autoEncapsulate !== false) { command = this.encapsulateCommands(command, options); @@ -6218,7 +6231,7 @@ ${handlers.length} left`, msg.nodeUpdateTimeout = options.reportTimeoutMs; } - return msg; + return msg as SendDataMessage & ContainsCC; } /** @@ -6241,7 +6254,7 @@ ${handlers.length} left`, const resp = await this.sendMessage(msg, options); // And unwrap the response if there was any - if (isCommandClassContainer(resp)) { + if (containsCC(resp)) { this.unwrapCommands(resp); return resp.command as unknown as TResponse; } @@ -6398,7 +6411,7 @@ ${handlers.length} left`, try { const abort = new SendDataAbort(); await this.writeSerial( - abort.serialize(this.getCCEncodingContext()), + abort.serialize(this.getEncodingContext()), ); this.driverLog.logMessage(abort, { direction: "outbound", @@ -6644,7 +6657,7 @@ ${handlers.length} left`, case messageIsPing(msg): case transaction.priority === MessagePriority.Immediate: // We also don't want to immediately send the node to sleep when it wakes up - case isCommandClassContainer(msg) + case containsCC(msg) && msg.command instanceof WakeUpCCNoMoreInformation: // compat queries because they will be recreated when the node wakes up case transaction.tag === "compat": @@ -7024,11 +7037,13 @@ ${handlers.length} left`, /** Computes the maximum net CC payload size for the given CC or SendDataRequest */ public computeNetCCPayloadSize( - commandOrMsg: CommandClass | SendDataRequest | SendDataBridgeRequest, + commandOrMsg: + | CommandClass + | (SendDataRequest | SendDataBridgeRequest) & ContainsCC, ignoreEncapsulation: boolean = false, ): number { // Recreate the correct encapsulation structure - let msg: SendDataRequest | SendDataBridgeRequest; + let msg: (SendDataRequest | SendDataBridgeRequest) & ContainsCC; if (isSendDataSinglecast(commandOrMsg)) { msg = commandOrMsg; } else { @@ -7036,7 +7051,7 @@ ${handlers.length} left`, msg = new SendDataConstructor({ sourceNodeId: this.ownNodeId, command: commandOrMsg, - }); + }) as (SendDataRequest | SendDataBridgeRequest) & ContainsCC; } if (!ignoreEncapsulation) { msg.command = this.encapsulateCommands( @@ -7090,7 +7105,7 @@ ${handlers.length} left`, } public exceedsMaxPayloadLength(msg: SendDataMessage): boolean { - return msg.serializeCC(this.getCCEncodingContext()).length + return msg.serializeCC(this.getEncodingContext()).length > this.getMaxPayloadLength(msg); } diff --git a/packages/zwave-js/src/lib/driver/MessageGenerators.ts b/packages/zwave-js/src/lib/driver/MessageGenerators.ts index e0f667a5c258..b1d1e6f7067e 100644 --- a/packages/zwave-js/src/lib/driver/MessageGenerators.ts +++ b/packages/zwave-js/src/lib/driver/MessageGenerators.ts @@ -9,7 +9,6 @@ import { SupervisionCCReport, SupervisionCommand, getInnermostCommandClass, - isCommandClassContainer, } from "@zwave-js/cc"; import { SecurityCCCommandEncapsulation, @@ -51,18 +50,20 @@ import { createDeferredPromise, } from "alcalzone-shared/deferred-promise"; import { + type SendDataMessage, isSendData, isTransmitReport, } from "../serialapi/transport/SendDataShared"; +import { type ContainsCC, containsCC } from "../serialapi/utils"; import type { Driver } from "./Driver"; import type { MessageGenerator } from "./Transaction"; -export type MessageGeneratorImplementation = ( +export type MessageGeneratorImplementation = ( /** A reference to the driver */ driver: Driver, ctx: CCEncodingContext, /** The "primary" message */ - message: Message, + message: T, /** * A hook to get notified about each sent message and the result of the Serial API call * without waiting for the message generator to finish completely. @@ -75,7 +76,7 @@ export type MessageGeneratorImplementation = ( function maybePartialNodeUpdate(sent: Message, received: Message): boolean { // Some commands are returned in multiple segments, which may take longer than // the configured timeout. - if (!isCommandClassContainer(sent) || !isCommandClassContainer(received)) { + if (!containsCC(sent) || !containsCC(received)) { return false; } @@ -127,7 +128,7 @@ function getNodeUpdateTimeout( } /** A simple message generator that simply sends a message, waits for the ACK (and the response if one is expected) */ -export const simpleMessageGenerator: MessageGeneratorImplementation = +export const simpleMessageGenerator: MessageGeneratorImplementation = async function*( driver, ctx, @@ -209,267 +210,268 @@ export const simpleMessageGenerator: MessageGeneratorImplementation = }; /** A generator for singlecast SendData messages that automatically uses Transport Service when necessary */ -export const maybeTransportServiceGenerator: MessageGeneratorImplementation = - async function*( - driver, - ctx, - msg, - onMessageSent, - additionalCommandTimeoutMs, - ) { - // Make sure we can send this message - if (!isSendData(msg)) { +export const maybeTransportServiceGenerator: MessageGeneratorImplementation< + SendDataMessage & ContainsCC +> = async function*( + driver, + ctx, + msg, + onMessageSent, + additionalCommandTimeoutMs, +) { + // Make sure we can send this message + /*if (!isSendData(msg) || !containsCC(msg)) { throw new ZWaveError( "Cannot use the Transport Service message generator for messages that are not SendData!", ZWaveErrorCodes.Argument_Invalid, ); - } else if (typeof msg.command.nodeId !== "number") { - throw new ZWaveError( - "Cannot use the Transport Service message generator for multicast commands!", - ZWaveErrorCodes.Argument_Invalid, - ); - } + } else*/ if (typeof msg.command.nodeId !== "number") { + throw new ZWaveError( + "Cannot use the Transport Service message generator for multicast commands!", + ZWaveErrorCodes.Argument_Invalid, + ); + } - const node = msg.tryGetNode(driver); - const mayUseTransportService = - node?.supportsCC(CommandClasses["Transport Service"]) - && node.getCCVersion(CommandClasses["Transport Service"]) >= 2; + const node = msg.tryGetNode(driver); + const mayUseTransportService = + node?.supportsCC(CommandClasses["Transport Service"]) + && node.getCCVersion(CommandClasses["Transport Service"]) >= 2; - if (!mayUseTransportService || !driver.exceedsMaxPayloadLength(msg)) { - // Transport Service isn't needed for this message - return yield* simpleMessageGenerator( - driver, - ctx, - msg, - onMessageSent, - additionalCommandTimeoutMs, - ); - } + if (!mayUseTransportService || !driver.exceedsMaxPayloadLength(msg)) { + // Transport Service isn't needed for this message + return yield* simpleMessageGenerator( + driver, + ctx, + msg, + onMessageSent, + additionalCommandTimeoutMs, + ); + } - // Send the command split into multiple segments - const payload = msg.serializeCC(ctx); - const numSegments = Math.ceil(payload.length / MAX_SEGMENT_SIZE); - const segmentDelay = numSegments > RELAXED_TIMING_THRESHOLD - ? TransportServiceTimeouts.relaxedTimingDelayR2 - : 0; - const sessionId = driver.getNextTransportServiceSessionId(); - const nodeId = msg.command.nodeId; - - // Since the command is never logged, we do it here - driver.driverLog.print( - "The following message is too large, using Transport Service to transmit it:", + // Send the command split into multiple segments + const payload = msg.serializeCC(ctx); + const numSegments = Math.ceil(payload.length / MAX_SEGMENT_SIZE); + const segmentDelay = numSegments > RELAXED_TIMING_THRESHOLD + ? TransportServiceTimeouts.relaxedTimingDelayR2 + : 0; + const sessionId = driver.getNextTransportServiceSessionId(); + const nodeId = msg.command.nodeId; + + // Since the command is never logged, we do it here + driver.driverLog.print( + "The following message is too large, using Transport Service to transmit it:", + ); + driver.driverLog.logMessage(msg, { + direction: "outbound", + }); + + // I don't see an elegant way to wait for possible responses, so we just register a handler in the driver + // and remember the received commands + let unhandledResponses: TransportServiceCC[] = []; + const { unregister: unregisterHandler } = driver.registerCommandHandler( + (cc) => + cc.nodeId === nodeId + && (cc instanceof TransportServiceCCSegmentWait + || (cc instanceof TransportServiceCCSegmentRequest + && cc.sessionId === sessionId)), + (cc) => { + unhandledResponses.push(cc as TransportServiceCC); + }, + ); + + const receivedSegmentWait = () => { + const index = unhandledResponses.findIndex( + (cc) => cc instanceof TransportServiceCCSegmentWait, ); - driver.driverLog.logMessage(msg, { - direction: "outbound", - }); + if (index >= 0) { + const cc = unhandledResponses[ + index + ] as TransportServiceCCSegmentWait; + unhandledResponses.splice(index, 1); + return cc; + } + }; - // I don't see an elegant way to wait for possible responses, so we just register a handler in the driver - // and remember the received commands - let unhandledResponses: TransportServiceCC[] = []; - const { unregister: unregisterHandler } = driver.registerCommandHandler( - (cc) => - cc.nodeId === nodeId - && (cc instanceof TransportServiceCCSegmentWait - || (cc instanceof TransportServiceCCSegmentRequest - && cc.sessionId === sessionId)), - (cc) => { - unhandledResponses.push(cc as TransportServiceCC); - }, + const receivedSegmentRequest = () => { + const index = unhandledResponses.findIndex( + (cc) => cc instanceof TransportServiceCCSegmentRequest, ); + if (index >= 0) { + const cc = unhandledResponses[ + index + ] as TransportServiceCCSegmentRequest; + unhandledResponses.splice(index, 1); + return cc; + } + }; - const receivedSegmentWait = () => { - const index = unhandledResponses.findIndex( - (cc) => cc instanceof TransportServiceCCSegmentWait, - ); - if (index >= 0) { - const cc = unhandledResponses[ - index - ] as TransportServiceCCSegmentWait; - unhandledResponses.splice(index, 1); - return cc; - } - }; + // We have to deal with multiple messages, but can only return a single result. + // Therefore we use the last one as the result. + let result!: Message; - const receivedSegmentRequest = () => { - const index = unhandledResponses.findIndex( - (cc) => cc instanceof TransportServiceCCSegmentRequest, - ); - if (index >= 0) { - const cc = unhandledResponses[ - index - ] as TransportServiceCCSegmentRequest; - unhandledResponses.splice(index, 1); - return cc; - } - }; + try { + attempts: for (let attempt = 1; attempt <= 2; attempt++) { + driver.controllerLog.logNode(nodeId, { + message: + `Beginning Transport Service TX session #${sessionId}...`, + level: "debug", + direction: "outbound", + }); - // We have to deal with multiple messages, but can only return a single result. - // Therefore we use the last one as the result. - let result!: Message; + // Clear the list of unhandled responses + unhandledResponses = []; + // Fill the list of unsent segments + const unsentSegments = new Array(numSegments) + .fill(0) + .map((_, i) => i); + let didRetryLastSegment = false; + let isFirstTransferredSegment = true; + + while (unsentSegments.length > 0) { + // Wait if necessary + if (isFirstTransferredSegment) { + isFirstTransferredSegment = false; + } else if (segmentDelay) { + await wait(segmentDelay, true); + } + const segment = unsentSegments.shift()!; - try { - attempts: for (let attempt = 1; attempt <= 2; attempt++) { - driver.controllerLog.logNode(nodeId, { - message: - `Beginning Transport Service TX session #${sessionId}...`, - level: "debug", - direction: "outbound", + const chunk = payload.subarray( + segment * MAX_SEGMENT_SIZE, + (segment + 1) * MAX_SEGMENT_SIZE, + ); + let cc: TransportServiceCC; + if (segment === 0) { + cc = new TransportServiceCCFirstSegment({ + nodeId, + sessionId, + datagramSize: payload.length, + partialDatagram: chunk, + }); + } else { + cc = new TransportServiceCCSubsequentSegment({ + nodeId, + sessionId, + datagramSize: payload.length, + datagramOffset: segment * MAX_SEGMENT_SIZE, + partialDatagram: chunk, + }); + } + + const tmsg = driver.createSendDataMessage(cc, { + autoEncapsulate: false, + maxSendAttempts: msg.maxSendAttempts, + transmitOptions: msg.transmitOptions, }); + result = yield* simpleMessageGenerator( + driver, + ctx, + tmsg, + onMessageSent, + ); - // Clear the list of unhandled responses - unhandledResponses = []; - // Fill the list of unsent segments - const unsentSegments = new Array(numSegments) - .fill(0) - .map((_, i) => i); - let didRetryLastSegment = false; - let isFirstTransferredSegment = true; - - while (unsentSegments.length > 0) { - // Wait if necessary - if (isFirstTransferredSegment) { - isFirstTransferredSegment = false; - } else if (segmentDelay) { - await wait(segmentDelay, true); - } - const segment = unsentSegments.shift()!; + let segmentComplete: + | TransportServiceCCSegmentComplete + | undefined = undefined; + // After sending the last segment, wait for a SegmentComplete response, at the same time + // give the node a chance to send a SegmentWait or SegmentRequest(s) + if (segment === numSegments - 1) { + segmentComplete = await driver + .waitForCommand( + (cc) => + cc.nodeId === nodeId + && cc + instanceof TransportServiceCCSegmentComplete + && cc.sessionId === sessionId, + TransportServiceTimeouts.segmentCompleteR2, + ) + .catch(() => undefined); + } - const chunk = payload.subarray( - segment * MAX_SEGMENT_SIZE, - (segment + 1) * MAX_SEGMENT_SIZE, - ); - let cc: TransportServiceCC; - if (segment === 0) { - cc = new TransportServiceCCFirstSegment({ - nodeId, - sessionId, - datagramSize: payload.length, - partialDatagram: chunk, - }); - } else { - cc = new TransportServiceCCSubsequentSegment({ - nodeId, - sessionId, - datagramSize: payload.length, - datagramOffset: segment * MAX_SEGMENT_SIZE, - partialDatagram: chunk, - }); - } + if (segmentComplete) { + // We're done! + driver.controllerLog.logNode(nodeId, { + message: + `Transport Service TX session #${sessionId} complete`, + level: "debug", + direction: "outbound", + }); + break attempts; + } - const tmsg = driver.createSendDataMessage(cc, { - autoEncapsulate: false, - maxSendAttempts: msg.maxSendAttempts, - transmitOptions: msg.transmitOptions, + // If we received a SegmentWait, we need to wait and restart + const segmentWait = receivedSegmentWait(); + if (segmentWait) { + const waitTime = segmentWait.pendingSegments * 100; + driver.controllerLog.logNode(nodeId, { + message: + `Restarting Transport Service TX session #${sessionId} in ${waitTime} ms...`, + level: "debug", }); - result = yield* simpleMessageGenerator( - driver, - ctx, - tmsg, - onMessageSent, - ); - let segmentComplete: - | TransportServiceCCSegmentComplete - | undefined = undefined; - // After sending the last segment, wait for a SegmentComplete response, at the same time - // give the node a chance to send a SegmentWait or SegmentRequest(s) - if (segment === numSegments - 1) { - segmentComplete = await driver - .waitForCommand( - (cc) => - cc.nodeId === nodeId - && cc - instanceof TransportServiceCCSegmentComplete - && cc.sessionId === sessionId, - TransportServiceTimeouts.segmentCompleteR2, - ) - .catch(() => undefined); - } + await wait(waitTime, true); + continue attempts; + } - if (segmentComplete) { - // We're done! + // If the node requested missing segments, add them to the list of unsent segments and continue transmitting + let segmentRequest: + | TransportServiceCCSegmentRequest + | undefined = undefined; + let readdedSegments = false; + while ((segmentRequest = receivedSegmentRequest())) { + unsentSegments.push( + segmentRequest.datagramOffset / MAX_SEGMENT_SIZE, + ); + readdedSegments = true; + } + if (readdedSegments) continue; + + // If we didn't receive anything after sending the last segment, retry the last segment + if (segment === numSegments - 1) { + if (didRetryLastSegment) { driver.controllerLog.logNode(nodeId, { message: - `Transport Service TX session #${sessionId} complete`, + `Transport Service TX session #${sessionId} failed`, level: "debug", direction: "outbound", }); break attempts; - } - - // If we received a SegmentWait, we need to wait and restart - const segmentWait = receivedSegmentWait(); - if (segmentWait) { - const waitTime = segmentWait.pendingSegments * 100; + } else { + // Try the last segment again driver.controllerLog.logNode(nodeId, { message: - `Restarting Transport Service TX session #${sessionId} in ${waitTime} ms...`, + `Transport Service TX session #${sessionId}: Segment Complete missing - re-transmitting last segment...`, level: "debug", + direction: "outbound", }); - - await wait(waitTime, true); - continue attempts; - } - - // If the node requested missing segments, add them to the list of unsent segments and continue transmitting - let segmentRequest: - | TransportServiceCCSegmentRequest - | undefined = undefined; - let readdedSegments = false; - while ((segmentRequest = receivedSegmentRequest())) { - unsentSegments.push( - segmentRequest.datagramOffset / MAX_SEGMENT_SIZE, - ); - readdedSegments = true; - } - if (readdedSegments) continue; - - // If we didn't receive anything after sending the last segment, retry the last segment - if (segment === numSegments - 1) { - if (didRetryLastSegment) { - driver.controllerLog.logNode(nodeId, { - message: - `Transport Service TX session #${sessionId} failed`, - level: "debug", - direction: "outbound", - }); - break attempts; - } else { - // Try the last segment again - driver.controllerLog.logNode(nodeId, { - message: - `Transport Service TX session #${sessionId}: Segment Complete missing - re-transmitting last segment...`, - level: "debug", - direction: "outbound", - }); - didRetryLastSegment = true; - unsentSegments.unshift(segment); - continue; - } + didRetryLastSegment = true; + unsentSegments.unshift(segment); + continue; } } } - } finally { - // We're done, unregister the handler - unregisterHandler(); } + } finally { + // We're done, unregister the handler + unregisterHandler(); + } - // Transport Service CCs do not expect a node update and have no knowledge about the encapsulated CC. - // Therefore we need to replicate the waiting from simpleMessageGenerator here + // Transport Service CCs do not expect a node update and have no knowledge about the encapsulated CC. + // Therefore we need to replicate the waiting from simpleMessageGenerator here - // If the sent message expects an update from the node, wait for it - if (msg.expectsNodeUpdate()) { - // TODO: Figure out if we can handle premature updates with Transport Service CC - const timeout = getNodeUpdateTimeout( - driver, - msg, - additionalCommandTimeoutMs, - ); - return waitForNodeUpdate(driver, msg, timeout); - } + // If the sent message expects an update from the node, wait for it + if (msg.expectsNodeUpdate()) { + // TODO: Figure out if we can handle premature updates with Transport Service CC + const timeout = getNodeUpdateTimeout( + driver, + msg, + additionalCommandTimeoutMs, + ); + return waitForNodeUpdate(driver, msg, timeout); + } - return result; - }; + return result; +}; /** A simple (internal) generator that simply sends a command, and optionally returns the response command */ async function* sendCommandGenerator< @@ -489,151 +491,223 @@ async function* sendCommandGenerator< msg, onMessageSent, ); - if (resp && isCommandClassContainer(resp)) { + if (resp && containsCC(resp)) { driver.unwrapCommands(resp); return resp.command as TResponse; } } /** A message generator for security encapsulated messages (S0) */ -export const secureMessageGeneratorS0: MessageGeneratorImplementation = - async function*(driver, ctx, msg, onMessageSent) { - if (!isSendData(msg)) { +export const secureMessageGeneratorS0: MessageGeneratorImplementation< + SendDataMessage & ContainsCC +> = async function*(driver, ctx, msg, onMessageSent) { + /*if (!isSendData(msg)) { throw new ZWaveError( "Cannot use the S0 message generator for a command that's not a SendData message!", ZWaveErrorCodes.Argument_Invalid, ); - } else if (typeof msg.command.nodeId !== "number") { - throw new ZWaveError( - "Cannot use the S0 message generator for multicast commands!", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (!(msg.command instanceof SecurityCCCommandEncapsulation)) { + } else*/ if (typeof msg.command.nodeId !== "number") { + throw new ZWaveError( + "Cannot use the S0 message generator for multicast commands!", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (!(msg.command instanceof SecurityCCCommandEncapsulation)) { + throw new ZWaveError( + "The S0 message generator can only be used for Security S0 command encapsulation!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + // Step 1: Acquire a nonce + const secMan = driver.securityManager!; + const nodeId = msg.command.nodeId; + let additionalTimeoutMs: number | undefined; + + // Try to get a free nonce before requesting a new one + let nonce: Buffer | undefined = secMan.getFreeNonce(nodeId); + if (!nonce) { + // No free nonce, request a new one + const cc = new SecurityCCNonceGet({ + nodeId: nodeId, + endpointIndex: msg.command.endpointIndex, + }); + const nonceResp = yield* sendCommandGenerator< + SecurityCCNonceReport + >( + driver, + ctx, + cc, + (msg, result) => { + additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6); + onMessageSent(msg, result); + }, + { + // Only try getting a nonce once + maxSendAttempts: 1, + }, + ); + if (!nonceResp) { throw new ZWaveError( - "The S0 message generator can only be used for Security S0 command encapsulation!", - ZWaveErrorCodes.Argument_Invalid, + "No nonce received from the node, cannot send secure command!", + ZWaveErrorCodes.SecurityCC_NoNonce, ); } + nonce = nonceResp.nonce; + } + msg.command.nonce = nonce; - // Step 1: Acquire a nonce - const secMan = driver.securityManager!; - const nodeId = msg.command.nodeId; - let additionalTimeoutMs: number | undefined; - - // Try to get a free nonce before requesting a new one - let nonce: Buffer | undefined = secMan.getFreeNonce(nodeId); - if (!nonce) { - // No free nonce, request a new one - const cc = new SecurityCCNonceGet({ - nodeId: nodeId, - endpointIndex: msg.command.endpointIndex, - }); - const nonceResp = yield* sendCommandGenerator< - SecurityCCNonceReport - >( - driver, - ctx, - cc, - (msg, result) => { - additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6); - onMessageSent(msg, result); - }, - { - // Only try getting a nonce once - maxSendAttempts: 1, - }, - ); - if (!nonceResp) { - throw new ZWaveError( - "No nonce received from the node, cannot send secure command!", - ZWaveErrorCodes.SecurityCC_NoNonce, - ); - } - nonce = nonceResp.nonce; - } - msg.command.nonce = nonce; + // Now send the actual secure command + return yield* simpleMessageGenerator( + driver, + ctx, + msg, + onMessageSent, + additionalTimeoutMs, + ); +}; - // Now send the actual secure command - return yield* simpleMessageGenerator( +/** A message generator for security encapsulated messages (S2) */ +export const secureMessageGeneratorS2: MessageGeneratorImplementation< + SendDataMessage & ContainsCC +> = async function*(driver, ctx, msg, onMessageSent) { + if (!isSendData(msg) || !containsCC(msg)) { + throw new ZWaveError( + "Cannot use the S2 message generator for a command that's not a SendData message!", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (typeof msg.command.nodeId !== "number") { + throw new ZWaveError( + "Cannot use the S2 message generator for multicast commands!", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { + throw new ZWaveError( + "The S2 message generator can only be used for Security S2 command encapsulation!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + const nodeId = msg.command.nodeId; + const secMan = driver.getSecurityManager2(nodeId)!; + const spanState = secMan.getSPANState(nodeId); + let additionalTimeoutMs: number | undefined; + + // We need a new nonce when there is no shared SPAN state, or the SPAN state is for a lower security class + // than the command we want to send + const expectedSecurityClass = msg.command.securityClass + ?? driver.getHighestSecurityClass(nodeId); + + if ( + spanState.type === SPANState.None + || spanState.type === SPANState.LocalEI + || (spanState.type === SPANState.SPAN + && spanState.securityClass !== SecurityClass.Temporary + && spanState.securityClass !== expectedSecurityClass) + ) { + // Request a new nonce + + // No free nonce, request a new one + const cc = new Security2CCNonceGet({ + nodeId: nodeId, + endpointIndex: msg.command.endpointIndex, + }); + const nonceResp = yield* sendCommandGenerator< + Security2CCNonceReport + >( driver, ctx, - msg, - onMessageSent, - additionalTimeoutMs, + cc, + (msg, result) => { + additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6); + onMessageSent(msg, result); + }, + { + // Only try getting a nonce once + maxSendAttempts: 1, + }, ); - }; - -/** A message generator for security encapsulated messages (S2) */ -export const secureMessageGeneratorS2: MessageGeneratorImplementation = - async function*(driver, ctx, msg, onMessageSent) { - if (!isSendData(msg)) { - throw new ZWaveError( - "Cannot use the S2 message generator for a command that's not a SendData message!", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (typeof msg.command.nodeId !== "number") { + if (!nonceResp) { throw new ZWaveError( - "Cannot use the S2 message generator for multicast commands!", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { - throw new ZWaveError( - "The S2 message generator can only be used for Security S2 command encapsulation!", - ZWaveErrorCodes.Argument_Invalid, + "No nonce received from the node, cannot send secure command!", + ZWaveErrorCodes.Security2CC_NoSPAN, ); } - const nodeId = msg.command.nodeId; - const secMan = driver.getSecurityManager2(nodeId)!; - const spanState = secMan.getSPANState(nodeId); - let additionalTimeoutMs: number | undefined; + // Storing the nonce is not necessary, this will be done automatically when the nonce is received + } - // We need a new nonce when there is no shared SPAN state, or the SPAN state is for a lower security class - // than the command we want to send - const expectedSecurityClass = msg.command.securityClass - ?? driver.getHighestSecurityClass(nodeId); + // Now send the actual secure command + let response = yield* maybeTransportServiceGenerator( + driver, + ctx, + msg, + onMessageSent, + additionalTimeoutMs, + ); - if ( - spanState.type === SPANState.None - || spanState.type === SPANState.LocalEI - || (spanState.type === SPANState.SPAN - && spanState.securityClass !== SecurityClass.Temporary - && spanState.securityClass !== expectedSecurityClass) - ) { - // Request a new nonce + // If we want to make sure that a node understood a SET-type S2-encapsulated message, we either need to use + // Supervision and wait for the Supervision Report (handled by the simpleMessageGenerator), or we need to add a + // short delay between commands and wait if a NonceReport is received. + // However, in situations where timing is critical (e.g. S2 bootstrapping), verifyDelivery is set to false, and we don't do this. + let nonceReport: Security2CCNonceReport | undefined; + if ( + isTransmitReport(response) + && msg.command.verifyDelivery + && !msg.command.expectsCCResponse() + && !msg.command.getEncapsulatedCC( + CommandClasses.Supervision, + SupervisionCommand.Get, + ) + ) { + nonceReport = await driver + .waitForCommand( + (cc) => + cc.nodeId === nodeId + && cc instanceof Security2CCNonceReport, + 500, + ) + .catch(() => undefined); + } else if ( + containsCC(response) + && response.command instanceof Security2CCNonceReport + ) { + nonceReport = response.command; + } - // No free nonce, request a new one - const cc = new Security2CCNonceGet({ - nodeId: nodeId, - endpointIndex: msg.command.endpointIndex, - }); - const nonceResp = yield* sendCommandGenerator< - Security2CCNonceReport - >( - driver, - ctx, - cc, - (msg, result) => { - additionalTimeoutMs = Math.ceil(msg.rtt! / 1e6); - onMessageSent(msg, result); - }, - { - // Only try getting a nonce once - maxSendAttempts: 1, - }, - ); - if (!nonceResp) { - throw new ZWaveError( - "No nonce received from the node, cannot send secure command!", - ZWaveErrorCodes.Security2CC_NoSPAN, - ); + if (nonceReport) { + if (nonceReport.SOS && nonceReport.receiverEI) { + // The node couldn't decrypt the last command we sent it. Invalidate + // the shared SPAN, since it did the same + secMan.storeRemoteEI(nodeId, nonceReport.receiverEI); + } + if (nonceReport.MOS) { + const multicastGroupId = msg.command.getMulticastGroupId(); + if (multicastGroupId != undefined) { + // The node couldn't decrypt the previous S2 multicast. Tell it the MPAN (again) + const mpan = secMan.getInnerMPANState(multicastGroupId); + if (mpan) { + // Replace the MGRP extension with an MPAN extension + msg.command.extensions = msg.command.extensions.filter( + (e) => !(e instanceof MGRPExtension), + ); + msg.command.extensions.push( + new MPANExtension({ + groupId: multicastGroupId, + innerMPANState: mpan, + }), + ); + } } - - // Storing the nonce is not necessary, this will be done automatically when the nonce is received } + driver.controllerLog.logNode(nodeId, { + message: + `failed to decode the message, retrying with SPAN extension...`, + direction: "none", + }); - // Now send the actual secure command - let response = yield* maybeTransportServiceGenerator( + // Send the message again + msg.prepareRetransmission(); + response = yield* maybeTransportServiceGenerator( driver, ctx, msg, @@ -641,272 +715,203 @@ export const secureMessageGeneratorS2: MessageGeneratorImplementation = additionalTimeoutMs, ); - // If we want to make sure that a node understood a SET-type S2-encapsulated message, we either need to use - // Supervision and wait for the Supervision Report (handled by the simpleMessageGenerator), or we need to add a - // short delay between commands and wait if a NonceReport is received. - // However, in situations where timing is critical (e.g. S2 bootstrapping), verifyDelivery is set to false, and we don't do this. - let nonceReport: Security2CCNonceReport | undefined; if ( - isTransmitReport(response) - && msg.command.verifyDelivery - && !msg.command.expectsCCResponse() - && !msg.command.getEncapsulatedCC( - CommandClasses.Supervision, - SupervisionCommand.Get, - ) - ) { - nonceReport = await driver - .waitForCommand( - (cc) => - cc.nodeId === nodeId - && cc instanceof Security2CCNonceReport, - 500, - ) - .catch(() => undefined); - } else if ( - isCommandClassContainer(response) + containsCC(response) && response.command instanceof Security2CCNonceReport ) { - nonceReport = response.command; - } - - if (nonceReport) { - if (nonceReport.SOS && nonceReport.receiverEI) { - // The node couldn't decrypt the last command we sent it. Invalidate - // the shared SPAN, since it did the same - secMan.storeRemoteEI(nodeId, nonceReport.receiverEI); - } - if (nonceReport.MOS) { - const multicastGroupId = msg.command.getMulticastGroupId(); - if (multicastGroupId != undefined) { - // The node couldn't decrypt the previous S2 multicast. Tell it the MPAN (again) - const mpan = secMan.getInnerMPANState(multicastGroupId); - if (mpan) { - // Replace the MGRP extension with an MPAN extension - msg.command.extensions = msg.command.extensions.filter( - (e) => !(e instanceof MGRPExtension), - ); - msg.command.extensions.push( - new MPANExtension({ - groupId: multicastGroupId, - innerMPANState: mpan, - }), - ); - } - } - } + // No dice driver.controllerLog.logNode(nodeId, { message: - `failed to decode the message, retrying with SPAN extension...`, + `failed to decode the message after re-transmission with SPAN extension, dropping the message.`, direction: "none", + level: "warn", }); - - // Send the message again - msg.prepareRetransmission(); - response = yield* maybeTransportServiceGenerator( - driver, - ctx, - msg, - onMessageSent, - additionalTimeoutMs, - ); - - if ( - isCommandClassContainer(response) - && response.command instanceof Security2CCNonceReport - ) { - // No dice - driver.controllerLog.logNode(nodeId, { - message: - `failed to decode the message after re-transmission with SPAN extension, dropping the message.`, - direction: "none", - level: "warn", - }); - throw new ZWaveError( - "The node failed to decode the message.", - ZWaveErrorCodes.Security2CC_CannotDecode, - ); - } - } - - return response; - }; - -/** A message generator for security encapsulated messages (S2 Multicast) */ -export const secureMessageGeneratorS2Multicast: MessageGeneratorImplementation = - async function*(driver, ctx, msg, onMessageSent) { - if (!isSendData(msg)) { throw new ZWaveError( - "Cannot use the S2 multicast message generator for a command that's not a SendData message!", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (msg.command.isSinglecast()) { - throw new ZWaveError( - "Cannot use the S2 multicast message generator for singlecast commands!", - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { - throw new ZWaveError( - "The S2 multicast message generator can only be used for Security S2 command encapsulation!", - ZWaveErrorCodes.Argument_Invalid, + "The node failed to decode the message.", + ZWaveErrorCodes.Security2CC_CannotDecode, ); } + } - const groupId = msg.command.getMulticastGroupId(); - if (groupId == undefined) { - throw new ZWaveError( - "Cannot use the S2 multicast message generator without a multicast group ID!", - ZWaveErrorCodes.Argument_Invalid, - ); - } + return response; +}; - const secMan = driver.getSecurityManager2(msg.command.nodeId)!; - const group = secMan.getMulticastGroup(groupId); - if (!group) { - throw new ZWaveError( - `Multicast group ${groupId} does not exist!`, - ZWaveErrorCodes.Argument_Invalid, - ); - } +/** A message generator for security encapsulated messages (S2 Multicast) */ +export const secureMessageGeneratorS2Multicast: MessageGeneratorImplementation< + SendDataMessage & ContainsCC +> = async function*(driver, ctx, msg, onMessageSent) { + if (!isSendData(msg) || !containsCC(msg)) { + throw new ZWaveError( + "Cannot use the S2 multicast message generator for a command that's not a SendData message!", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (msg.command.isSinglecast()) { + throw new ZWaveError( + "Cannot use the S2 multicast message generator for singlecast commands!", + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (!(msg.command instanceof Security2CCMessageEncapsulation)) { + throw new ZWaveError( + "The S2 multicast message generator can only be used for Security S2 command encapsulation!", + ZWaveErrorCodes.Argument_Invalid, + ); + } - // Send the multicast command. We remember the transmit report and treat it as the result of the multicast command - const response = yield* simpleMessageGenerator( - driver, - ctx, - msg, - onMessageSent, + const groupId = msg.command.getMulticastGroupId(); + if (groupId == undefined) { + throw new ZWaveError( + "Cannot use the S2 multicast message generator without a multicast group ID!", + ZWaveErrorCodes.Argument_Invalid, ); + } - // If a node in the group is out of sync, we need to transfer the MPAN state we're going to use for the next command. - // Therefore increment the MPAN state now and not after the followups like the specs mention - secMan.tryIncrementMPAN(groupId); - - // Unwrap the command again, so we can make the following encapsulation depend on the target node - driver.unwrapCommands(msg); - const command = msg.command; - // Remember the original encapsulation flags - const encapsulationFlags = command.encapsulationFlags; - - // In case someone sneaked a node ID into the group multiple times, remove duplicates for the singlecast followups - // Otherwise, the node will increase its MPAN multiple times, going out of sync. - const distinctNodeIDs = [...new Set(group.nodeIDs)]; - - const supervisionResults: (SupervisionResult | undefined)[] = []; - - // Now do singlecast followups with every node in the group - for (const nodeId of distinctNodeIDs) { - // Point the CC at the target node - (command.nodeId as number) = nodeId; - // Figure out if supervision should be used - command.encapsulationFlags = encapsulationFlags; - command.toggleEncapsulationFlag( - EncapsulationFlags.Supervision, - SupervisionCC.mayUseSupervision(driver, command), - ); + const secMan = driver.getSecurityManager2(msg.command.nodeId)!; + const group = secMan.getMulticastGroup(groupId); + if (!group) { + throw new ZWaveError( + `Multicast group ${groupId} does not exist!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } - const scMsg = driver.createSendDataMessage(command, { - transmitOptions: msg.transmitOptions, - maxSendAttempts: msg.maxSendAttempts, - }); - // The outermost command is a Security2CCMessageEncapsulation, we need to set the MGRP extension on this again - (scMsg.command as Security2CCMessageEncapsulation).extensions.push( - new MGRPExtension({ groupId }), - ); + // Send the multicast command. We remember the transmit report and treat it as the result of the multicast command + const response = yield* simpleMessageGenerator( + driver, + ctx, + msg, + onMessageSent, + ); - // Reuse the S2 singlecast message generator for sending this new message - try { - const scResponse = yield* secureMessageGeneratorS2( - driver, - ctx, - scMsg, - onMessageSent, - ); - if ( - isCommandClassContainer(scResponse) - && scResponse.command - instanceof Security2CCMessageEncapsulation - && scResponse.command.hasMOSExtension() - ) { - // The node understood the S2 singlecast followup, but told us that its MPAN is out of sync - - const innerMPANState = secMan.getInnerMPANState(groupId); - // This should always be defined, but better not throw unnecessarily here - if (innerMPANState) { - const cc = new Security2CCMessageEncapsulation({ - nodeId, - extensions: [ - new MPANExtension({ - groupId, - innerMPANState, - }), - ], - }); + // If a node in the group is out of sync, we need to transfer the MPAN state we're going to use for the next command. + // Therefore increment the MPAN state now and not after the followups like the specs mention + secMan.tryIncrementMPAN(groupId); + + // Unwrap the command again, so we can make the following encapsulation depend on the target node + driver.unwrapCommands(msg); + const command = msg.command; + // Remember the original encapsulation flags + const encapsulationFlags = command.encapsulationFlags; + + // In case someone sneaked a node ID into the group multiple times, remove duplicates for the singlecast followups + // Otherwise, the node will increase its MPAN multiple times, going out of sync. + const distinctNodeIDs = [...new Set(group.nodeIDs)]; + + const supervisionResults: (SupervisionResult | undefined)[] = []; + + // Now do singlecast followups with every node in the group + for (const nodeId of distinctNodeIDs) { + // Point the CC at the target node + (command.nodeId as number) = nodeId; + // Figure out if supervision should be used + command.encapsulationFlags = encapsulationFlags; + command.toggleEncapsulationFlag( + EncapsulationFlags.Supervision, + SupervisionCC.mayUseSupervision(driver, command), + ); - // Send it the MPAN - yield* sendCommandGenerator( - driver, - ctx, - cc, - onMessageSent, - { - // Seems we need these options or some nodes won't accept the nonce - transmitOptions: TransmitOptions.ACK - | TransmitOptions.AutoRoute, - // Only try sending a nonce once - maxSendAttempts: 1, - // Nonce requests must be handled immediately - priority: MessagePriority.Immediate, - // We don't want failures causing us to treat the node as asleep or dead - changeNodeStatusOnMissingACK: false, - }, - ); - } - } + const scMsg = driver.createSendDataMessage(command, { + transmitOptions: msg.transmitOptions, + maxSendAttempts: msg.maxSendAttempts, + }); + // The outermost command is a Security2CCMessageEncapsulation, we need to set the MGRP extension on this again + (scMsg.command as Security2CCMessageEncapsulation).extensions.push( + new MGRPExtension({ groupId }), + ); - // Collect supervision results if possible - if (isCommandClassContainer(scResponse)) { - const supervisionReport = scResponse.command - .getEncapsulatedCC( - CommandClasses.Supervision, - SupervisionCommand.Report, - ) as SupervisionCCReport | undefined; + // Reuse the S2 singlecast message generator for sending this new message + try { + const scResponse = yield* secureMessageGeneratorS2( + driver, + ctx, + scMsg, + onMessageSent, + ); + if ( + containsCC(scResponse) + && scResponse.command + instanceof Security2CCMessageEncapsulation + && scResponse.command.hasMOSExtension() + ) { + // The node understood the S2 singlecast followup, but told us that its MPAN is out of sync + + const innerMPANState = secMan.getInnerMPANState(groupId); + // This should always be defined, but better not throw unnecessarily here + if (innerMPANState) { + const cc = new Security2CCMessageEncapsulation({ + nodeId, + extensions: [ + new MPANExtension({ + groupId, + innerMPANState, + }), + ], + }); - supervisionResults.push( - supervisionReport?.toSupervisionResult(), + // Send it the MPAN + yield* sendCommandGenerator( + driver, + ctx, + cc, + onMessageSent, + { + // Seems we need these options or some nodes won't accept the nonce + transmitOptions: TransmitOptions.ACK + | TransmitOptions.AutoRoute, + // Only try sending a nonce once + maxSendAttempts: 1, + // Nonce requests must be handled immediately + priority: MessagePriority.Immediate, + // We don't want failures causing us to treat the node as asleep or dead + changeNodeStatusOnMissingACK: false, + }, ); } - } catch (e) { - driver.driverLog.print(getErrorMessage(e), "error"); - // TODO: Figure out how we got here, and what to do now. - // In any case, keep going with the next nodes - // Report that there was a failure, so the application can show it - supervisionResults.push({ - status: SupervisionStatus.Fail, - }); } - } - const finalSupervisionResult = mergeSupervisionResults( - supervisionResults, - ); - if (finalSupervisionResult) { - // We can return return information about the success of this multicast - so we should - // TODO: Not sure if we need to "wrap" the response for something. For now, try faking it - const cc = new SupervisionCCReport({ - nodeId: NODE_ID_BROADCAST, - sessionId: 0, // fake - moreUpdatesFollow: false, // fake - ...(finalSupervisionResult as any), - }); - const ret = new (driver.getSendDataSinglecastConstructor())({ - sourceNodeId: driver.ownNodeId, - command: cc, + // Collect supervision results if possible + if (containsCC(scResponse)) { + const supervisionReport = scResponse.command + .getEncapsulatedCC( + CommandClasses.Supervision, + SupervisionCommand.Report, + ) as SupervisionCCReport | undefined; + + supervisionResults.push( + supervisionReport?.toSupervisionResult(), + ); + } + } catch (e) { + driver.driverLog.print(getErrorMessage(e), "error"); + // TODO: Figure out how we got here, and what to do now. + // In any case, keep going with the next nodes + // Report that there was a failure, so the application can show it + supervisionResults.push({ + status: SupervisionStatus.Fail, }); - return ret; - } else { - return response; } - }; + } + + const finalSupervisionResult = mergeSupervisionResults( + supervisionResults, + ); + if (finalSupervisionResult) { + // We can return return information about the success of this multicast - so we should + // TODO: Not sure if we need to "wrap" the response for something. For now, try faking it + const cc = new SupervisionCCReport({ + nodeId: NODE_ID_BROADCAST, + sessionId: 0, // fake + moreUpdatesFollow: false, // fake + ...(finalSupervisionResult as any), + }); + const ret = new (driver.getSendDataSinglecastConstructor())({ + sourceNodeId: driver.ownNodeId, + command: cc, + }); + return ret; + } else { + return response; + } +}; export function createMessageGenerator( driver: Driver, @@ -930,21 +935,35 @@ export function createMessageGenerator( start: () => { async function* gen() { // Determine which message generator implementation should be used - let implementation: MessageGeneratorImplementation = + let implementation: MessageGeneratorImplementation = simpleMessageGenerator; if (isSendData(msg)) { + if (!containsCC(msg)) { + throw new ZWaveError( + "Cannot create a message generator for a message that doesn't contain a command class", + ZWaveErrorCodes.Argument_Invalid, + ); + } if ( msg.command instanceof Security2CCMessageEncapsulation ) { - implementation = msg.command.isSinglecast() - ? secureMessageGeneratorS2 - : secureMessageGeneratorS2Multicast; + implementation = ( + msg.command.isSinglecast() + ? secureMessageGeneratorS2 + : secureMessageGeneratorS2Multicast + ) as MessageGeneratorImplementation; } else if ( msg.command instanceof SecurityCCCommandEncapsulation ) { - implementation = secureMessageGeneratorS0; + implementation = + secureMessageGeneratorS0 as MessageGeneratorImplementation< + Message + >; } else if (msg.command.isSinglecast()) { - implementation = maybeTransportServiceGenerator; + implementation = + maybeTransportServiceGenerator as MessageGeneratorImplementation< + Message + >; } } diff --git a/packages/zwave-js/src/lib/log/Driver.ts b/packages/zwave-js/src/lib/log/Driver.ts index 332e381f60ad..d359ca70f57a 100644 --- a/packages/zwave-js/src/lib/log/Driver.ts +++ b/packages/zwave-js/src/lib/log/Driver.ts @@ -1,6 +1,5 @@ import { type CommandClass, - isCommandClassContainer, isEncapsulatingCommandClass, isMultiEncapsulatingCommandClass, } from "@zwave-js/cc"; @@ -21,6 +20,7 @@ import type { Driver } from "../driver/Driver"; import { type TransactionQueue } from "../driver/Queue"; import type { Transaction } from "../driver/Transaction"; import { NodeStatus } from "../node/_Types"; +import { containsCC } from "../serialapi/utils"; export const DRIVER_LABEL = "DRIVER"; const DRIVER_LOGLEVEL = "verbose"; @@ -122,7 +122,7 @@ export class DriverLogger extends ZWaveLoggerBase { return; } - const isCCContainer = isCommandClassContainer(message); + const isCCContainer = containsCC(message); const logEntry = message.toLogEntry(); let msg: string[] = [tagify(logEntry.tags)]; @@ -136,7 +136,7 @@ export class DriverLogger extends ZWaveLoggerBase { try { // If possible, include information about the CCs - if (isCommandClassContainer(message)) { + if (isCCContainer) { // Remove the default payload message and draw a bracket msg = msg.filter((line) => !line.startsWith("│ payload:")); @@ -209,7 +209,7 @@ export class DriverLogger extends ZWaveLoggerBase { ) }]` : ""; - const command = isCommandClassContainer(trns.message) + const command = containsCC(trns.message) ? `: ${trns.message.command.constructor.name}` : ""; message += `\n· ${prefix} ${ diff --git a/packages/zwave-js/src/lib/node/Node.ts b/packages/zwave-js/src/lib/node/Node.ts index 38a79be6ea48..a7e42ef5b2ab 100644 --- a/packages/zwave-js/src/lib/node/Node.ts +++ b/packages/zwave-js/src/lib/node/Node.ts @@ -39,7 +39,6 @@ import { entryControlEventTypeLabels, getEffectiveCCVersion, getImplementedVersion, - isCommandClassContainer, utils as ccUtils, } from "@zwave-js/cc"; import { @@ -241,6 +240,7 @@ import { RequestNodeInfoRequest, RequestNodeInfoResponse, } from "../serialapi/network-mgmt/RequestNodeInfoMessages"; +import { containsCC } from "../serialapi/utils"; import { DeviceClass } from "./DeviceClass"; import { type NodeDump, type ValueDump } from "./Dump"; import { type Endpoint } from "./Endpoint"; @@ -1133,7 +1133,7 @@ export class ZWaveNode extends ZWaveNodeMixins implements QuerySecurityClasses { // The GetNodeProtocolInfoRequest needs to know the node ID to distinguish // between ZWLR and ZW classic. We store it on the driver's context, so it // can be retrieved when needed. - this.driver.requestContext.set(FunctionType.GetNodeProtocolInfo, { + this.driver.requestStorage.set(FunctionType.GetNodeProtocolInfo, { nodeId: this.id, }); const resp = await this.driver.sendMessage( @@ -2578,7 +2578,7 @@ protocol version: ${this.protocolVersion}`; // Ensure that we're not flooding the queue with unnecessary NonceReports (GH#1059) const isNonceReport = (t: Transaction) => t.message.getNodeId() === this.id - && isCommandClassContainer(t.message) + && containsCC(t.message) && t.message.command instanceof SecurityCCNonceReport; if (this.driver.hasPendingTransactions(isNonceReport)) { @@ -2655,7 +2655,7 @@ protocol version: ${this.protocolVersion}`; // Ensure that we're not flooding the queue with unnecessary NonceReports (GH#1059) const isNonceReport = (t: Transaction) => t.message.getNodeId() === this.id - && isCommandClassContainer(t.message) + && containsCC(t.message) && t.message.command instanceof Security2CCNonceReport; if (this.driver.hasPendingTransactions(isNonceReport)) { diff --git a/packages/zwave-js/src/lib/node/mixins/50_Endpoints.ts b/packages/zwave-js/src/lib/node/mixins/50_Endpoints.ts index 1e0985293f26..6993355b6910 100644 --- a/packages/zwave-js/src/lib/node/mixins/50_Endpoints.ts +++ b/packages/zwave-js/src/lib/node/mixins/50_Endpoints.ts @@ -177,7 +177,6 @@ export abstract class EndpointsMixin extends NodeValuesMixin /** Returns a list of all endpoints of this node, including the root endpoint (index 0) */ public getAllEndpoints(): Endpoint[] { - // FIXME: GH#7261 we should not need to cast here return nodeUtils.getAllEndpoints(this.driver, this); } } diff --git a/packages/zwave-js/src/lib/node/mixins/70_FirmwareUpdate.ts b/packages/zwave-js/src/lib/node/mixins/70_FirmwareUpdate.ts index 8214c9756b92..ab119c6df098 100644 --- a/packages/zwave-js/src/lib/node/mixins/70_FirmwareUpdate.ts +++ b/packages/zwave-js/src/lib/node/mixins/70_FirmwareUpdate.ts @@ -12,7 +12,6 @@ import { type FirmwareUpdateResult, FirmwareUpdateStatus, getEffectiveCCVersion, - isCommandClassContainer, } from "@zwave-js/cc"; import { CRC16_CCITT, @@ -36,6 +35,7 @@ import { roundTo } from "alcalzone-shared/math"; import { randomBytes } from "node:crypto"; import { type Task, type TaskBuilder, TaskPriority } from "../../driver/Task"; import { type Transaction } from "../../driver/Transaction"; +import { containsCC } from "../../serialapi/utils"; import { SchedulePollMixin } from "./60_ScheduledPoll"; interface AbortFirmwareUpdateContext { @@ -779,7 +779,7 @@ export abstract class FirmwareUpdateMixin extends SchedulePollMixin // Avoid queuing duplicate fragments const isCurrentFirmwareFragment = (t: Transaction) => t.message.getNodeId() === this.id - && isCommandClassContainer(t.message) + && containsCC(t.message) && t.message.command instanceof FirmwareUpdateMetaDataCCReport && t.message.command.reportNumber === fragmentNumber; diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts index 3dd6b9393ce4..11abd57e1f91 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationCommandRequest.ts @@ -1,26 +1,27 @@ -import { CommandClass, type ICommandClassContainer } from "@zwave-js/cc"; +import { type CommandClass } from "@zwave-js/cc"; import { type FrameType, type MessageOrCCLogEntry, MessagePriority, type MessageRecord, - type SinglecastCC, ZWaveError, ZWaveErrorCodes, encodeNodeID, parseNodeID, } from "@zwave-js/core"; +import { type CCEncodingContext } from "@zwave-js/host"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; +import { type MessageWithCC } from "../utils"; export enum ApplicationCommandStatusFlags { RoutedBusy = 0b1, // A response route is locked by the application @@ -37,83 +38,101 @@ export enum ApplicationCommandStatusFlags { ForeignHomeId = 0b1000_0000, // The received frame is received from a foreign HomeID. Only Controllers in Smart Start AddNode mode can receive this status. } -interface ApplicationCommandRequestOptions extends MessageBaseOptions { - command: CommandClass; - frameType?: ApplicationCommandRequest["frameType"]; - routedBusy?: boolean; -} +export type ApplicationCommandRequestOptions = + & ( + | { command: CommandClass } + | { + nodeId: number; + serializedCC: Buffer; + } + ) + & { + frameType?: ApplicationCommandRequest["frameType"]; + routedBusy?: boolean; + isExploreFrame?: boolean; + isForeignFrame?: boolean; + fromForeignHomeId?: boolean; + }; @messageTypes(MessageType.Request, FunctionType.ApplicationCommand) // This does not expect a response. The controller sends us this when a node sends a command @priority(MessagePriority.Normal) export class ApplicationCommandRequest extends Message - implements ICommandClassContainer + implements MessageWithCC { public constructor( - options: - | MessageDeserializationOptions - | ApplicationCommandRequestOptions, + options: ApplicationCommandRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - // first byte is a status flag - const status = this.payload[0]; - this.routedBusy = !!( - status & ApplicationCommandStatusFlags.RoutedBusy - ); - switch (status & ApplicationCommandStatusFlags.TypeMask) { - case ApplicationCommandStatusFlags.TypeMulti: - this.frameType = "multicast"; - break; - case ApplicationCommandStatusFlags.TypeBroad: - this.frameType = "broadcast"; - break; - default: - this.frameType = "singlecast"; - } - this.isExploreFrame = this.frameType === "broadcast" - && !!(status & ApplicationCommandStatusFlags.Explore); - this.isForeignFrame = !!( - status & ApplicationCommandStatusFlags.ForeignFrame - ); - this.fromForeignHomeId = !!( - status & ApplicationCommandStatusFlags.ForeignHomeId - ); - - // followed by a node ID - let offset = 1; - const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( - this.payload, - options.ctx.nodeIdType, - offset, - ); - offset += nodeIdBytes; - // and a command class - const commandLength = this.payload[offset++]; - this.command = CommandClass.parse( - this.payload.subarray(offset, offset + commandLength), - { - sourceNodeId: nodeId, - ...options.ctx, - frameType: this.frameType, - }, - ) as SinglecastCC; - } else { - // TODO: This logic is unsound - if (!options.command.isSinglecast()) { - throw new ZWaveError( - `ApplicationCommandRequest can only be used for singlecast CCs`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.frameType = options.frameType ?? "singlecast"; - this.routedBusy = !!options.routedBusy; + if ("command" in options) { this.command = options.command; - this.isExploreFrame = false; - this.isForeignFrame = false; - this.fromForeignHomeId = false; + } else { + this._nodeId = options.nodeId; + this.serializedCC = options.serializedCC; + } + + this.frameType = options.frameType ?? "singlecast"; + this.routedBusy = options.routedBusy ?? false; + this.isExploreFrame = options.isExploreFrame ?? false; + this.isForeignFrame = options.isForeignFrame ?? false; + this.fromForeignHomeId = options.fromForeignHomeId ?? false; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ApplicationCommandRequest { + // first byte is a status flag + const status = raw.payload[0]; + const routedBusy = !!( + status & ApplicationCommandStatusFlags.RoutedBusy + ); + let frameType: FrameType; + + switch (status & ApplicationCommandStatusFlags.TypeMask) { + case ApplicationCommandStatusFlags.TypeMulti: + frameType = "multicast"; + break; + case ApplicationCommandStatusFlags.TypeBroad: + frameType = "broadcast"; + break; + default: + frameType = "singlecast"; } + const isExploreFrame: boolean = frameType === "broadcast" + && !!(status & ApplicationCommandStatusFlags.Explore); + const isForeignFrame = !!( + status & ApplicationCommandStatusFlags.ForeignFrame + ); + const fromForeignHomeId = !!( + status & ApplicationCommandStatusFlags.ForeignHomeId + ); + + // followed by a node ID + let offset = 1; + const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( + raw.payload, + ctx.nodeIdType, + offset, + ); + offset += nodeIdBytes; + // and a command class + const commandLength = raw.payload[offset++]; + const serializedCC = raw.payload.subarray( + offset, + offset + commandLength, + ); + + return new this({ + routedBusy, + frameType, + isExploreFrame, + isForeignFrame, + fromForeignHomeId, + serializedCC, + nodeId, + }); } public readonly routedBusy: boolean; @@ -123,13 +142,30 @@ export class ApplicationCommandRequest extends Message public readonly fromForeignHomeId: boolean; // This needs to be writable or unwrapping MultiChannelCCs crashes - public command: SinglecastCC; // TODO: why is this a SinglecastCC? + public command: CommandClass | undefined; + private _nodeId: number | undefined; public override getNodeId(): number | undefined { - if (this.command.isSinglecast()) { + if (this.command?.isSinglecast()) { return this.command.nodeId; } - return super.getNodeId(); + + return this._nodeId ?? super.getNodeId(); + } + + public serializedCC: Buffer | undefined; + /** @internal */ + public serializeCC(ctx: CCEncodingContext): Buffer { + if (!this.serializedCC) { + if (!this.command) { + throw new ZWaveError( + `Cannot serialize a ${this.constructor.name} without a command`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.serializedCC = this.command.serialize(ctx); + } + return this.serializedCC; } public serialize(ctx: MessageEncodingContext): Buffer { @@ -140,7 +176,7 @@ export class ApplicationCommandRequest extends Message : 0) | (this.routedBusy ? ApplicationCommandStatusFlags.RoutedBusy : 0); - const serializedCC = this.command.serialize(ctx); + const serializedCC = this.serializeCC(ctx); const nodeId = encodeNodeID( this.getNodeId() ?? ctx.ownNodeId, ctx.nodeIdType, diff --git a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts index 11d61cd26831..1f9b5b32d87b 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ApplicationUpdateRequest.ts @@ -12,16 +12,15 @@ import { parseNodeUpdatePayload, } from "@zwave-js/core"; import { - type DeserializingMessageConstructor, FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, + type MessageConstructor, type MessageEncodingContext, - type MessageOptions, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, - gotDeserializationOptions, messageTypes, } from "@zwave-js/serial"; import { buffer2hex, getEnumMemberName } from "@zwave-js/shared"; @@ -46,34 +45,48 @@ const { } = createSimpleReflectionDecorator< ApplicationUpdateRequest, [updateType: ApplicationUpdateTypes], - DeserializingMessageConstructor + MessageConstructor >({ name: "applicationUpdateType", }); +export interface ApplicationUpdateRequestOptions { + updateType?: ApplicationUpdateTypes; +} + @messageTypes(MessageType.Request, FunctionType.ApplicationUpdateRequest) // this is only received, not sent! export class ApplicationUpdateRequest extends Message { - public constructor(options?: MessageOptions) { + public constructor( + options: ApplicationUpdateRequestOptions & MessageBaseOptions = {}, + ) { super(options); - if (gotDeserializationOptions(options)) { - this.updateType = this.payload[0]; - - const CommandConstructor = getApplicationUpdateRequestConstructor( - this.updateType, - ); - if ( - CommandConstructor - && (new.target as any) !== CommandConstructor - ) { - return new CommandConstructor(options); - } - - this.payload = this.payload.subarray(1); - } else { - this.updateType = getApplicationUpdateType(this)!; + this.updateType = options.updateType ?? getApplicationUpdateType(this)!; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ApplicationUpdateRequest { + const updateType: ApplicationUpdateTypes = raw.payload[0]; + const payload = raw.payload.subarray(1); + + const CommandConstructor = getApplicationUpdateRequestConstructor( + updateType, + ); + if (CommandConstructor) { + return CommandConstructor.from( + raw.withPayload(payload), + ctx, + ) as ApplicationUpdateRequest; } + + const ret = new ApplicationUpdateRequest({ + updateType, + }); + ret.payload = payload; + return ret; } public readonly updateType: ApplicationUpdateTypes; @@ -87,9 +100,7 @@ export class ApplicationUpdateRequest extends Message { } } -interface ApplicationUpdateRequestWithNodeInfoOptions - extends MessageBaseOptions -{ +export interface ApplicationUpdateRequestWithNodeInfoOptions { nodeInformation: NodeUpdatePayload; } @@ -98,21 +109,27 @@ export class ApplicationUpdateRequestWithNodeInfo { public constructor( options: - | MessageDeserializationOptions - | ApplicationUpdateRequestWithNodeInfoOptions, + & ApplicationUpdateRequestWithNodeInfoOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.nodeInformation = parseNodeUpdatePayload( - this.payload, - options.ctx.nodeIdType, - ); - this.nodeId = this.nodeInformation.nodeId; - } else { - this.nodeId = options.nodeInformation.nodeId; - this.nodeInformation = options.nodeInformation; - } + this.nodeId = options.nodeInformation.nodeId; + this.nodeInformation = options.nodeInformation; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ApplicationUpdateRequestWithNodeInfo { + const nodeInformation: NodeUpdatePayload = parseNodeUpdatePayload( + raw.payload, + ctx.nodeIdType, + ); + + return new this({ + nodeInformation, + }); } public nodeId: number; @@ -147,52 +164,97 @@ export class ApplicationUpdateRequestNodeAdded extends ApplicationUpdateRequestWithNodeInfo {} +export interface ApplicationUpdateRequestNodeRemovedOptions { + nodeId: number; +} + @applicationUpdateType(ApplicationUpdateTypes.Node_Removed) export class ApplicationUpdateRequestNodeRemoved extends ApplicationUpdateRequest { public constructor( - options: MessageDeserializationOptions, + options: + & ApplicationUpdateRequestNodeRemovedOptions + & MessageBaseOptions, ) { super(options); + this.nodeId = options.nodeId; + } - const { nodeId } = parseNodeID(this.payload, options.ctx.nodeIdType, 0); - this.nodeId = nodeId; + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ApplicationUpdateRequestNodeRemoved { + const { nodeId } = parseNodeID(raw.payload, ctx.nodeIdType, 0); // byte 1/2 is 0, meaning unknown + + return new this({ + nodeId, + }); } public nodeId: number; } +export interface ApplicationUpdateRequestSmartStartHomeIDReceivedBaseOptions { + remoteNodeId: number; + nwiHomeId: Buffer; + basicDeviceClass: BasicDeviceClass; + genericDeviceClass: number; + specificDeviceClass: number; + supportedCCs: CommandClasses[]; +} + class ApplicationUpdateRequestSmartStartHomeIDReceivedBase extends ApplicationUpdateRequest { public constructor( - options: MessageDeserializationOptions, + options: + & ApplicationUpdateRequestSmartStartHomeIDReceivedBaseOptions + & MessageBaseOptions, ) { super(options); + + // TODO: Check implementation: + this.remoteNodeId = options.remoteNodeId; + this.nwiHomeId = options.nwiHomeId; + this.basicDeviceClass = options.basicDeviceClass; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + this.supportedCCs = options.supportedCCs; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ApplicationUpdateRequestSmartStartHomeIDReceivedBase { let offset = 0; - const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( - this.payload, - options.ctx.nodeIdType, + const { nodeId: remoteNodeId, bytesRead: nodeIdBytes } = parseNodeID( + raw.payload, + ctx.nodeIdType, offset, ); offset += nodeIdBytes; - this.remoteNodeId = nodeId; - // next byte is rxStatus offset++; - - this.nwiHomeId = this.payload.subarray(offset, offset + 4); + const nwiHomeId: Buffer = raw.payload.subarray(offset, offset + 4); offset += 4; - - const ccLength = this.payload[offset++]; - this.basicDeviceClass = this.payload[offset++]; - this.genericDeviceClass = this.payload[offset++]; - this.specificDeviceClass = this.payload[offset++]; - this.supportedCCs = parseCCList( - this.payload.subarray(offset, offset + ccLength), + const ccLength = raw.payload[offset++]; + const basicDeviceClass: BasicDeviceClass = raw.payload[offset++]; + const genericDeviceClass = raw.payload[offset++]; + const specificDeviceClass = raw.payload[offset++]; + const supportedCCs = parseCCList( + raw.payload.subarray(offset, offset + ccLength), ).supportedCCs; + + return new this({ + remoteNodeId, + nwiHomeId, + basicDeviceClass, + genericDeviceClass, + specificDeviceClass, + supportedCCs, + }); } public readonly remoteNodeId: number; @@ -237,18 +299,38 @@ export class ApplicationUpdateRequestSmartStartLongRangeHomeIDReceived extends ApplicationUpdateRequestSmartStartHomeIDReceivedBase {} +export interface ApplicationUpdateRequestSUCIdChangedOptions { + sucNodeID: number; +} + @applicationUpdateType(ApplicationUpdateTypes.SUC_IdChanged) export class ApplicationUpdateRequestSUCIdChanged extends ApplicationUpdateRequest { public constructor( - options: MessageDeserializationOptions, + options: + & ApplicationUpdateRequestSUCIdChangedOptions + & MessageBaseOptions, ) { super(options); - const { nodeId } = parseNodeID(this.payload, options.ctx.nodeIdType, 0); - this.sucNodeID = nodeId; + this.sucNodeID = options.sucNodeID; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ApplicationUpdateRequestSUCIdChanged { + const { nodeId: sucNodeID } = parseNodeID( + raw.payload, + ctx.nodeIdType, + 0, + ); // byte 1/2 is 0, meaning unknown + + return new this({ + sucNodeID, + }); } public sucNodeID: number; diff --git a/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.test.ts b/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.test.ts index 923f0e17cdbd..87ace9ef00d6 100644 --- a/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.test.ts +++ b/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.test.ts @@ -5,12 +5,12 @@ import test from "ava"; test("BridgeApplicationCommandRequest can be parsed without RSSI", async (t) => { // Repro for https://github.com/zwave-js/node-zwave-js/issues/4335 t.notThrows(() => - Message.from({ - data: Buffer.from( + Message.parse( + Buffer.from( "011200a80001020a320221340000000000000069", "hex", ), - ctx: {} as any, - }) + {} as any, + ) ); }); diff --git a/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts b/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts index 397d8a3fd8c3..3b81f34a557c 100644 --- a/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/BridgeApplicationCommandRequest.ts @@ -1,4 +1,4 @@ -import { CommandClass, type ICommandClassContainer } from "@zwave-js/cc"; +import { type CommandClass } from "@zwave-js/cc"; import { type FrameType, type MessageOrCCLogEntry, @@ -8,7 +8,6 @@ import { NODE_ID_BROADCAST_LR, type RSSI, RssiError, - type SinglecastCC, isLongRangeNodeId, parseNodeBitMask, parseNodeID, @@ -16,89 +15,142 @@ import { import { FunctionType, Message, - type MessageDeserializationOptions, + type MessageBaseOptions, + type MessageParsingContext, + type MessageRaw, MessageType, messageTypes, priority, } from "@zwave-js/serial"; import { getEnumMemberName } from "@zwave-js/shared"; import { tryParseRSSI } from "../transport/SendDataShared"; +import { type MessageWithCC } from "../utils"; import { ApplicationCommandStatusFlags } from "./ApplicationCommandRequest"; +export type BridgeApplicationCommandRequestOptions = + & ( + | { command: CommandClass } + | { + nodeId: number; + serializedCC: Buffer; + } + ) + & { + routedBusy: boolean; + frameType: FrameType; + isExploreFrame: boolean; + isForeignFrame: boolean; + fromForeignHomeId: boolean; + ownNodeId: number; + targetNodeId: number | number[]; + rssi?: number; + }; + @messageTypes(MessageType.Request, FunctionType.BridgeApplicationCommand) // This does not expect a response. The controller sends us this when a node sends a command @priority(MessagePriority.Normal) export class BridgeApplicationCommandRequest extends Message - implements ICommandClassContainer + implements MessageWithCC { public constructor( - options: MessageDeserializationOptions, + options: BridgeApplicationCommandRequestOptions & MessageBaseOptions, ) { super(options); - this._ownNodeId = options.ctx.ownNodeId; - // if (gotDeserializationOptions(options)) { + if ("command" in options) { + this.command = options.command; + } else { + this._nodeId = options.nodeId; + this.serializedCC = options.serializedCC; + } + + this.routedBusy = options.routedBusy; + this.frameType = options.frameType; + this.isExploreFrame = options.isExploreFrame; + this.isForeignFrame = options.isForeignFrame; + this.fromForeignHomeId = options.fromForeignHomeId; + // FIXME: We only need this in the toLogEntry context + this.ownNodeId = options.ownNodeId; + this.targetNodeId = options.targetNodeId; + this.rssi = options.rssi; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): BridgeApplicationCommandRequest { // first byte is a status flag - const status = this.payload[0]; - this.routedBusy = !!(status & ApplicationCommandStatusFlags.RoutedBusy); + const status = raw.payload[0]; + const routedBusy = + !!(status & ApplicationCommandStatusFlags.RoutedBusy); + let frameType: FrameType; switch (status & ApplicationCommandStatusFlags.TypeMask) { case ApplicationCommandStatusFlags.TypeMulti: - this.frameType = "multicast"; + frameType = "multicast"; break; case ApplicationCommandStatusFlags.TypeBroad: - this.frameType = "broadcast"; + frameType = "broadcast"; break; default: - this.frameType = "singlecast"; + frameType = "singlecast"; } - this.isExploreFrame = this.frameType === "broadcast" + + const isExploreFrame = frameType === "broadcast" && !!(status & ApplicationCommandStatusFlags.Explore); - this.isForeignFrame = !!( + const isForeignFrame = !!( status & ApplicationCommandStatusFlags.ForeignFrame ); - this.fromForeignHomeId = !!( + const fromForeignHomeId = !!( status & ApplicationCommandStatusFlags.ForeignHomeId ); - let offset = 1; const { nodeId: destinationNodeId, bytesRead: dstNodeIdBytes } = - parseNodeID(this.payload, options.ctx.nodeIdType, offset); + parseNodeID(raw.payload, ctx.nodeIdType, offset); offset += dstNodeIdBytes; const { nodeId: sourceNodeId, bytesRead: srcNodeIdBytes } = parseNodeID( - this.payload, - options.ctx.nodeIdType, + raw.payload, + ctx.nodeIdType, offset, ); offset += srcNodeIdBytes; - // Parse the CC - const commandLength = this.payload[offset++]; - this.command = CommandClass.parse( - this.payload.subarray(offset, offset + commandLength), - { - sourceNodeId, - ...options.ctx, - frameType: this.frameType, - }, - ) as SinglecastCC; + // Extract the CC payload + const commandLength = raw.payload[offset++]; + const serializedCC = raw.payload.subarray( + offset, + offset + commandLength, + ); offset += commandLength; - // Read the correct target node id - const multicastNodesLength = this.payload[offset]; + const multicastNodesLength = raw.payload[offset]; offset++; - if (this.frameType === "multicast") { - this.targetNodeId = parseNodeBitMask( - this.payload.subarray(offset, offset + multicastNodesLength), + let targetNodeId: number | number[]; + if (frameType === "multicast") { + targetNodeId = parseNodeBitMask( + raw.payload.subarray(offset, offset + multicastNodesLength), ); - } else if (this.frameType === "singlecast") { - this.targetNodeId = destinationNodeId; + } else if (frameType === "singlecast") { + targetNodeId = destinationNodeId; } else { - this.targetNodeId = isLongRangeNodeId(sourceNodeId) + targetNodeId = isLongRangeNodeId(sourceNodeId) ? NODE_ID_BROADCAST_LR : NODE_ID_BROADCAST; } + offset += multicastNodesLength; + const rssi: number | undefined = tryParseRSSI(raw.payload, offset); - this.rssi = tryParseRSSI(this.payload, offset); + return new this({ + routedBusy, + frameType, + isExploreFrame, + isForeignFrame, + fromForeignHomeId, + nodeId: sourceNodeId, + serializedCC, + ownNodeId: ctx.ownNodeId, + targetNodeId, + rssi, + }); } public readonly routedBusy: boolean; @@ -109,16 +161,19 @@ export class BridgeApplicationCommandRequest extends Message public readonly fromForeignHomeId: boolean; public readonly rssi?: RSSI; - private _ownNodeId: number; + public readonly ownNodeId: number; + + public readonly serializedCC: Buffer | undefined; // This needs to be writable or unwrapping MultiChannelCCs crashes - public command: SinglecastCC; // TODO: why is this a SinglecastCC? + public command: CommandClass | undefined; + private _nodeId: number | undefined; public override getNodeId(): number | undefined { - if (this.command.isSinglecast()) { + if (this.command?.isSinglecast()) { return this.command.nodeId; } - return super.getNodeId(); + return this._nodeId ?? super.getNodeId(); } public toLogEntry(): MessageOrCCLogEntry { @@ -126,7 +181,7 @@ export class BridgeApplicationCommandRequest extends Message if (this.frameType !== "singlecast") { message.type = this.frameType; } - if (this.targetNodeId !== this._ownNodeId) { + if (this.targetNodeId !== this.ownNodeId) { if (typeof this.targetNodeId === "number") { message["target node"] = this.targetNodeId; } else if (this.targetNodeId.length === 1) { diff --git a/packages/zwave-js/src/lib/serialapi/application/SerialAPIStartedRequest.ts b/packages/zwave-js/src/lib/serialapi/application/SerialAPIStartedRequest.ts index 10adc96cf2be..a432ffd8fdd3 100644 --- a/packages/zwave-js/src/lib/serialapi/application/SerialAPIStartedRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/application/SerialAPIStartedRequest.ts @@ -9,10 +9,9 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageRaw, MessageType, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -43,7 +42,7 @@ export enum SerialAPIWakeUpReason { Unknown = 0xff, } -export interface SerialAPIStartedRequestOptions extends MessageBaseOptions { +export interface SerialAPIStartedRequestOptions { wakeUpReason: SerialAPIWakeUpReason; watchdogEnabled: boolean; genericDeviceClass: number; @@ -59,42 +58,54 @@ export interface SerialAPIStartedRequestOptions extends MessageBaseOptions { @priority(MessagePriority.Normal) export class SerialAPIStartedRequest extends Message { public constructor( - options: MessageDeserializationOptions | SerialAPIStartedRequestOptions, + options: SerialAPIStartedRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.wakeUpReason = this.payload[0]; - this.watchdogEnabled = this.payload[1] === 0x01; - - const deviceOption = this.payload[2]; - this.isListening = !!(deviceOption & 0b10_000_000); + this.wakeUpReason = options.wakeUpReason; + this.watchdogEnabled = options.watchdogEnabled; + this.isListening = options.isListening; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + this.supportedCCs = options.supportedCCs; + this.controlledCCs = options.controlledCCs; + this.supportsLongRange = options.supportsLongRange; + } - this.genericDeviceClass = this.payload[3]; - this.specificDeviceClass = this.payload[4]; + public static from( + raw: MessageRaw, + ): SerialAPIStartedRequest { + const wakeUpReason: SerialAPIWakeUpReason = raw.payload[0]; + const watchdogEnabled = raw.payload[1] === 0x01; + const deviceOption = raw.payload[2]; + const isListening = !!(deviceOption & 0b10_000_000); + const genericDeviceClass = raw.payload[3]; + const specificDeviceClass = raw.payload[4]; - // Parse list of CCs - const numCCBytes = this.payload[5]; - const ccBytes = this.payload.subarray(6, 6 + numCCBytes); - const ccList = parseCCList(ccBytes); - this.supportedCCs = ccList.supportedCCs; - this.controlledCCs = ccList.controlledCCs; + // Parse list of CCs + const numCCBytes = raw.payload[5]; + const ccBytes = raw.payload.subarray(6, 6 + numCCBytes); + const ccList = parseCCList(ccBytes); + const supportedCCs: CommandClasses[] = ccList.supportedCCs; + const controlledCCs: CommandClasses[] = ccList.controlledCCs; - // Parse supported protocols - if (this.payload.length >= 6 + numCCBytes + 1) { - const protocols = this.payload[6 + numCCBytes]; - this.supportsLongRange = !!(protocols & 0b1); - } - } else { - this.wakeUpReason = options.wakeUpReason; - this.watchdogEnabled = options.watchdogEnabled; - this.isListening = options.isListening; - this.genericDeviceClass = options.genericDeviceClass; - this.specificDeviceClass = options.specificDeviceClass; - this.supportedCCs = options.supportedCCs; - this.controlledCCs = options.controlledCCs; - this.supportsLongRange = options.supportsLongRange; + // Parse supported protocols + let supportsLongRange = false; + if (raw.payload.length >= 6 + numCCBytes + 1) { + const protocols = raw.payload[6 + numCCBytes]; + supportsLongRange = !!(protocols & 0b1); } + + return new this({ + wakeUpReason, + watchdogEnabled, + isListening, + genericDeviceClass, + specificDeviceClass, + supportedCCs, + controlledCCs, + supportsLongRange, + }); } public wakeUpReason: SerialAPIWakeUpReason; diff --git a/packages/zwave-js/src/lib/serialapi/application/ShutdownMessages.ts b/packages/zwave-js/src/lib/serialapi/application/ShutdownMessages.ts index e2f58de80106..87872a441381 100644 --- a/packages/zwave-js/src/lib/serialapi/application/ShutdownMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/application/ShutdownMessages.ts @@ -3,14 +3,15 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, priority, } from "@zwave-js/serial"; -export interface ShutdownRequestOptions extends MessageBaseOptions { +export interface ShutdownRequestOptions { someProperty: number; } @@ -19,13 +20,30 @@ export interface ShutdownRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.Shutdown) export class ShutdownRequest extends Message {} +export interface ShutdownResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.Shutdown) export class ShutdownResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: ShutdownResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ShutdownResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } public readonly success: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetControllerCapabilitiesMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetControllerCapabilitiesMessages.ts index b3cfe7bcdb9a..7e0fdbe10880 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetControllerCapabilitiesMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetControllerCapabilitiesMessages.ts @@ -3,11 +3,11 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -17,9 +17,7 @@ import { @priority(MessagePriority.Controller) export class GetControllerCapabilitiesRequest extends Message {} -export interface GetControllerCapabilitiesResponseOptions - extends MessageBaseOptions -{ +export interface GetControllerCapabilitiesResponseOptions { isSecondary: boolean; isUsingHomeIdFromOtherNetwork: boolean; isSISPresent: boolean; @@ -31,41 +29,51 @@ export interface GetControllerCapabilitiesResponseOptions @messageTypes(MessageType.Response, FunctionType.GetControllerCapabilities) export class GetControllerCapabilitiesResponse extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetControllerCapabilitiesResponseOptions, + options: GetControllerCapabilitiesResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - const capabilityFlags = this.payload[0]; - this.isSecondary = !!( - capabilityFlags & ControllerCapabilityFlags.Secondary - ); - this.isUsingHomeIdFromOtherNetwork = !!( - capabilityFlags & ControllerCapabilityFlags.OnOtherNetwork - ); - this.isSISPresent = !!( - capabilityFlags & ControllerCapabilityFlags.SISPresent - ); - this.wasRealPrimary = !!( - capabilityFlags & ControllerCapabilityFlags.WasRealPrimary - ); - this.isStaticUpdateController = !!( - capabilityFlags & ControllerCapabilityFlags.SUC - ); - this.noNodesIncluded = !!( - capabilityFlags & ControllerCapabilityFlags.NoNodesIncluded - ); - } else { - this.isSecondary = options.isSecondary; - this.isUsingHomeIdFromOtherNetwork = - options.isUsingHomeIdFromOtherNetwork; - this.isSISPresent = options.isSISPresent; - this.wasRealPrimary = options.wasRealPrimary; - this.isStaticUpdateController = options.isStaticUpdateController; - this.noNodesIncluded = options.noNodesIncluded; - } + this.isSecondary = options.isSecondary; + this.isUsingHomeIdFromOtherNetwork = + options.isUsingHomeIdFromOtherNetwork; + this.isSISPresent = options.isSISPresent; + this.wasRealPrimary = options.wasRealPrimary; + this.isStaticUpdateController = options.isStaticUpdateController; + this.noNodesIncluded = options.noNodesIncluded; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetControllerCapabilitiesResponse { + const capabilityFlags = raw.payload[0]; + const isSecondary = !!( + capabilityFlags & ControllerCapabilityFlags.Secondary + ); + const isUsingHomeIdFromOtherNetwork = !!( + capabilityFlags & ControllerCapabilityFlags.OnOtherNetwork + ); + const isSISPresent = !!( + capabilityFlags & ControllerCapabilityFlags.SISPresent + ); + const wasRealPrimary = !!( + capabilityFlags & ControllerCapabilityFlags.WasRealPrimary + ); + const isStaticUpdateController = !!( + capabilityFlags & ControllerCapabilityFlags.SUC + ); + const noNodesIncluded = !!( + capabilityFlags & ControllerCapabilityFlags.NoNodesIncluded + ); + + return new this({ + isSecondary, + isUsingHomeIdFromOtherNetwork, + isSISPresent, + wasRealPrimary, + isStaticUpdateController, + noNodesIncluded, + }); } public isSecondary: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetControllerVersionMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetControllerVersionMessages.ts index 78a34fe8aecf..59136e898ccd 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetControllerVersionMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetControllerVersionMessages.ts @@ -3,11 +3,11 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -19,9 +19,7 @@ import type { ZWaveLibraryTypes } from "../_Types"; @priority(MessagePriority.Controller) export class GetControllerVersionRequest extends Message {} -export interface GetControllerVersionResponseOptions - extends MessageBaseOptions -{ +export interface GetControllerVersionResponseOptions { controllerType: ZWaveLibraryTypes; libraryVersion: string; } @@ -29,20 +27,27 @@ export interface GetControllerVersionResponseOptions @messageTypes(MessageType.Response, FunctionType.GetControllerVersion) export class GetControllerVersionResponse extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetControllerVersionResponseOptions, + options: GetControllerVersionResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - // The payload consists of a zero-terminated string and a uint8 for the controller type - this.libraryVersion = cpp2js(this.payload.toString("ascii")); - this.controllerType = this.payload[this.libraryVersion.length + 1]; - } else { - this.controllerType = options.controllerType; - this.libraryVersion = options.libraryVersion; - } + this.controllerType = options.controllerType; + this.libraryVersion = options.libraryVersion; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetControllerVersionResponse { + // The payload consists of a zero-terminated string and a uint8 for the controller type + const libraryVersion = cpp2js(raw.payload.toString("ascii")); + const controllerType: ZWaveLibraryTypes = + raw.payload[libraryVersion.length + 1]; + + return new this({ + libraryVersion, + controllerType, + }); } public controllerType: ZWaveLibraryTypes; diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts index e16ed603837c..5424ecfb865b 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetLongRangeNodesMessages.ts @@ -9,16 +9,20 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; -export interface GetLongRangeNodesRequestOptions extends MessageBaseOptions { +function getFirstNodeId(segmentNumber: number): number { + return 256 + NUM_LR_NODES_PER_SEGMENT * segmentNumber; +} + +export interface GetLongRangeNodesRequestOptions { segmentNumber: number; } @@ -27,17 +31,22 @@ export interface GetLongRangeNodesRequestOptions extends MessageBaseOptions { @priority(MessagePriority.Controller) export class GetLongRangeNodesRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetLongRangeNodesRequestOptions, + options: GetLongRangeNodesRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.segmentNumber = this.payload[0]; - } else { - this.segmentNumber = options.segmentNumber; - } + this.segmentNumber = options.segmentNumber; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetLongRangeNodesRequest { + const segmentNumber = raw.payload[0]; + + return new this({ + segmentNumber, + }); } public segmentNumber: number; @@ -48,7 +57,7 @@ export class GetLongRangeNodesRequest extends Message { } } -export interface GetLongRangeNodesResponseOptions extends MessageBaseOptions { +export interface GetLongRangeNodesResponseOptions { moreNodes: boolean; segmentNumber: number; nodeIds: number[]; @@ -57,36 +66,44 @@ export interface GetLongRangeNodesResponseOptions extends MessageBaseOptions { @messageTypes(MessageType.Response, FunctionType.GetLongRangeNodes) export class GetLongRangeNodesResponse extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetLongRangeNodesResponseOptions, + options: GetLongRangeNodesResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.moreNodes = this.payload[0] != 0; - this.segmentNumber = this.payload[1]; - const listLength = this.payload[2]; - - const listStart = 3; - const listEnd = listStart + listLength; - if (listEnd <= this.payload.length) { - const nodeBitMask = this.payload.subarray( - listStart, - listEnd, - ); - this.nodeIds = parseLongRangeNodeBitMask( - nodeBitMask, - this.listStartNode(), - ); - } else { - this.nodeIds = []; - } + this.moreNodes = options.moreNodes; + this.segmentNumber = options.segmentNumber; + this.nodeIds = options.nodeIds; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetLongRangeNodesResponse { + const moreNodes: boolean = raw.payload[0] != 0; + const segmentNumber = raw.payload[1]; + const listLength = raw.payload[2]; + + const listStart = 3; + const listEnd = listStart + listLength; + let nodeIds: number[]; + if (listEnd <= raw.payload.length) { + const nodeBitMask = raw.payload.subarray( + listStart, + listEnd, + ); + nodeIds = parseLongRangeNodeBitMask( + nodeBitMask, + getFirstNodeId(segmentNumber), + ); } else { - this.moreNodes = options.moreNodes; - this.segmentNumber = options.segmentNumber; - this.nodeIds = options.nodeIds; + nodeIds = []; } + + return new this({ + moreNodes, + segmentNumber, + nodeIds, + }); } public moreNodes: boolean; @@ -104,14 +121,10 @@ export class GetLongRangeNodesResponse extends Message { const nodeBitMask = encodeLongRangeNodeBitMask( this.nodeIds, - this.listStartNode(), + getFirstNodeId(this.segmentNumber), ); nodeBitMask.copy(this.payload, 3); return super.serialize(ctx); } - - private listStartNode(): number { - return 256 + NUM_LR_NODES_PER_SEGMENT * this.segmentNumber; - } } diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetProtocolVersionMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetProtocolVersionMessages.ts index dbb5c5a54d3f..0a15618c40bc 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetProtocolVersionMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetProtocolVersionMessages.ts @@ -3,7 +3,9 @@ import { MessagePriority } from "@zwave-js/core"; import { FunctionType, Message, - type MessageDeserializationOptions, + type MessageBaseOptions, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, @@ -15,28 +17,58 @@ import { @expectedResponse(FunctionType.GetProtocolVersion) export class GetProtocolVersionRequest extends Message {} +export interface GetProtocolVersionResponseOptions { + protocolType: ProtocolType; + protocolVersion: string; + applicationFrameworkBuildNumber?: number; + gitCommitHash?: string; +} + @messageTypes(MessageType.Response, FunctionType.GetProtocolVersion) export class GetProtocolVersionResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: GetProtocolVersionResponseOptions & MessageBaseOptions, ) { super(options); - this.protocolType = this.payload[0]; - this.protocolVersion = [ - this.payload[1], - this.payload[2], - this.payload[3], + + // TODO: Check implementation: + this.protocolType = options.protocolType; + this.protocolVersion = options.protocolVersion; + this.applicationFrameworkBuildNumber = + options.applicationFrameworkBuildNumber; + this.gitCommitHash = options.gitCommitHash; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetProtocolVersionResponse { + const protocolType: ProtocolType = raw.payload[0]; + const protocolVersion = [ + raw.payload[1], + raw.payload[2], + raw.payload[3], ].join("."); - if (this.payload.length >= 6) { - const appBuild = this.payload.readUInt16BE(4); - if (appBuild !== 0) this.applicationFrameworkBuildNumber = appBuild; + let applicationFrameworkBuildNumber: number | undefined; + if (raw.payload.length >= 6) { + const appBuild = raw.payload.readUInt16BE(4); + if (appBuild !== 0) applicationFrameworkBuildNumber = appBuild; } - if (this.payload.length >= 22) { - const commitHash = this.payload.subarray(6, 22); + + let gitCommitHash: string | undefined; + if (raw.payload.length >= 22) { + const commitHash = raw.payload.subarray(6, 22); if (!commitHash.every((b) => b === 0)) { - this.gitCommitHash = commitHash.toString("hex"); + gitCommitHash = commitHash.toString("hex"); } } + + return new this({ + protocolType, + protocolVersion, + applicationFrameworkBuildNumber, + gitCommitHash, + }); } public readonly protocolType: ProtocolType; diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts index 7cfca7b6d858..45f749ec072c 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiCapabilitiesMessages.ts @@ -3,11 +3,11 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -20,9 +20,7 @@ const NUM_FUNCTION_BYTES = NUM_FUNCTIONS / 8; @priority(MessagePriority.Controller) export class GetSerialApiCapabilitiesRequest extends Message {} -export interface GetSerialApiCapabilitiesResponseOptions - extends MessageBaseOptions -{ +export interface GetSerialApiCapabilitiesResponseOptions { firmwareVersion: string; manufacturerId: number; productType: number; @@ -33,31 +31,43 @@ export interface GetSerialApiCapabilitiesResponseOptions @messageTypes(MessageType.Response, FunctionType.GetSerialApiCapabilities) export class GetSerialApiCapabilitiesResponse extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetSerialApiCapabilitiesResponseOptions, + options: GetSerialApiCapabilitiesResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - // The first 8 bytes are the api version, manufacturer id, product type and product id - this.firmwareVersion = `${this.payload[0]}.${this.payload[1]}`; - this.manufacturerId = this.payload.readUInt16BE(2); - this.productType = this.payload.readUInt16BE(4); - this.productId = this.payload.readUInt16BE(6); - // then a 256bit bitmask for the supported command classes follows - const functionBitMask = this.payload.subarray( - 8, - 8 + NUM_FUNCTION_BYTES, - ); - this.supportedFunctionTypes = parseBitMask(functionBitMask); - } else { - this.firmwareVersion = options.firmwareVersion; - this.manufacturerId = options.manufacturerId; - this.productType = options.productType; - this.productId = options.productId; - this.supportedFunctionTypes = options.supportedFunctionTypes; - } + this.firmwareVersion = options.firmwareVersion; + this.manufacturerId = options.manufacturerId; + this.productType = options.productType; + this.productId = options.productId; + this.supportedFunctionTypes = options.supportedFunctionTypes; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetSerialApiCapabilitiesResponse { + // The first 8 bytes are the api version, manufacturer id, product type and product id + const firmwareVersion = `${raw.payload[0]}.${raw.payload[1]}`; + const manufacturerId = raw.payload.readUInt16BE(2); + const productType = raw.payload.readUInt16BE(4); + const productId = raw.payload.readUInt16BE(6); + + // then a 256bit bitmask for the supported command classes follows + const functionBitMask = raw.payload.subarray( + 8, + 8 + NUM_FUNCTION_BYTES, + ); + const supportedFunctionTypes: FunctionType[] = parseBitMask( + functionBitMask, + ); + + return new this({ + firmwareVersion, + manufacturerId, + productType, + productId, + supportedFunctionTypes, + }); } public firmwareVersion: string; diff --git a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts index aa57539c5d26..0b17a7eda7a5 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/GetSerialApiInitDataMessages.ts @@ -16,11 +16,11 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -31,78 +31,93 @@ import type { ZWaveApiVersion } from "../_Types"; @priority(MessagePriority.Controller) export class GetSerialApiInitDataRequest extends Message {} +// eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface GetSerialApiInitDataResponseOptions - extends MessageBaseOptions, SerialApiInitData + extends SerialApiInitData {} @messageTypes(MessageType.Response, FunctionType.GetSerialApiInitData) export class GetSerialApiInitDataResponse extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetSerialApiInitDataResponseOptions, + options: GetSerialApiInitDataResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - const apiVersion = this.payload[0]; - if (apiVersion < 10) { - this.zwaveApiVersion = { - kind: "legacy", - version: apiVersion, - }; - } else { - // this module uses the officially specified Host API - this.zwaveApiVersion = { - kind: "official", - version: apiVersion - 9, - }; - } + this.zwaveApiVersion = options.zwaveApiVersion; + this.isPrimary = options.isPrimary; + this.nodeType = options.nodeType; + this.supportsTimers = options.supportsTimers; + this.isSIS = options.isSIS; + this.nodeIds = options.nodeIds; + this.zwaveChipType = options.zwaveChipType; + } - const capabilities = this.payload[1]; - // The new "official" Host API specs incorrectly switched the meaning of some flags - // Apparently this was never intended, and the firmware correctly uses the "old" encoding. - // https://community.silabs.com/s/question/0D58Y00009qjEghSAE/bug-in-firmware-7191-get-init-data-response-does-not-match-host-api-specification?language=en_US - this.nodeType = capabilities & 0b0001 - ? NodeType["End Node"] - : NodeType.Controller; - this.supportsTimers = !!(capabilities & 0b0010); - this.isPrimary = !(capabilities & 0b0100); - this.isSIS = !!(capabilities & 0b1000); - - let offset = 2; - this.nodeIds = []; - if (this.payload.length > offset) { - const nodeListLength = this.payload[offset]; - // Controller Nodes MUST set this field to 29 - if ( - nodeListLength === NUM_NODEMASK_BYTES - && this.payload.length >= offset + 1 + nodeListLength - ) { - const nodeBitMask = this.payload.subarray( - offset + 1, - offset + 1 + nodeListLength, - ); - this.nodeIds = parseNodeBitMask(nodeBitMask); - } - offset += 1 + nodeListLength; - } + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetSerialApiInitDataResponse { + const apiVersion = raw.payload[0]; + let zwaveApiVersion: ZWaveApiVersion; + + if (apiVersion < 10) { + zwaveApiVersion = { + kind: "legacy", + version: apiVersion, + }; + } else { + // this module uses the officially specified Host API + zwaveApiVersion = { + kind: "official", + version: apiVersion - 9, + }; + } - // these might not be present: - const chipType = this.payload[offset]; - const chipVersion = this.payload[offset + 1]; - if (chipType != undefined && chipVersion != undefined) { - this.zwaveChipType = getZWaveChipType(chipType, chipVersion); + const capabilities = raw.payload[1]; + // The new "official" Host API specs incorrectly switched the meaning of some flags + // Apparently this was never intended, and the firmware correctly uses the "old" encoding. + // https://community.silabs.com/s/question/0D58Y00009qjEghSAE/bug-in-firmware-7191-get-init-data-response-does-not-match-host-api-specification?language=en_US + const nodeType: NodeType = capabilities & 0b0001 + ? NodeType["End Node"] + : NodeType.Controller; + const supportsTimers = !!(capabilities & 0b0010); + const isPrimary = !(capabilities & 0b0100); + const isSIS = !!(capabilities & 0b1000); + let offset = 2; + let nodeIds: number[] = []; + if (raw.payload.length > offset) { + const nodeListLength = raw.payload[offset]; + // Controller Nodes MUST set this field to 29 + if ( + nodeListLength === NUM_NODEMASK_BYTES + && raw.payload.length >= offset + 1 + nodeListLength + ) { + const nodeBitMask = raw.payload.subarray( + offset + 1, + offset + 1 + nodeListLength, + ); + nodeIds = parseNodeBitMask(nodeBitMask); } - } else { - this.zwaveApiVersion = options.zwaveApiVersion; - this.isPrimary = options.isPrimary; - this.nodeType = options.nodeType; - this.supportsTimers = options.supportsTimers; - this.isSIS = options.isSIS; - this.nodeIds = options.nodeIds; - this.zwaveChipType = options.zwaveChipType; + offset += 1 + nodeListLength; } + + // these might not be present: + const chipType = raw.payload[offset]; + const chipVersion = raw.payload[offset + 1]; + let zwaveChipType: string | UnknownZWaveChipType | undefined; + + if (chipType != undefined && chipVersion != undefined) { + zwaveChipType = getZWaveChipType(chipType, chipVersion); + } + + return new this({ + zwaveApiVersion, + nodeType, + supportsTimers, + isPrimary, + isSIS, + nodeIds, + zwaveChipType, + }); } public zwaveApiVersion: ZWaveApiVersion; diff --git a/packages/zwave-js/src/lib/serialapi/capability/HardResetRequest.ts b/packages/zwave-js/src/lib/serialapi/capability/HardResetRequest.ts index cf7e121a4bc0..f83825ffb9c7 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/HardResetRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/HardResetRequest.ts @@ -3,13 +3,12 @@ import { MessagePriority } from "@zwave-js/core"; import { FunctionType, Message, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, MessageOrigin, + type MessageParsingContext, + type MessageRaw, MessageType, expectedCallback, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -17,21 +16,15 @@ import { @messageTypes(MessageType.Request, FunctionType.HardReset) @priority(MessagePriority.Controller) export class HardResetRequestBase extends Message { - public constructor(options?: MessageOptions) { - if (gotDeserializationOptions(options)) { - if ( - options.origin === MessageOrigin.Host - && (new.target as any) !== HardResetRequest - ) { - return new HardResetRequest(options); - } else if ( - options.origin !== MessageOrigin.Host - && (new.target as any) !== HardResetCallback - ) { - return new HardResetCallback(options); - } + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): HardResetRequestBase { + if (ctx.origin === MessageOrigin.Host) { + return HardResetRequest.from(raw, ctx); + } else { + return HardResetCallback.from(raw, ctx); } - super(options); } } @@ -54,11 +47,15 @@ export class HardResetRequest extends HardResetRequestBase { } export class HardResetCallback extends HardResetRequestBase { - public constructor( - options: MessageDeserializationOptions, - ) { - super(options); - this.callbackId = this.payload[0]; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): HardResetCallback { + const callbackId = raw.payload[0]; + + return new this({ + callbackId, + }); } public toLogEntry(): MessageOrCCLogEntry { diff --git a/packages/zwave-js/src/lib/serialapi/capability/LongRangeChannelMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/LongRangeChannelMessages.ts index d77d783a9b92..cedd32833176 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/LongRangeChannelMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/LongRangeChannelMessages.ts @@ -9,12 +9,12 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -25,23 +25,51 @@ import { getEnumMemberName } from "@zwave-js/shared"; @priority(MessagePriority.Controller) export class GetLongRangeChannelRequest extends Message {} +export interface GetLongRangeChannelResponseOptions { + channel: + | LongRangeChannel.Unsupported + | LongRangeChannel.A + | LongRangeChannel.B; + supportsAutoChannelSelection: boolean; + autoChannelSelectionActive: boolean; +} + @messageTypes(MessageType.Response, FunctionType.GetLongRangeChannel) @priority(MessagePriority.Controller) export class GetLongRangeChannelResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: GetLongRangeChannelResponseOptions & MessageBaseOptions, ) { super(options); - this.channel = this.payload[0]; - if (this.payload.length >= 2) { - this.supportsAutoChannelSelection = - !!(this.payload[1] & 0b0001_0000); - this.autoChannelSelectionActive = !!(this.payload[1] & 0b0010_0000); + // TODO: Check implementation: + this.channel = options.channel; + this.supportsAutoChannelSelection = + options.supportsAutoChannelSelection; + this.autoChannelSelectionActive = options.autoChannelSelectionActive; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetLongRangeChannelResponse { + const channel: GetLongRangeChannelResponseOptions["channel"] = + raw.payload[0]; + let supportsAutoChannelSelection: boolean; + let autoChannelSelectionActive: boolean; + if (raw.payload.length >= 2) { + supportsAutoChannelSelection = !!(raw.payload[1] & 0b0001_0000); + autoChannelSelectionActive = !!(raw.payload[1] & 0b0010_0000); } else { - this.supportsAutoChannelSelection = false; - this.autoChannelSelectionActive = false; + supportsAutoChannelSelection = false; + autoChannelSelectionActive = false; } + + return new this({ + channel, + supportsAutoChannelSelection, + autoChannelSelectionActive, + }); } public readonly channel: @@ -52,7 +80,7 @@ export class GetLongRangeChannelResponse extends Message { public readonly autoChannelSelectionActive: boolean; } -export interface SetLongRangeChannelRequestOptions extends MessageBaseOptions { +export interface SetLongRangeChannelRequestOptions { channel: LongRangeChannel; } @@ -61,19 +89,22 @@ export interface SetLongRangeChannelRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.SetLongRangeChannel) export class SetLongRangeChannelRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | SetLongRangeChannelRequestOptions, + options: SetLongRangeChannelRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.channel = options.channel; - } + this.channel = options.channel; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetLongRangeChannelRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SetLongRangeChannelRequest({}); } public channel: LongRangeChannel; @@ -93,15 +124,32 @@ export class SetLongRangeChannelRequest extends Message { } } +export interface SetLongRangeChannelResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SetLongRangeChannel) export class SetLongRangeChannelResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetLongRangeChannelResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetLongRangeChannelResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.test.ts b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.test.ts index a35d3570ab53..87f181df0d87 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.test.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.test.ts @@ -8,11 +8,10 @@ test("GetSupportedCommandsResponse with extended bitmask parses correctly (pre-7 "hex", ); - const msg = Message.from({ + const msg = Message.parse( data, - sdkVersion: "7.19.0", - ctx: {} as any, - }); + { sdkVersion: "7.19.0" } as any, + ); t.true(msg instanceof SerialAPISetup_GetSupportedCommandsResponse); const supported = (msg as SerialAPISetup_GetSupportedCommandsResponse) .supportedCommands; @@ -29,11 +28,10 @@ test("GetSupportedCommandsResponse with extended bitmask parses correctly (post- "hex", ); - const msg = Message.from({ + const msg = Message.parse( data, - sdkVersion: "7.19.1", - ctx: {} as any, - }); + { sdkVersion: "7.19.1" } as any, + ); t.true(msg instanceof SerialAPISetup_GetSupportedCommandsResponse); const supported = (msg as SerialAPISetup_GetSupportedCommandsResponse) .supportedCommands; diff --git a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts index 1e048bcc3919..fbc76aef5cc7 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/SerialAPISetupMessages.ts @@ -11,19 +11,18 @@ import { validatePayload, } from "@zwave-js/core"; import type { - DeserializingMessageConstructor, + MessageConstructor, MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -53,12 +52,12 @@ export enum SerialAPISetupCommand { // We need to define the decorators for Requests and Responses separately const { decorator: subCommandRequest, - // lookupConstructor: getSubCommandRequestConstructor, + lookupConstructor: getSubCommandRequestConstructor, lookupValue: getSubCommandForRequest, } = createSimpleReflectionDecorator< SerialAPISetupRequest, [command: SerialAPISetupCommand], - DeserializingMessageConstructor + MessageConstructor >({ name: "subCommandRequest", }); @@ -66,10 +65,11 @@ const { const { decorator: subCommandResponse, lookupConstructor: getSubCommandResponseConstructor, + lookupValue: getSubCommandForResponse, } = createSimpleReflectionDecorator< SerialAPISetupResponse, [command: SerialAPISetupCommand], - DeserializingMessageConstructor + MessageConstructor >({ name: "subCommandResponse", }); @@ -82,20 +82,43 @@ function testResponseForSerialAPISetupRequest( return (sent as SerialAPISetupRequest).command === received.command; } +export interface SerialAPISetupRequestOptions { + command?: SerialAPISetupCommand; +} + @messageTypes(MessageType.Request, FunctionType.SerialAPISetup) @priority(MessagePriority.Controller) @expectedResponse(testResponseForSerialAPISetupRequest) export class SerialAPISetupRequest extends Message { - public constructor(options: MessageOptions = {}) { + public constructor( + options: SerialAPISetupRequestOptions & MessageBaseOptions = {}, + ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.command = getSubCommandForRequest(this)!; + this.command = options.command ?? getSubCommandForRequest(this)!; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SerialAPISetupRequest { + const command: SerialAPISetupCommand = raw.payload[0]; + const payload = raw.payload.subarray(1); + + const CommandConstructor = getSubCommandRequestConstructor( + command, + ); + if (CommandConstructor) { + return CommandConstructor.from( + raw.withPayload(payload), + ctx, + ) as SerialAPISetupRequest; } + + const ret = new SerialAPISetupRequest({ + command, + }); + ret.payload = payload; + return ret; } public command: SerialAPISetupCommand; @@ -123,22 +146,41 @@ export class SerialAPISetupRequest extends Message { } } +export interface SerialAPISetupResponseOptions { + command?: SerialAPISetupCommand; +} + @messageTypes(MessageType.Response, FunctionType.SerialAPISetup) export class SerialAPISetupResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: SerialAPISetupResponseOptions & MessageBaseOptions, ) { super(options); - this.command = this.payload[0]; + this.command = options.command ?? getSubCommandForResponse(this)!; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SerialAPISetupResponse { + const command: SerialAPISetupCommand = raw.payload[0]; + const payload = raw.payload.subarray(1); const CommandConstructor = getSubCommandResponseConstructor( - this.command, + command, ); - if (CommandConstructor && (new.target as any) !== CommandConstructor) { - return new CommandConstructor(options); + if (CommandConstructor) { + return CommandConstructor.from( + raw.withPayload(payload), + ctx, + ) as SerialAPISetupResponse; } - this.payload = this.payload.subarray(1); + const ret = new SerialAPISetupResponse({ + command, + }); + ret.payload = payload; + return ret; } public command: SerialAPISetupCommand; @@ -157,16 +199,33 @@ export class SerialAPISetupResponse extends Message { } } +export interface SerialAPISetup_CommandUnsupportedResponseOptions { + command: SerialAPISetupCommand; +} + @subCommandResponse(0x00) export class SerialAPISetup_CommandUnsupportedResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_CommandUnsupportedResponseOptions + & MessageBaseOptions, ) { super(options); + this.command = options.command; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_CommandUnsupportedResponse { // The payload contains which command is unsupported - this.command = this.payload[0]; + const command: any = raw.payload[0]; + + return new this({ + command, + }); } public toLogEntry(): MessageOrCCLogEntry { @@ -187,11 +246,10 @@ export class SerialAPISetup_CommandUnsupportedResponse @subCommandRequest(SerialAPISetupCommand.GetSupportedCommands) export class SerialAPISetup_GetSupportedCommandsRequest extends SerialAPISetupRequest -{ - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetSupportedCommands; - } +{} + +export interface SerialAPISetup_GetSupportedCommandsResponseOptions { + supportedCommands: SerialAPISetupCommand[]; } @subCommandResponse(SerialAPISetupCommand.GetSupportedCommands) @@ -199,26 +257,35 @@ export class SerialAPISetup_GetSupportedCommandsResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetSupportedCommandsResponseOptions + & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 1); + this.supportedCommands = options.supportedCommands; + } - if (this.payload.length > 1) { + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SerialAPISetup_GetSupportedCommandsResponse { + validatePayload(raw.payload.length >= 1); + let supportedCommands: SerialAPISetupCommand[]; + if (raw.payload.length > 1) { // This module supports the extended bitmask to report the supported serial API setup commands // Parse it as a bitmask - this.supportedCommands = parseBitMask( - this.payload.subarray(1), + supportedCommands = parseBitMask( + raw.payload.subarray(1), // According to the Host API specification, the first bit (bit 0) should be GetSupportedCommands // However, in Z-Wave SDK < 7.19.1, the entire bitmask is shifted by 1 bit and // GetSupportedCommands is encoded in the second bit (bit 1) - sdkVersionLt(options.sdkVersion, "7.19.1") + sdkVersionLt(ctx.sdkVersion, "7.19.1") ? SerialAPISetupCommand.Unsupported : SerialAPISetupCommand.GetSupportedCommands, ); } else { // This module only uses the single byte power-of-2 bitmask. Decode it manually - this.supportedCommands = []; + supportedCommands = []; for ( const cmd of [ SerialAPISetupCommand.GetSupportedCommands, @@ -231,20 +298,25 @@ export class SerialAPISetup_GetSupportedCommandsResponse SerialAPISetupCommand.SetNodeIDType, ] as const ) { - if (!!(this.payload[0] & cmd)) this.supportedCommands.push(cmd); + if (!!(raw.payload[0] & cmd)) supportedCommands.push(cmd); } } + // Apparently GetSupportedCommands is not always included in the bitmask, although we // just received a response to the command if ( - !this.supportedCommands.includes( + !supportedCommands.includes( SerialAPISetupCommand.GetSupportedCommands, ) ) { - this.supportedCommands.unshift( + supportedCommands.unshift( SerialAPISetupCommand.GetSupportedCommands, ); } + + return new this({ + supportedCommands, + }); } public readonly supportedCommands: SerialAPISetupCommand[]; @@ -252,9 +324,7 @@ export class SerialAPISetup_GetSupportedCommandsResponse // ============================================================================= -export interface SerialAPISetup_SetTXStatusReportOptions - extends MessageBaseOptions -{ +export interface SerialAPISetup_SetTXStatusReportOptions { enabled: boolean; } @@ -263,21 +333,21 @@ export class SerialAPISetup_SetTXStatusReportRequest extends SerialAPISetupRequest { public constructor( - options: - | MessageDeserializationOptions - | SerialAPISetup_SetTXStatusReportOptions, + options: SerialAPISetup_SetTXStatusReportOptions & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.SetTxStatusReport; + this.enabled = options.enabled; + } - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.enabled = options.enabled; - } + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetTXStatusReportRequest { + const enabled = raw.payload[0] === 0xff; + + return new this({ + enabled, + }); } public enabled: boolean; @@ -297,16 +367,33 @@ export class SerialAPISetup_SetTXStatusReportRequest } } +export interface SerialAPISetup_SetTXStatusReportResponseOptions { + success: boolean; +} + @subCommandResponse(SerialAPISetupCommand.SetTxStatusReport) export class SerialAPISetup_SetTXStatusReportResponse extends SerialAPISetupResponse implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_SetTXStatusReportResponseOptions + & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetTXStatusReportResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { @@ -326,30 +413,29 @@ export class SerialAPISetup_SetTXStatusReportResponse // ============================================================================= -export interface SerialAPISetup_SetNodeIDTypeOptions - extends MessageBaseOptions -{ +export interface SerialAPISetup_SetNodeIDTypeOptions { nodeIdType: NodeIDType; } @subCommandRequest(SerialAPISetupCommand.SetNodeIDType) export class SerialAPISetup_SetNodeIDTypeRequest extends SerialAPISetupRequest { public constructor( - options: - | MessageDeserializationOptions - | SerialAPISetup_SetNodeIDTypeOptions, + options: SerialAPISetup_SetNodeIDTypeOptions & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.SetNodeIDType; + this.nodeIdType = options.nodeIdType; + } - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.nodeIdType = options.nodeIdType; - } + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetNodeIDTypeRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SerialAPISetup_SetNodeIDTypeRequest({}); } public nodeIdType: NodeIDType; @@ -371,15 +457,32 @@ export class SerialAPISetup_SetNodeIDTypeRequest extends SerialAPISetupRequest { } } +export interface SerialAPISetup_SetNodeIDTypeResponseOptions { + success: boolean; +} + @subCommandResponse(SerialAPISetupCommand.SetNodeIDType) export class SerialAPISetup_SetNodeIDTypeResponse extends SerialAPISetupResponse implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_SetNodeIDTypeResponseOptions + & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetNodeIDTypeResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { @@ -400,20 +503,30 @@ export class SerialAPISetup_SetNodeIDTypeResponse extends SerialAPISetupResponse // ============================================================================= @subCommandRequest(SerialAPISetupCommand.GetRFRegion) -export class SerialAPISetup_GetRFRegionRequest extends SerialAPISetupRequest { - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetRFRegion; - } +export class SerialAPISetup_GetRFRegionRequest extends SerialAPISetupRequest {} + +export interface SerialAPISetup_GetRFRegionResponseOptions { + region: RFRegion; } @subCommandResponse(SerialAPISetupCommand.GetRFRegion) export class SerialAPISetup_GetRFRegionResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: SerialAPISetup_GetRFRegionResponseOptions & MessageBaseOptions, ) { super(options); - this.region = this.payload[0]; + this.region = options.region; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetRFRegionResponse { + const region: RFRegion = raw.payload[0]; + + return new this({ + region, + }); } public readonly region: RFRegion; @@ -429,28 +542,28 @@ export class SerialAPISetup_GetRFRegionResponse extends SerialAPISetupResponse { // ============================================================================= -export interface SerialAPISetup_SetRFRegionOptions extends MessageBaseOptions { +export interface SerialAPISetup_SetRFRegionOptions { region: RFRegion; } @subCommandRequest(SerialAPISetupCommand.SetRFRegion) export class SerialAPISetup_SetRFRegionRequest extends SerialAPISetupRequest { public constructor( - options: - | MessageDeserializationOptions - | SerialAPISetup_SetRFRegionOptions, + options: SerialAPISetup_SetRFRegionOptions & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.SetRFRegion; + this.region = options.region; + } - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.region = options.region; - } + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetRFRegionRequest { + const region: RFRegion = raw.payload[0]; + + return new this({ + region, + }); } public region: RFRegion; @@ -469,15 +582,30 @@ export class SerialAPISetup_SetRFRegionRequest extends SerialAPISetupRequest { } } +export interface SerialAPISetup_SetRFRegionResponseOptions { + success: boolean; +} + @subCommandResponse(SerialAPISetupCommand.SetRFRegion) export class SerialAPISetup_SetRFRegionResponse extends SerialAPISetupResponse implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SerialAPISetup_SetRFRegionResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetRFRegionResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { @@ -498,11 +626,13 @@ export class SerialAPISetup_SetRFRegionResponse extends SerialAPISetupResponse // ============================================================================= @subCommandRequest(SerialAPISetupCommand.GetPowerlevel) -export class SerialAPISetup_GetPowerlevelRequest extends SerialAPISetupRequest { - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetPowerlevel; - } +export class SerialAPISetup_GetPowerlevelRequest + extends SerialAPISetupRequest +{} + +export interface SerialAPISetup_GetPowerlevelResponseOptions { + powerlevel: number; + measured0dBm: number; } @subCommandResponse(SerialAPISetupCommand.GetPowerlevel) @@ -510,13 +640,28 @@ export class SerialAPISetup_GetPowerlevelResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetPowerlevelResponseOptions + & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 2); + this.powerlevel = options.powerlevel; + this.measured0dBm = options.measured0dBm; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetPowerlevelResponse { + validatePayload(raw.payload.length >= 2); // The values are in 0.1 dBm, signed - this.powerlevel = this.payload.readInt8(0) / 10; - this.measured0dBm = this.payload.readInt8(1) / 10; + const powerlevel = raw.payload.readInt8(0) / 10; + const measured0dBm = raw.payload.readInt8(1) / 10; + + return new this({ + powerlevel, + measured0dBm, + }); } /** The configured normal powerlevel in dBm */ @@ -539,9 +684,7 @@ export class SerialAPISetup_GetPowerlevelResponse // ============================================================================= -export interface SerialAPISetup_SetPowerlevelOptions - extends MessageBaseOptions -{ +export interface SerialAPISetup_SetPowerlevelOptions { powerlevel: number; measured0dBm: number; } @@ -549,34 +692,36 @@ export interface SerialAPISetup_SetPowerlevelOptions @subCommandRequest(SerialAPISetupCommand.SetPowerlevel) export class SerialAPISetup_SetPowerlevelRequest extends SerialAPISetupRequest { public constructor( - options: - | MessageDeserializationOptions - | SerialAPISetup_SetPowerlevelOptions, + options: SerialAPISetup_SetPowerlevelOptions & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.SetPowerlevel; - if (gotDeserializationOptions(options)) { + if (options.powerlevel < -12.8 || options.powerlevel > 12.7) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The normal powerlevel must be between -12.8 and +12.7 dBm`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + if (options.measured0dBm < -12.8 || options.measured0dBm > 12.7) { + throw new ZWaveError( + `The measured output power at 0 dBm must be between -12.8 and +12.7 dBm`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.powerlevel < -12.8 || options.powerlevel > 12.7) { - throw new ZWaveError( - `The normal powerlevel must be between -12.8 and +12.7 dBm`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.measured0dBm < -12.8 || options.measured0dBm > 12.7) { - throw new ZWaveError( - `The measured output power at 0 dBm must be between -12.8 and +12.7 dBm`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.powerlevel = options.powerlevel; - this.measured0dBm = options.measured0dBm; } + this.powerlevel = options.powerlevel; + this.measured0dBm = options.measured0dBm; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetPowerlevelRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SerialAPISetup_SetPowerlevelRequest({}); } public powerlevel: number; @@ -604,15 +749,32 @@ export class SerialAPISetup_SetPowerlevelRequest extends SerialAPISetupRequest { } } +export interface SerialAPISetup_SetPowerlevelResponseOptions { + success: boolean; +} + @subCommandResponse(SerialAPISetupCommand.SetPowerlevel) export class SerialAPISetup_SetPowerlevelResponse extends SerialAPISetupResponse implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_SetPowerlevelResponseOptions + & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetPowerlevelResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { @@ -635,11 +797,11 @@ export class SerialAPISetup_SetPowerlevelResponse extends SerialAPISetupResponse @subCommandRequest(SerialAPISetupCommand.GetPowerlevel16Bit) export class SerialAPISetup_GetPowerlevel16BitRequest extends SerialAPISetupRequest -{ - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetPowerlevel16Bit; - } +{} + +export interface SerialAPISetup_GetPowerlevel16BitResponseOptions { + powerlevel: number; + measured0dBm: number; } @subCommandResponse(SerialAPISetupCommand.GetPowerlevel16Bit) @@ -647,13 +809,28 @@ export class SerialAPISetup_GetPowerlevel16BitResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetPowerlevel16BitResponseOptions + & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 4); + this.powerlevel = options.powerlevel; + this.measured0dBm = options.measured0dBm; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetPowerlevel16BitResponse { + validatePayload(raw.payload.length >= 4); // The values are in 0.1 dBm, signed - this.powerlevel = this.payload.readInt16BE(0) / 10; - this.measured0dBm = this.payload.readInt16BE(2) / 10; + const powerlevel = raw.payload.readInt16BE(0) / 10; + const measured0dBm = raw.payload.readInt16BE(2) / 10; + + return new this({ + powerlevel, + measured0dBm, + }); } /** The configured normal powerlevel in dBm */ @@ -676,9 +853,7 @@ export class SerialAPISetup_GetPowerlevel16BitResponse // ============================================================================= -export interface SerialAPISetup_SetPowerlevel16BitOptions - extends MessageBaseOptions -{ +export interface SerialAPISetup_SetPowerlevel16BitOptions { powerlevel: number; measured0dBm: number; } @@ -688,34 +863,36 @@ export class SerialAPISetup_SetPowerlevel16BitRequest extends SerialAPISetupRequest { public constructor( - options: - | MessageDeserializationOptions - | SerialAPISetup_SetPowerlevel16BitOptions, + options: SerialAPISetup_SetPowerlevel16BitOptions & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.SetPowerlevel16Bit; - if (gotDeserializationOptions(options)) { + if (options.powerlevel < -10 || options.powerlevel > 20) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The normal powerlevel must be between -10.0 and +20.0 dBm`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + if (options.measured0dBm < -10 || options.measured0dBm > 10) { + throw new ZWaveError( + `The measured output power at 0 dBm must be between -10.0 and +10.0 dBm`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.powerlevel < -10 || options.powerlevel > 20) { - throw new ZWaveError( - `The normal powerlevel must be between -10.0 and +20.0 dBm`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.measured0dBm < -10 || options.measured0dBm > 10) { - throw new ZWaveError( - `The measured output power at 0 dBm must be between -10.0 and +10.0 dBm`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.powerlevel = options.powerlevel; - this.measured0dBm = options.measured0dBm; } + this.powerlevel = options.powerlevel; + this.measured0dBm = options.measured0dBm; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetPowerlevel16BitRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SerialAPISetup_SetPowerlevel16BitRequest({}); } public powerlevel: number; @@ -743,16 +920,33 @@ export class SerialAPISetup_SetPowerlevel16BitRequest } } +export interface SerialAPISetup_SetPowerlevel16BitResponseOptions { + success: boolean; +} + @subCommandResponse(SerialAPISetupCommand.SetPowerlevel16Bit) export class SerialAPISetup_SetPowerlevel16BitResponse extends SerialAPISetupResponse implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_SetPowerlevel16BitResponseOptions + & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetPowerlevel16BitResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { @@ -775,11 +969,10 @@ export class SerialAPISetup_SetPowerlevel16BitResponse @subCommandRequest(SerialAPISetupCommand.GetLongRangeMaximumTxPower) export class SerialAPISetup_GetLongRangeMaximumTxPowerRequest extends SerialAPISetupRequest -{ - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetLongRangeMaximumTxPower; - } +{} + +export interface SerialAPISetup_GetLongRangeMaximumTxPowerResponseOptions { + limit: number; } @subCommandResponse(SerialAPISetupCommand.GetLongRangeMaximumTxPower) @@ -787,12 +980,25 @@ export class SerialAPISetup_GetLongRangeMaximumTxPowerResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetLongRangeMaximumTxPowerResponseOptions + & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 2); + this.limit = options.limit; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetLongRangeMaximumTxPowerResponse { + validatePayload(raw.payload.length >= 2); // The values are in 0.1 dBm, signed - this.limit = this.payload.readInt16BE(0) / 10; + const limit = raw.payload.readInt16BE(0) / 10; + + return new this({ + limit, + }); } /** The maximum LR TX power in dBm */ @@ -812,9 +1018,7 @@ export class SerialAPISetup_GetLongRangeMaximumTxPowerResponse // ============================================================================= -export interface SerialAPISetup_SetLongRangeMaximumTxPowerOptions - extends MessageBaseOptions -{ +export interface SerialAPISetup_SetLongRangeMaximumTxPowerOptions { limit: number; } @@ -824,27 +1028,31 @@ export class SerialAPISetup_SetLongRangeMaximumTxPowerRequest { public constructor( options: - | MessageDeserializationOptions - | SerialAPISetup_SetLongRangeMaximumTxPowerOptions, + & SerialAPISetup_SetLongRangeMaximumTxPowerOptions + & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.SetLongRangeMaximumTxPower; - if (gotDeserializationOptions(options)) { + if (options.limit < -10 || options.limit > 20) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The maximum LR TX power must be between -10.0 and +20.0 dBm`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.limit < -10 || options.limit > 20) { - throw new ZWaveError( - `The maximum LR TX power must be between -10.0 and +20.0 dBm`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.limit = options.limit; } + + this.limit = options.limit; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetLongRangeMaximumTxPowerRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SerialAPISetup_SetLongRangeMaximumTxPowerRequest({}); } /** The maximum LR TX power in dBm */ @@ -870,16 +1078,33 @@ export class SerialAPISetup_SetLongRangeMaximumTxPowerRequest } } +export interface SerialAPISetup_SetLongRangeMaximumTxPowerResponseOptions { + success: boolean; +} + @subCommandResponse(SerialAPISetupCommand.SetLongRangeMaximumTxPower) export class SerialAPISetup_SetLongRangeMaximumTxPowerResponse extends SerialAPISetupResponse implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_SetLongRangeMaximumTxPowerResponseOptions + & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_SetLongRangeMaximumTxPowerResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { @@ -902,11 +1127,10 @@ export class SerialAPISetup_SetLongRangeMaximumTxPowerResponse @subCommandRequest(SerialAPISetupCommand.GetMaximumPayloadSize) export class SerialAPISetup_GetMaximumPayloadSizeRequest extends SerialAPISetupRequest -{ - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetMaximumPayloadSize; - } +{} + +export interface SerialAPISetup_GetMaximumPayloadSizeResponseOptions { + maxPayloadSize: number; } @subCommandResponse(SerialAPISetupCommand.GetMaximumPayloadSize) @@ -914,10 +1138,23 @@ export class SerialAPISetup_GetMaximumPayloadSizeResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetMaximumPayloadSizeResponseOptions + & MessageBaseOptions, ) { super(options); - this.maxPayloadSize = this.payload[0]; + this.maxPayloadSize = options.maxPayloadSize; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetMaximumPayloadSizeResponse { + const maxPayloadSize = raw.payload[0]; + + return new this({ + maxPayloadSize, + }); } public readonly maxPayloadSize: number; @@ -936,11 +1173,10 @@ export class SerialAPISetup_GetMaximumPayloadSizeResponse @subCommandRequest(SerialAPISetupCommand.GetLongRangeMaximumPayloadSize) export class SerialAPISetup_GetLongRangeMaximumPayloadSizeRequest extends SerialAPISetupRequest -{ - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetLongRangeMaximumPayloadSize; - } +{} + +export interface SerialAPISetup_GetLongRangeMaximumPayloadSizeResponseOptions { + maxPayloadSize: number; } @subCommandResponse(SerialAPISetupCommand.GetLongRangeMaximumPayloadSize) @@ -948,10 +1184,23 @@ export class SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetLongRangeMaximumPayloadSizeResponseOptions + & MessageBaseOptions, ) { super(options); - this.maxPayloadSize = this.payload[0]; + this.maxPayloadSize = options.maxPayloadSize; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse { + const maxPayloadSize = raw.payload[0]; + + return new this({ + maxPayloadSize, + }); } public readonly maxPayloadSize: number; @@ -970,11 +1219,10 @@ export class SerialAPISetup_GetLongRangeMaximumPayloadSizeResponse @subCommandRequest(SerialAPISetupCommand.GetSupportedRegions) export class SerialAPISetup_GetSupportedRegionsRequest extends SerialAPISetupRequest -{ - public constructor(options?: MessageOptions) { - super(options); - this.command = SerialAPISetupCommand.GetSupportedRegions; - } +{} + +export interface SerialAPISetup_GetSupportedRegionsResponseOptions { + supportedRegions: RFRegion[]; } @subCommandResponse(SerialAPISetupCommand.GetSupportedRegions) @@ -982,15 +1230,28 @@ export class SerialAPISetup_GetSupportedRegionsResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetSupportedRegionsResponseOptions + & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 1); + this.supportedRegions = options.supportedRegions; + } - const numRegions = this.payload[0]; - validatePayload(numRegions > 0, this.payload.length >= 1 + numRegions); + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetSupportedRegionsResponse { + validatePayload(raw.payload.length >= 1); + const numRegions = raw.payload[0]; + validatePayload(numRegions > 0, raw.payload.length >= 1 + numRegions); + const supportedRegions: RFRegion[] = [ + ...raw.payload.subarray(1, 1 + numRegions), + ]; - this.supportedRegions = [...this.payload.subarray(1, 1 + numRegions)]; + return new this({ + supportedRegions, + }); } public readonly supportedRegions: RFRegion[]; @@ -998,30 +1259,28 @@ export class SerialAPISetup_GetSupportedRegionsResponse // ============================================================================= -export interface SerialAPISetup_GetRegionInfoOptions - extends MessageBaseOptions -{ +export interface SerialAPISetup_GetRegionInfoOptions { region: RFRegion; } @subCommandRequest(SerialAPISetupCommand.GetRegionInfo) export class SerialAPISetup_GetRegionInfoRequest extends SerialAPISetupRequest { public constructor( - options: - | MessageDeserializationOptions - | SerialAPISetup_GetRegionInfoOptions, + options: SerialAPISetup_GetRegionInfoOptions & MessageBaseOptions, ) { super(options); - this.command = SerialAPISetupCommand.GetRegionInfo; + this.region = options.region; + } - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.region = options.region; - } + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetRegionInfoRequest { + const region: RFRegion = raw.payload[0]; + + return new this({ + region, + }); } public region: RFRegion; @@ -1043,23 +1302,50 @@ export class SerialAPISetup_GetRegionInfoRequest extends SerialAPISetupRequest { } } +export interface SerialAPISetup_GetRegionInfoResponseOptions { + region: RFRegion; + supportsZWave: boolean; + supportsLongRange: boolean; + includesRegion?: RFRegion; +} + @subCommandResponse(SerialAPISetupCommand.GetRegionInfo) export class SerialAPISetup_GetRegionInfoResponse extends SerialAPISetupResponse { public constructor( - options: MessageDeserializationOptions, + options: + & SerialAPISetup_GetRegionInfoResponseOptions + & MessageBaseOptions, ) { super(options); - this.region = this.payload[0]; - this.supportsZWave = !!(this.payload[1] & 0b1); - this.supportsLongRange = !!(this.payload[1] & 0b10); - if (this.payload.length > 2) { - this.includesRegion = this.payload[2]; - if (this.includesRegion === RFRegion.Unknown) { - this.includesRegion = undefined; + this.region = options.region; + this.supportsZWave = options.supportsZWave; + this.supportsLongRange = options.supportsLongRange; + this.includesRegion = options.includesRegion; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SerialAPISetup_GetRegionInfoResponse { + const region: RFRegion = raw.payload[0]; + const supportsZWave = !!(raw.payload[1] & 0b1); + const supportsLongRange = !!(raw.payload[1] & 0b10); + let includesRegion: RFRegion | undefined; + if (raw.payload.length > 2) { + includesRegion = raw.payload[2]; + if (includesRegion === RFRegion.Unknown) { + includesRegion = undefined; } } + + return new this({ + region, + supportsZWave, + supportsLongRange, + includesRegion, + }); } public readonly region: RFRegion; diff --git a/packages/zwave-js/src/lib/serialapi/capability/SetLongRangeShadowNodeIDsRequest.ts b/packages/zwave-js/src/lib/serialapi/capability/SetLongRangeShadowNodeIDsRequest.ts index 079df6cfc39b..54e14ce968ac 100644 --- a/packages/zwave-js/src/lib/serialapi/capability/SetLongRangeShadowNodeIDsRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/capability/SetLongRangeShadowNodeIDsRequest.ts @@ -3,17 +3,15 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; -export interface LongRangeShadowNodeIDsRequestOptions - extends MessageBaseOptions -{ +export interface LongRangeShadowNodeIDsRequestOptions { shadowNodeIds: number[]; } @@ -24,21 +22,26 @@ const NUM_LONG_RANGE_SHADOW_NODE_IDS = 4; @priority(MessagePriority.Controller) export class SetLongRangeShadowNodeIDsRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | LongRangeShadowNodeIDsRequestOptions, + options: LongRangeShadowNodeIDsRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.shadowNodeIds = parseBitMask( - this.payload.subarray(0, 1), - LONG_RANGE_SHADOW_NODE_IDS_START, - NUM_LONG_RANGE_SHADOW_NODE_IDS, - ); - } else { - this.shadowNodeIds = options.shadowNodeIds; - } + this.shadowNodeIds = options.shadowNodeIds; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetLongRangeShadowNodeIDsRequest { + const shadowNodeIds = parseBitMask( + raw.payload.subarray(0, 1), + LONG_RANGE_SHADOW_NODE_IDS_START, + NUM_LONG_RANGE_SHADOW_NODE_IDS, + ); + + return new this({ + shadowNodeIds, + }); } public shadowNodeIds: number[]; diff --git a/packages/zwave-js/src/lib/serialapi/memory/GetControllerIdMessages.ts b/packages/zwave-js/src/lib/serialapi/memory/GetControllerIdMessages.ts index 75126f6c84f7..a6a6968bfc17 100644 --- a/packages/zwave-js/src/lib/serialapi/memory/GetControllerIdMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/memory/GetControllerIdMessages.ts @@ -4,11 +4,11 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -19,7 +19,7 @@ import { num2hex } from "@zwave-js/shared"; @priority(MessagePriority.Controller) export class GetControllerIdRequest extends Message {} -export interface GetControllerIdResponseOptions extends MessageBaseOptions { +export interface GetControllerIdResponseOptions { homeId: number; ownNodeId: number; } @@ -27,22 +27,29 @@ export interface GetControllerIdResponseOptions extends MessageBaseOptions { @messageTypes(MessageType.Response, FunctionType.GetControllerId) export class GetControllerIdResponse extends Message { public constructor( - options: MessageDeserializationOptions | GetControllerIdResponseOptions, + options: GetControllerIdResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - // The payload is 4 bytes home id, followed by the controller node id - this.homeId = this.payload.readUInt32BE(0); - const { nodeId } = parseNodeID( - this.payload, - options.ctx.nodeIdType, - 4, - ); - this.ownNodeId = nodeId; - } else { - this.homeId = options.homeId; - this.ownNodeId = options.ownNodeId; - } + this.homeId = options.homeId; + this.ownNodeId = options.ownNodeId; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): GetControllerIdResponse { + // The payload is 4 bytes home id, followed by the controller node id + const homeId = raw.payload.readUInt32BE(0); + const { nodeId: ownNodeId } = parseNodeID( + raw.payload, + ctx.nodeIdType, + 4, + ); + + return new this({ + homeId, + ownNodeId, + }); } public homeId: number; diff --git a/packages/zwave-js/src/lib/serialapi/misc/GetBackgroundRSSIMessages.ts b/packages/zwave-js/src/lib/serialapi/misc/GetBackgroundRSSIMessages.ts index 62cc328bc90b..5d7770fc06de 100644 --- a/packages/zwave-js/src/lib/serialapi/misc/GetBackgroundRSSIMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/misc/GetBackgroundRSSIMessages.ts @@ -8,7 +8,9 @@ import { import { FunctionType, Message, - type MessageDeserializationOptions, + type MessageBaseOptions, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, @@ -21,16 +23,42 @@ import { parseRSSI, tryParseRSSI } from "../transport/SendDataShared"; @expectedResponse(FunctionType.GetBackgroundRSSI) export class GetBackgroundRSSIRequest extends Message {} +export interface GetBackgroundRSSIResponseOptions { + rssiChannel0: number; + rssiChannel1: number; + rssiChannel2?: number; + rssiChannel3?: number; +} + @messageTypes(MessageType.Response, FunctionType.GetBackgroundRSSI) export class GetBackgroundRSSIResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: GetBackgroundRSSIResponseOptions & MessageBaseOptions, ) { super(options); - this.rssiChannel0 = parseRSSI(this.payload, 0); - this.rssiChannel1 = parseRSSI(this.payload, 1); - this.rssiChannel2 = tryParseRSSI(this.payload, 2); - this.rssiChannel3 = tryParseRSSI(this.payload, 3); + + // TODO: Check implementation: + this.rssiChannel0 = options.rssiChannel0; + this.rssiChannel1 = options.rssiChannel1; + this.rssiChannel2 = options.rssiChannel2; + this.rssiChannel3 = options.rssiChannel3; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetBackgroundRSSIResponse { + const rssiChannel0 = parseRSSI(raw.payload, 0); + const rssiChannel1 = parseRSSI(raw.payload, 1); + const rssiChannel2 = tryParseRSSI(raw.payload, 2); + const rssiChannel3 = tryParseRSSI(raw.payload, 3); + + return new this({ + rssiChannel0, + rssiChannel1, + rssiChannel2, + rssiChannel3, + }); } public readonly rssiChannel0: RSSI; diff --git a/packages/zwave-js/src/lib/serialapi/misc/SetRFReceiveModeMessages.ts b/packages/zwave-js/src/lib/serialapi/misc/SetRFReceiveModeMessages.ts index 502f3b30a1c1..a33f499a6f8b 100644 --- a/packages/zwave-js/src/lib/serialapi/misc/SetRFReceiveModeMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/misc/SetRFReceiveModeMessages.ts @@ -6,21 +6,21 @@ import { } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; -export interface SetRFReceiveModeRequestOptions extends MessageBaseOptions { +export interface SetRFReceiveModeRequestOptions { /** Whether the stick should receive (true) or not (false) */ enabled: boolean; } @@ -30,17 +30,22 @@ export interface SetRFReceiveModeRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.SetRFReceiveMode) export class SetRFReceiveModeRequest extends Message { public constructor( - options: MessageDeserializationOptions | SetRFReceiveModeRequestOptions, + options: SetRFReceiveModeRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.enabled = options.enabled; - } + this.enabled = options.enabled; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetRFReceiveModeRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SetRFReceiveModeRequest({}); } public enabled: boolean; @@ -61,15 +66,32 @@ export class SetRFReceiveModeRequest extends Message { } } +export interface SetRFReceiveModeResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SetRFReceiveMode) export class SetRFReceiveModeResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetRFReceiveModeResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetRFReceiveModeResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/misc/SetSerialApiTimeoutsMessages.ts b/packages/zwave-js/src/lib/serialapi/misc/SetSerialApiTimeoutsMessages.ts index 97ca50aaae70..5130568affdb 100644 --- a/packages/zwave-js/src/lib/serialapi/misc/SetSerialApiTimeoutsMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/misc/SetSerialApiTimeoutsMessages.ts @@ -3,15 +3,16 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, priority, } from "@zwave-js/serial"; -interface SetSerialApiTimeoutsRequestOptions extends MessageBaseOptions { +export interface SetSerialApiTimeoutsRequestOptions { ackTimeout: number; byteTimeout: number; } @@ -21,7 +22,7 @@ interface SetSerialApiTimeoutsRequestOptions extends MessageBaseOptions { @priority(MessagePriority.Controller) export class SetSerialApiTimeoutsRequest extends Message { public constructor( - options: SetSerialApiTimeoutsRequestOptions, + options: SetSerialApiTimeoutsRequestOptions & MessageBaseOptions, ) { super(options); this.ackTimeout = options.ackTimeout; @@ -40,23 +41,36 @@ export class SetSerialApiTimeoutsRequest extends Message { } } +export interface SetSerialApiTimeoutsResponseOptions { + oldAckTimeout: number; + oldByteTimeout: number; +} + @messageTypes(MessageType.Response, FunctionType.SetSerialApiTimeouts) export class SetSerialApiTimeoutsResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: SetSerialApiTimeoutsResponseOptions & MessageBaseOptions, ) { super(options); - this._oldAckTimeout = this.payload[0] * 10; - this._oldByteTimeout = this.payload[1] * 10; - } - private _oldAckTimeout: number; - public get oldAckTimeout(): number { - return this._oldAckTimeout; + // TODO: Check implementation: + this.oldAckTimeout = options.oldAckTimeout; + this.oldByteTimeout = options.oldByteTimeout; } - private _oldByteTimeout: number; - public get oldByteTimeout(): number { - return this._oldByteTimeout; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetSerialApiTimeoutsResponse { + const oldAckTimeout = raw.payload[0] * 10; + const oldByteTimeout = raw.payload[1] * 10; + + return new this({ + oldAckTimeout, + oldByteTimeout, + }); } + + public oldAckTimeout: number; + public oldByteTimeout: number; } diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts index 59609fb46c88..f98550b6316b 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AddNodeToNetworkRequest.ts @@ -14,17 +14,16 @@ import { import type { GetAllNodes } from "@zwave-js/host"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -58,13 +57,13 @@ enum AddNodeFlags { ProtocolLongRange = 0x20, } -interface AddNodeToNetworkRequestOptions extends MessageBaseOptions { +export interface AddNodeToNetworkRequestOptions { addNodeType?: AddNodeType; highPower?: boolean; networkWide?: boolean; } -interface AddNodeDSKToNetworkRequestOptions extends MessageBaseOptions { +export interface AddNodeDSKToNetworkRequestOptions { nwiHomeId: Buffer; authHomeId: Buffer; highPower?: boolean; @@ -94,14 +93,11 @@ export function computeNeighborDiscoveryTimeout( // no expected response, the controller will respond with multiple AddNodeToNetworkRequests @priority(MessagePriority.Controller) export class AddNodeToNetworkRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== AddNodeToNetworkRequestStatusReport - ) { - return new AddNodeToNetworkRequestStatusReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): AddNodeToNetworkRequestBase { + return AddNodeToNetworkRequestStatusReport.from(raw, ctx); } } @@ -135,7 +131,7 @@ function testCallbackForAddNodeRequest( @expectedCallback(testCallbackForAddNodeRequest) export class AddNodeToNetworkRequest extends AddNodeToNetworkRequestBase { public constructor( - options: AddNodeToNetworkRequestOptions = {}, + options: AddNodeToNetworkRequestOptions & MessageBaseOptions, ) { super(options); @@ -210,7 +206,7 @@ export class EnableSmartStartListenRequest extends AddNodeToNetworkRequestBase { export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { public constructor( - options: AddNodeDSKToNetworkRequestOptions, + options: AddNodeDSKToNetworkRequestOptions & MessageBaseOptions, ) { super(options); @@ -270,17 +266,36 @@ export class AddNodeDSKToNetworkRequest extends AddNodeToNetworkRequestBase { } } +export interface AddNodeToNetworkRequestStatusReportOptions { + status: AddNodeStatus; + statusContext?: AddNodeStatusContext; +} + export class AddNodeToNetworkRequestStatusReport extends AddNodeToNetworkRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & AddNodeToNetworkRequestStatusReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this.status = this.payload[1]; - switch (this.status) { + + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.status = options.status; + this.statusContext = options.statusContext; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): AddNodeToNetworkRequestStatusReport { + const callbackId = raw.payload[0]; + const status: AddNodeStatus = raw.payload[1]; + let statusContext: AddNodeStatusContext | undefined; + switch (status) { case AddNodeStatus.Ready: case AddNodeStatus.NodeFound: case AddNodeStatus.ProtocolDone: @@ -290,24 +305,30 @@ export class AddNodeToNetworkRequestStatusReport case AddNodeStatus.Done: { const { nodeId } = parseNodeID( - this.payload, - options.ctx.nodeIdType, + raw.payload, + ctx.nodeIdType, 2, ); - this.statusContext = { nodeId }; + statusContext = { nodeId }; break; } case AddNodeStatus.AddingController: case AddNodeStatus.AddingSlave: { // the payload contains a node information frame - this.statusContext = parseNodeUpdatePayload( - this.payload.subarray(2), - options.ctx.nodeIdType, + statusContext = parseNodeUpdatePayload( + raw.payload.subarray(2), + ctx.nodeIdType, ); break; } } + + return new this({ + callbackId, + status, + statusContext, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.ts index 3ad9ff228202..65c912e8576f 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPriorityReturnRouteMessages.ts @@ -13,14 +13,13 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -29,21 +28,15 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.AssignPriorityReturnRoute) @priority(MessagePriority.Normal) export class AssignPriorityReturnRouteRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) - !== AssignPriorityReturnRouteRequestTransmitReport - ) { - return new AssignPriorityReturnRouteRequestTransmitReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): AssignPriorityReturnRouteRequestBase { + return AssignPriorityReturnRouteRequestTransmitReport.from(raw, ctx); } } -export interface AssignPriorityReturnRouteRequestOptions - extends MessageBaseOptions -{ +export interface AssignPriorityReturnRouteRequestOptions { nodeId: number; destinationNodeId: number; repeaters: number[]; @@ -56,38 +49,41 @@ export class AssignPriorityReturnRouteRequest extends AssignPriorityReturnRouteRequestBase { public constructor( - options: - | MessageDeserializationOptions - | AssignPriorityReturnRouteRequestOptions, + options: AssignPriorityReturnRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if (options.nodeId === options.destinationNodeId) { + throw new ZWaveError( + `The source and destination node must not be identical`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + if ( + options.repeaters.length > MAX_REPEATERS + || options.repeaters.some((id) => id < 1 || id > MAX_NODES) + ) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The repeaters array must contain at most ${MAX_REPEATERS} node IDs between 1 and ${MAX_NODES}`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.nodeId === options.destinationNodeId) { - throw new ZWaveError( - `The source and destination node must not be identical`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - if ( - options.repeaters.length > MAX_REPEATERS - || options.repeaters.some((id) => id < 1 || id > MAX_NODES) - ) { - throw new ZWaveError( - `The repeaters array must contain at most ${MAX_REPEATERS} node IDs between 1 and ${MAX_NODES}`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.nodeId = options.nodeId; - this.destinationNodeId = options.destinationNodeId; - this.repeaters = options.repeaters; - this.routeSpeed = options.routeSpeed; } + + this.nodeId = options.nodeId; + this.destinationNodeId = options.destinationNodeId; + this.repeaters = options.repeaters; + this.routeSpeed = options.routeSpeed; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignPriorityReturnRouteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new AssignPriorityReturnRouteRequest({}); } public nodeId: number; @@ -137,15 +133,32 @@ export class AssignPriorityReturnRouteRequest } } +export interface AssignPriorityReturnRouteResponseOptions { + hasStarted: boolean; +} + @messageTypes(MessageType.Response, FunctionType.AssignPriorityReturnRoute) export class AssignPriorityReturnRouteResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: AssignPriorityReturnRouteResponseOptions & MessageBaseOptions, ) { super(options); - this.hasStarted = this.payload[0] !== 0; + + // TODO: Check implementation: + this.hasStarted = options.hasStarted; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignPriorityReturnRouteResponse { + const hasStarted = raw.payload[0] !== 0; + + return new this({ + hasStarted, + }); } public isOK(): boolean { @@ -162,12 +175,18 @@ export class AssignPriorityReturnRouteResponse extends Message } } +export interface AssignPriorityReturnRouteRequestTransmitReportOptions { + transmitStatus: TransmitStatus; +} + export class AssignPriorityReturnRouteRequestTransmitReport extends AssignPriorityReturnRouteRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & AssignPriorityReturnRouteRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); @@ -175,6 +194,19 @@ export class AssignPriorityReturnRouteRequestTransmitReport this.transmitStatus = this.payload[1]; } + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignPriorityReturnRouteRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); + } + public isOK(): boolean { // The other statuses are technically "not OK", but they are caused by // not being able to contact the node. We don't want the node to be marked diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.ts index 4fee6d7b593e..db68ca15362e 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignPrioritySUCReturnRouteMessages.ts @@ -13,14 +13,13 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -29,23 +28,15 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.AssignPrioritySUCReturnRoute) @priority(MessagePriority.Normal) export class AssignPrioritySUCReturnRouteRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) - !== AssignPrioritySUCReturnRouteRequestTransmitReport - ) { - return new AssignPrioritySUCReturnRouteRequestTransmitReport( - options, - ); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): AssignPrioritySUCReturnRouteRequestBase { + return AssignPrioritySUCReturnRouteRequestTransmitReport.from(raw, ctx); } } -export interface AssignPrioritySUCReturnRouteRequestOptions - extends MessageBaseOptions -{ +export interface AssignPrioritySUCReturnRouteRequestOptions { nodeId: number; repeaters: number[]; routeSpeed: ZWaveDataRate; @@ -58,30 +49,35 @@ export class AssignPrioritySUCReturnRouteRequest { public constructor( options: - | MessageDeserializationOptions - | AssignPrioritySUCReturnRouteRequestOptions, + & AssignPrioritySUCReturnRouteRequestOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if ( + options.repeaters.length > MAX_REPEATERS + || options.repeaters.some((id) => id < 1 || id > MAX_NODES) + ) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The repeaters array must contain at most ${MAX_REPEATERS} node IDs between 1 and ${MAX_NODES}`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if ( - options.repeaters.length > MAX_REPEATERS - || options.repeaters.some((id) => id < 1 || id > MAX_NODES) - ) { - throw new ZWaveError( - `The repeaters array must contain at most ${MAX_REPEATERS} node IDs between 1 and ${MAX_NODES}`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.nodeId = options.nodeId; - this.repeaters = options.repeaters; - this.routeSpeed = options.routeSpeed; } + + this.nodeId = options.nodeId; + this.repeaters = options.repeaters; + this.routeSpeed = options.routeSpeed; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignPrioritySUCReturnRouteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new AssignPrioritySUCReturnRouteRequest({}); } public nodeId: number; @@ -124,15 +120,34 @@ export class AssignPrioritySUCReturnRouteRequest } } +export interface AssignPrioritySUCReturnRouteResponseOptions { + hasStarted: boolean; +} + @messageTypes(MessageType.Response, FunctionType.AssignPrioritySUCReturnRoute) export class AssignPrioritySUCReturnRouteResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & AssignPrioritySUCReturnRouteResponseOptions + & MessageBaseOptions, ) { super(options); - this.hasStarted = this.payload[0] !== 0; + + // TODO: Check implementation: + this.hasStarted = options.hasStarted; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignPrioritySUCReturnRouteResponse { + const hasStarted = raw.payload[0] !== 0; + + return new this({ + hasStarted, + }); } public isOK(): boolean { @@ -149,17 +164,37 @@ export class AssignPrioritySUCReturnRouteResponse extends Message } } +export interface AssignPrioritySUCReturnRouteRequestTransmitReportOptions { + transmitStatus: TransmitStatus; +} + export class AssignPrioritySUCReturnRouteRequestTransmitReport extends AssignPrioritySUCReturnRouteRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & AssignPrioritySUCReturnRouteRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignPrioritySUCReturnRouteRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } public isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignReturnRouteMessages.ts index 256326a5250c..244a239f3f63 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignReturnRouteMessages.ts @@ -9,18 +9,17 @@ import { import type { INodeQuery, MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -29,18 +28,15 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.AssignReturnRoute) @priority(MessagePriority.Normal) export class AssignReturnRouteRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== AssignReturnRouteRequestTransmitReport - ) { - return new AssignReturnRouteRequestTransmitReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): AssignReturnRouteRequestBase { + return AssignReturnRouteRequestTransmitReport.from(raw, ctx); } } -export interface AssignReturnRouteRequestOptions extends MessageBaseOptions { +export interface AssignReturnRouteRequestOptions { nodeId: number; destinationNodeId: number; } @@ -51,26 +47,29 @@ export class AssignReturnRouteRequest extends AssignReturnRouteRequestBase implements INodeQuery { public constructor( - options: - | MessageDeserializationOptions - | AssignReturnRouteRequestOptions, + options: AssignReturnRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if (options.nodeId === options.destinationNodeId) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + `The source and destination node must not be identical`, + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.nodeId === options.destinationNodeId) { - throw new ZWaveError( - `The source and destination node must not be identical`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.nodeId = options.nodeId; - this.destinationNodeId = options.destinationNodeId; } + this.nodeId = options.nodeId; + this.destinationNodeId = options.destinationNodeId; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignReturnRouteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new AssignReturnRouteRequest({}); } public nodeId: number; @@ -94,15 +93,32 @@ export class AssignReturnRouteRequest extends AssignReturnRouteRequestBase } } +export interface AssignReturnRouteResponseOptions { + hasStarted: boolean; +} + @messageTypes(MessageType.Response, FunctionType.AssignReturnRoute) export class AssignReturnRouteResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: AssignReturnRouteResponseOptions & MessageBaseOptions, ) { super(options); - this.hasStarted = this.payload[0] !== 0; + + // TODO: Check implementation: + this.hasStarted = options.hasStarted; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignReturnRouteResponse { + const hasStarted = raw.payload[0] !== 0; + + return new this({ + hasStarted, + }); } public isOK(): boolean { @@ -119,17 +135,37 @@ export class AssignReturnRouteResponse extends Message } } +export interface AssignReturnRouteRequestTransmitReportOptions { + transmitStatus: TransmitStatus; +} + export class AssignReturnRouteRequestTransmitReport extends AssignReturnRouteRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & AssignReturnRouteRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignReturnRouteRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } public isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts index 7b3ceda16802..8234779d2807 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/AssignSUCReturnRouteMessages.ts @@ -9,15 +9,14 @@ import { type INodeQuery, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, MessageOrigin, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -26,27 +25,19 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.AssignSUCReturnRoute) @priority(MessagePriority.Normal) export class AssignSUCReturnRouteRequestBase extends Message { - public constructor(options: MessageOptions) { - if (gotDeserializationOptions(options)) { - if ( - options.origin === MessageOrigin.Host - && (new.target as any) !== AssignSUCReturnRouteRequest - ) { - return new AssignSUCReturnRouteRequest(options); - } else if ( - options.origin !== MessageOrigin.Host - && (new.target as any) - !== AssignSUCReturnRouteRequestTransmitReport - ) { - return new AssignSUCReturnRouteRequestTransmitReport(options); - } + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): AssignSUCReturnRouteRequestBase { + if (ctx.origin === MessageOrigin.Host) { + return AssignSUCReturnRouteRequest.from(raw, ctx); + } else { + return AssignSUCReturnRouteRequestTransmitReport.from(raw, ctx); } - - super(options); } } -export interface AssignSUCReturnRouteRequestOptions extends MessageBaseOptions { +export interface AssignSUCReturnRouteRequestOptions { nodeId: number; disableCallbackFunctionTypeCheck?: boolean; } @@ -68,19 +59,25 @@ export class AssignSUCReturnRouteRequest extends AssignSUCReturnRouteRequestBase implements INodeQuery { public constructor( - options: - | MessageDeserializationOptions - | AssignSUCReturnRouteRequestOptions, + options: AssignSUCReturnRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.nodeId = this.payload[0]; - this.callbackId = this.payload[1]; - } else { - this.nodeId = options.nodeId; - this.disableCallbackFunctionTypeCheck = - options.disableCallbackFunctionTypeCheck; - } + this.nodeId = options.nodeId; + this.disableCallbackFunctionTypeCheck = + options.disableCallbackFunctionTypeCheck; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignSUCReturnRouteRequest { + const nodeId = raw.payload[0]; + const callbackId = raw.payload[1]; + + return new this({ + nodeId, + callbackId, + }); } public nodeId: number; @@ -95,7 +92,7 @@ export class AssignSUCReturnRouteRequest extends AssignSUCReturnRouteRequestBase } } -interface AssignSUCReturnRouteResponseOptions extends MessageBaseOptions { +export interface AssignSUCReturnRouteResponseOptions { wasExecuted: boolean; } @@ -104,16 +101,21 @@ export class AssignSUCReturnRouteResponse extends Message implements SuccessIndicator { public constructor( - options: - | MessageDeserializationOptions - | AssignSUCReturnRouteResponseOptions, + options: AssignSUCReturnRouteResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.wasExecuted = this.payload[0] !== 0; - } else { - this.wasExecuted = options.wasExecuted; - } + this.wasExecuted = options.wasExecuted; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignSUCReturnRouteResponse { + const wasExecuted = raw.payload[0] !== 0; + + return new this({ + wasExecuted, + }); } public isOK(): boolean { @@ -135,11 +137,8 @@ export class AssignSUCReturnRouteResponse extends Message } } -interface AssignSUCReturnRouteRequestTransmitReportOptions - extends MessageBaseOptions -{ +export interface AssignSUCReturnRouteRequestTransmitReportOptions { transmitStatus: TransmitStatus; - callbackId: number; } export class AssignSUCReturnRouteRequestTransmitReport @@ -148,18 +147,26 @@ export class AssignSUCReturnRouteRequestTransmitReport { public constructor( options: - | MessageDeserializationOptions - | AssignSUCReturnRouteRequestTransmitReportOptions, + & AssignSUCReturnRouteRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; - } else { - this.callbackId = options.callbackId; - this.transmitStatus = options.transmitStatus; - } + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): AssignSUCReturnRouteRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } public isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.ts index 0dc86ed5a5dd..e4047757fe47 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteReturnRouteMessages.ts @@ -9,18 +9,17 @@ import { import type { INodeQuery, MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -29,18 +28,15 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.DeleteReturnRoute) @priority(MessagePriority.Normal) export class DeleteReturnRouteRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== DeleteReturnRouteRequestTransmitReport - ) { - return new DeleteReturnRouteRequestTransmitReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): DeleteReturnRouteRequestBase { + return DeleteReturnRouteRequestTransmitReport.from(raw, ctx); } } -export interface DeleteReturnRouteRequestOptions extends MessageBaseOptions { +export interface DeleteReturnRouteRequestOptions { nodeId: number; } @@ -50,19 +46,22 @@ export class DeleteReturnRouteRequest extends DeleteReturnRouteRequestBase implements INodeQuery { public constructor( - options: - | MessageDeserializationOptions - | DeleteReturnRouteRequestOptions, + options: DeleteReturnRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.nodeId = options.nodeId; - } + this.nodeId = options.nodeId; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): DeleteReturnRouteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new DeleteReturnRouteRequest({}); } public nodeId: number; @@ -76,15 +75,32 @@ export class DeleteReturnRouteRequest extends DeleteReturnRouteRequestBase } } +export interface DeleteReturnRouteResponseOptions { + hasStarted: boolean; +} + @messageTypes(MessageType.Response, FunctionType.DeleteReturnRoute) export class DeleteReturnRouteResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: DeleteReturnRouteResponseOptions & MessageBaseOptions, ) { super(options); - this.hasStarted = this.payload[0] !== 0; + + // TODO: Check implementation: + this.hasStarted = options.hasStarted; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): DeleteReturnRouteResponse { + const hasStarted = raw.payload[0] !== 0; + + return new this({ + hasStarted, + }); } public isOK(): boolean { @@ -101,17 +117,37 @@ export class DeleteReturnRouteResponse extends Message } } +export interface DeleteReturnRouteRequestTransmitReportOptions { + transmitStatus: TransmitStatus; +} + export class DeleteReturnRouteRequestTransmitReport extends DeleteReturnRouteRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & DeleteReturnRouteRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): DeleteReturnRouteRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } public isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts index 10ed44691215..4f6dff91b8a8 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/DeleteSUCReturnRouteMessages.ts @@ -7,19 +7,18 @@ import { import type { INodeQuery, MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageOrigin, MessageType, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -28,27 +27,19 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.DeleteSUCReturnRoute) @priority(MessagePriority.Normal) export class DeleteSUCReturnRouteRequestBase extends Message { - public constructor(options: MessageOptions) { - if (gotDeserializationOptions(options)) { - if ( - options.origin === MessageOrigin.Host - && (new.target as any) !== DeleteSUCReturnRouteRequest - ) { - return new DeleteSUCReturnRouteRequest(options); - } else if ( - options.origin !== MessageOrigin.Host - && (new.target as any) - !== DeleteSUCReturnRouteRequestTransmitReport - ) { - return new DeleteSUCReturnRouteRequestTransmitReport(options); - } + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): DeleteSUCReturnRouteRequestBase { + if (ctx.origin === MessageOrigin.Host) { + return DeleteSUCReturnRouteRequest.from(raw, ctx); + } else { + return DeleteSUCReturnRouteRequestTransmitReport.from(raw, ctx); } - - super(options); } } -export interface DeleteSUCReturnRouteRequestOptions extends MessageBaseOptions { +export interface DeleteSUCReturnRouteRequestOptions { nodeId: number; disableCallbackFunctionTypeCheck?: boolean; } @@ -70,19 +61,25 @@ export class DeleteSUCReturnRouteRequest extends DeleteSUCReturnRouteRequestBase implements INodeQuery { public constructor( - options: - | MessageDeserializationOptions - | DeleteSUCReturnRouteRequestOptions, + options: DeleteSUCReturnRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.nodeId = this.payload[0]; - this.callbackId = this.payload[1]; - } else { - this.nodeId = options.nodeId; - this.disableCallbackFunctionTypeCheck = - options.disableCallbackFunctionTypeCheck; - } + this.nodeId = options.nodeId; + this.disableCallbackFunctionTypeCheck = + options.disableCallbackFunctionTypeCheck; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): DeleteSUCReturnRouteRequest { + const nodeId = raw.payload[0]; + const callbackId = raw.payload[1]; + + return new this({ + nodeId, + callbackId, + }); } public nodeId: number; @@ -97,7 +94,7 @@ export class DeleteSUCReturnRouteRequest extends DeleteSUCReturnRouteRequestBase } } -interface DeleteSUCReturnRouteResponseOptions extends MessageBaseOptions { +export interface DeleteSUCReturnRouteResponseOptions { wasExecuted: boolean; } @@ -106,16 +103,21 @@ export class DeleteSUCReturnRouteResponse extends Message implements SuccessIndicator { public constructor( - options: - | MessageDeserializationOptions - | DeleteSUCReturnRouteResponseOptions, + options: DeleteSUCReturnRouteResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.wasExecuted = this.payload[0] !== 0; - } else { - this.wasExecuted = options.wasExecuted; - } + this.wasExecuted = options.wasExecuted; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): DeleteSUCReturnRouteResponse { + const wasExecuted = raw.payload[0] !== 0; + + return new this({ + wasExecuted, + }); } public isOK(): boolean { @@ -137,11 +139,8 @@ export class DeleteSUCReturnRouteResponse extends Message } } -interface DeleteSUCReturnRouteRequestTransmitReportOptions - extends MessageBaseOptions -{ +export interface DeleteSUCReturnRouteRequestTransmitReportOptions { transmitStatus: TransmitStatus; - callbackId: number; } export class DeleteSUCReturnRouteRequestTransmitReport @@ -150,18 +149,26 @@ export class DeleteSUCReturnRouteRequestTransmitReport { public constructor( options: - | MessageDeserializationOptions - | DeleteSUCReturnRouteRequestTransmitReportOptions, + & DeleteSUCReturnRouteRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; - } else { - this.callbackId = options.callbackId; - this.transmitStatus = options.transmitStatus; - } + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): DeleteSUCReturnRouteRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } public isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts index fccd3b49b5ad..82280bbad8f9 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetNodeProtocolInfoMessages.ts @@ -16,17 +16,17 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; import { isObject } from "alcalzone-shared/typeguards"; -interface GetNodeProtocolInfoRequestOptions extends MessageBaseOptions { +export interface GetNodeProtocolInfoRequestOptions { requestedNodeId: number; } @@ -35,17 +35,22 @@ interface GetNodeProtocolInfoRequestOptions extends MessageBaseOptions { @priority(MessagePriority.Controller) export class GetNodeProtocolInfoRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetNodeProtocolInfoRequestOptions, + options: GetNodeProtocolInfoRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.requestedNodeId = - parseNodeID(this.payload, options.ctx.nodeIdType, 0).nodeId; - } else { - this.requestedNodeId = options.requestedNodeId; - } + this.requestedNodeId = options.requestedNodeId; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): GetNodeProtocolInfoRequest { + const requestedNodeId = + parseNodeID(raw.payload, ctx.nodeIdType, 0).nodeId; + + return new this({ + requestedNodeId, + }); } // This must not be called nodeId or the message will be treated as a node query @@ -58,66 +63,86 @@ export class GetNodeProtocolInfoRequest extends Message { } } -interface GetNodeProtocolInfoResponseOptions - extends MessageBaseOptions, NodeProtocolInfoAndDeviceClass +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface GetNodeProtocolInfoResponseOptions + extends NodeProtocolInfoAndDeviceClass {} @messageTypes(MessageType.Response, FunctionType.GetNodeProtocolInfo) export class GetNodeProtocolInfoResponse extends Message { public constructor( - options: - | MessageDeserializationOptions - | GetNodeProtocolInfoResponseOptions, + options: GetNodeProtocolInfoResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - // The context should contain the node ID the protocol info was requested for. - // We use it here to determine whether the node is long range. - let isLongRange = false; - if ( - isObject(options.context) - && "nodeId" in options.context - && typeof options.context.nodeId === "number" - ) { - isLongRange = isLongRangeNodeId(options.context.nodeId); - } - - const { hasSpecificDeviceClass, ...rest } = parseNodeProtocolInfo( - this.payload, - 0, - isLongRange, - ); - this.isListening = rest.isListening; - this.isFrequentListening = rest.isFrequentListening; - this.isRouting = rest.isRouting; - this.supportedDataRates = rest.supportedDataRates; - this.protocolVersion = rest.protocolVersion; - this.optionalFunctionality = rest.optionalFunctionality; - this.nodeType = rest.nodeType; - this.supportsSecurity = rest.supportsSecurity; - this.supportsBeaming = rest.supportsBeaming; - - // parse the device class - this.basicDeviceClass = this.payload[3]; - this.genericDeviceClass = this.payload[4]; - this.specificDeviceClass = hasSpecificDeviceClass - ? this.payload[5] - : 0x00; - } else { - 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 = 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 = options.basicDeviceClass; + this.genericDeviceClass = options.genericDeviceClass; + this.specificDeviceClass = options.specificDeviceClass; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): GetNodeProtocolInfoResponse { + // The context should contain the node ID the protocol info was requested for. + // We use it here to determine whether the node is long range. + let isLongRange = false; + const requestContext = ctx.requestStorage?.get( + FunctionType.GetNodeProtocolInfo, + ); + if ( + isObject(requestContext) + && "nodeId" in requestContext + && typeof requestContext.nodeId === "number" + ) { + isLongRange = isLongRangeNodeId(requestContext.nodeId); } + + const { hasSpecificDeviceClass, ...rest } = parseNodeProtocolInfo( + raw.payload, + 0, + isLongRange, + ); + const isListening: boolean = rest.isListening; + const isFrequentListening: FLiRS = rest.isFrequentListening; + const isRouting: boolean = rest.isRouting; + const supportedDataRates: DataRate[] = rest.supportedDataRates; + const protocolVersion: ProtocolVersion = rest.protocolVersion; + const optionalFunctionality: boolean = rest.optionalFunctionality; + const nodeType: NodeType = rest.nodeType; + const supportsSecurity: boolean = rest.supportsSecurity; + const supportsBeaming: boolean = rest.supportsBeaming; + + // parse the device class + const basicDeviceClass: BasicDeviceClass = raw.payload[3]; + const genericDeviceClass = raw.payload[4]; + const specificDeviceClass = hasSpecificDeviceClass + ? raw.payload[5] + : 0x00; + + return new this({ + isListening, + isFrequentListening, + isRouting, + supportedDataRates, + protocolVersion, + optionalFunctionality, + nodeType, + supportsSecurity, + supportsBeaming, + basicDeviceClass, + genericDeviceClass, + specificDeviceClass, + }); } /** Whether this node is always listening or not */ diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetPriorityRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetPriorityRouteMessages.ts index 4558515637cb..22705b08cefe 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetPriorityRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetPriorityRouteMessages.ts @@ -14,17 +14,17 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; import { getEnumMemberName } from "@zwave-js/shared"; -export interface GetPriorityRouteRequestOptions extends MessageBaseOptions { +export interface GetPriorityRouteRequestOptions { destinationNodeId: number; } @@ -33,17 +33,22 @@ export interface GetPriorityRouteRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.GetPriorityRoute) export class GetPriorityRouteRequest extends Message { public constructor( - options: MessageDeserializationOptions | GetPriorityRouteRequestOptions, + options: GetPriorityRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.destinationNodeId = options.destinationNodeId; - } + this.destinationNodeId = options.destinationNodeId; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetPriorityRouteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new GetPriorityRouteRequest({}); } public destinationNodeId: number; @@ -67,27 +72,55 @@ export class GetPriorityRouteRequest extends Message { } } +export interface GetPriorityRouteResponseOptions { + destinationNodeId: number; + routeKind: RouteKind; + repeaters?: number[]; + routeSpeed?: ZWaveDataRate; +} + @messageTypes(MessageType.Response, FunctionType.GetPriorityRoute) export class GetPriorityRouteResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: GetPriorityRouteResponseOptions & MessageBaseOptions, ) { super(options); + + // TODO: Check implementation: + this.destinationNodeId = options.destinationNodeId; + this.routeKind = options.routeKind; + this.repeaters = options.repeaters; + this.routeSpeed = options.routeSpeed; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): GetPriorityRouteResponse { let offset = 0; const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( - this.payload, - options.ctx.nodeIdType, + raw.payload, + ctx.nodeIdType, offset, ); offset += nodeIdBytes; - this.destinationNodeId = nodeId; - this.routeKind = this.payload[offset++]; - if (this.routeKind) { - this.repeaters = [ - ...this.payload.subarray(offset, offset + MAX_REPEATERS), + const destinationNodeId = nodeId; + const routeKind: RouteKind = raw.payload[offset++]; + let repeaters: number[] | undefined; + let routeSpeed: ZWaveDataRate | undefined; + if (routeKind) { + repeaters = [ + ...raw.payload.subarray(offset, offset + MAX_REPEATERS), ].filter((id) => id > 0); - this.routeSpeed = this.payload[offset + MAX_REPEATERS]; + routeSpeed = raw.payload[offset + MAX_REPEATERS]; } + + return new this({ + destinationNodeId, + routeKind, + repeaters, + routeSpeed, + }); } public readonly destinationNodeId: number; diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetRoutingInfoMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetRoutingInfoMessages.ts index 8da062d7f431..e2b8b98ea046 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetRoutingInfoMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetRoutingInfoMessages.ts @@ -9,15 +9,16 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, priority, } from "@zwave-js/serial"; -interface GetRoutingInfoRequestOptions extends MessageBaseOptions { +export interface GetRoutingInfoRequestOptions { nodeId: number; removeNonRepeaters?: boolean; removeBadLinks?: boolean; @@ -27,7 +28,9 @@ interface GetRoutingInfoRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.GetRoutingInfo) @priority(MessagePriority.Controller) export class GetRoutingInfoRequest extends Message { - public constructor(options: GetRoutingInfoRequestOptions) { + public constructor( + options: GetRoutingInfoRequestOptions & MessageBaseOptions, + ) { super(options); this.sourceNodeId = options.nodeId; this.removeNonRepeaters = !!options.removeNonRepeaters; @@ -63,30 +66,44 @@ export class GetRoutingInfoRequest extends Message { } } +export interface GetRoutingInfoResponseOptions { + nodeIds: number[]; +} + @messageTypes(MessageType.Response, FunctionType.GetRoutingInfo) export class GetRoutingInfoResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: GetRoutingInfoResponseOptions & MessageBaseOptions, ) { super(options); - if (this.payload.length === NUM_NODEMASK_BYTES) { + // TODO: Check implementation: + this.nodeIds = options.nodeIds; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetRoutingInfoResponse { + let nodeIds: number[]; + if (raw.payload.length === NUM_NODEMASK_BYTES) { // the payload contains a bit mask of all neighbor nodes - this._nodeIds = parseNodeBitMask(this.payload); + nodeIds = parseNodeBitMask(raw.payload); } else { - this._nodeIds = []; + nodeIds = []; } - } - private _nodeIds: number[]; - public get nodeIds(): number[] { - return this._nodeIds; + return new this({ + nodeIds, + }); } + public nodeIds: number[]; + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), - message: { "node ids": `${this._nodeIds.join(", ")}` }, + message: { "node ids": `${this.nodeIds.join(", ")}` }, }; } } diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.ts index 351262ea3f73..5726b1097d6b 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/GetSUCNodeIdMessages.ts @@ -3,11 +3,11 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -17,26 +17,33 @@ import { @priority(MessagePriority.Controller) export class GetSUCNodeIdRequest extends Message {} -export interface GetSUCNodeIdResponseOptions extends MessageBaseOptions { +export interface GetSUCNodeIdResponseOptions { sucNodeId: number; } @messageTypes(MessageType.Response, FunctionType.GetSUCNodeId) export class GetSUCNodeIdResponse extends Message { public constructor( - options: MessageDeserializationOptions | GetSUCNodeIdResponseOptions, + options: GetSUCNodeIdResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.sucNodeId = parseNodeID( - this.payload, - options.ctx.nodeIdType, - 0, - ).nodeId; - } else { - this.sucNodeId = options.sucNodeId; - } + this.sucNodeId = options.sucNodeId; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): GetSUCNodeIdResponse { + const sucNodeId = parseNodeID( + raw.payload, + ctx.nodeIdType, + 0, + ).nodeId; + + return new this({ + sucNodeId, + }); } /** The node id of the SUC or 0 if none is present */ diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/IsFailedNodeMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/IsFailedNodeMessages.ts index ab7f7dc71c53..f7e262908e93 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/IsFailedNodeMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/IsFailedNodeMessages.ts @@ -3,15 +3,16 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, priority, } from "@zwave-js/serial"; -export interface IsFailedNodeRequestOptions extends MessageBaseOptions { +export interface IsFailedNodeRequestOptions { // This must not be called nodeId or rejectAllTransactions may reject the request failedNodeId: number; } @@ -20,7 +21,9 @@ export interface IsFailedNodeRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.IsFailedNode) @priority(MessagePriority.Controller) export class IsFailedNodeRequest extends Message { - public constructor(options: IsFailedNodeRequestOptions) { + public constructor( + options: IsFailedNodeRequestOptions & MessageBaseOptions, + ) { super(options); this.failedNodeId = options.failedNodeId; } @@ -34,13 +37,30 @@ export class IsFailedNodeRequest extends Message { } } +export interface IsFailedNodeResponseOptions { + result: boolean; +} + @messageTypes(MessageType.Response, FunctionType.IsFailedNode) export class IsFailedNodeResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: IsFailedNodeResponseOptions & MessageBaseOptions, ) { super(options); - this.result = !!this.payload[0]; + + // TODO: Check implementation: + this.result = options.result; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): IsFailedNodeResponse { + const result = !!raw.payload[0]; + + return new this({ + result, + }); } public readonly result: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.ts index 172a1abaab10..0c5529a8ea82 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveFailedNodeMessages.ts @@ -1,18 +1,17 @@ import { MessagePriority, encodeNodeID } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -46,18 +45,15 @@ export enum RemoveFailedNodeStatus { @messageTypes(MessageType.Request, FunctionType.RemoveFailedNode) @priority(MessagePriority.Controller) export class RemoveFailedNodeRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== RemoveFailedNodeRequestStatusReport - ) { - return new RemoveFailedNodeRequestStatusReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): RemoveFailedNodeRequestBase { + return RemoveFailedNodeRequestStatusReport.from(raw, ctx); } } -interface RemoveFailedNodeRequestOptions extends MessageBaseOptions { +export interface RemoveFailedNodeRequestOptions { // This must not be called nodeId or rejectAllTransactions may reject the request failedNodeId: number; } @@ -66,7 +62,7 @@ interface RemoveFailedNodeRequestOptions extends MessageBaseOptions { @expectedCallback(FunctionType.RemoveFailedNode) export class RemoveFailedNodeRequest extends RemoveFailedNodeRequestBase { public constructor( - options: RemoveFailedNodeRequestOptions, + options: RemoveFailedNodeRequestOptions & MessageBaseOptions, ) { super(options); this.failedNodeId = options.failedNodeId; @@ -84,46 +80,77 @@ export class RemoveFailedNodeRequest extends RemoveFailedNodeRequestBase { } } +export interface RemoveFailedNodeRequestStatusReportOptions { + removeStatus: RemoveFailedNodeStatus; +} + export class RemoveFailedNodeRequestStatusReport extends RemoveFailedNodeRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & RemoveFailedNodeRequestStatusReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this._removeStatus = this.payload[1]; + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.removeStatus = options.removeStatus; } - private _removeStatus: RemoveFailedNodeStatus; - public get removeStatus(): RemoveFailedNodeStatus { - return this._removeStatus; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): RemoveFailedNodeRequestStatusReport { + const callbackId = raw.payload[0]; + const removeStatus: RemoveFailedNodeStatus = raw.payload[1]; + + return new this({ + callbackId, + removeStatus, + }); } + public removeStatus: RemoveFailedNodeStatus; + public isOK(): boolean { - return this._removeStatus === RemoveFailedNodeStatus.NodeRemoved; + return this.removeStatus === RemoveFailedNodeStatus.NodeRemoved; } } +export interface RemoveFailedNodeResponseOptions { + removeStatus: RemoveFailedNodeStartFlags; +} + @messageTypes(MessageType.Response, FunctionType.RemoveFailedNode) export class RemoveFailedNodeResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: RemoveFailedNodeResponseOptions & MessageBaseOptions, ) { super(options); - this._removeStatus = this.payload[0]; + + // TODO: Check implementation: + this.removeStatus = options.removeStatus; } - private _removeStatus: RemoveFailedNodeStartFlags; - public get removeStatus(): RemoveFailedNodeStartFlags { - return this._removeStatus; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): RemoveFailedNodeResponse { + const removeStatus: RemoveFailedNodeStartFlags = raw.payload[0]; + + return new this({ + removeStatus, + }); } + public removeStatus: RemoveFailedNodeStartFlags; + public isOK(): boolean { - return this._removeStatus === RemoveFailedNodeStartFlags.OK; + return this.removeStatus === RemoveFailedNodeStartFlags.OK; } } diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts index cf62859b1074..dd0640da4833 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/RemoveNodeFromNetworkRequest.ts @@ -5,17 +5,16 @@ import { } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -43,7 +42,7 @@ enum RemoveNodeFlags { NetworkWide = 0x40, } -interface RemoveNodeFromNetworkRequestOptions extends MessageBaseOptions { +export interface RemoveNodeFromNetworkRequestOptions { removeNodeType?: RemoveNodeType; highPower?: boolean; networkWide?: boolean; @@ -53,14 +52,11 @@ interface RemoveNodeFromNetworkRequestOptions extends MessageBaseOptions { // no expected response, the controller will respond with multiple RemoveNodeFromNetworkRequests @priority(MessagePriority.Controller) export class RemoveNodeFromNetworkRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== RemoveNodeFromNetworkRequestStatusReport - ) { - return new RemoveNodeFromNetworkRequestStatusReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): RemoveNodeFromNetworkRequestBase { + return RemoveNodeFromNetworkRequestStatusReport.from(raw, ctx); } } @@ -96,7 +92,7 @@ export class RemoveNodeFromNetworkRequest extends RemoveNodeFromNetworkRequestBase { public constructor( - options: RemoveNodeFromNetworkRequestOptions = {}, + options: RemoveNodeFromNetworkRequestOptions & MessageBaseOptions, ) { super(options); @@ -124,17 +120,36 @@ export class RemoveNodeFromNetworkRequest } } +export interface RemoveNodeFromNetworkRequestStatusReportOptions { + status: RemoveNodeStatus; + statusContext?: RemoveNodeStatusContext; +} + export class RemoveNodeFromNetworkRequestStatusReport extends RemoveNodeFromNetworkRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & RemoveNodeFromNetworkRequestStatusReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this.status = this.payload[1]; - switch (this.status) { + + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.status = options.status; + this.statusContext = options.statusContext; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): RemoveNodeFromNetworkRequestStatusReport { + const callbackId = raw.payload[0]; + const status: RemoveNodeStatus = raw.payload[1]; + let statusContext: RemoveNodeStatusContext | undefined; + switch (status) { case RemoveNodeStatus.Ready: case RemoveNodeStatus.NodeFound: case RemoveNodeStatus.Failed: @@ -150,13 +165,19 @@ export class RemoveNodeFromNetworkRequestStatusReport case RemoveNodeStatus.RemovingSlave: { // the payload contains the node ID const { nodeId } = parseNodeID( - this.payload.subarray(2), - options.ctx.nodeIdType, + raw.payload.subarray(2), + ctx.nodeIdType, ); - this.statusContext = { nodeId }; + statusContext = { nodeId }; break; } } + + return new this({ + callbackId, + status, + statusContext, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.ts index 2514eaafa4f6..eb813f39f5d3 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/ReplaceFailedNodeRequest.ts @@ -1,17 +1,16 @@ import { MessagePriority, encodeNodeID } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -46,18 +45,15 @@ export enum ReplaceFailedNodeStatus { @messageTypes(MessageType.Request, FunctionType.ReplaceFailedNode) @priority(MessagePriority.Controller) export class ReplaceFailedNodeRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== ReplaceFailedNodeRequestStatusReport - ) { - return new ReplaceFailedNodeRequestStatusReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): ReplaceFailedNodeRequestBase { + return ReplaceFailedNodeRequestStatusReport.from(raw, ctx); } } -interface ReplaceFailedNodeRequestOptions extends MessageBaseOptions { +export interface ReplaceFailedNodeRequestOptions { // This must not be called nodeId or rejectAllTransactions may reject the request failedNodeId: number; } @@ -65,7 +61,7 @@ interface ReplaceFailedNodeRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.ReplaceFailedNode) export class ReplaceFailedNodeRequest extends ReplaceFailedNodeRequestBase { public constructor( - options: ReplaceFailedNodeRequestOptions, + options: ReplaceFailedNodeRequestOptions & MessageBaseOptions, ) { super(options); this.failedNodeId = options.failedNodeId; @@ -83,48 +79,79 @@ export class ReplaceFailedNodeRequest extends ReplaceFailedNodeRequestBase { } } +export interface ReplaceFailedNodeResponseOptions { + replaceStatus: ReplaceFailedNodeStartFlags; +} + @messageTypes(MessageType.Response, FunctionType.ReplaceFailedNode) export class ReplaceFailedNodeResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: ReplaceFailedNodeResponseOptions & MessageBaseOptions, ) { super(options); - this._replaceStatus = this.payload[0]; + + // TODO: Check implementation: + this.replaceStatus = options.replaceStatus; } - private _replaceStatus: ReplaceFailedNodeStartFlags; - public get replaceStatus(): ReplaceFailedNodeStartFlags { - return this._replaceStatus; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ReplaceFailedNodeResponse { + const replaceStatus: ReplaceFailedNodeStartFlags = raw.payload[0]; + + return new this({ + replaceStatus, + }); } + public replaceStatus: ReplaceFailedNodeStartFlags; + public isOK(): boolean { - return this._replaceStatus === ReplaceFailedNodeStartFlags.OK; + return this.replaceStatus === ReplaceFailedNodeStartFlags.OK; } } +export interface ReplaceFailedNodeRequestStatusReportOptions { + replaceStatus: ReplaceFailedNodeStatus; +} + export class ReplaceFailedNodeRequestStatusReport extends ReplaceFailedNodeRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: + & ReplaceFailedNodeRequestStatusReportOptions + & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this._replaceStatus = this.payload[1]; + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.replaceStatus = options.replaceStatus; } - private _replaceStatus: ReplaceFailedNodeStatus; - public get replaceStatus(): ReplaceFailedNodeStatus { - return this._replaceStatus; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ReplaceFailedNodeRequestStatusReport { + const callbackId = raw.payload[0]; + const replaceStatus: ReplaceFailedNodeStatus = raw.payload[1]; + + return new this({ + callbackId, + replaceStatus, + }); } + public replaceStatus: ReplaceFailedNodeStatus; + public isOK(): boolean { return ( - this._replaceStatus + this.replaceStatus === ReplaceFailedNodeStatus.FailedNodeReplaceDone ); } diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeInfoMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeInfoMessages.ts index 5a058a6619fe..7e0d976641b6 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeInfoMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeInfoMessages.ts @@ -9,13 +9,13 @@ import { type INodeQuery, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -24,7 +24,7 @@ import { ApplicationUpdateRequestNodeInfoRequestFailed, } from "../application/ApplicationUpdateRequest"; -interface RequestNodeInfoResponseOptions extends MessageBaseOptions { +export interface RequestNodeInfoResponseOptions { wasSent: boolean; } @@ -33,14 +33,21 @@ export class RequestNodeInfoResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions | RequestNodeInfoResponseOptions, + options: RequestNodeInfoResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.wasSent = this.payload[0] !== 0; - } else { - this.wasSent = options.wasSent; - } + this.wasSent = options.wasSent; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): RequestNodeInfoResponse { + const wasSent = raw.payload[0] !== 0; + + return new this({ + wasSent, + }); } public wasSent: boolean; @@ -62,7 +69,7 @@ export class RequestNodeInfoResponse extends Message } } -interface RequestNodeInfoRequestOptions extends MessageBaseOptions { +export interface RequestNodeInfoRequestOptions { nodeId: number; } @@ -83,18 +90,25 @@ function testCallbackForRequestNodeInfoRequest( @priority(MessagePriority.NodeQuery) export class RequestNodeInfoRequest extends Message implements INodeQuery { public constructor( - options: RequestNodeInfoRequestOptions | MessageDeserializationOptions, + options: RequestNodeInfoRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.nodeId = parseNodeID( - this.payload, - options.ctx.nodeIdType, - 0, - ).nodeId; - } else { - this.nodeId = options.nodeId; - } + this.nodeId = options.nodeId; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): RequestNodeInfoRequest { + const nodeId = parseNodeID( + raw.payload, + ctx.nodeIdType, + 0, + ).nodeId; + + return new this({ + nodeId, + }); } public nodeId: number; diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.ts index 93f95cdcb764..7d9217d73d05 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/RequestNodeNeighborUpdateMessages.ts @@ -2,6 +2,8 @@ import type { MessageOrCCLogEntry } from "@zwave-js/core"; import { MessagePriority, encodeNodeID } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, MultiStageCallback, SuccessIndicator, } from "@zwave-js/serial"; @@ -9,11 +11,8 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -25,34 +24,29 @@ export enum NodeNeighborUpdateStatus { UpdateFailed = 0x23, } -export interface RequestNodeNeighborUpdateRequestOptions - extends MessageBaseOptions -{ - nodeId: number; - /** This must be determined with {@link computeNeighborDiscoveryTimeout} */ - discoveryTimeout: number; -} - @messageTypes(MessageType.Request, FunctionType.RequestNodeNeighborUpdate) @priority(MessagePriority.Controller) export class RequestNodeNeighborUpdateRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== RequestNodeNeighborUpdateReport - ) { - return new RequestNodeNeighborUpdateReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): RequestNodeNeighborUpdateRequestBase { + return RequestNodeNeighborUpdateReport.from(raw, ctx); } } +export interface RequestNodeNeighborUpdateRequestOptions { + nodeId: number; + /** This must be determined with {@link computeNeighborDiscoveryTimeout} */ + discoveryTimeout: number; +} + @expectedCallback(FunctionType.RequestNodeNeighborUpdate) export class RequestNodeNeighborUpdateRequest extends RequestNodeNeighborUpdateRequestBase { public constructor( - options: RequestNodeNeighborUpdateRequestOptions, + options: RequestNodeNeighborUpdateRequestOptions & MessageBaseOptions, ) { super(options); this.nodeId = options.nodeId; @@ -83,31 +77,45 @@ export class RequestNodeNeighborUpdateRequest } } +export interface RequestNodeNeighborUpdateReportOptions { + updateStatus: NodeNeighborUpdateStatus; +} + export class RequestNodeNeighborUpdateReport extends RequestNodeNeighborUpdateRequestBase implements SuccessIndicator, MultiStageCallback { public constructor( - options: MessageDeserializationOptions, + options: RequestNodeNeighborUpdateReportOptions & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this._updateStatus = this.payload[1]; + this.callbackId = options.callbackId; + this.updateStatus = options.updateStatus; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): RequestNodeNeighborUpdateReport { + const callbackId = raw.payload[0]; + const updateStatus: NodeNeighborUpdateStatus = raw.payload[1]; + + return new this({ + callbackId, + updateStatus, + }); } isOK(): boolean { - return this._updateStatus !== NodeNeighborUpdateStatus.UpdateFailed; + return this.updateStatus !== NodeNeighborUpdateStatus.UpdateFailed; } isFinal(): boolean { - return this._updateStatus === NodeNeighborUpdateStatus.UpdateDone; + return this.updateStatus === NodeNeighborUpdateStatus.UpdateDone; } - private _updateStatus: NodeNeighborUpdateStatus; - public get updateStatus(): NodeNeighborUpdateStatus { - return this._updateStatus; - } + public updateStatus: NodeNeighborUpdateStatus; public toLogEntry(): MessageOrCCLogEntry { return { @@ -116,7 +124,7 @@ export class RequestNodeNeighborUpdateReport "callback id": this.callbackId ?? "(not set)", "update status": getEnumMemberName( NodeNeighborUpdateStatus, - this._updateStatus, + this.updateStatus, ), }, }; diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/SetLearnModeMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/SetLearnModeMessages.ts index 849c484748cc..78dd6f87e1f4 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/SetLearnModeMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/SetLearnModeMessages.ts @@ -9,13 +9,12 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -47,18 +46,15 @@ export enum LearnModeStatus { @messageTypes(MessageType.Request, FunctionType.SetLearnMode) @priority(MessagePriority.Controller) export class SetLearnModeRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== SetLearnModeCallback - ) { - return new SetLearnModeCallback(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SetLearnModeRequestBase { + return SetLearnModeCallback.from(raw, ctx); } } -export interface SetLearnModeRequestOptions extends MessageBaseOptions { +export interface SetLearnModeRequestOptions { intent: LearnModeIntent; } @@ -66,17 +62,22 @@ export interface SetLearnModeRequestOptions extends MessageBaseOptions { // The callback may come much (30+ seconds), so we wait for it outside of the queue export class SetLearnModeRequest extends SetLearnModeRequestBase { public constructor( - options: MessageDeserializationOptions | SetLearnModeRequestOptions, + options: SetLearnModeRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.intent = options.intent; - } + this.intent = options.intent; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetLearnModeRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SetLearnModeRequest({}); } public intent: LearnModeIntent; @@ -102,13 +103,30 @@ export class SetLearnModeRequest extends SetLearnModeRequestBase { } } +export interface SetLearnModeResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SetLearnMode) export class SetLearnModeResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetLearnModeResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetLearnModeResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } public readonly success: boolean; @@ -125,21 +143,46 @@ export class SetLearnModeResponse extends Message implements SuccessIndicator { } } +export interface SetLearnModeCallbackOptions { + status: LearnModeStatus; + assignedNodeId: number; + statusMessage?: Buffer; +} + export class SetLearnModeCallback extends SetLearnModeRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetLearnModeCallbackOptions & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this.status = this.payload[1]; - this.assignedNodeId = this.payload[2]; - if (this.payload.length > 3) { - const msgLength = this.payload[3]; - this.statusMessage = this.payload.subarray(4, 4 + msgLength); + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.status = options.status; + this.assignedNodeId = options.assignedNodeId; + this.statusMessage = options.statusMessage; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetLearnModeCallback { + const callbackId = raw.payload[0]; + const status: LearnModeStatus = raw.payload[1]; + const assignedNodeId = raw.payload[2]; + let statusMessage: Buffer | undefined; + if (raw.payload.length > 3) { + const msgLength = raw.payload[3]; + statusMessage = raw.payload.subarray(4, 4 + msgLength); } + + return new this({ + callbackId, + status, + assignedNodeId, + statusMessage, + }); } public readonly status: LearnModeStatus; diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/SetPriorityRouteMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/SetPriorityRouteMessages.ts index 555a632091bd..de19e54e8398 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/SetPriorityRouteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/SetPriorityRouteMessages.ts @@ -14,12 +14,12 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -39,39 +39,42 @@ export type SetPriorityRouteRequestOptions = @expectedResponse(FunctionType.SetPriorityRoute) export class SetPriorityRouteRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | (MessageBaseOptions & SetPriorityRouteRequestOptions), + options: SetPriorityRouteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - if (options.repeaters) { - if ( - options.repeaters.length > MAX_REPEATERS - || options.repeaters.some((id) => id < 1 || id > MAX_NODES) - ) { - throw new ZWaveError( - `The repeaters array must contain at most ${MAX_REPEATERS} node IDs between 1 and ${MAX_NODES}`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.routeSpeed == undefined) { - throw new ZWaveError( - `When setting a priority route, repeaters and route speed must be set together`, - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.repeaters = options.repeaters; - this.routeSpeed = options.routeSpeed; + if (options.repeaters) { + if ( + options.repeaters.length > MAX_REPEATERS + || options.repeaters.some((id) => id < 1 || id > MAX_NODES) + ) { + throw new ZWaveError( + `The repeaters array must contain at most ${MAX_REPEATERS} node IDs between 1 and ${MAX_NODES}`, + ZWaveErrorCodes.Argument_Invalid, + ); } - - this.destinationNodeId = options.destinationNodeId; + if (options.routeSpeed == undefined) { + throw new ZWaveError( + `When setting a priority route, repeaters and route speed must be set together`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.repeaters = options.repeaters; + this.routeSpeed = options.routeSpeed; } + + this.destinationNodeId = options.destinationNodeId; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetPriorityRouteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SetPriorityRouteRequest({}); } public destinationNodeId: number; @@ -126,22 +129,38 @@ export class SetPriorityRouteRequest extends Message { } } +export interface SetPriorityRouteResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SetPriorityRoute) export class SetPriorityRouteResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetPriorityRouteResponseOptions & MessageBaseOptions, ) { super(options); + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SetPriorityRouteResponse { // Byte(s) 0/1 are the node ID - this is missing from the Host API specs const { /* nodeId, */ bytesRead } = parseNodeID( - this.payload, - options.ctx.nodeIdType, + raw.payload, + ctx.nodeIdType, 0, ); + const success = raw.payload[bytesRead] !== 0; - this.success = this.payload[bytesRead] !== 0; + return new this({ + success, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.ts b/packages/zwave-js/src/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.ts index e4bb2c02473d..d3e438c52877 100644 --- a/packages/zwave-js/src/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/network-mgmt/SetSUCNodeIDMessages.ts @@ -8,18 +8,17 @@ import { } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -29,7 +28,7 @@ export enum SetSUCNodeIdStatus { Failed = 0x06, } -export interface SetSUCNodeIdRequestOptions extends MessageBaseOptions { +export interface SetSUCNodeIdRequestOptions { ownNodeId: number; sucNodeId: number; enableSUC: boolean; @@ -40,14 +39,11 @@ export interface SetSUCNodeIdRequestOptions extends MessageBaseOptions { @messageTypes(MessageType.Request, FunctionType.SetSUCNodeId) @priority(MessagePriority.Controller) export class SetSUCNodeIdRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== SetSUCNodeIdRequestStatusReport - ) { - return new SetSUCNodeIdRequestStatusReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SetSUCNodeIdRequestBase { + return SetSUCNodeIdRequestStatusReport.from(raw, ctx); } } @@ -55,22 +51,27 @@ export class SetSUCNodeIdRequestBase extends Message { @expectedCallback(FunctionType.SetSUCNodeId) export class SetSUCNodeIdRequest extends SetSUCNodeIdRequestBase { public constructor( - options: MessageDeserializationOptions | SetSUCNodeIdRequestOptions, + options: SetSUCNodeIdRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.sucNodeId = options.sucNodeId; - this.enableSUC = options.enableSUC; - this.enableSIS = options.enableSIS; - this.transmitOptions = options.transmitOptions - ?? TransmitOptions.DEFAULT; - this._ownNodeId = options.ownNodeId; - } + this.sucNodeId = options.sucNodeId; + this.enableSUC = options.enableSUC; + this.enableSIS = options.enableSIS; + this.transmitOptions = options.transmitOptions + ?? TransmitOptions.DEFAULT; + this._ownNodeId = options.ownNodeId; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetSUCNodeIdRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new SetSUCNodeIdRequest({}); } public sucNodeId: number; @@ -102,24 +103,38 @@ export class SetSUCNodeIdRequest extends SetSUCNodeIdRequestBase { } } +export interface SetSUCNodeIdResponseOptions { + wasExecuted: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SetSUCNodeId) export class SetSUCNodeIdResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetSUCNodeIdResponseOptions & MessageBaseOptions, ) { super(options); - this._wasExecuted = this.payload[0] !== 0; + + // TODO: Check implementation: + this.wasExecuted = options.wasExecuted; } - isOK(): boolean { - return this._wasExecuted; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetSUCNodeIdResponse { + const wasExecuted = raw.payload[0] !== 0; + + return new this({ + wasExecuted, + }); } - private _wasExecuted: boolean; - public get wasExecuted(): boolean { - return this._wasExecuted; + isOK(): boolean { + return this.wasExecuted; } + public wasExecuted: boolean; + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), @@ -128,24 +143,39 @@ export class SetSUCNodeIdResponse extends Message implements SuccessIndicator { } } +export interface SetSUCNodeIdRequestStatusReportOptions { + status: SetSUCNodeIdStatus; +} + export class SetSUCNodeIdRequestStatusReport extends SetSUCNodeIdRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SetSUCNodeIdRequestStatusReportOptions & MessageBaseOptions, ) { super(options); - this.callbackId = this.payload[0]; - this._status = this.payload[1]; + // TODO: Check implementation: + this.callbackId = options.callbackId; + this.status = options.status; } - private _status: SetSUCNodeIdStatus; - public get status(): SetSUCNodeIdStatus { - return this._status; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SetSUCNodeIdRequestStatusReport { + const callbackId = raw.payload[0]; + const status: SetSUCNodeIdStatus = raw.payload[1]; + + return new this({ + callbackId, + status, + }); } + public status: SetSUCNodeIdStatus; + public isOK(): boolean { - return this._status === SetSUCNodeIdStatus.Succeeded; + return this.status === SetSUCNodeIdStatus.Succeeded; } } diff --git a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.ts index 750daeb548e9..6c6a7c382592 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongBufferMessages.ts @@ -8,17 +8,17 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; import { num2hex } from "@zwave-js/shared"; -export interface ExtNVMReadLongBufferRequestOptions extends MessageBaseOptions { +export interface ExtNVMReadLongBufferRequestOptions { offset: number; length: number; } @@ -28,33 +28,36 @@ export interface ExtNVMReadLongBufferRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.ExtNVMReadLongBuffer) export class ExtNVMReadLongBufferRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | ExtNVMReadLongBufferRequestOptions, + options: ExtNVMReadLongBufferRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if (options.offset < 0 || options.offset > 0xffffff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The offset must be a 24-bit number!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + if (options.length < 1 || options.length > 0xffff) { + throw new ZWaveError( + "The length must be between 1 and 65535", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.offset < 0 || options.offset > 0xffffff) { - throw new ZWaveError( - "The offset must be a 24-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.length < 1 || options.length > 0xffff) { - throw new ZWaveError( - "The length must be between 1 and 65535", - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.offset = options.offset; - this.length = options.length; } + + this.offset = options.offset; + this.length = options.length; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMReadLongBufferRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ExtNVMReadLongBufferRequest({}); } public offset: number; @@ -78,13 +81,28 @@ export class ExtNVMReadLongBufferRequest extends Message { } } +export interface ExtNVMReadLongBufferResponseOptions { + buffer: Buffer; +} + @messageTypes(MessageType.Response, FunctionType.ExtNVMReadLongBuffer) export class ExtNVMReadLongBufferResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: ExtNVMReadLongBufferResponseOptions & MessageBaseOptions, ) { super(options); - this.buffer = this.payload; + + // TODO: Check implementation: + this.buffer = options.buffer; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMReadLongBufferResponse { + return new this({ + buffer: raw.payload, + }); } public readonly buffer: Buffer; diff --git a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongByteMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongByteMessages.ts index 30ed8ca21abd..e4f731952e2b 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongByteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMReadLongByteMessages.ts @@ -8,17 +8,17 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; import { num2hex } from "@zwave-js/shared"; -export interface ExtNVMReadLongByteRequestOptions extends MessageBaseOptions { +export interface ExtNVMReadLongByteRequestOptions { offset: number; } @@ -27,25 +27,28 @@ export interface ExtNVMReadLongByteRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.ExtNVMReadLongByte) export class ExtNVMReadLongByteRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | ExtNVMReadLongByteRequestOptions, + options: ExtNVMReadLongByteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if (options.offset < 0 || options.offset > 0xffffff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The offset must be a 24-bit number!", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.offset < 0 || options.offset > 0xffffff) { - throw new ZWaveError( - "The offset must be a 24-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.offset = options.offset; } + this.offset = options.offset; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMReadLongByteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ExtNVMReadLongByteRequest({}); } public offset: number; @@ -64,13 +67,30 @@ export class ExtNVMReadLongByteRequest extends Message { } } +export interface ExtNVMReadLongByteResponseOptions { + byte: number; +} + @messageTypes(MessageType.Response, FunctionType.ExtNVMReadLongByte) export class ExtNVMReadLongByteResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: ExtNVMReadLongByteResponseOptions & MessageBaseOptions, ) { super(options); - this.byte = this.payload[0]; + + // TODO: Check implementation: + this.byte = options.byte; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMReadLongByteResponse { + const byte = raw.payload[0]; + + return new this({ + byte, + }); } public readonly byte: number; diff --git a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.ts index 8b27db4fe920..16ec55c2fc32 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongBufferMessages.ts @@ -8,19 +8,17 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; import { num2hex } from "@zwave-js/shared"; -export interface ExtNVMWriteLongBufferRequestOptions - extends MessageBaseOptions -{ +export interface ExtNVMWriteLongBufferRequestOptions { offset: number; buffer: Buffer; } @@ -30,32 +28,35 @@ export interface ExtNVMWriteLongBufferRequestOptions @expectedResponse(FunctionType.ExtNVMWriteLongBuffer) export class ExtNVMWriteLongBufferRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | ExtNVMWriteLongBufferRequestOptions, + options: ExtNVMWriteLongBufferRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if (options.offset < 0 || options.offset > 0xffffff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The offset must be a 24-bit number!", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.offset < 0 || options.offset > 0xffffff) { - throw new ZWaveError( - "The offset must be a 24-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.buffer.length < 1 || options.buffer.length > 0xffff) { - throw new ZWaveError( - "The buffer must be between 1 and 65535 bytes long", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.offset = options.offset; - this.buffer = options.buffer; } + if (options.buffer.length < 1 || options.buffer.length > 0xffff) { + throw new ZWaveError( + "The buffer must be between 1 and 65535 bytes long", + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.offset = options.offset; + this.buffer = options.buffer; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMWriteLongBufferRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ExtNVMWriteLongBufferRequest({}); } public offset: number; @@ -82,13 +83,30 @@ export class ExtNVMWriteLongBufferRequest extends Message { } } +export interface ExtNVMWriteLongBufferResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.ExtNVMWriteLongBuffer) export class ExtNVMWriteLongBufferResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: ExtNVMWriteLongBufferResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMWriteLongBufferResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } public readonly success: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.ts index b645d01ecefb..f37424913f10 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/ExtNVMWriteLongByteMessages.ts @@ -8,17 +8,17 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; import { num2hex } from "@zwave-js/shared"; -export interface ExtNVMWriteLongByteRequestOptions extends MessageBaseOptions { +export interface ExtNVMWriteLongByteRequestOptions { offset: number; byte: number; } @@ -28,32 +28,35 @@ export interface ExtNVMWriteLongByteRequestOptions extends MessageBaseOptions { @expectedResponse(FunctionType.ExtExtWriteLongByte) export class ExtNVMWriteLongByteRequest extends Message { public constructor( - options: - | MessageDeserializationOptions - | ExtNVMWriteLongByteRequestOptions, + options: ExtNVMWriteLongByteRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { + if (options.offset < 0 || options.offset > 0xffffff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The offset must be a 24-bit number!", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.offset < 0 || options.offset > 0xffffff) { - throw new ZWaveError( - "The offset must be a 24-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if ((options.byte & 0xff) !== options.byte) { - throw new ZWaveError( - "The data must be a byte!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - this.offset = options.offset; - this.byte = options.byte; } + if ((options.byte & 0xff) !== options.byte) { + throw new ZWaveError( + "The data must be a byte!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.offset = options.offset; + this.byte = options.byte; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMWriteLongByteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ExtNVMWriteLongByteRequest({}); } public offset: number; @@ -77,13 +80,30 @@ export class ExtNVMWriteLongByteRequest extends Message { } } +export interface ExtNVMWriteLongByteResponseOptions { + success: boolean; +} + @messageTypes(MessageType.Response, FunctionType.ExtExtWriteLongByte) export class ExtNVMWriteLongByteResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: ExtNVMWriteLongByteResponseOptions & MessageBaseOptions, ) { super(options); - this.success = this.payload[0] !== 0; + + // TODO: Check implementation: + this.success = options.success; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtNVMWriteLongByteResponse { + const success = raw.payload[0] !== 0; + + return new this({ + success, + }); } public readonly success: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/nvm/ExtendedNVMOperationsMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/ExtendedNVMOperationsMessages.ts index 4bfd2177595b..3b7ef415d22f 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/ExtendedNVMOperationsMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/ExtendedNVMOperationsMessages.ts @@ -8,17 +8,16 @@ import { } from "@zwave-js/core"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -75,7 +74,7 @@ export class ExtendedNVMOperationsRequest extends Message { export class ExtendedNVMOperationsOpenRequest extends ExtendedNVMOperationsRequest { - public constructor(options?: MessageOptions) { + public constructor(options: MessageBaseOptions = {}) { super(options); this.command = ExtendedNVMOperationsCommand.Open; } @@ -86,7 +85,7 @@ export class ExtendedNVMOperationsOpenRequest export class ExtendedNVMOperationsCloseRequest extends ExtendedNVMOperationsRequest { - public constructor(options?: MessageOptions) { + public constructor(options: MessageBaseOptions = {}) { super(options); this.command = ExtendedNVMOperationsCommand.Close; } @@ -94,9 +93,7 @@ export class ExtendedNVMOperationsCloseRequest // ============================================================================= -export interface ExtendedNVMOperationsReadRequestOptions - extends MessageBaseOptions -{ +export interface ExtendedNVMOperationsReadRequestOptions { length: number; offset: number; } @@ -105,35 +102,38 @@ export class ExtendedNVMOperationsReadRequest extends ExtendedNVMOperationsRequest { public constructor( - options: - | MessageDeserializationOptions - | ExtendedNVMOperationsReadRequestOptions, + options: ExtendedNVMOperationsReadRequestOptions & MessageBaseOptions, ) { super(options); this.command = ExtendedNVMOperationsCommand.Read; - if (gotDeserializationOptions(options)) { + if (options.length < 0 || options.length > 0xff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The length must be between 0 and 255!", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.length < 0 || options.length > 0xff) { - throw new ZWaveError( - "The length must be between 0 and 255!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.offset < 0 || options.offset > 0xffffffff) { - throw new ZWaveError( - "The offset must be a 32-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.length = options.length; - this.offset = options.offset; } + if (options.offset < 0 || options.offset > 0xffffffff) { + throw new ZWaveError( + "The offset must be a 32-bit number!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + + this.length = options.length; + this.offset = options.offset; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtendedNVMOperationsReadRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ExtendedNVMOperationsReadRequest({}); } public length: number; @@ -162,9 +162,7 @@ export class ExtendedNVMOperationsReadRequest // ============================================================================= -export interface ExtendedNVMOperationsWriteRequestOptions - extends MessageBaseOptions -{ +export interface ExtendedNVMOperationsWriteRequestOptions { offset: number; buffer: Buffer; } @@ -173,35 +171,38 @@ export class ExtendedNVMOperationsWriteRequest extends ExtendedNVMOperationsRequest { public constructor( - options: - | MessageDeserializationOptions - | ExtendedNVMOperationsWriteRequestOptions, + options: ExtendedNVMOperationsWriteRequestOptions & MessageBaseOptions, ) { super(options); this.command = ExtendedNVMOperationsCommand.Write; - if (gotDeserializationOptions(options)) { + if (options.offset < 0 || options.offset > 0xffffffff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The offset must be a 32-bit number!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + if (options.buffer.length < 1 || options.buffer.length > 0xff) { + throw new ZWaveError( + "The buffer must be between 1 and 255 bytes long", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.offset < 0 || options.offset > 0xffffffff) { - throw new ZWaveError( - "The offset must be a 32-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.buffer.length < 1 || options.buffer.length > 0xff) { - throw new ZWaveError( - "The buffer must be between 1 and 255 bytes long", - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.offset = options.offset; - this.buffer = options.buffer; } + + this.offset = options.offset; + this.buffer = options.buffer; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtendedNVMOperationsWriteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new ExtendedNVMOperationsWriteRequest({}); } public offset: number; @@ -231,42 +232,60 @@ export class ExtendedNVMOperationsWriteRequest } // ============================================================================= +export interface ExtendedNVMOperationsResponseOptions { + status: ExtendedNVMOperationStatus; + offsetOrSize: number; + bufferOrBitmask: Buffer; +} @messageTypes(MessageType.Response, FunctionType.ExtendedNVMOperations) export class ExtendedNVMOperationsResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: ExtendedNVMOperationsResponseOptions & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 2); - this.status = this.payload[0]; - const dataLength = this.payload[1]; + // TODO: Check implementation: + this.status = options.status; + this.offsetOrSize = options.offsetOrSize; + this.bufferOrBitmask = options.bufferOrBitmask; + } + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): ExtendedNVMOperationsResponse { + validatePayload(raw.payload.length >= 2); + const status: ExtendedNVMOperationStatus = raw.payload[0]; + const dataLength = raw.payload[1]; let offset = 2; - - if (this.payload.length >= offset + 4) { - this.offsetOrSize = this.payload.readUInt32BE(offset); - } else { - this.offsetOrSize = 0; + let offsetOrSize = 0; + if (raw.payload.length >= offset + 4) { + offsetOrSize = raw.payload.readUInt32BE(offset); } offset += 4; - // The buffer will contain: // - Read command: the read NVM data // - Write/Close command: nothing // - Open command: bit mask of supported sub-commands - if (dataLength > 0 && this.payload.length >= offset + dataLength) { - this.bufferOrBitmask = this.payload.subarray( + let bufferOrBitmask: Buffer; + if (dataLength > 0 && raw.payload.length >= offset + dataLength) { + bufferOrBitmask = raw.payload.subarray( offset, offset + dataLength, ); } else { - this.bufferOrBitmask = Buffer.from([]); + bufferOrBitmask = Buffer.from([]); } + + return new this({ + status, + offsetOrSize, + bufferOrBitmask, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/nvm/FirmwareUpdateNVMMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/FirmwareUpdateNVMMessages.ts index 874d40ce710d..ffc021cd4393 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/FirmwareUpdateNVMMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/FirmwareUpdateNVMMessages.ts @@ -2,24 +2,21 @@ import { type MessageOrCCLogEntry, MessagePriority, type MessageRecord, - ZWaveError, - ZWaveErrorCodes, createSimpleReflectionDecorator, validatePayload, } from "@zwave-js/core"; import type { - DeserializingMessageConstructor, MessageBaseOptions, + MessageConstructor, MessageEncodingContext, + MessageParsingContext, + MessageRaw, } from "@zwave-js/serial"; import { FunctionType, Message, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -37,12 +34,12 @@ export enum FirmwareUpdateNVMCommand { // We need to define the decorators for Requests and Responses separately const { decorator: subCommandRequest, - // lookupConstructor: getSubCommandRequestConstructor, + lookupConstructor: getSubCommandRequestConstructor, lookupValue: getSubCommandForRequest, } = createSimpleReflectionDecorator< FirmwareUpdateNVMRequest, [command: FirmwareUpdateNVMCommand], - DeserializingMessageConstructor + MessageConstructor >({ name: "subCommandRequest", }); @@ -50,10 +47,11 @@ const { const { decorator: subCommandResponse, lookupConstructor: getSubCommandResponseConstructor, + lookupValue: getSubCommandForResponse, } = createSimpleReflectionDecorator< FirmwareUpdateNVMResponse, [command: FirmwareUpdateNVMCommand], - DeserializingMessageConstructor + MessageConstructor >({ name: "subCommandResponse", }); @@ -66,20 +64,43 @@ function testResponseForFirmwareUpdateNVMRequest( return (sent as FirmwareUpdateNVMRequest).command === received.command; } +export interface FirmwareUpdateNVMRequestOptions { + command?: FirmwareUpdateNVMCommand; +} + @messageTypes(MessageType.Request, FunctionType.FirmwareUpdateNVM) @priority(MessagePriority.Controller) @expectedResponse(testResponseForFirmwareUpdateNVMRequest) export class FirmwareUpdateNVMRequest extends Message { - public constructor(options: MessageOptions = {}) { + public constructor( + options: FirmwareUpdateNVMRequestOptions & MessageBaseOptions = {}, + ) { super(options); - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.command = getSubCommandForRequest(this)!; + this.command = options.command ?? getSubCommandForRequest(this)!; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): FirmwareUpdateNVMRequest { + const command: FirmwareUpdateNVMCommand = raw.payload[0]; + const payload = raw.payload.subarray(1); + + const CommandConstructor = getSubCommandRequestConstructor( + command, + ); + if (CommandConstructor) { + return CommandConstructor.from( + raw.withPayload(payload), + ctx, + ) as FirmwareUpdateNVMRequest; } + + const ret = new FirmwareUpdateNVMRequest({ + command, + }); + ret.payload = payload; + return ret; } public command: FirmwareUpdateNVMCommand; @@ -107,22 +128,41 @@ export class FirmwareUpdateNVMRequest extends Message { } } +export interface FirmwareUpdateNVMResponseOptions { + command?: FirmwareUpdateNVMCommand; +} + @messageTypes(MessageType.Response, FunctionType.FirmwareUpdateNVM) export class FirmwareUpdateNVMResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: FirmwareUpdateNVMResponseOptions & MessageBaseOptions, ) { super(options); - this.command = this.payload[0]; + this.command = options.command ?? getSubCommandForResponse(this)!; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): FirmwareUpdateNVMResponse { + const command: FirmwareUpdateNVMCommand = raw.payload[0]; + const payload = raw.payload.subarray(1); const CommandConstructor = getSubCommandResponseConstructor( - this.command, + command, ); - if (CommandConstructor && (new.target as any) !== CommandConstructor) { - return new CommandConstructor(options); + if (CommandConstructor) { + return CommandConstructor.from( + raw.withPayload(payload), + ctx, + ) as FirmwareUpdateNVMResponse; } - this.payload = this.payload.subarray(1); + const ret = new FirmwareUpdateNVMResponse({ + command, + }); + ret.payload = payload; + return ret; } public command: FirmwareUpdateNVMCommand; @@ -146,13 +186,29 @@ export class FirmwareUpdateNVMResponse extends Message { @subCommandRequest(FirmwareUpdateNVMCommand.Init) export class FirmwareUpdateNVM_InitRequest extends FirmwareUpdateNVMRequest {} +export interface FirmwareUpdateNVM_InitResponseOptions { + supported: boolean; +} + @subCommandResponse(FirmwareUpdateNVMCommand.Init) export class FirmwareUpdateNVM_InitResponse extends FirmwareUpdateNVMResponse { public constructor( - options: MessageDeserializationOptions, + options: FirmwareUpdateNVM_InitResponseOptions & MessageBaseOptions, ) { super(options); - this.supported = this.payload[0] !== 0; + + this.supported = options.supported; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_InitResponse { + const supported = raw.payload[0] !== 0; + + return new this({ + supported, + }); } public readonly supported: boolean; @@ -168,9 +224,7 @@ export class FirmwareUpdateNVM_InitResponse extends FirmwareUpdateNVMResponse { // ============================================================================= -export interface FirmwareUpdateNVM_SetNewImageRequestOptions - extends MessageBaseOptions -{ +export interface FirmwareUpdateNVM_SetNewImageRequestOptions { newImage: boolean; } @@ -180,20 +234,23 @@ export class FirmwareUpdateNVM_SetNewImageRequest { public constructor( options: - | MessageDeserializationOptions - | FirmwareUpdateNVM_SetNewImageRequestOptions, + & FirmwareUpdateNVM_SetNewImageRequestOptions + & MessageBaseOptions, ) { super(options); - this.command = FirmwareUpdateNVMCommand.SetNewImage; - - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.newImage = options.newImage; - } + + this.newImage = options.newImage; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_SetNewImageRequest { + const newImage: boolean = raw.payload[0] !== 0; + + return new this({ + newImage, + }); } public newImage: boolean; @@ -213,15 +270,33 @@ export class FirmwareUpdateNVM_SetNewImageRequest } } +export interface FirmwareUpdateNVM_SetNewImageResponseOptions { + changed: boolean; +} + @subCommandResponse(FirmwareUpdateNVMCommand.SetNewImage) export class FirmwareUpdateNVM_SetNewImageResponse extends FirmwareUpdateNVMResponse { public constructor( - options: MessageDeserializationOptions, + options: + & FirmwareUpdateNVM_SetNewImageResponseOptions + & MessageBaseOptions, ) { super(options); - this.changed = this.payload[0] !== 0; + + this.changed = options.changed; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_SetNewImageResponse { + const changed = raw.payload[0] !== 0; + + return new this({ + changed, + }); } public readonly changed: boolean; @@ -242,15 +317,33 @@ export class FirmwareUpdateNVM_GetNewImageRequest extends FirmwareUpdateNVMRequest {} +export interface FirmwareUpdateNVM_GetNewImageResponseOptions { + newImage: boolean; +} + @subCommandResponse(FirmwareUpdateNVMCommand.GetNewImage) export class FirmwareUpdateNVM_GetNewImageResponse extends FirmwareUpdateNVMResponse { public constructor( - options: MessageDeserializationOptions, + options: + & FirmwareUpdateNVM_GetNewImageResponseOptions + & MessageBaseOptions, ) { super(options); - this.newImage = this.payload[0] !== 0; + + this.newImage = options.newImage; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_GetNewImageResponse { + const newImage: boolean = raw.payload[0] !== 0; + + return new this({ + newImage, + }); } public readonly newImage: boolean; @@ -266,9 +359,7 @@ export class FirmwareUpdateNVM_GetNewImageResponse // ============================================================================= -export interface FirmwareUpdateNVM_UpdateCRC16RequestOptions - extends MessageBaseOptions -{ +export interface FirmwareUpdateNVM_UpdateCRC16RequestOptions { crcSeed: number; offset: number; blockLength: number; @@ -280,22 +371,30 @@ export class FirmwareUpdateNVM_UpdateCRC16Request { public constructor( options: - | MessageDeserializationOptions - | FirmwareUpdateNVM_UpdateCRC16RequestOptions, + & FirmwareUpdateNVM_UpdateCRC16RequestOptions + & MessageBaseOptions, ) { super(options); this.command = FirmwareUpdateNVMCommand.UpdateCRC16; - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.crcSeed = options.crcSeed; - this.offset = options.offset; - this.blockLength = options.blockLength; - } + this.crcSeed = options.crcSeed; + this.offset = options.offset; + this.blockLength = options.blockLength; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_UpdateCRC16Request { + const offset = raw.payload.readUIntBE(0, 3); + const blockLength = raw.payload.readUInt16BE(3); + const crcSeed = raw.payload.readUInt16BE(5); + + return new this({ + crcSeed, + offset, + blockLength, + }); } public crcSeed: number; @@ -327,16 +426,34 @@ export class FirmwareUpdateNVM_UpdateCRC16Request } } +export interface FirmwareUpdateNVM_UpdateCRC16ResponseOptions { + crc16: number; +} + @subCommandResponse(FirmwareUpdateNVMCommand.UpdateCRC16) export class FirmwareUpdateNVM_UpdateCRC16Response extends FirmwareUpdateNVMResponse { public constructor( - options: MessageDeserializationOptions, + options: + & FirmwareUpdateNVM_UpdateCRC16ResponseOptions + & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 2); - this.crc16 = this.payload.readUint16BE(0); + + this.crc16 = options.crc16; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_UpdateCRC16Response { + validatePayload(raw.payload.length >= 2); + const crc16 = raw.payload.readUInt16BE(0); + + return new this({ + crc16, + }); } public readonly crc16: number; @@ -362,16 +479,34 @@ export class FirmwareUpdateNVM_IsValidCRC16Request } } +export interface FirmwareUpdateNVM_IsValidCRC16ResponseOptions { + isValid: boolean; +} + @subCommandResponse(FirmwareUpdateNVMCommand.IsValidCRC16) export class FirmwareUpdateNVM_IsValidCRC16Response extends FirmwareUpdateNVMResponse { public constructor( - options: MessageDeserializationOptions, + options: + & FirmwareUpdateNVM_IsValidCRC16ResponseOptions + & MessageBaseOptions, ) { super(options); - this.isValid = this.payload[0] !== 0; + + this.isValid = options.isValid; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_IsValidCRC16Response { + const isValid = raw.payload[0] !== 0; // There are two more bytes containing the CRC result, but we don't care about that + + return new this({ + isValid, + }); } public readonly isValid: boolean; @@ -387,9 +522,7 @@ export class FirmwareUpdateNVM_IsValidCRC16Response // ============================================================================= -export interface FirmwareUpdateNVM_WriteRequestOptions - extends MessageBaseOptions -{ +export interface FirmwareUpdateNVM_WriteRequestOptions { offset: number; buffer: Buffer; } @@ -397,22 +530,26 @@ export interface FirmwareUpdateNVM_WriteRequestOptions @subCommandRequest(FirmwareUpdateNVMCommand.Write) export class FirmwareUpdateNVM_WriteRequest extends FirmwareUpdateNVMRequest { public constructor( - options: - | MessageDeserializationOptions - | FirmwareUpdateNVM_WriteRequestOptions, + options: FirmwareUpdateNVM_WriteRequestOptions & MessageBaseOptions, ) { super(options); - this.command = FirmwareUpdateNVMCommand.Write; - - if (gotDeserializationOptions(options)) { - throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, - ); - } else { - this.offset = options.offset; - this.buffer = options.buffer; - } + + this.offset = options.offset; + this.buffer = options.buffer; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_WriteRequest { + const offset = raw.payload.readUIntBE(0, 3); + const bufferLength = raw.payload.readUInt16BE(3); + const buffer = raw.payload.subarray(5, 5 + bufferLength); + + return new this({ + offset, + buffer, + }); } public offset: number; @@ -420,7 +557,7 @@ export class FirmwareUpdateNVM_WriteRequest extends FirmwareUpdateNVMRequest { public serialize(ctx: MessageEncodingContext): Buffer { this.payload = Buffer.concat([Buffer.allocUnsafe(5), this.buffer]); - this.payload.writeUintBE(this.offset, 0, 3); + this.payload.writeUIntBE(this.offset, 0, 3); this.payload.writeUInt16BE(this.buffer.length, 3); return super.serialize(ctx); @@ -440,13 +577,29 @@ export class FirmwareUpdateNVM_WriteRequest extends FirmwareUpdateNVMRequest { } } +export interface FirmwareUpdateNVM_WriteResponseOptions { + overwritten: boolean; +} + @subCommandResponse(FirmwareUpdateNVMCommand.Write) export class FirmwareUpdateNVM_WriteResponse extends FirmwareUpdateNVMResponse { public constructor( - options: MessageDeserializationOptions, + options: FirmwareUpdateNVM_WriteResponseOptions & MessageBaseOptions, ) { super(options); - this.overwritten = this.payload[0] !== 0; + + this.overwritten = options.overwritten; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): FirmwareUpdateNVM_WriteResponse { + const overwritten = raw.payload[0] !== 0; + + return new this({ + overwritten, + }); } public readonly overwritten: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/nvm/GetNVMIdMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/GetNVMIdMessages.ts index 32fbe189dd50..928e79fd10b1 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/GetNVMIdMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/GetNVMIdMessages.ts @@ -2,7 +2,9 @@ import { type MessageOrCCLogEntry, MessagePriority } from "@zwave-js/core"; import { FunctionType, Message, - type MessageDeserializationOptions, + type MessageBaseOptions, + type MessageParsingContext, + type MessageRaw, MessageType, expectedResponse, messageTypes, @@ -70,15 +72,38 @@ export type NVMId = Pick< @priority(MessagePriority.Controller) export class GetNVMIdRequest extends Message {} +export interface GetNVMIdResponseOptions { + nvmManufacturerId: number; + memoryType: NVMType; + memorySize: NVMSize; +} + @messageTypes(MessageType.Response, FunctionType.GetNVMId) export class GetNVMIdResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: GetNVMIdResponseOptions & MessageBaseOptions, ) { super(options); - this.nvmManufacturerId = this.payload[1]; - this.memoryType = this.payload[2]; - this.memorySize = this.payload[3]; + + // TODO: Check implementation: + this.nvmManufacturerId = options.nvmManufacturerId; + this.memoryType = options.memoryType; + this.memorySize = options.memorySize; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): GetNVMIdResponse { + const nvmManufacturerId = raw.payload[1]; + const memoryType: NVMType = raw.payload[2]; + const memorySize: NVMSize = raw.payload[3]; + + return new this({ + nvmManufacturerId, + memoryType, + memorySize, + }); } public readonly nvmManufacturerId: number; diff --git a/packages/zwave-js/src/lib/serialapi/nvm/NVMOperationsMessages.ts b/packages/zwave-js/src/lib/serialapi/nvm/NVMOperationsMessages.ts index 0b8854f803aa..0f9908669b09 100644 --- a/packages/zwave-js/src/lib/serialapi/nvm/NVMOperationsMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/nvm/NVMOperationsMessages.ts @@ -10,13 +10,12 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -67,7 +66,7 @@ export class NVMOperationsRequest extends Message { // ============================================================================= export class NVMOperationsOpenRequest extends NVMOperationsRequest { - public constructor(options?: MessageOptions) { + public constructor(options: MessageBaseOptions = {}) { super(options); this.command = NVMOperationsCommand.Open; } @@ -76,7 +75,7 @@ export class NVMOperationsOpenRequest extends NVMOperationsRequest { // ============================================================================= export class NVMOperationsCloseRequest extends NVMOperationsRequest { - public constructor(options?: MessageOptions) { + public constructor(options: MessageBaseOptions = {}) { super(options); this.command = NVMOperationsCommand.Close; } @@ -84,42 +83,45 @@ export class NVMOperationsCloseRequest extends NVMOperationsRequest { // ============================================================================= -export interface NVMOperationsReadRequestOptions extends MessageBaseOptions { +export interface NVMOperationsReadRequestOptions { length: number; offset: number; } export class NVMOperationsReadRequest extends NVMOperationsRequest { public constructor( - options: - | MessageDeserializationOptions - | NVMOperationsReadRequestOptions, + options: NVMOperationsReadRequestOptions & MessageBaseOptions, ) { super(options); this.command = NVMOperationsCommand.Read; - if (gotDeserializationOptions(options)) { + if (options.length < 0 || options.length > 0xff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The length must be between 0 and 255!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + if (options.offset < 0 || options.offset > 0xffff) { + throw new ZWaveError( + "The offset must be a 16-bit number!", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.length < 0 || options.length > 0xff) { - throw new ZWaveError( - "The length must be between 0 and 255!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.offset < 0 || options.offset > 0xffff) { - throw new ZWaveError( - "The offset must be a 16-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.length = options.length; - this.offset = options.offset; } + + this.length = options.length; + this.offset = options.offset; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): NVMOperationsReadRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new NVMOperationsReadRequest({}); } public length: number; @@ -148,42 +150,45 @@ export class NVMOperationsReadRequest extends NVMOperationsRequest { // ============================================================================= -export interface NVMOperationsWriteRequestOptions extends MessageBaseOptions { +export interface NVMOperationsWriteRequestOptions { offset: number; buffer: Buffer; } export class NVMOperationsWriteRequest extends NVMOperationsRequest { public constructor( - options: - | MessageDeserializationOptions - | NVMOperationsWriteRequestOptions, + options: NVMOperationsWriteRequestOptions & MessageBaseOptions, ) { super(options); this.command = NVMOperationsCommand.Write; - if (gotDeserializationOptions(options)) { + if (options.offset < 0 || options.offset > 0xffff) { throw new ZWaveError( - `${this.constructor.name}: deserialization not implemented`, - ZWaveErrorCodes.Deserialization_NotImplemented, + "The offset must be a 16-bit number!", + ZWaveErrorCodes.Argument_Invalid, + ); + } + if (options.buffer.length < 1 || options.buffer.length > 0xff) { + throw new ZWaveError( + "The buffer must be between 1 and 255 bytes long", + ZWaveErrorCodes.Argument_Invalid, ); - } else { - if (options.offset < 0 || options.offset > 0xffff) { - throw new ZWaveError( - "The offset must be a 16-bit number!", - ZWaveErrorCodes.Argument_Invalid, - ); - } - if (options.buffer.length < 1 || options.buffer.length > 0xff) { - throw new ZWaveError( - "The buffer must be between 1 and 255 bytes long", - ZWaveErrorCodes.Argument_Invalid, - ); - } - - this.offset = options.offset; - this.buffer = options.buffer; } + + this.offset = options.offset; + this.buffer = options.buffer; + } + + public static from( + _raw: MessageRaw, + _ctx: MessageParsingContext, + ): NVMOperationsWriteRequest { + throw new ZWaveError( + `${this.name}: deserialization not implemented`, + ZWaveErrorCodes.Deserialization_NotImplemented, + ); + + // return new NVMOperationsWriteRequest({}); } public offset: number; @@ -213,30 +218,52 @@ export class NVMOperationsWriteRequest extends NVMOperationsRequest { } // ============================================================================= +export interface NVMOperationsResponseOptions { + status: NVMOperationStatus; + offsetOrSize: number; + buffer: Buffer; +} @messageTypes(MessageType.Response, FunctionType.NVMOperations) export class NVMOperationsResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: NVMOperationsResponseOptions & MessageBaseOptions, ) { super(options); - validatePayload(this.payload.length >= 2); - this.status = this.payload[0]; + // TODO: Check implementation: + this.status = options.status; + this.offsetOrSize = options.offsetOrSize; + this.buffer = options.buffer; + } - if (this.payload.length >= 4) { - this.offsetOrSize = this.payload.readUInt16BE(2); + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): NVMOperationsResponse { + validatePayload(raw.payload.length >= 2); + const status: NVMOperationStatus = raw.payload[0]; + let offsetOrSize; + if (raw.payload.length >= 4) { + offsetOrSize = raw.payload.readUInt16BE(2); } else { - this.offsetOrSize = 0; + offsetOrSize = 0; } - const dataLength = this.payload[1]; + const dataLength = raw.payload[1]; // The response to the write command contains the offset and written data length, but no data - if (dataLength > 0 && this.payload.length >= 4 + dataLength) { - this.buffer = this.payload.subarray(4, 4 + dataLength); + let buffer: Buffer; + if (dataLength > 0 && raw.payload.length >= 4 + dataLength) { + buffer = raw.payload.subarray(4, 4 + dataLength); } else { - this.buffer = Buffer.from([]); + buffer = Buffer.from([]); } + + return new this({ + status, + offsetOrSize, + buffer, + }); } isOK(): boolean { diff --git a/packages/zwave-js/src/lib/serialapi/transport/SendDataBridgeMessages.ts b/packages/zwave-js/src/lib/serialapi/transport/SendDataBridgeMessages.ts index d26e049f9cc1..9c3299f0968d 100644 --- a/packages/zwave-js/src/lib/serialapi/transport/SendDataBridgeMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/transport/SendDataBridgeMessages.ts @@ -1,9 +1,10 @@ -import type { CommandClass, ICommandClassContainer } from "@zwave-js/cc"; +import type { CommandClass } from "@zwave-js/cc"; import { MAX_NODES, type MessageOrCCLogEntry, MessagePriority, type MulticastCC, + type MulticastDestination, type SinglecastCC, type TXReport, TransmitOptions, @@ -15,18 +16,17 @@ import { import type { CCEncodingContext } from "@zwave-js/host"; import type { MessageEncodingContext, + MessageParsingContext, + MessageRaw, SuccessIndicator, } from "@zwave-js/serial"; import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, - type MessageOptions, MessageType, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -34,53 +34,67 @@ import { getEnumMemberName, num2hex } from "@zwave-js/shared"; import { clamp } from "alcalzone-shared/math"; import { ApplicationCommandRequest } from "../application/ApplicationCommandRequest"; import { BridgeApplicationCommandRequest } from "../application/BridgeApplicationCommandRequest"; +import { type MessageWithCC, containsCC } from "../utils"; import { MAX_SEND_ATTEMPTS } from "./SendDataMessages"; import { parseTXReport, txReportToMessageRecord } from "./SendDataShared"; @messageTypes(MessageType.Request, FunctionType.SendDataBridge) @priority(MessagePriority.Normal) export class SendDataBridgeRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== SendDataBridgeRequestTransmitReport - ) { - return new SendDataBridgeRequestTransmitReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendDataBridgeRequestBase { + return SendDataBridgeRequestTransmitReport.from(raw, ctx); } } -interface SendDataBridgeRequestOptions< +export type SendDataBridgeRequestOptions< CCType extends CommandClass = CommandClass, -> extends MessageBaseOptions { - command: CCType; - sourceNodeId: number; - transmitOptions?: TransmitOptions; - maxSendAttempts?: number; -} +> = + & ( + | { command: CCType } + | { + nodeId: number; + serializedCC: Buffer; + } + ) + & { + sourceNodeId: number; + transmitOptions?: TransmitOptions; + maxSendAttempts?: number; + }; @expectedResponse(FunctionType.SendDataBridge) @expectedCallback(FunctionType.SendDataBridge) export class SendDataBridgeRequest extends SendDataBridgeRequestBase - implements ICommandClassContainer + implements MessageWithCC { public constructor( - options: SendDataBridgeRequestOptions, + options: SendDataBridgeRequestOptions & MessageBaseOptions, ) { super(options); - if (!options.command.isSinglecast() && !options.command.isBroadcast()) { - throw new ZWaveError( - `SendDataBridgeRequest can only be used for singlecast and broadcast CCs`, - ZWaveErrorCodes.Argument_Invalid, - ); + if ("command" in options) { + if ( + !options.command.isSinglecast() + && !options.command.isBroadcast() + ) { + throw new ZWaveError( + `SendDataBridgeRequest can only be used for singlecast and broadcast CCs`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.command = options.command; + this._nodeId = options.command.nodeId; + } else { + this._nodeId = options.nodeId; + this.serializedCC = options.serializedCC; } this.sourceNodeId = options.sourceNodeId; - this.command = options.command; this.transmitOptions = options.transmitOptions ?? TransmitOptions.DEFAULT; if (options.maxSendAttempts != undefined) { @@ -92,7 +106,7 @@ export class SendDataBridgeRequest public sourceNodeId: number; /** The command this message contains */ - public command: SinglecastCC; + public command: SinglecastCC | undefined; /** Options regarding the transmission of the message */ public transmitOptions: TransmitOptions; @@ -105,23 +119,29 @@ export class SendDataBridgeRequest this._maxSendAttempts = clamp(value, 1, MAX_SEND_ATTEMPTS); } + private _nodeId: number; public override getNodeId(): number | undefined { - return this.command.nodeId; + return this.command?.nodeId ?? this._nodeId; } - // Cache the serialized CC, so we can check if it needs to be fragmented - private _serializedCC: Buffer | undefined; + public serializedCC: Buffer | undefined; /** @internal */ public serializeCC(ctx: CCEncodingContext): Buffer { - if (!this._serializedCC) { - this._serializedCC = this.command.serialize(ctx); + if (!this.serializedCC) { + if (!this.command) { + throw new ZWaveError( + `Cannot serialize a ${this.constructor.name} without a command`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.serializedCC = this.command.serialize(ctx); } - return this._serializedCC; + return this.serializedCC; } public prepareRetransmission(): void { - this.command.prepareRetransmission(); - this._serializedCC = undefined; + this.command?.prepareRetransmission(); + this.serializedCC = undefined; this.callbackId = undefined; } @@ -132,7 +152,7 @@ export class SendDataBridgeRequest ctx.nodeIdType, ); const destinationNodeId = encodeNodeID( - this.command.nodeId, + this.command?.nodeId ?? this._nodeId, ctx.nodeIdType, ); const serializedCC = this.serializeCC(ctx); @@ -161,8 +181,10 @@ export class SendDataBridgeRequest public expectsNodeUpdate(): boolean { return ( + // We can only answer this if the command is known + this.command != undefined // Only true singlecast commands may expect a response - this.command.isSinglecast() + && this.command.isSinglecast() // ... and only if the command expects a response && this.command.expectsCCResponse() ); @@ -170,18 +192,19 @@ export class SendDataBridgeRequest public isExpectedNodeUpdate(msg: Message): boolean { return ( - (msg instanceof ApplicationCommandRequest + // We can only answer this if the command is known + this.command != undefined + && (msg instanceof ApplicationCommandRequest || msg instanceof BridgeApplicationCommandRequest) + && containsCC(msg) && this.command.isExpectedCCResponse(msg.command) ); } } -interface SendDataBridgeRequestTransmitReportOptions - extends MessageBaseOptions -{ +export interface SendDataBridgeRequestTransmitReportOptions { transmitStatus: TransmitStatus; - callbackId: number; + txReport?: TXReport; } export class SendDataBridgeRequestTransmitReport @@ -190,23 +213,34 @@ export class SendDataBridgeRequestTransmitReport { public constructor( options: - | MessageDeserializationOptions - | SendDataBridgeRequestTransmitReportOptions, + & SendDataBridgeRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; - // TODO: Consider NOT parsing this for transmit status other than OK or NoACK - this.txReport = parseTXReport( - this.transmitStatus !== TransmitStatus.NoAck, - this.payload.subarray(2), - ); - } else { - this.callbackId = options.callbackId; - this.transmitStatus = options.transmitStatus; - } + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + this.txReport = options.txReport; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataBridgeRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + // TODO: Consider NOT parsing this for transmit status other than OK or NoACK + const txReport = parseTXReport( + transmitStatus !== TransmitStatus.NoAck, + raw.payload.subarray(2), + ); + + return new this({ + callbackId, + transmitStatus, + txReport, + }); } public readonly transmitStatus: TransmitStatus; @@ -234,26 +268,40 @@ export class SendDataBridgeRequestTransmitReport } } +export interface SendDataBridgeResponseOptions { + wasSent: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SendDataBridge) export class SendDataBridgeResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SendDataBridgeResponseOptions & MessageBaseOptions, ) { super(options); - this._wasSent = this.payload[0] !== 0; + + // TODO: Check implementation: + this.wasSent = options.wasSent; } - isOK(): boolean { - return this._wasSent; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataBridgeResponse { + const wasSent = raw.payload[0] !== 0; + + return new this({ + wasSent, + }); } - private _wasSent: boolean; - public get wasSent(): boolean { - return this._wasSent; + isOK(): boolean { + return this.wasSent; } + public wasSent: boolean; + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), @@ -265,56 +313,70 @@ export class SendDataBridgeResponse extends Message @messageTypes(MessageType.Request, FunctionType.SendDataMulticastBridge) @priority(MessagePriority.Normal) export class SendDataMulticastBridgeRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) - !== SendDataMulticastBridgeRequestTransmitReport - ) { - return new SendDataMulticastBridgeRequestTransmitReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendDataMulticastBridgeRequestBase { + return SendDataMulticastBridgeRequestTransmitReport.from(raw, ctx); } } -interface SendDataMulticastBridgeRequestOptions - extends MessageBaseOptions -{ - command: CCType; - sourceNodeId: number; - transmitOptions?: TransmitOptions; - maxSendAttempts?: number; -} +export type SendDataMulticastBridgeRequestOptions< + CCType extends CommandClass, +> = + & ( + | { command: CCType } + | { + nodeIds: MulticastDestination; + serializedCC: Buffer; + } + ) + & { + sourceNodeId: number; + transmitOptions?: TransmitOptions; + maxSendAttempts?: number; + }; @expectedResponse(FunctionType.SendDataMulticastBridge) @expectedCallback(FunctionType.SendDataMulticastBridge) export class SendDataMulticastBridgeRequest< CCType extends CommandClass = CommandClass, -> extends SendDataMulticastBridgeRequestBase implements ICommandClassContainer { +> extends SendDataMulticastBridgeRequestBase implements MessageWithCC { public constructor( - options: SendDataMulticastBridgeRequestOptions, + options: + & SendDataMulticastBridgeRequestOptions + & MessageBaseOptions, ) { super(options); - if (!options.command.isMulticast()) { - throw new ZWaveError( - `SendDataMulticastBridgeRequest can only be used for multicast CCs`, - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (options.command.nodeId.length === 0) { - throw new ZWaveError( - `At least one node must be targeted`, - ZWaveErrorCodes.Argument_Invalid, - ); - } else if (options.command.nodeId.some((n) => n < 1 || n > MAX_NODES)) { - throw new ZWaveError( - `All node IDs must be between 1 and ${MAX_NODES}!`, - ZWaveErrorCodes.Argument_Invalid, - ); + if ("command" in options) { + if (!options.command.isMulticast()) { + throw new ZWaveError( + `SendDataMulticastBridgeRequest can only be used for multicast CCs`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if (options.command.nodeId.length === 0) { + throw new ZWaveError( + `At least one node must be targeted`, + ZWaveErrorCodes.Argument_Invalid, + ); + } else if ( + options.command.nodeId.some((n) => n < 1 || n > MAX_NODES) + ) { + throw new ZWaveError( + `All node IDs must be between 1 and ${MAX_NODES}!`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + + this.command = options.command; + this.nodeIds = this.command.nodeId; + } else { + this.nodeIds = options.nodeIds; + this.serializedCC = options.serializedCC; } this.sourceNodeId = options.sourceNodeId; - this.command = options.command; this.transmitOptions = options.transmitOptions ?? TransmitOptions.DEFAULT; if (options.maxSendAttempts != undefined) { @@ -326,7 +388,7 @@ export class SendDataMulticastBridgeRequest< public sourceNodeId: number; /** The command this message contains */ - public command: MulticastCC; + public command: MulticastCC | undefined; /** Options regarding the transmission of the message */ public transmitOptions: TransmitOptions; @@ -339,24 +401,30 @@ export class SendDataMulticastBridgeRequest< this._maxSendAttempts = clamp(value, 1, MAX_SEND_ATTEMPTS); } + public nodeIds: MulticastDestination; public override getNodeId(): number | undefined { // This is multicast, getNodeId must return undefined here return undefined; } - // Cache the serialized CC, so we can check if it needs to be fragmented - private _serializedCC: Buffer | undefined; + public serializedCC: Buffer | undefined; /** @internal */ public serializeCC(ctx: CCEncodingContext): Buffer { - if (!this._serializedCC) { - this._serializedCC = this.command.serialize(ctx); + if (!this.serializedCC) { + if (!this.command) { + throw new ZWaveError( + `Cannot serialize a ${this.constructor.name} without a command`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.serializedCC = this.command.serialize(ctx); } - return this._serializedCC; + return this.serializedCC; } public prepareRetransmission(): void { - this.command.prepareRetransmission(); - this._serializedCC = undefined; + this.command?.prepareRetransmission(); + this.serializedCC = undefined; this.callbackId = undefined; } @@ -367,14 +435,13 @@ export class SendDataMulticastBridgeRequest< this.sourceNodeId, ctx.nodeIdType, ); - const destinationNodeIDs = this.command.nodeId.map((id) => - encodeNodeID(id, ctx.nodeIdType) - ); + const destinationNodeIDs = (this.command?.nodeId ?? this.nodeIds) + .map((id) => encodeNodeID(id, ctx.nodeIdType)); this.payload = Buffer.concat([ sourceNodeId, // # of target nodes, not # of bytes - Buffer.from([this.command.nodeId.length]), + Buffer.from([destinationNodeIDs.length]), ...destinationNodeIDs, Buffer.from([serializedCC.length]), // payload @@ -390,7 +457,9 @@ export class SendDataMulticastBridgeRequest< ...super.toLogEntry(), message: { "source node id": this.sourceNodeId, - "target nodes": this.command.nodeId.join(", "), + "target nodes": (this.command?.nodeId ?? this.nodeIds).join( + ", ", + ), "transmit options": num2hex(this.transmitOptions), "callback id": this.callbackId ?? "(not set)", }, @@ -398,11 +467,8 @@ export class SendDataMulticastBridgeRequest< } } -interface SendDataMulticastBridgeRequestTransmitReportOptions - extends MessageBaseOptions -{ +export interface SendDataMulticastBridgeRequestTransmitReportOptions { transmitStatus: TransmitStatus; - callbackId: number; } export class SendDataMulticastBridgeRequestTransmitReport @@ -411,27 +477,32 @@ export class SendDataMulticastBridgeRequestTransmitReport { public constructor( options: - | MessageDeserializationOptions - | SendDataMulticastBridgeRequestTransmitReportOptions, + & SendDataMulticastBridgeRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.callbackId = this.payload[0]; - this._transmitStatus = this.payload[1]; - } else { - this.callbackId = options.callbackId; - this._transmitStatus = options.transmitStatus; - } + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; } - private _transmitStatus: TransmitStatus; - public get transmitStatus(): TransmitStatus { - return this._transmitStatus; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataMulticastBridgeRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } + public transmitStatus: TransmitStatus; + public isOK(): boolean { - return this._transmitStatus === TransmitStatus.OK; + return this.transmitStatus === TransmitStatus.OK; } public toLogEntry(): MessageOrCCLogEntry { @@ -448,26 +519,40 @@ export class SendDataMulticastBridgeRequestTransmitReport } } +export interface SendDataMulticastBridgeResponseOptions { + wasSent: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SendDataMulticastBridge) export class SendDataMulticastBridgeResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SendDataMulticastBridgeResponseOptions & MessageBaseOptions, ) { super(options); - this._wasSent = this.payload[0] !== 0; + + // TODO: Check implementation: + this.wasSent = options.wasSent; } - public isOK(): boolean { - return this._wasSent; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataMulticastBridgeResponse { + const wasSent = raw.payload[0] !== 0; + + return new this({ + wasSent, + }); } - private _wasSent: boolean; - public get wasSent(): boolean { - return this._wasSent; + public isOK(): boolean { + return this.wasSent; } + public wasSent: boolean; + public toLogEntry(): MessageOrCCLogEntry { return { ...super.toLogEntry(), diff --git a/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts b/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts index 1c6d36f277a1..8bd2a66cba72 100644 --- a/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/transport/SendDataMessages.ts @@ -1,11 +1,10 @@ -import { CommandClass, type ICommandClassContainer } from "@zwave-js/cc"; +import { type CommandClass } from "@zwave-js/cc"; import { MAX_NODES, type MessageOrCCLogEntry, MessagePriority, type MulticastCC, type MulticastDestination, - NODE_ID_BROADCAST, type SerializableTXReport, type SinglecastCC, type TXReport, @@ -21,15 +20,14 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, MessageOrigin, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -37,9 +35,11 @@ import { getEnumMemberName, num2hex } from "@zwave-js/shared"; import { clamp } from "alcalzone-shared/math"; import { ApplicationCommandRequest } from "../application/ApplicationCommandRequest"; import { BridgeApplicationCommandRequest } from "../application/BridgeApplicationCommandRequest"; +import { type MessageWithCC, containsCC } from "../utils"; import { encodeTXReport, parseTXReport, + serializableTXReportToTXReport, txReportToMessageRecord, } from "./SendDataShared"; @@ -48,71 +48,45 @@ export const MAX_SEND_ATTEMPTS = 5; @messageTypes(MessageType.Request, FunctionType.SendData) @priority(MessagePriority.Normal) export class SendDataRequestBase extends Message { - public constructor(options: MessageOptions) { - if (gotDeserializationOptions(options)) { - if ( - options.origin === MessageOrigin.Host - && (new.target as any) !== SendDataRequest - ) { - return new SendDataRequest(options); - } else if ( - options.origin !== MessageOrigin.Host - && (new.target as any) !== SendDataRequestTransmitReport - ) { - return new SendDataRequestTransmitReport(options); - } + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendDataRequestBase { + if (ctx.origin === MessageOrigin.Host) { + return SendDataRequest.from(raw, ctx); + } else { + return SendDataRequestTransmitReport.from(raw, ctx); } - super(options); } } -interface SendDataRequestOptions - extends MessageBaseOptions -{ - command: CCType; - transmitOptions?: TransmitOptions; - maxSendAttempts?: number; -} +export type SendDataRequestOptions< + CCType extends CommandClass = CommandClass, +> = + & ( + | { command: CCType } + | { + nodeId: number; + serializedCC: Buffer; + } + ) + & { + transmitOptions?: TransmitOptions; + maxSendAttempts?: number; + }; @expectedResponse(FunctionType.SendData) @expectedCallback(FunctionType.SendData) export class SendDataRequest extends SendDataRequestBase - implements ICommandClassContainer + implements MessageWithCC { public constructor( - options: MessageDeserializationOptions | SendDataRequestOptions, + options: SendDataRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - let offset = 0; - const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( - this.payload, - options.ctx.nodeIdType, - offset, - ); - offset += nodeIdBytes; - this._nodeId = nodeId; - - const serializedCCLength = this.payload[offset++]; - this.transmitOptions = this.payload[offset + serializedCCLength]; - this.callbackId = this.payload[offset + 1 + serializedCCLength]; - this.payload = this.payload.subarray( - offset, - offset + serializedCCLength, - ); - - if (options.parseCCs !== false) { - 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. - this.command = undefined as any; - } - } else { + if ("command" in options) { if ( !options.command.isSinglecast() && !options.command.isBroadcast() @@ -125,16 +99,49 @@ export class SendDataRequest this.command = options.command; this._nodeId = this.command.nodeId; - this.transmitOptions = options.transmitOptions - ?? TransmitOptions.DEFAULT; - if (options.maxSendAttempts != undefined) { - this.maxSendAttempts = options.maxSendAttempts; - } + } else { + this._nodeId = options.nodeId; + this.serializedCC = options.serializedCC; } + + this.transmitOptions = options.transmitOptions + ?? TransmitOptions.DEFAULT; + if (options.maxSendAttempts != undefined) { + this.maxSendAttempts = options.maxSendAttempts; + } + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendDataRequest { + let offset = 0; + const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( + raw.payload, + ctx.nodeIdType, + offset, + ); + offset += nodeIdBytes; + const serializedCCLength = raw.payload[offset++]; + const transmitOptions: TransmitOptions = + raw.payload[offset + serializedCCLength]; + const callbackId = raw.payload[offset + 1 + serializedCCLength]; + + const serializedCC = raw.payload.subarray( + offset, + offset + serializedCCLength, + ); + + return new this({ + transmitOptions, + callbackId, + nodeId, + serializedCC, + }); } /** The command this message contains */ - public command: SinglecastCC; + public command: SinglecastCC | undefined; /** Options regarding the transmission of the message */ public transmitOptions: TransmitOptions; @@ -149,28 +156,36 @@ export class SendDataRequest private _nodeId: number; public override getNodeId(): number | undefined { - return this._nodeId; + return this.command?.nodeId ?? this._nodeId; } - // Cache the serialized CC, so we can check if it needs to be fragmented - private _serializedCC: Buffer | undefined; + public serializedCC: Buffer | undefined; /** @internal */ public serializeCC(ctx: CCEncodingContext): Buffer { - if (!this._serializedCC) { - this._serializedCC = this.command.serialize(ctx); + if (!this.serializedCC) { + if (!this.command) { + throw new ZWaveError( + `Cannot serialize a ${this.constructor.name} without a command`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.serializedCC = this.command.serialize(ctx); } - return this._serializedCC; + return this.serializedCC; } public prepareRetransmission(): void { - this.command.prepareRetransmission(); - this._serializedCC = undefined; + this.command?.prepareRetransmission(); + this.serializedCC = undefined; this.callbackId = undefined; } public serialize(ctx: MessageEncodingContext): Buffer { this.assertCallbackId(); - const nodeId = encodeNodeID(this.command.nodeId, ctx.nodeIdType); + const nodeId = encodeNodeID( + this.command?.nodeId ?? this._nodeId, + ctx.nodeIdType, + ); const serializedCC = this.serializeCC(ctx); this.payload = Buffer.concat([ nodeId, @@ -194,8 +209,10 @@ export class SendDataRequest public expectsNodeUpdate(): boolean { return ( + // We can only answer this if the command is known + this.command != undefined // Only true singlecast commands may expect a response - this.command.isSinglecast() + && this.command.isSinglecast() // ... and only if the command expects a response && this.command.expectsCCResponse() ); @@ -203,16 +220,18 @@ export class SendDataRequest public isExpectedNodeUpdate(msg: Message): boolean { return ( - (msg instanceof ApplicationCommandRequest + // We can only answer this if the command is known + this.command != undefined + && (msg instanceof ApplicationCommandRequest || msg instanceof BridgeApplicationCommandRequest) + && containsCC(msg) && this.command.isExpectedCCResponse(msg.command) ); } } -interface SendDataRequestTransmitReportOptions extends MessageBaseOptions { +export interface SendDataRequestTransmitReportOptions { transmitStatus: TransmitStatus; - callbackId: number; txReport?: SerializableTXReport; } @@ -220,29 +239,37 @@ export class SendDataRequestTransmitReport extends SendDataRequestBase implements SuccessIndicator { public constructor( - options: - | MessageDeserializationOptions - | SendDataRequestTransmitReportOptions, + options: SendDataRequestTransmitReportOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; - // TODO: Consider NOT parsing this for transmit status other than OK or NoACK - this.txReport = parseTXReport( - this.transmitStatus !== TransmitStatus.NoAck, - this.payload.subarray(2), - ); - } else { - this.callbackId = options.callbackId; - this.transmitStatus = options.transmitStatus; - this._txReport = options.txReport; - } + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + this.txReport = options.txReport + && serializableTXReportToTXReport(options.txReport); + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + // TODO: Consider NOT parsing this for transmit status other than OK or NoACK + const txReport = parseTXReport( + transmitStatus !== TransmitStatus.NoAck, + raw.payload.subarray(2), + ); + + return new this({ + callbackId, + transmitStatus, + txReport, + }); } public transmitStatus: TransmitStatus; - private _txReport: SerializableTXReport | undefined; public txReport: TXReport | undefined; public serialize(ctx: MessageEncodingContext): Buffer { @@ -251,10 +278,10 @@ export class SendDataRequestTransmitReport extends SendDataRequestBase this.callbackId, this.transmitStatus, ]); - if (this._txReport) { + if (this.txReport) { this.payload = Buffer.concat([ this.payload, - encodeTXReport(this._txReport), + encodeTXReport(this.txReport), ]); } @@ -283,21 +310,28 @@ export class SendDataRequestTransmitReport extends SendDataRequestBase } } -export interface SendDataResponseOptions extends MessageBaseOptions { +export interface SendDataResponseOptions { wasSent: boolean; } @messageTypes(MessageType.Response, FunctionType.SendData) export class SendDataResponse extends Message implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions | SendDataResponseOptions, + options: SendDataResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.wasSent = this.payload[0] !== 0; - } else { - this.wasSent = options.wasSent; - } + this.wasSent = options.wasSent; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataResponse { + const wasSent = raw.payload[0] !== 0; + + return new this({ + wasSent, + }); } public wasSent: boolean; @@ -322,84 +356,42 @@ export class SendDataResponse extends Message implements SuccessIndicator { @messageTypes(MessageType.Request, FunctionType.SendDataMulticast) @priority(MessagePriority.Normal) export class SendDataMulticastRequestBase extends Message { - public constructor(options: MessageOptions) { - if (gotDeserializationOptions(options)) { - if ( - options.origin === MessageOrigin.Host - && (new.target as any) !== SendDataMulticastRequest - ) { - return new SendDataMulticastRequest(options); - } else if ( - options.origin !== MessageOrigin.Host - && (new.target as any) - !== SendDataMulticastRequestTransmitReport - ) { - return new SendDataMulticastRequestTransmitReport(options); - } + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendDataMulticastRequestBase { + if (ctx.origin === MessageOrigin.Host) { + return SendDataMulticastRequest.from(raw, ctx); + } else { + return SendDataMulticastRequestTransmitReport.from(raw, ctx); } - - super(options); } } -interface SendDataMulticastRequestOptions - extends MessageBaseOptions -{ - command: CCType; - transmitOptions?: TransmitOptions; - maxSendAttempts?: number; -} +export type SendDataMulticastRequestOptions = + & ( + | { command: CCType } + | { + nodeIds: MulticastDestination; + serializedCC: Buffer; + } + ) + & { + transmitOptions?: TransmitOptions; + maxSendAttempts?: number; + }; @expectedResponse(FunctionType.SendDataMulticast) @expectedCallback(FunctionType.SendDataMulticast) export class SendDataMulticastRequest< CCType extends CommandClass = CommandClass, -> extends SendDataMulticastRequestBase implements ICommandClassContainer { +> extends SendDataMulticastRequestBase implements MessageWithCC { public constructor( - options: - | MessageDeserializationOptions - | SendDataMulticastRequestOptions, + options: SendDataMulticastRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - const numNodeIDs = this.payload[0]; - let offset = 1; - const nodeIds: number[] = []; - for (let i = 0; i < numNodeIDs; i++) { - const { nodeId, bytesRead } = parseNodeID( - this.payload, - options.ctx.nodeIdType, - offset, - ); - nodeIds.push(nodeId); - offset += bytesRead; - } - this._nodeIds = nodeIds as MulticastDestination; - - const serializedCCLength = this.payload[offset]; - offset++; - const serializedCC = this.payload.subarray( - offset, - offset + serializedCCLength, - ); - offset += serializedCCLength; - this.transmitOptions = this.payload[offset]; - offset++; - this.callbackId = this.payload[offset]; - - this.payload = serializedCC; - - if (options.parseCCs !== false) { - this.command = CommandClass.parse(this.payload, { - sourceNodeId: NODE_ID_BROADCAST, // FIXME: Unknown? - ...options.ctx, - }) as MulticastCC; - } else { - // Little hack for testing with a network mock. This will be parsed in the next step. - this.command = undefined as any; - } - } else { + if ("command" in options) { if (!options.command.isMulticast()) { throw new ZWaveError( `SendDataMulticastRequest can only be used for multicast CCs`, @@ -420,16 +412,56 @@ export class SendDataMulticastRequest< } this.command = options.command; - this.transmitOptions = options.transmitOptions - ?? TransmitOptions.DEFAULT; - if (options.maxSendAttempts != undefined) { - this.maxSendAttempts = options.maxSendAttempts; - } + this.nodeIds = this.command.nodeId; + } else { + this.nodeIds = options.nodeIds; + this.serializedCC = options.serializedCC; + } + this.transmitOptions = options.transmitOptions + ?? TransmitOptions.DEFAULT; + if (options.maxSendAttempts != undefined) { + this.maxSendAttempts = options.maxSendAttempts; + } + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendDataMulticastRequest { + const numNodeIDs = raw.payload[0]; + let offset = 1; + const nodeIds: number[] = []; + for (let i = 0; i < numNodeIDs; i++) { + const { nodeId, bytesRead } = parseNodeID( + raw.payload, + ctx.nodeIdType, + offset, + ); + nodeIds.push(nodeId); + offset += bytesRead; } + const serializedCCLength = raw.payload[offset]; + offset++; + const serializedCC = raw.payload.subarray( + offset, + offset + serializedCCLength, + ); + offset += serializedCCLength; + const transmitOptions: TransmitOptions = raw.payload[offset]; + + offset++; + const callbackId: any = raw.payload[offset]; + + return new this({ + transmitOptions, + callbackId, + nodeIds: nodeIds as MulticastDestination, + serializedCC, + }); } /** The command this message contains */ - public command: MulticastCC; + public command: MulticastCC | undefined; /** Options regarding the transmission of the message */ public transmitOptions: TransmitOptions; @@ -442,37 +474,41 @@ export class SendDataMulticastRequest< this._maxSendAttempts = clamp(value, 1, MAX_SEND_ATTEMPTS); } - private _nodeIds: MulticastDestination | undefined; + public nodeIds: MulticastDestination; public override getNodeId(): number | undefined { // This is multicast, getNodeId must return undefined here return undefined; } - // Cache the serialized CC, so we can check if it needs to be fragmented - private _serializedCC: Buffer | undefined; + public serializedCC: Buffer | undefined; /** @internal */ public serializeCC(ctx: CCEncodingContext): Buffer { - if (!this._serializedCC) { - this._serializedCC = this.command.serialize(ctx); + if (!this.serializedCC) { + if (!this.command) { + throw new ZWaveError( + `Cannot serialize a ${this.constructor.name} without a command`, + ZWaveErrorCodes.Argument_Invalid, + ); + } + this.serializedCC = this.command.serialize(ctx); } - return this._serializedCC; + return this.serializedCC; } public prepareRetransmission(): void { - this.command.prepareRetransmission(); - this._serializedCC = undefined; + this.command?.prepareRetransmission(); + this.serializedCC = undefined; this.callbackId = undefined; } public serialize(ctx: MessageEncodingContext): Buffer { this.assertCallbackId(); const serializedCC = this.serializeCC(ctx); - const destinationNodeIDs = this.command.nodeId.map((id) => - encodeNodeID(id, ctx.nodeIdType) - ); + const destinationNodeIDs = (this.command?.nodeId ?? this.nodeIds) + .map((id) => encodeNodeID(id, ctx.nodeIdType)); this.payload = Buffer.concat([ // # of target nodes, not # of bytes - Buffer.from([this.command.nodeId.length]), + Buffer.from([destinationNodeIDs.length]), ...destinationNodeIDs, Buffer.from([serializedCC.length]), // payload @@ -487,7 +523,9 @@ export class SendDataMulticastRequest< return { ...super.toLogEntry(), message: { - "target nodes": this.command.nodeId.join(", "), + "target nodes": (this.command?.nodeId ?? this.nodeIds).join( + ", ", + ), "transmit options": num2hex(this.transmitOptions), "callback id": this.callbackId ?? "(not set)", }, @@ -495,11 +533,8 @@ export class SendDataMulticastRequest< } } -interface SendDataMulticastRequestTransmitReportOptions - extends MessageBaseOptions -{ +export interface SendDataMulticastRequestTransmitReportOptions { transmitStatus: TransmitStatus; - callbackId: number; } export class SendDataMulticastRequestTransmitReport @@ -508,34 +543,38 @@ export class SendDataMulticastRequestTransmitReport { public constructor( options: - | MessageDeserializationOptions - | SendDataMulticastRequestTransmitReportOptions, + & SendDataMulticastRequestTransmitReportOptions + & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.callbackId = this.payload[0]; - this._transmitStatus = this.payload[1]; - // not sure what bytes 2 and 3 mean - } else { - this.callbackId = options.callbackId; - this._transmitStatus = options.transmitStatus; - } + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; } - private _transmitStatus: TransmitStatus; - public get transmitStatus(): TransmitStatus { - return this._transmitStatus; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataMulticastRequestTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } + public transmitStatus: TransmitStatus; + public serialize(ctx: MessageEncodingContext): Buffer { this.assertCallbackId(); - this.payload = Buffer.from([this.callbackId, this._transmitStatus]); + this.payload = Buffer.from([this.callbackId, this.transmitStatus]); return super.serialize(ctx); } public isOK(): boolean { - return this._transmitStatus === TransmitStatus.OK; + return this.transmitStatus === TransmitStatus.OK; } public toLogEntry(): MessageOrCCLogEntry { @@ -552,7 +591,7 @@ export class SendDataMulticastRequestTransmitReport } } -export interface SendDataMulticastResponseOptions extends MessageBaseOptions { +export interface SendDataMulticastResponseOptions { wasSent: boolean; } @@ -561,16 +600,21 @@ export class SendDataMulticastResponse extends Message implements SuccessIndicator { public constructor( - options: - | MessageDeserializationOptions - | SendDataMulticastResponseOptions, + options: SendDataMulticastResponseOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - this.wasSent = this.payload[0] !== 0; - } else { - this.wasSent = options.wasSent; - } + this.wasSent = options.wasSent; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendDataMulticastResponse { + const wasSent = raw.payload[0] !== 0; + + return new this({ + wasSent, + }); } public wasSent: boolean; diff --git a/packages/zwave-js/src/lib/serialapi/transport/SendDataShared.ts b/packages/zwave-js/src/lib/serialapi/transport/SendDataShared.ts index 801392fb3c89..d227e42cf985 100644 --- a/packages/zwave-js/src/lib/serialapi/transport/SendDataShared.ts +++ b/packages/zwave-js/src/lib/serialapi/transport/SendDataShared.ts @@ -1,5 +1,6 @@ import { type MessageRecord, + ProtocolDataRate, type RSSI, RssiError, type SerializableTXReport, @@ -89,9 +90,9 @@ export function parseTXReport( payload: Buffer, ): TXReport | undefined { if (payload.length < 17) return; + const numRepeaters = payload[2]; const ret: TXReport = { txTicks: payload.readUInt16BE(0), - numRepeaters: payload[2], ackRSSI: includeACK ? parseRSSI(payload, 3) : undefined, ackRepeaterRSSI: includeACK ? [ @@ -125,21 +126,47 @@ export function parseTXReport( : undefined, }; // Remove unused repeaters from arrays - const firstMissingRepeater = ret.repeaterNodeIds.indexOf(0); ret.repeaterNodeIds = ret.repeaterNodeIds.slice( 0, - firstMissingRepeater, + numRepeaters, ) as any; if (ret.ackRepeaterRSSI) { ret.ackRepeaterRSSI = ret.ackRepeaterRSSI.slice( 0, - firstMissingRepeater, + numRepeaters, ) as any; } return stripUndefined(ret as any) as any; } +export function serializableTXReportToTXReport( + report: SerializableTXReport, +): TXReport { + return { + txTicks: report.txTicks, + ackRSSI: report.ackRSSI, + ackRepeaterRSSI: report.ackRepeaterRSSI, + ackChannelNo: report.ackChannelNo, + txChannelNo: report.txChannelNo ?? 0, + routeSchemeState: report.routeSchemeState ?? 0, + repeaterNodeIds: report.repeaterNodeIds ?? [], + beam1000ms: report.beam1000ms ?? false, + beam250ms: report.beam250ms ?? false, + routeSpeed: report.routeSpeed ?? ProtocolDataRate.ZWave_100k, + routingAttempts: report.routingAttempts ?? 1, + failedRouteLastFunctionalNodeId: report.failedRouteLastFunctionalNodeId, + failedRouteFirstNonFunctionalNodeId: + report.failedRouteFirstNonFunctionalNodeId, + txPower: report.txPower, + measuredNoiseFloor: report.measuredNoiseFloor, + destinationAckTxPower: report.destinationAckTxPower, + destinationAckMeasuredRSSI: report.destinationAckMeasuredRSSI, + destinationAckMeasuredNoiseFloor: + report.destinationAckMeasuredNoiseFloor, + }; +} + export function encodeTXReport(report: SerializableTXReport): Buffer { const ret = Buffer.alloc(24, 0); ret.writeUInt16BE(report.txTicks, 0); diff --git a/packages/zwave-js/src/lib/serialapi/transport/SendTestFrameMessages.ts b/packages/zwave-js/src/lib/serialapi/transport/SendTestFrameMessages.ts index 9224515076ea..e3330c3242de 100644 --- a/packages/zwave-js/src/lib/serialapi/transport/SendTestFrameMessages.ts +++ b/packages/zwave-js/src/lib/serialapi/transport/SendTestFrameMessages.ts @@ -10,14 +10,13 @@ import { FunctionType, Message, type MessageBaseOptions, - type MessageDeserializationOptions, type MessageEncodingContext, - type MessageOptions, + type MessageParsingContext, + type MessageRaw, MessageType, type SuccessIndicator, expectedCallback, expectedResponse, - gotDeserializationOptions, messageTypes, priority, } from "@zwave-js/serial"; @@ -26,18 +25,15 @@ import { getEnumMemberName } from "@zwave-js/shared"; @messageTypes(MessageType.Request, FunctionType.SendTestFrame) @priority(MessagePriority.Normal) export class SendTestFrameRequestBase extends Message { - public constructor(options: MessageOptions) { - if ( - gotDeserializationOptions(options) - && (new.target as any) !== SendTestFrameTransmitReport - ) { - return new SendTestFrameTransmitReport(options); - } - super(options); + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendTestFrameRequestBase { + return SendTestFrameTransmitReport.from(raw, ctx); } } -export interface SendTestFrameRequestOptions extends MessageBaseOptions { +export interface SendTestFrameRequestOptions { testNodeId: number; powerlevel: Powerlevel; } @@ -46,25 +42,32 @@ export interface SendTestFrameRequestOptions extends MessageBaseOptions { @expectedCallback(FunctionType.SendTestFrame) export class SendTestFrameRequest extends SendTestFrameRequestBase { public constructor( - options: MessageDeserializationOptions | SendTestFrameRequestOptions, + options: SendTestFrameRequestOptions & MessageBaseOptions, ) { super(options); - if (gotDeserializationOptions(options)) { - let offset = 0; - const { nodeId, bytesRead: nodeIdBytes } = parseNodeID( - this.payload, - options.ctx.nodeIdType, - offset, - ); - offset += nodeIdBytes; - this.testNodeId = nodeId; - - this.powerlevel = this.payload[offset++]; - this.callbackId = this.payload[offset++]; - } else { - this.testNodeId = options.testNodeId; - this.powerlevel = options.powerlevel; - } + this.testNodeId = options.testNodeId; + this.powerlevel = options.powerlevel; + } + + public static from( + raw: MessageRaw, + ctx: MessageParsingContext, + ): SendTestFrameRequest { + let offset = 0; + const { nodeId: testNodeId, bytesRead: nodeIdBytes } = parseNodeID( + raw.payload, + ctx.nodeIdType, + offset, + ); + offset += nodeIdBytes; + const powerlevel: Powerlevel = raw.payload[offset++]; + const callbackId = raw.payload[offset++]; + + return new this({ + testNodeId, + powerlevel, + callbackId, + }); } public testNodeId: number; @@ -96,13 +99,28 @@ export class SendTestFrameRequest extends SendTestFrameRequestBase { } } +export interface SendTestFrameResponseOptions { + wasSent: boolean; +} + @messageTypes(MessageType.Response, FunctionType.SendTestFrame) export class SendTestFrameResponse extends Message { public constructor( - options: MessageDeserializationOptions, + options: SendTestFrameResponseOptions & MessageBaseOptions, ) { super(options); - this.wasSent = this.payload[0] !== 0; + this.wasSent = options.wasSent; + } + + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendTestFrameResponse { + const wasSent = raw.payload[0] !== 0; + + return new this({ + wasSent, + }); } public readonly wasSent: boolean; @@ -115,16 +133,32 @@ export class SendTestFrameResponse extends Message { } } +export interface SendTestFrameTransmitReportOptions { + transmitStatus: TransmitStatus; +} + export class SendTestFrameTransmitReport extends SendTestFrameRequestBase implements SuccessIndicator { public constructor( - options: MessageDeserializationOptions, + options: SendTestFrameTransmitReportOptions & MessageBaseOptions, ) { super(options); + this.callbackId = options.callbackId; + this.transmitStatus = options.transmitStatus; + } - this.callbackId = this.payload[0]; - this.transmitStatus = this.payload[1]; + public static from( + raw: MessageRaw, + _ctx: MessageParsingContext, + ): SendTestFrameTransmitReport { + const callbackId = raw.payload[0]; + const transmitStatus: TransmitStatus = raw.payload[1]; + + return new this({ + callbackId, + transmitStatus, + }); } public transmitStatus: TransmitStatus; diff --git a/packages/zwave-js/src/lib/serialapi/utils.ts b/packages/zwave-js/src/lib/serialapi/utils.ts new file mode 100644 index 000000000000..42e157f97377 --- /dev/null +++ b/packages/zwave-js/src/lib/serialapi/utils.ts @@ -0,0 +1,54 @@ +import { CommandClass } from "@zwave-js/cc"; +import { type Message } from "@zwave-js/serial"; +import { ApplicationCommandRequest } from "./application/ApplicationCommandRequest"; +import { BridgeApplicationCommandRequest } from "./application/BridgeApplicationCommandRequest"; +import { type SendDataMessage, isSendData } from "./transport/SendDataShared"; + +export type CommandRequest = + | ApplicationCommandRequest + | BridgeApplicationCommandRequest; + +export function isCommandRequest( + msg: Message, +): msg is CommandRequest { + return msg instanceof ApplicationCommandRequest + || msg instanceof BridgeApplicationCommandRequest; +} + +export interface MessageWithCC { + serializedCC: Buffer | undefined; + command: CommandClass | undefined; +} + +export function isMessageWithCC( + msg: Message, +): msg is + | SendDataMessage + | CommandRequest +{ + return isSendData(msg) || isCommandRequest(msg); +} + +export interface ContainsSerializedCC { + serializedCC: Buffer; +} + +export function containsSerializedCC( + container: T | undefined, +): container is T & ContainsSerializedCC { + return !!container + && "serializedCC" in container + && Buffer.isBuffer(container.serializedCC); +} + +export interface ContainsCC { + command: T; +} + +export function containsCC( + container: T | undefined, +): container is T & ContainsCC { + return !!container + && "command" in container + && container.command instanceof CommandClass; +} 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 258ec82ee2a1..3c9732f9c809 100644 --- a/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts +++ b/packages/zwave-js/src/lib/test/compliance/discardInsecureCommands.test.ts @@ -64,9 +64,9 @@ integrationTest( driver.options.securityKeys!.S2_Unauthenticated!, ); controller.securityManagers.securityManager2 = smCtrlr; - controller.parsingContext.getHighestSecurityClass = - controller.encodingContext.getHighestSecurityClass = - () => SecurityClass.S2_Unauthenticated; + // controller.parsingContext.getHighestSecurityClass = + controller.encodingContext.getHighestSecurityClass = () => + SecurityClass.S2_Unauthenticated; // Respond to Nonce Get const respondToNonceGet: MockNodeBehavior = { diff --git a/packages/zwave-js/src/lib/test/driver/unresponsiveStick.test.ts b/packages/zwave-js/src/lib/test/driver/unresponsiveStick.test.ts index 6cb55c48f74d..97119f7365fe 100644 --- a/packages/zwave-js/src/lib/test/driver/unresponsiveStick.test.ts +++ b/packages/zwave-js/src/lib/test/driver/unresponsiveStick.test.ts @@ -46,7 +46,7 @@ integrationTest( mockController.autoAckHostMessages = false; const ids = await driver.sendMessage( - new GetControllerIdRequest(driver), + new GetControllerIdRequest(), { supportCheck: false }, ); @@ -97,7 +97,7 @@ integrationTest( t, () => driver.sendMessage( - new GetControllerIdRequest(driver), + new GetControllerIdRequest(), { supportCheck: false }, ), { @@ -164,7 +164,7 @@ integrationTest( t, () => driver.sendMessage( - new GetControllerIdRequest(driver), + new GetControllerIdRequest(), { supportCheck: false }, ), { diff --git a/packages/zwave-js/src/lib/zniffer/Zniffer.ts b/packages/zwave-js/src/lib/zniffer/Zniffer.ts index a7ed91dc2be2..8e4bfcf9261a 100644 --- a/packages/zwave-js/src/lib/zniffer/Zniffer.ts +++ b/packages/zwave-js/src/lib/zniffer/Zniffer.ts @@ -8,9 +8,12 @@ import { import { DeviceConfig } from "@zwave-js/config"; import { CommandClasses, + type FrameType, type LogConfig, MPDUHeaderType, type MaybeNotKnown, + NODE_ID_BROADCAST, + NODE_ID_BROADCAST_LR, type RSSI, SPANState, SecurityClass, @@ -265,7 +268,7 @@ export class Zniffer extends TypedEventEmitter { private serial: ZnifferSerialPortBase | undefined; private parsingContext: Omit< CCParsingContext, - keyof HostIDs | "sourceNodeId" | keyof SecurityManagers + keyof HostIDs | "sourceNodeId" | "frameType" | keyof SecurityManagers >; private _destroyPromise: DeferredPromise | undefined; @@ -565,6 +568,13 @@ supported frequencies: ${ } // TODO: Support parsing multicast S2 frames + const frameType: FrameType = + mpdu.headerType === MPDUHeaderType.Multicast + ? "multicast" + : (destNodeId === NODE_ID_BROADCAST + || destNodeId === NODE_ID_BROADCAST_LR) + ? "broadcast" + : "singlecast"; try { cc = CommandClass.parse( mpdu.payload, @@ -572,6 +582,7 @@ supported frequencies: ${ homeId: mpdu.homeId, ownNodeId: destNodeId, sourceNodeId: mpdu.sourceNodeId, + frameType, securityManager: destSecurityManager, securityManager2: destSecurityManager2, securityManagerLR: destSecurityManagerLR, diff --git a/test/decodeMessage.ts b/test/decodeMessage.ts index df22dbc05bb1..8eba4c6a7ca7 100644 --- a/test/decodeMessage.ts +++ b/test/decodeMessage.ts @@ -2,7 +2,6 @@ import "reflect-metadata"; import "zwave-js"; -import { isCommandClassContainer } from "@zwave-js/cc"; import { ConfigManager } from "@zwave-js/config"; import { NodeIDType, @@ -14,6 +13,7 @@ import { generateEncryptionKey, } from "@zwave-js/core"; import { Message } from "@zwave-js/serial"; +import { containsCC } from "zwave-js"; (async () => { const configManager = new ConfigManager(); @@ -54,7 +54,6 @@ import { Message } from "@zwave-js/serial"; receiverEI: Buffer.from("3664023a7971465342fe3d82ebb4b8e9", "hex"), }); - console.log(Message.getMessageLength(data)); const host = { getSafeCCVersion: () => 1, getSupportedCCVersion: () => 1, @@ -97,9 +96,9 @@ import { Message } from "@zwave-js/serial"; getHighestSecurityClass: () => SecurityClass.S2_AccessControl, hasSecurityClass: () => true, }; - const msg = Message.from({ data, ctx: ctx as any }); + const msg = Message.parse(data, ctx as any); - if (isCommandClassContainer(msg)) { + if (containsCC(msg)) { msg.command.mergePartialCCs([], {} as any); } msg;