| name | implement-zwave-cc |
| description | Guide for implementing new Z-Wave Command Classes (CCs) in zwave-js |
Implementing Z-Wave Command Classes
This skill guides the implementation of new Z-Wave Command Classes (CCs) in the zwave-js codebase.
Reference Documents
Overview
Command Classes are implemented in packages/cc/src/cc/ as TypeScript files. Each CC file typically contains:
- Helper functions for parsing/encoding (CC-specific)
- CC Values definitions
- API class (for controlling nodes)
- Base CC class (with interview logic)
- Individual command classes (Get, Set, Report for each command pair)
Reference implementations:
- Simple CC:
BinarySwitchCC.ts
- Complex CC with many commands:
DoorLockCC.ts
- CC with schedules/slots:
ScheduleEntryLockCC.ts, ActiveScheduleCC.ts
- CC with versioning:
MultilevelSwitchCC.ts
Reading the Specification
Z-Wave CC specifications define:
- Command byte values and their encoding
- Field sizes (8-bit, 16-bit, bitmasks)
- Reserved bits and their handling
- Requirements (MUST/MAY/MUST NOT)
Key patterns in specs:
(MSB)/(LSB) indicates 16-bit big-endian values
Reserved (N bits) - set to 0 on send, ignore on receive
- Bitmasks show bit positions for flags
- Report Code/Reason fields indicate why a report was sent
File Structure and Imports
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,
} ;
{
,
,
ccValueProperty,
ccValues,
commandClass,
expectedCCResponse,
implementedVersion,
useSupervision,
} ;
{ V } ;
Types in _Types.ts
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;
}
CC Values Definition
Values store CC state in the driver's value DB. There are two categories:
Internal vs User-Facing Values
- Capabilities (number of slots, supported features, etc.) →
internal: true
- Device states (current value, target value, enabled state, etc.) → exposed to users
When unclear, discuss with the developer on a case-by-case basis.
Value Definition Patterns
export const MyCommandClassCCValues = V.defineCCValues(
CommandClasses["My Command Class"],
{
...V.staticProperty("supportedFeatures", undefined, {
internal: true,
}),
...V.staticProperty(
"currentValue",
{
...ValueMetadata.ReadOnlyBoolean,
label: "Current value",
} as const,
{ minVersion: 2 },
),
...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 },
),
...V.staticProperty(
"optionalFeatureValue",
{} ,
{
: ,
: shouldAutoCreateOptionalFeatureValue,
} ,
),
},
);
(): {
valueDB = ctx.(endpoint.);
(!valueDB) ;
!!valueDB.(
..(
endpoint.,
),
);
}
Reference: See DoorLockCC.ts for comprehensive autoCreate examples.
Version Handling
CCs evolve across versions. Always document version requirements:
In @implementedVersion() Decorator
@commandClass(CommandClasses["My Command Class"])
@implementedVersion(3)
@ccValues(MyCommandClassCCValues)
export class MyCommandClassCC extends CommandClass {
In supportsCommand()
public supportsCommand(cmd: MyCommandClassCommand): MaybeNotKnown<boolean> {
switch (cmd) {
case MyCommandClassCommand.Get:
case MyCommandClassCommand.Set:
return true;
case MyCommandClassCommand.CapabilitiesGet:
return this.version >= 2;
}
return super.supportsCommand(cmd);
}
In Value Definitions
Use minVersion to indicate when a value was introduced:
...V.staticProperty("duration", {...}, { minVersion: 2 } as const)
Conditional Parsing
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];
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.
API Class
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;
case MyCommandClassCommand.CapabilitiesGet:
return this.version >= 2;
}
return super.supportsCommand(cmd);
}
public async getCapabilities(): Promise<MaybeNotKnown<CapabilitiesData>> {
this.assertSupportsCommand(
MyCommandClassCommand,
MyCommandClassCommand.CapabilitiesGet,
);
const cc = new MyCommandClassCCCapabilitiesGet({
: ..,
: ..,
});
result = ..<
>(
cc,
.,
);
(result) {
(result, [, ]);
}
}
()
(
: ,
: ,
): < | > {
.(
,
.,
);
cc = ({
: ..,
: ..,
...target,
value,
});
result = ..(cc, .);
(.() && (result)) {
valueId = .(target.);
.
.(..)
.(valueId.(..), value);
}
result;
}
}
Multicast Handling for Target/Current Value CCs
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.
Base CC Class
Interview and RefreshValues
Design decision: Discuss with the developer whether refreshValues() should be exposed to users.
interview() - Called once during node interview, discovers capabilities
refreshValues() - Called to refresh current state, can be triggered by users
If 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",
});
(api. >= ) {
ctx.(node., {
: .,
: ,
: ,
});
caps = api.();
(caps) {
ctx.(node., {
: .,
: ,
: ,
});
}
}
.(ctx, { : });
.(ctx, );
}
(
: ,
?: ,
): <> {
endpoint = .(ctx)!;
api = .(
[],
ctx,
endpoint,
).({
: options?. ?? .,
: options?.,
});
}
(
: ,
: ,
): <> {
ctx
.(endpoint.)
.(
..(endpoint.),
);
}
}
Reference: See BinarySwitchCC.ts for simple interview/refresh, MultilevelSwitchCC.ts for version-specific interview logic.
Command Classes
See Command Patterns for detailed examples of:
- Report class (received from device)
- Get class (simple and with parameters)
- Set class (with supervision)
- Discriminated unions for Set actions (Erase vs Modify)
Error Handling
Always use ZWaveError with appropriate error codes. Never use standard Error.
import { ZWaveError, ZWaveErrorCodes } from "@zwave-js/core";
if (options.slotId < 1) {
throw new ZWaveError(
"The slot ID must be greater than 0",
ZWaveErrorCodes.Argument_Invalid,
);
}
validatePayload(raw.payload.length >= 10);
Checklist for New CC Implementation
-
Types (packages/cc/src/lib/_Types.ts)
-
CC File (packages/cc/src/cc/MyCommandClassCC.ts)
-
Generate Exports
-
Validation
Questions to Ask the Developer
When implementing a new CC, clarify these decisions:
- Should
refreshValues() be available to users, or is interview-only sufficient?
- Which values should be user-facing vs internal?
- Are there optional features that need
autoCreate based on capabilities?
- What error messages are appropriate for validation failures?