Skip to content

Commit

Permalink
refactor: decouple CCs and messages from host, split parsing and crea…
Browse files Browse the repository at this point in the history
…tion, split ZWaveNode class (#7305)
  • Loading branch information
AlCalzone authored Oct 25, 2024
1 parent 1f00b76 commit 249567d
Show file tree
Hide file tree
Showing 387 changed files with 34,775 additions and 25,800 deletions.
642 changes: 419 additions & 223 deletions .vscode/typescript.code-snippets

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/api/endpoint.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ createCCInstanceUnsafe<T>(cc: CommandClasses): T | undefined

Like [`createCCInstance`](#createCCInstance) but returns `undefined` instead of throwing when a CC is not supported.

### `getNodeUnsafe`
### `tryGetNode`

```ts
getNodeUnsafe(): ZWaveNode | undefined
tryGetNode(): ZWaveNode | undefined
```

Returns the node this endpoint belongs to (or undefined if the node doesn't exist).
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started/migrating/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Migrating from previous versions

- [Migrating to v14](getting-started/migrating/v14.md)
- [Migrating to v13](getting-started/migrating/v13.md)
- [Migrating to v12](getting-started/migrating/v12.md)
- [Migrating to v11](getting-started/migrating/v11.md)
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started/migrating/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Security S2](getting-started/security-s2.md)
- [Z-Wave Long Range](getting-started/long-range.md)
- [Migrating from previous versions](getting-started/migrating/)
- [Migrating to v14](getting-started/migrating/v14.md)
- [Migrating to v13](getting-started/migrating/v13.md)
- [Migrating to v12](getting-started/migrating/v12.md)
- [Migrating to v11](getting-started/migrating/v11.md)
Expand Down
195 changes: 195 additions & 0 deletions docs/getting-started/migrating/v14.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
# Migrating to v14 <!-- {docsify-ignore-all} -->

This version became necessary more or less by accident. I originally planned to split some of the files that have grown too large (like `Node.ts`) into multiple smaller chunks. During this refactor, I noticed that several methods depended on a full-blown driver instance (or an appropriate abstraction), when all that was needed was access to the value DB for example. This problem appeared throughout the entire codebase, so I decided to do something about it.

The `ZWaveHost` and `ZWaveApplicationHost` interfaces have been replaced by multiple smaller interfaces, each defining a specific subset of the functionality. All the code that previously expected to be passed one of those interfaces has been update to just accept what it actually needs. The `Driver` class still implements all of this functionality.

Furthermore, `Message` and `CommandClass` implementations are no longer bound to a specific host instance. Instead, their methods that need access to host functionality (like value DBs, home ID, device configuration, etc.) now receive a method-specific context object. Parsing of those instances no longer happens in the constructor, but in a separate `from` method.

All in all, this release contains a huge list of breaking changes, but most of those should not affect any application, unless very low-level APIs are frequently used.

## Decoupled Serial API messages from host instances, split constructors and parsing

`Message` instances no longer store a reference to their host. They are now "just data". Therefore, message constructors no longer take an instance of `ZWaveHost` as the first parameter. All needed information is passed to the relevant methods as context arguments.

**Old: constructor for creation and parsing**

```ts
public constructor(
options:
| MessageDeserializationOptions
| MyMessageOptions,
) {
super(options);

if (gotDeserializationOptions(options)) {
// deserialize message from this.payload
} else {
// populate message properties from options
}
}
```

**New: constructor for creation, separate method for parsing**

```ts
public constructor(
options: MyMessageOptions & MessageBaseOptions,
) {
super(options);
// populate message properties from options
}

public static from(
raw: MessageRaw,
ctx: MessageParsingContext,
): MyMessage {
// deserialize message from raw.payload

return new this({
// ...
});
}
```

> [!NOTE]: For messages that contain a serialized CC instance, Message.parse no longer deserializes it automatically. This has to be done in a separate step.
## Decoupled CCs from host instances, split constructors and parsing

`CommandClass` instances no longer store a reference to their host. They are now "just data". Therefore, CC constructors no longer take an instance of `ZWaveHost` as the first parameter. All needed information is passed to the relevant methods as context arguments.

**Old: constructor for creation and parsing**

```ts
public constructor(
host: ZWaveHost,
options: CommandClassDeserializationOptions | BinarySwitchCCSetOptions,
) {
super(host, options);
if (gotDeserializationOptions(options)) {
validatePayload(this.payload.length >= 1);
this.targetValue = !!this.payload[0];
if (this.payload.length >= 2) {
this.duration = Duration.parseSet(this.payload[1]);
}
} else {
this.targetValue = options.targetValue;
this.duration = Duration.from(options.duration);
}
}
```

**New: constructor for creation, separate method for parsing**

```ts
public constructor(
options: WithAddress<BinarySwitchCCSetOptions>,
) {
super(options);
this.targetValue = options.targetValue;
this.duration = Duration.from(options.duration);
}

public static from(raw: CCRaw, ctx: CCParsingContext): BinarySwitchCCSet {
validatePayload(raw.payload.length >= 1);
const targetValue = !!raw.payload[0];

let duration: Duration | undefined;
if (raw.payload.length >= 2) {
duration = Duration.parseSet(raw.payload[1]);
}

return new BinarySwitchCCSet({
nodeId: ctx.sourceNodeId,
targetValue,
duration,
});
}
```

**Updated CC method signatures:**

```diff
- interview(applHost: ZWaveApplicationHost): Promise<void>;
+ interview(ctx: InterviewContext): Promise<void>;

- refreshValues(applHost: ZWaveApplicationHost): Promise<void>;
+ refreshValues(ctx: RefreshValuesContext): Promise<void>;

- persistValues(applHost: ZWaveApplicationHost): Promise<void>;
+ persistValues(ctx: PersistValuesContext): Promise<void>;

- toLogEntry(host?: ZWaveValueHost): MessageOrCCLogEntry;
+ toLogEntry(ctx?: GetValueDB): MessageOrCCLogEntry;
```

> [!NOTE]: Applications calling these methods can simply pass the driver instance.
## Moved Serial API message implementations to `@zwave-js/serial` package

All serial API message implementations have been moved to the `@zwave-js/serial` package. In addition to the main entry point, they are also available via `@zwave-js/serial/serialapi`.

## Other notable changes

These are the only ones I found to break dependent projects.

- All `getNodeUnsafe` methods have been renamed to `tryGetNode` to better indicate what they do - it was not really clear what was "unsafe" about them.
- The `CommandClass.version` property has been removed. To determine the CC version a node or endpoint supports, use the `node.getCCVersion(cc)` or `endpoint.getCCVersion(cc)` methods.
- The `TXReport` object no longer has the `numRepeaters` property, as this was implied by the length of the `repeaterNodeIds` array.

## Other breaking changes

Most of these should not affect any application:

- The `ZWaveHost` and `ZWaveApplicationHost` interfaces have been split into multiple smaller "traits"
- The `Driver` class still implements all those traits, but receiving functions now just require the necessary subset.
- CC Constructors no longer take an instance of `ZWaveHost` as the first parameter. All needed information has been merged into the `CCParsingContext` type.
- The `CommandClass.from` method no longer takes an instance of `ZWaveHost` as the first parameter for the same reason.
- `CommandClass` instances no longer store a reference to their host. They are now "just data".
- The `serialize()` implementations of CCs now take an argument of type `CCEncodingContext`
- The `toLogEntry()` implementations of CCs now take an argument of type `GetValueDB` instead of `ZWaveHost`
- The `interview()` implementations of CCs now take an argument of type `InterviewContext` instead of `ZWaveApplicationHost`
- The `refreshValues()` implementations of CCs now take an argument of type `RefreshValuesContext` instead of `ZWaveApplicationHost`
- The `persistValues()` implementations of CCs now take an argument of type `PersistValuesContext` instead of `ZWaveApplicationHost`
- The `translateProperty()` and `translatePropertyKey()` implementations of CCs now take an argument of type `GetValueDB` instead of `ZWaveApplicationHost`
- The `mergePartialCCs` signature has changed:
```diff
- mergePartialCCs(applHost: ZWaveApplicationHost, partials: CommandClass[]): void;
+ mergePartialCCs(partials: CommandClass[], ctx: CCParsingContext): void;
```
- The `ICommandClass` interface has been removed. Where applicable, it has been replaced with the `CCId` interface whose sole purpose is to identify the command and destination of a CC.
- The `IZWaveNode`, `IZWaveEndpoint`, `IVirtualNode` and `IVirtualEndpoint` interfaces have been replaced with more specific "trait" interfaces.
- The `TestingHost`, `TestNode` and `TestEndpoint` interfaces and implementations have been reworked to be more declarative and require less input in tests.
- Serial API `Message` constructors no longer take an instance of `ZWaveHost` as the first parameter. All needed information has been merged into the `MessageParsingContext` type.
- The `Message.from` method no longer takes an instance of `ZWaveHost` as the first parameter for the same reason.
- The `callbackId` of a `Message` instance is no longer automatically generated when reading it for the first time. Instead, the `callbackId` can be `undefined` and has to be explicitly set before serializing if necessary.
- The `serialize()` implementations of `Message`s now take an argument of type `MessageEncodingContext`
- The `Message.getNodeUnsafe()` method has been renamed to `Message.tryGetNode`
- The `Driver.getNodeUnsafe(message)` method has been renamed to `Driver.tryGetNode(message)`
- `Driver.controllerLog` is no longer public.
- `Driver.getSafeCCVersion` can now return `undefined` if the requested CC is not implemented in `zwave-js`
- `Driver.isControllerNode(nodeId)` has been removed. Compare with `ownNodeId` instead or use the `Node.isControllerNode` property.
- The `Endpoint.getNodeUnsafe()` method has been renamed to `tryGetNode`
- The `[CCName]CC[CCCommand]Options` interfaces no longer extend another interface and now exclusively contain the CC-specific properties required for constructing that particular CC
- CC-specific constructors now accept a single options object of type `WithAddress<...>`, which is the CC-specific options interface, extended by `nodeId` and `endpointIndex`. Note that `endpointIndex` was previously just called `endpoint`.
- The `CommandClassDeserializationOptions` interface and the `gotDeserializationOptions` method no longer exist. Instead, CCs parse their specific payload using the new static `from` method, which takes a pre-parsed `CCRaw` (ccId, ccCommand, binary payload) and the `CCParsingContext`. All CCs with a custom constructor implementations are expected to implement this aswell.
To parse an arbitrary CC buffer, use `CommandClass.parse(buffer, context)`.
- Several CCs had their constructor options type reworked slightly for correctness. In some cases it is now necessary to pass properties that were previously inferred.
- The `CommandClassCreationOptions` type has been merged into the `CommandClassOptions` type, which now consists only of the CC destination and optional overrides for ccId, ccCommand and payload.
- `CommandClass.deserialize` has been removed. It's functionality is now provided by the `parse` method and `CCRaw`.
- `CommandClass.getCommandClass`, `CommandClass.getCCCommand` have been removed. Their functionality is now provided by `CCRaw.parse`
- `CommandClass.getConstructor` has been removed. If really needed, the functionality is available via the `getCCConstructor` and `getCCCommandConstructor` methods exported by `@zwave-js/cc`.
- The `origin` property of the `CommandClass` class was removed, since it served no real purpose.
- The `[MessageName]Options` interfaces no longer extend another interface and now exclusively contain the message-specific properties required for constructing that particular instance
- Message-specific constructors now accept a single options object of type `[MessageName]Options & MessageBaseOptions`.
- The `MessageDeserializationOptions` interface and the `gotDeserializationOptions` method no longer exist
- The `DeserializingMessageConstructor` interface no longer exists
- Instead, message instances parse their specific payload using the new static `from` method, which takes a pre-parsed `MessageRaw` (message type, function type, binary payload) and the `MessageParsingContext`. All Message implementations with a custom constructor are expected to implement the `from` method aswell.
To parse an arbitrary message buffer use `Message.parse(buffer, context)`.
- The `MessageCreationOptions` type has been renamed to `MessageOptions`
- Several Message implementations had their constructor options type reworked slightly for correctness. In some cases it is now necessary to pass properties that were previously inferred.
- The static `Message` methods `extractPayload`, `getConstructor`, `getMessageLength` and `isComplete` were removed.
- The `Message.options` property was removed
- The `ICommandClassContainer` interface was replaced with the new `ContainsCC` interface, which indicates that something contains a deserialized CC instance.
- The `Driver.computeNetCCPayloadSize` method now requires that the passed message instance contains a deserialized CC instance.
- The `INodeQuery` interface and the `isNodeQuery` function were removed. To test whether a message references a node, use `hasNodeId(msg)` instead, which communicates the intent more clearly.
4 changes: 2 additions & 2 deletions docs/usage/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The only downside is that you won't necessarily receive any responses that would
Here's an example how to turn off the LED on a **Aeotec Gen 5 stick**.

```ts
const turnLEDOff = new Message(driver, {
const turnLEDOff = new Message({
type: MessageType.Request,
functionType: 0xf2,
payload: Buffer.from("5101000501", "hex"),
Expand All @@ -28,7 +28,7 @@ await driver.sendMessage(turnLEDOff, {
This example sends an `Anti-theft Get` command (which is currently unsupported) to node 2:

```ts
const cc = new CommandClass(driver, {
const cc = new CommandClass({
nodeId: 2,
ccId: CommandClasses["Anti-theft"], // or 0x5d
ccCommand: 0x02,
Expand Down
Loading

0 comments on commit 249567d

Please sign in to comment.