Skip to content

Commit

Permalink
feat: add compliance tests for Z-Wave certification, answer requests …
Browse files Browse the repository at this point in the history
…with the same encapsulation (#4832)
  • Loading branch information
AlCalzone committed Jul 26, 2022
1 parent a1b896e commit 73c3798
Show file tree
Hide file tree
Showing 20 changed files with 462 additions and 91 deletions.
21 changes: 19 additions & 2 deletions packages/cc/src/cc/CRC16CC.ts
@@ -1,7 +1,9 @@
import type { Maybe, MessageOrCCLogEntry } from "@zwave-js/core/safe";
import {
CommandClasses,
CRC16_CCITT,
EncapsulationFlags,
Maybe,
MessageOrCCLogEntry,
validatePayload,
} from "@zwave-js/core/safe";
import type { ZWaveApplicationHost, ZWaveHost } from "@zwave-js/host/safe";
Expand Down Expand Up @@ -56,15 +58,30 @@ export class CRC16CCAPI extends CCAPI {
export class CRC16CC extends CommandClass {
declare ccCommand: CRC16Command;

/** Tests if a command should be supervised and thus requires encapsulation */
public static requiresEncapsulation(cc: CommandClass): boolean {
return (
!!(cc.encapsulationFlags & EncapsulationFlags.CRC16) &&
!(cc instanceof CRC16CCCommandEncapsulation)
);
}

/** Encapsulates a command in a CRC-16 CC */
public static encapsulate(
host: ZWaveHost,
cc: CommandClass,
): CRC16CCCommandEncapsulation {
return new CRC16CCCommandEncapsulation(host, {
const ret = new CRC16CCCommandEncapsulation(host, {
nodeId: cc.nodeId,
encapsulated: cc,
});

// Copy the encapsulation flags from the encapsulated command
// but omit CRC-16, since we're doing that right now
ret.encapsulationFlags =
cc.encapsulationFlags & ~EncapsulationFlags.CRC16;

return ret;
}
}

Expand Down
15 changes: 11 additions & 4 deletions packages/cc/src/cc/MultiChannelCC.ts
Expand Up @@ -368,18 +368,26 @@ export class MultiChannelCC extends CommandClass {
CommandClasses["Multi Channel"],
cc.nodeId as number,
);
let ret:
| MultiChannelCCCommandEncapsulation
| MultiChannelCCV1CommandEncapsulation;
if (ccVersion === 1) {
return new MultiChannelCCV1CommandEncapsulation(host, {
ret = new MultiChannelCCV1CommandEncapsulation(host, {
nodeId: cc.nodeId,
encapsulated: cc,
});
} else {
return new MultiChannelCCCommandEncapsulation(host, {
ret = new MultiChannelCCCommandEncapsulation(host, {
nodeId: cc.nodeId,
encapsulated: cc,
destination: cc.endpointIndex,
});
}

// Copy the encapsulation flags from the encapsulated command
ret.encapsulationFlags = cc.encapsulationFlags;

return ret;
}

public skipEndpointInterview(): boolean {
Expand Down Expand Up @@ -1170,8 +1178,7 @@ export class MultiChannelCCCommandEncapsulation extends MultiChannelCC {
this.encapsulated = options.encapsulated;
options.encapsulated.encapsulatingCC = this as any;
this.destination = options.destination;
// If the encapsulated command requires security, so does this one
if (this.encapsulated.secure) this.secure = true;

if (
this.host.getDeviceConfig?.(this.nodeId as number)?.compat
?.treatDestinationEndpointAsSource
Expand Down
25 changes: 20 additions & 5 deletions packages/cc/src/cc/MultiCommandCC.ts
@@ -1,5 +1,9 @@
import type { Maybe, MessageOrCCLogEntry } from "@zwave-js/core/safe";
import { CommandClasses, validatePayload } from "@zwave-js/core/safe";
import {
CommandClasses,
EncapsulationFlags,
validatePayload,
} from "@zwave-js/core/safe";
import type { ZWaveApplicationHost, ZWaveHost } from "@zwave-js/host/safe";
import { validateArgs } from "@zwave-js/transformers";
import { CCAPI } from "../lib/API";
Expand Down Expand Up @@ -62,17 +66,28 @@ export class MultiCommandCC extends CommandClass {
);
}

/** Encapsulates a command that targets a specific endpoint */
public static encapsulate(
host: ZWaveHost,
CCs: CommandClass[],
): MultiCommandCCCommandEncapsulation {
return new MultiCommandCCCommandEncapsulation(host, {
const ret = new MultiCommandCCCommandEncapsulation(host, {
nodeId: CCs[0].nodeId,
encapsulated: CCs,
// MultiCommand CC is wrapped inside Supervision CC, so the supervision status must be preserved
supervised: CCs.some((cc) => cc.supervised),
});

// Copy the "sum" of the encapsulation flags from the encapsulated CCs
for (const flag of [
EncapsulationFlags.Supervision,
EncapsulationFlags.Security,
EncapsulationFlags.CRC16,
] as const) {
ret.setEncapsulationFlag(
flag,
CCs.some((cc) => cc.encapsulationFlags & flag),
);
}

return ret;
}
}

Expand Down
14 changes: 12 additions & 2 deletions packages/cc/src/cc/Security2CC.ts
Expand Up @@ -24,6 +24,7 @@ import {
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import { EncapsulationFlags } from "@zwave-js/core/safe";
import type { ZWaveApplicationHost, ZWaveHost } from "@zwave-js/host/safe";
import { gotDeserializationOptions } from "@zwave-js/serial";
import { buffer2hex, getEnumMemberName, pick } from "@zwave-js/shared/safe";
Expand Down Expand Up @@ -503,7 +504,9 @@ export class Security2CC extends CommandClass {
/** Tests if a command should be sent secure and thus requires encapsulation */
public static requiresEncapsulation(cc: CommandClass): boolean {
// Everything that's not an S2 CC needs to be encapsulated if the CC is secure
if (!cc.secure) return false;
if (!(cc.encapsulationFlags & EncapsulationFlags.Security)) {
return false;
}
if (!(cc instanceof Security2CC)) return true;
// These S2 commands need additional encapsulation
switch (cc.ccCommand) {
Expand Down Expand Up @@ -541,11 +544,18 @@ export class Security2CC extends CommandClass {
cc: CommandClass,
securityClass?: SecurityClass,
): Security2CCMessageEncapsulation {
return new Security2CCMessageEncapsulation(host, {
const ret = new Security2CCMessageEncapsulation(host, {
nodeId: cc.nodeId,
encapsulated: cc,
securityClass,
});

// Copy the encapsulation flags from the encapsulated command
// but omit Security, since we're doing that right now
ret.encapsulationFlags =
cc.encapsulationFlags & ~EncapsulationFlags.Security;

return ret;
}
}

Expand Down
12 changes: 10 additions & 2 deletions packages/cc/src/cc/SecurityCC.ts
Expand Up @@ -2,6 +2,7 @@ import {
CommandClasses,
computeMAC,
decryptAES128OFB,
EncapsulationFlags,
encryptAES128OFB,
generateAuthKey,
generateEncryptionKey,
Expand Down Expand Up @@ -361,7 +362,7 @@ export class SecurityCC extends CommandClass {
/** Tests if a command should be sent secure and thus requires encapsulation */
public static requiresEncapsulation(cc: CommandClass): boolean {
return (
cc.secure &&
!!(cc.encapsulationFlags & EncapsulationFlags.Security) &&
// Already encapsulated (SecurityCCCommandEncapsulationNonceGet is a subclass)
!(cc instanceof Security2CC) &&
!(cc instanceof SecurityCCCommandEncapsulation) &&
Expand All @@ -379,10 +380,17 @@ export class SecurityCC extends CommandClass {
cc: CommandClass,
): SecurityCCCommandEncapsulation {
// TODO: When to return a SecurityCCCommandEncapsulationNonceGet?
return new SecurityCCCommandEncapsulation(host, {
const ret = new SecurityCCCommandEncapsulation(host, {
nodeId: cc.nodeId,
encapsulated: cc,
});

// Copy the encapsulation flags from the encapsulated command
// but omit Security, since we're doing that right now
ret.encapsulationFlags =
cc.encapsulationFlags & ~EncapsulationFlags.Security;

return ret;
}
}

Expand Down
43 changes: 20 additions & 23 deletions packages/cc/src/cc/SupervisionCC.ts
@@ -1,6 +1,7 @@
import {
CommandClasses,
Duration,
EncapsulationFlags,
isTransmissionError,
IZWaveEndpoint,
Maybe,
Expand Down Expand Up @@ -75,35 +76,23 @@ export class SupervisionCCAPI extends PhysicalCCAPI {
return super.supportsCommand(cmd);
}

public async sendEncapsulated(
encapsulated: CommandClass,
// If possible, keep us updated about the progress
requestStatusUpdates: boolean = true,
): Promise<void> {
this.assertSupportsCommand(SupervisionCommand, SupervisionCommand.Get);

const cc = new SupervisionCCGet(this.applHost, {
nodeId: this.endpoint.nodeId,
requestStatusUpdates,
encapsulated,
});
await this.applHost.sendCommand(cc, this.commandOptions);
}

public async sendReport(
options: SupervisionCCReportOptions & { secure?: boolean },
options: SupervisionCCReportOptions & {
encapsulationFlags?: EncapsulationFlags;
},
): Promise<void> {
// Here we don't assert support - some devices only half-support Supervision, so we treat them
// as if they don't support it. We still need to be able to respond to the Get command though.

const { secure = false, ...cmdOptions } = options;
const { encapsulationFlags = EncapsulationFlags.None, ...cmdOptions } =
options;
const cc = new SupervisionCCReport(this.applHost, {
nodeId: this.endpoint.nodeId,
...cmdOptions,
});

// The report should be sent back with security if the received command was secure
cc.secure = secure;
// The report must be sent back with the same encapsulation order
cc.encapsulationFlags = encapsulationFlags;

try {
await this.applHost.sendCommand(cc, {
Expand Down Expand Up @@ -137,7 +126,10 @@ export class SupervisionCC extends CommandClass {

/** Tests if a command should be supervised and thus requires encapsulation */
public static requiresEncapsulation(cc: CommandClass): boolean {
return cc.supervised && !(cc instanceof SupervisionCCGet);
return (
!!(cc.encapsulationFlags & EncapsulationFlags.Supervision) &&
!(cc instanceof SupervisionCCGet)
);
}

/** Encapsulates a command that targets a specific endpoint */
Expand All @@ -153,13 +145,20 @@ export class SupervisionCC extends CommandClass {
);
}

return new SupervisionCCGet(host, {
const ret = new SupervisionCCGet(host, {
nodeId: cc.nodeId,
// Supervision CC is wrapped inside MultiChannel CCs, so the endpoint must be copied
endpoint: cc.endpointIndex,
encapsulated: cc,
requestStatusUpdates,
});

// Copy the encapsulation flags from the encapsulated command
// but omit Supervision, since we're doing that right now
ret.encapsulationFlags =
cc.encapsulationFlags & ~EncapsulationFlags.Supervision;

return ret;
}

/**
Expand Down Expand Up @@ -375,8 +374,6 @@ export class SupervisionCCGet extends SupervisionCC {
this.requestStatusUpdates = options.requestStatusUpdates;
this.encapsulated = options.encapsulated;
options.encapsulated.encapsulatingCC = this as any;
// If the encapsulated command requires security, so does this one
if (this.encapsulated.secure) this.secure = true;
}
}

Expand Down
41 changes: 23 additions & 18 deletions packages/cc/src/lib/CommandClass.ts
@@ -1,5 +1,6 @@
import {
CommandClasses,
EncapsulationFlags,
getCCName,
ICommandClass,
isZWaveError,
Expand Down Expand Up @@ -79,7 +80,6 @@ export function gotDeserializationOptions(
export interface CCCommandOptions {
nodeId: number | MulticastDestination;
endpoint?: number;
supervised?: boolean;
}

interface CommandClassCreationOptions extends CCCommandOptions {
Expand Down Expand Up @@ -108,9 +108,6 @@ export class CommandClass implements ICommandClass {
// Default to the root endpoint - Inherited classes may override this behavior
this.endpointIndex =
("endpoint" in options ? options.endpoint : undefined) ?? 0;
// Default to non-supervised commands
this.supervised =
("supervised" in options ? options.supervised : undefined) ?? false;

// We cannot use @ccValue for non-derived classes, so register interviewComplete as an internal value here
// this.registerValue("interviewComplete", { internal: true });
Expand Down Expand Up @@ -177,10 +174,13 @@ export class CommandClass implements ICommandClass {
);

// Send secure commands if necessary
this.secure = this.host.isCCSecure(
this.ccId,
this.nodeId,
this.endpointIndex,
this.setEncapsulationFlag(
EncapsulationFlags.Security,
this.host.isCCSecure(
this.ccId,
this.nodeId,
this.endpointIndex,
),
);
} else {
// For multicast and broadcast CCs, we just use the highest implemented version to serialize
Expand Down Expand Up @@ -212,22 +212,27 @@ export class CommandClass implements ICommandClass {
public endpointIndex: number;

/**
* Whether the command progress should be supervised.
* This only has an effect if the target endpoint supports the Supervision CC.
* Which encapsulation CCs this CC is/was/should be encapsulated with.
*
* Don't use this directly, but rather use `Driver.sendCommand` with the corresponding supervision options.
* Don't use this directly, this is used internally.
*/
public supervised: boolean;
public encapsulationFlags: EncapsulationFlags = EncapsulationFlags.None;

/** Activates or deactivates the given encapsulation flag */
public setEncapsulationFlag(
flag: EncapsulationFlags,
active: boolean,
): void {
if (active) {
this.encapsulationFlags |= flag;
} else {
this.encapsulationFlags &= ~flag;
}
}

/** Contains a reference to the encapsulating CC if this CC is encapsulated */
public encapsulatingCC?: EncapsulatingCommandClass;

/**
* Whether the command should be sent encrypted
* This only has an effect if the target node supports Security.
*/
public secure: boolean = false;

/** Returns true if this CC is an extended CC (0xF100..0xFFFF) */
public isExtended(): boolean {
return this.ccId >= 0xf100;
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/consts/Transmission.ts
Expand Up @@ -162,6 +162,14 @@ export interface SendMessageOptions {
onTXReport?: (report: TXReport) => void;
}

export enum EncapsulationFlags {
None = 0,
Supervision = 1 << 0,
// Multi Channel is tracked through the endpoint index
Security = 1 << 1,
CRC16 = 1 << 2,
}

export type SupervisionOptions =
| ({
/** Whether supervision may be used. `false` disables supervision. Default: `"auto"`. */
Expand All @@ -185,6 +193,8 @@ export type SendCommandOptions = SendMessageOptions &
maxSendAttempts?: number;
/** Whether the driver should automatically handle the encapsulation. Default: true */
autoEncapsulate?: boolean;
/** Used to send a response with the same encapsulation flags as the corresponding request. */
encapsulationFlags?: EncapsulationFlags;
/** Overwrite the default transmit options */
transmitOptions?: TransmitOptions;
};
Expand Down

0 comments on commit 73c3798

Please sign in to comment.