Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mock-server): add mocks for Binary and Color Switch CC #7056

Merged
merged 2 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 106 additions & 35 deletions packages/cc/src/cc/ColorSwitchCC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ import {
supervisedCommandSucceeded,
validatePayload,
} from "@zwave-js/core";
import { type MaybeNotKnown } from "@zwave-js/core/safe";
import { type MaybeNotKnown, encodeBitMask } from "@zwave-js/core/safe";
import type {
ZWaveApplicationHost,
ZWaveHost,
ZWaveValueHost,
} from "@zwave-js/host/safe";
import {
type AllOrNone,
getEnumMemberName,
isEnumMember,
keysOf,
Expand Down Expand Up @@ -671,26 +672,46 @@ export class ColorSwitchCC extends CommandClass {
}
}

// @publicAPI
export interface ColorSwitchCCSupportedReportOptions {
supportedColorComponents: readonly ColorComponent[];
}

@CCCommand(ColorSwitchCommand.SupportedReport)
export class ColorSwitchCCSupportedReport extends ColorSwitchCC {
public constructor(
host: ZWaveHost,
options: CommandClassDeserializationOptions,
options:
| CommandClassDeserializationOptions
| (ColorSwitchCCSupportedReportOptions & CCCommandOptions),
) {
super(host, options);

// Docs say 'variable length', but the table shows 2 bytes.
validatePayload(this.payload.length >= 2);
if (gotDeserializationOptions(options)) {
// Docs say 'variable length', but the table shows 2 bytes.
validatePayload(this.payload.length >= 2);

this.supportedColorComponents = parseBitMask(
this.payload.subarray(0, 2),
ColorComponent["Warm White"],
);
this.supportedColorComponents = parseBitMask(
this.payload.subarray(0, 2),
ColorComponent["Warm White"],
);
} else {
this.supportedColorComponents = options.supportedColorComponents;
}
}

@ccValue(ColorSwitchCCValues.supportedColorComponents)
public readonly supportedColorComponents: readonly ColorComponent[];

public serialize(): Buffer {
this.payload = encodeBitMask(
this.supportedColorComponents,
15, // fixed 2 bytes
ColorComponent["Warm White"],
);
return super.serialize();
}

public toLogEntry(host?: ZWaveValueHost): MessageOrCCLogEntry {
return {
...super.toLogEntry(host),
Expand All @@ -707,21 +728,41 @@ export class ColorSwitchCCSupportedReport extends ColorSwitchCC {
@expectedCCResponse(ColorSwitchCCSupportedReport)
export class ColorSwitchCCSupportedGet extends ColorSwitchCC {}

// @publicAPI
export type ColorSwitchCCReportOptions =
& {
colorComponent: ColorComponent;
currentValue: number;
}
& AllOrNone<{
targetValue: number;
duration: Duration | string;
}>;

@CCCommand(ColorSwitchCommand.Report)
export class ColorSwitchCCReport extends ColorSwitchCC {
public constructor(
host: ZWaveHost,
options: CommandClassDeserializationOptions,
options:
| CommandClassDeserializationOptions
| (ColorSwitchCCReportOptions & CCCommandOptions),
) {
super(host, options);

validatePayload(this.payload.length >= 2);
this.colorComponent = this.payload[0];
this.currentValue = this.payload[1];
if (gotDeserializationOptions(options)) {
validatePayload(this.payload.length >= 2);
this.colorComponent = this.payload[0];
this.currentValue = this.payload[1];

if (this.version >= 3 && this.payload.length >= 4) {
this.targetValue = this.payload[2];
this.duration = Duration.parseReport(this.payload[3]);
if (this.version >= 3 && this.payload.length >= 4) {
this.targetValue = this.payload[2];
this.duration = Duration.parseReport(this.payload[3]);
}
} else {
this.colorComponent = options.colorComponent;
this.currentValue = options.currentValue;
this.targetValue = options.targetValue;
this.duration = Duration.from(options.duration);
}
}

Expand Down Expand Up @@ -803,6 +844,23 @@ export class ColorSwitchCCReport extends ColorSwitchCC {
@ccValue(ColorSwitchCCValues.duration)
public readonly duration: Duration | undefined;

public serialize(): Buffer {
this.payload = Buffer.from([
this.colorComponent,
this.currentValue,
]);
if (this.targetValue != undefined && this.duration != undefined) {
this.payload = Buffer.concat([
this.payload,
Buffer.from([
this.targetValue ?? 0xfe,
(this.duration ?? Duration.default()).serializeReport(),
]),
]);
}
return super.serialize();
}

public toLogEntry(host?: ZWaveValueHost): MessageOrCCLogEntry {
const message: MessageRecord = {
"color component": getEnumMemberName(
Expand Down Expand Up @@ -845,11 +903,8 @@ export class ColorSwitchCCGet extends ColorSwitchCC {
) {
super(host, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 1);
this._colorComponent = this.payload[0];
} else {
this._colorComponent = options.colorComponent;
}
Expand Down Expand Up @@ -903,11 +958,23 @@ export class ColorSwitchCCSet extends ColorSwitchCC {
) {
super(host, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 1);
const populatedColorCount = this.payload[0] & 0b11111;

validatePayload(this.payload.length >= 1 + populatedColorCount * 2);
this.colorTable = {};
let offset = 1;
for (let color = 0; color < populatedColorCount; color++) {
const component = this.payload[offset];
const value = this.payload[offset + 1];
const key = colorComponentToTableKey(component);
// @ts-expect-error
if (key) this.colorTable[key] = value;
offset += 2;
}
if (this.payload.length > offset) {
this.duration = Duration.parseSet(this.payload[offset]);
}
} else {
// Populate properties from options object
if ("hexColor" in options) {
Expand Down Expand Up @@ -1012,11 +1079,18 @@ export class ColorSwitchCCStartLevelChange extends ColorSwitchCC {
) {
super(host, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 3);
const ignoreStartLevel = (this.payload[0] & 0b0_0_1_00000) >>> 5;
this.ignoreStartLevel = !!ignoreStartLevel;
const direction = (this.payload[0] & 0b0_1_0_00000) >>> 6;
this.direction = direction ? "down" : "up";

this.colorComponent = this.payload[1];
this.startLevel = this.payload[2];

if (this.payload.length >= 4) {
this.duration = Duration.parseSet(this.payload[3]);
}
} else {
this.duration = Duration.from(options.duration);
this.ignoreStartLevel = options.ignoreStartLevel;
Expand Down Expand Up @@ -1091,11 +1165,8 @@ export class ColorSwitchCCStopLevelChange extends ColorSwitchCC {
) {
super(host, options);
if (gotDeserializationOptions(options)) {
// TODO: Deserialize payload
throw new ZWaveError(
`${this.constructor.name}: deserialization not implemented`,
ZWaveErrorCodes.Deserialization_NotImplemented,
);
validatePayload(this.payload.length >= 1);
this.colorComponent = this.payload[0];
} else {
this.colorComponent = options.colorComponent;
}
Expand Down
52 changes: 31 additions & 21 deletions packages/testing/src/CCSpecificCapabilities.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import {
type CommandClasses,
type ConfigValue,
type ConfigValueFormat,
import type {
ColorComponent,
KeypadMode,
ThermostatMode,
ThermostatSetpointType,
UserIDStatus,
WindowCoveringParameter,
} from "@zwave-js/cc";
import type {
CommandClasses,
ConfigValue,
ConfigValueFormat,
} from "@zwave-js/core";

export interface BinarySensorCCCapabilities {
Expand All @@ -28,6 +36,10 @@ export interface ConfigurationCCCapabilities {
}[];
}

export interface ColorSwitchCCCapabilities {
supportedColorComponents: ColorComponent[];
}

export interface NotificationCCCapabilities {
supportsV1Alarm: false;
notificationTypesAndEvents: Record<number, number[]>;
Expand Down Expand Up @@ -75,8 +87,7 @@ export interface SoundSwitchCCCapabilities {
}

export interface WindowCoveringCCCapabilities {
// FIXME: This should be WindowCoveringParameter[], but that would introduce a dependency cycle
supportedParameters: number[];
supportedParameters: WindowCoveringParameter[];
}

export interface EnergyProductionCCCapabilities {
Expand All @@ -101,20 +112,20 @@ export interface EnergyProductionCCCapabilities {
}

export interface ThermostatModeCCCapabilities {
// FIXME: This should be ThermostatMode[], but that would introduce a dependency cycle
supportedModes: number[];
supportedModes: ThermostatMode[];
}

export interface ThermostatSetpointCCCapabilities {
setpoints: Record<
// FIXME: This should be ThermostatSetpointType, but that would introduce a dependency cycle
number,
{
minValue: number;
maxValue: number;
defaultValue?: number;
scale: "°C" | "°F";
}
setpoints: Partial<
Record<
ThermostatSetpointType,
{
minValue: number;
maxValue: number;
defaultValue?: number;
scale: "°C" | "°F";
}
>
>;
}

Expand All @@ -126,10 +137,8 @@ export interface UserCodeCCCapabilities {
// Not implemented in mocks:
// supportsMultipleUserCodeReport?: boolean;
// supportsMultipleUserCodeSet?: boolean;
// FIXME: This should be UserCodeStatus[], but that would introduce a dependency cycle
supportedUserIDStatuses?: number[];
// FIXME: This should be KeypadMode[], but that would introduce a dependency cycle
supportedKeypadModes?: number[];
supportedUserIDStatuses?: UserIDStatus[];
supportedKeypadModes?: KeypadMode[];
supportedASCIIChars?: string;
}

Expand All @@ -144,6 +153,7 @@ export type CCSpecificCapabilities = {
[CommandClasses.Notification]: NotificationCCCapabilities;
[48 /* Binary Sensor */]: BinarySensorCCCapabilities;
[49 /* Multilevel Sensor */]: MultilevelSensorCCCapabilities;
[51 /* Color Switch */]: ColorSwitchCCCapabilities;
[121 /* Sound Switch */]: SoundSwitchCCCapabilities;
[106 /* Window Covering */]: WindowCoveringCCCapabilities;
[144 /* Energy Production */]: EnergyProductionCCCapabilities;
Expand Down
4 changes: 4 additions & 0 deletions packages/zwave-js/src/lib/node/MockNodeBehaviors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import { type MockNodeBehavior } from "@zwave-js/testing";

import { BasicCCBehaviors } from "./mockCCBehaviors/Basic";
import { BinarySensorCCBehaviors } from "./mockCCBehaviors/BinarySensor";
import { BinarySwitchCCBehaviors } from "./mockCCBehaviors/BinarySwitch";
import { ColorSwitchCCBehaviors } from "./mockCCBehaviors/ColorSwitch";
import { ConfigurationCCBehaviors } from "./mockCCBehaviors/Configuration";
import { EnergyProductionCCBehaviors } from "./mockCCBehaviors/EnergyProduction";
import { ManufacturerSpecificCCBehaviors } from "./mockCCBehaviors/ManufacturerSpecific";
Expand Down Expand Up @@ -177,6 +179,8 @@ export function createDefaultBehaviors(): MockNodeBehavior[] {

...BasicCCBehaviors,
...BinarySensorCCBehaviors,
...BinarySwitchCCBehaviors,
...ColorSwitchCCBehaviors,
...ConfigurationCCBehaviors,
...EnergyProductionCCBehaviors,
...ManufacturerSpecificCCBehaviors,
Expand Down
39 changes: 39 additions & 0 deletions packages/zwave-js/src/lib/node/mockCCBehaviors/BinarySwitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {
BinarySwitchCCGet,
BinarySwitchCCReport,
BinarySwitchCCSet,
} from "@zwave-js/cc/BinarySwitchCC";
import { type MaybeUnknown, UNKNOWN_STATE } from "@zwave-js/core";
import { type MockNodeBehavior } from "@zwave-js/testing";

const STATE_KEY_PREFIX = "BinarySwitch_";
const StateKeys = {
currentValue: `${STATE_KEY_PREFIX}currentValue`,
} as const;

const respondToBinarySwitchGet: MockNodeBehavior = {
handleCC(controller, self, receivedCC) {
if (receivedCC instanceof BinarySwitchCCGet) {
const cc = new BinarySwitchCCReport(self.host, {
nodeId: controller.host.ownNodeId,
currentValue: (self.state.get(StateKeys.currentValue)
?? UNKNOWN_STATE) as MaybeUnknown<boolean>,
});
return { action: "sendCC", cc };
}
},
};

const respondToBinarySwitchSet: MockNodeBehavior = {
handleCC(controller, self, receivedCC) {
if (receivedCC instanceof BinarySwitchCCSet) {
self.state.set(StateKeys.currentValue, receivedCC.targetValue);
return { action: "ok" };
}
},
};

export const BinarySwitchCCBehaviors = [
respondToBinarySwitchGet,
respondToBinarySwitchSet,
];
Loading
Loading