con un clic
implement-zwave-cc
Guide for implementing new Z-Wave Command Classes (CCs) in zwave-js
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Menú
Guide for implementing new Z-Wave Command Classes (CCs) in zwave-js
Instalar con Codex o Claude Copia este prompt, pégalo en Codex, Claude u otro asistente, y deja que revise la página de la skill y la instale por ti.
Basado en la clasificación ocupacional SOC
Author a Z-Wave JS device configuration file based on information scraped from a manufacturer website. Use when asked to create a device config file from a URL or scraped web data.
Guide for writing integration tests that verify CC interview behavior and runtime functionality using mock nodes. Use when user asks for help writing tests for a specific CC or feature.
Review a pull request that changes device configuration files against the config-file rules. Use when asked to review a config file PR or diff.
| name | implement-zwave-cc |
| description | Guide for implementing new Z-Wave Command Classes (CCs) in zwave-js |
This skill guides the implementation of new Z-Wave Command Classes (CCs) in the zwave-js codebase.
Command Classes are implemented in packages/cc/src/cc/ as TypeScript files. Each CC file typically contains:
Reference implementations:
BinarySwitchCC.tsDoorLockCC.tsScheduleEntryLockCC.ts, ActiveScheduleCC.tsMultilevelSwitchCC.tsZ-Wave CC specifications define:
Key patterns in specs:
(MSB)/(LSB) indicates 16-bit big-endian valuesReserved (N bits) - set to 0 on send, ignore on receive// Note: Import patterns may evolve. Reference recent CC implementations for current style.
import type { CCEncodingContext, CCParsingContext } from "@zwave-js/cc";
import {
CommandClasses,
type EndpointId,
type GetValueDB,
type MaybeNotKnown,
type MessageOrCCLogEntry,
MessagePriority,
type SupervisionResult,
type WithAddress,
ZWaveError,
ZWaveErrorCodes,
encodeCCId,
isUnsupervisedOrSucceeded,
parseCCId,
validatePayload,
} from "@zwave-js/core";
import {
Bytes,
type BytesView,
getEnumMemberName,
pick,
} from "@zwave-js/shared";
import { validateArgs } from "@zwave-js/transformers";
import { CCAPI } from "../lib/API.js";
import {
type CCRaw,
CommandClass,
type InterviewContext,
type PersistValuesContext,
type RefreshValuesContext,
} from "../lib/CommandClass.js";
import {
API,
CCCommand,
ccValueProperty,
ccValues,
commandClass,
expectedCCResponse,
implementedVersion,
useSupervision,
} from "../lib/CommandClassDecorators.js";
import { V } from "../lib/Values.js";
Add command enum and type definitions to packages/cc/src/lib/_Types.ts:
export enum MyCommandClassCommand {
CapabilitiesGet = 0x01,
CapabilitiesReport = 0x02,
Get = 0x03,
Report = 0x04,
Set = 0x05,
}
export enum MyReportReason {
ResponseToGet = 0x00,
ModifiedExternal = 0x01,
ModifiedZWave = 0x02,
}
export interface MyScheduleData {
startDate: ScheduleDate;
stopDate: ScheduleDate;
metadata?: BytesView;
}
Values store CC state in the driver's value DB. There are two categories:
internal: trueWhen unclear, discuss with the developer on a case-by-case basis.
export const MyCommandClassCCValues = V.defineCCValues(
CommandClasses["My Command Class"],
{
// Static property - internal capability
...V.staticProperty("supportedFeatures", undefined, {
internal: true,
}),
// Static property - user-facing state (V2+)
...V.staticProperty(
"currentValue",
{
...ValueMetadata.ReadOnlyBoolean,
label: "Current value",
} as const,
{ minVersion: 2 },
),
// Dynamic property with composite key
...V.dynamicPropertyAndKeyWithName(
"schedule",
"schedule",
(targetCC: CommandClasses, targetId: number, slotId: number) =>
(targetCC << 24) | (targetId << 8) | slotId,
({ property, propertyKey }) =>
property === "schedule" && typeof propertyKey === "number",
undefined,
{ internal: true },
),
// Conditional value creation based on capabilities
...V.staticProperty(
"optionalFeatureValue",
{/* metadata */} as const,
{
minVersion: 4,
autoCreate: shouldAutoCreateOptionalFeatureValue,
} as const,
),
},
);
// autoCreate function checks if feature was reported as supported
export function shouldAutoCreateOptionalFeatureValue(
ctx: GetValueDB,
endpoint: EndpointId,
): boolean {
const valueDB = ctx.tryGetValueDB(endpoint.nodeId);
if (!valueDB) return false;
return !!valueDB.getValue(
MyCommandClassCCValues.optionalFeatureSupported.endpoint(
endpoint.index,
),
);
}
Reference: See DoorLockCC.ts for comprehensive autoCreate examples.
CCs evolve across versions. Always document version requirements:
@commandClass(CommandClasses["My Command Class"])
@implementedVersion(3) // Latest version implemented
@ccValues(MyCommandClassCCValues)
export class MyCommandClassCC extends CommandClass {
public supportsCommand(cmd: MyCommandClassCommand): MaybeNotKnown<boolean> {
switch (cmd) {
case MyCommandClassCommand.Get:
case MyCommandClassCommand.Set:
return true; // V1
case MyCommandClassCommand.CapabilitiesGet:
return this.version >= 2;
}
return super.supportsCommand(cmd);
}
Use minVersion to indicate when a value was introduced:
...V.staticProperty("duration", {...}, { minVersion: 2 } as const)
Newer CC versions often extend the binary format. Parse conditionally:
public static from(raw: CCRaw, ctx: CCParsingContext): MyReport {
validatePayload(raw.payload.length >= 2);
const value1 = raw.payload[0];
const value2 = raw.payload[1];
// V2+ adds optional duration field
let duration: Duration | undefined;
if (raw.payload.length >= 3) {
duration = Duration.parseReport(raw.payload[2]);
}
return new this({ nodeId: ctx.sourceNodeId, value1, value2, duration });
}
Note: Conditional serialization (omitting fields for older versions) is a workaround for buggy devices. Do not implement this by default.
The API class provides methods for controlling nodes.
@API(CommandClasses["My Command Class"])
export class MyCommandClassCCAPI extends CCAPI {
public supportsCommand(cmd: MyCommandClassCommand): MaybeNotKnown<boolean> {
switch (cmd) {
case MyCommandClassCommand.Get:
case MyCommandClassCommand.Set:
return true; // V1
case MyCommandClassCommand.CapabilitiesGet:
return this.version >= 2;
}
return super.supportsCommand(cmd);
}
// GET method - returns parsed data or undefined
public async getCapabilities(): Promise<MaybeNotKnown<CapabilitiesData>> {
this.assertSupportsCommand(
MyCommandClassCommand,
MyCommandClassCommand.CapabilitiesGet,
);
const cc = new MyCommandClassCCCapabilitiesGet({
nodeId: this.endpoint.nodeId,
endpointIndex: this.endpoint.index,
});
const result = await this.host.sendCommand<
MyCommandClassCCCapabilitiesReport
>(
cc,
this.commandOptions,
);
if (result) {
return pick(result, ["field1", "field2"]);
}
}
// SET method with supervision support
@validateArgs()
public async setValue(
target: TargetId,
value: boolean,
): Promise<SupervisionResult | undefined> {
this.assertSupportsCommand(
MyCommandClassCommand,
MyCommandClassCommand.Set,
);
const cc = new MyCommandClassCCSet({
nodeId: this.endpoint.nodeId,
endpointIndex: this.endpoint.index,
...target,
value,
});
const result = await this.host.sendCommand(cc, this.commandOptions);
// Optimistically update cache on success (singlecast only)
if (this.isSinglecast() && isUnsupervisedOrSucceeded(result)) {
const valueId = MyCommandClassCCValues.someValue(target.id);
this.host
.getValueDB(this.endpoint.nodeId)
.setValue(valueId.endpoint(this.endpoint.index), value);
}
return result;
}
}
CCs that split values into target and current (like switches) typically support multicast:
if (this.isSinglecast() && isUnsupervisedOrSucceeded(result)) {
this.tryGetValueDB()?.setValue(currentValueValueId, value);
} else if (this.isMulticast()) {
const affectedNodes = this.endpoint.node.physicalNodes
.filter((node) =>
node.getEndpoint(this.endpoint.index)?.supportsCC(this.ccId)
);
for (const node of affectedNodes) {
this.host.tryGetValueDB(node.id)?.setValue(currentValueValueId, value);
}
}
Reference: See BinarySwitchCC.ts and MultilevelSwitchCC.ts for examples.
Design decision: Discuss with the developer whether refreshValues() should be exposed to users.
interview() - Called once during node interview, discovers capabilitiesrefreshValues() - Called to refresh current state, can be triggered by usersIf refreshValues() is implemented, call it from interview() to avoid code duplication.
All queries sent during the interview must be tagged with tag: "interview", so their transactions can be dropped when the interview is aborted. refreshValues() takes the priority and tag from its options, so callers other than the interview are not affected:
@commandClass(CommandClasses["My Command Class"])
@implementedVersion(2)
@ccValues(MyCommandClassCCValues)
export class MyCommandClassCC extends CommandClass {
declare ccCommand: MyCommandClassCommand;
public async interview(ctx: InterviewContext): Promise<void> {
const node = this.getNode(ctx)!;
const endpoint = this.getEndpoint(ctx)!;
const api = CCAPI.create(
CommandClasses["My Command Class"],
ctx,
endpoint,
).withOptions({
priority: MessagePriority.NodeQuery,
tag: "interview",
});
ctx.logNode(node.id, {
endpoint: this.endpointIndex,
message: `Interviewing ${this.ccName}...`,
direction: "none",
});
// Query capabilities (interview-only)
if (api.version >= 2) {
ctx.logNode(node.id, {
endpoint: this.endpointIndex,
message: "Querying capabilities...",
direction: "outbound",
});
const caps = await api.getCapabilities();
if (caps) {
ctx.logNode(node.id, {
endpoint: this.endpointIndex,
message: `Received: feature1=${caps.feature1}`,
direction: "inbound",
});
}
}
// Refresh current values (reuse refreshValues if defined)
await this.refreshValues(ctx, { tag: "interview" });
this.setInterviewComplete(ctx, true);
}
public async refreshValues(
ctx: RefreshValuesContext,
options?: RefreshValuesOptions,
): Promise<void> {
const endpoint = this.getEndpoint(ctx)!;
const api = CCAPI.create(
CommandClasses["My Command Class"],
ctx,
endpoint,
).withOptions({
priority: options?.priority ?? MessagePriority.NodeQuery,
tag: options?.tag,
});
// ... query current state
}
// Static cached value getters
public static getSomethingCached(
ctx: GetValueDB,
endpoint: EndpointId,
): MaybeNotKnown<SomeType> {
return ctx
.getValueDB(endpoint.nodeId)
.getValue(
MyCommandClassCCValues.something.endpoint(endpoint.index),
);
}
}
Reference: See BinarySwitchCC.ts for simple interview/refresh, MultilevelSwitchCC.ts for version-specific interview logic.
See Command Patterns for detailed examples of:
Always use ZWaveError with appropriate error codes. Never use standard Error.
import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core";
// In API methods - argument validation
if (options.slotId < 1) {
throw new ZWaveError(
"The slot ID must be greater than 0",
ZWaveErrorCodes.Argument_Invalid,
);
}
// In parsing - use validatePayload for payload structure
validatePayload(raw.payload.length >= 10);
Types (packages/cc/src/lib/_Types.ts)
CC File (packages/cc/src/cc/MyCommandClassCC.ts)
internal/minVersion/autoCreatesupportsCommand() and all methodsinterview() and optionally refreshValues()Generate Exports
yarn workspace @zwave-js/cc run codegenValidation
yarn build @zwave-js/ccyarn fmtyarn lint:ts:fixWhen implementing a new CC, clarify these decisions:
refreshValues() be available to users, or is interview-only sufficient?autoCreate based on capabilities?