This document describes the layered architecture of ZWave.NET and how the major components interact.
ZWave.NET is a host-side implementation of the Z-Wave serial protocol. It communicates with a Z-Wave USB controller (the "chip") over a serial port and exposes Z-Wave network nodes and their command classes to the host application.
┌─────────────────────────────────────────────────────┐
│ Host Application │
├─────────────────────────────────────────────────────┤
│ Driver / Controller / Node │
│ (src/ZWave/) │
├─────────────────────────────────────────────────────┤
│ Command Classes (application layer) │
│ (src/ZWave.CommandClasses/) │
│ CC payloads are carried inside Serial API commands │
├─────────────────────────────────────────────────────┤
│ Serial API Commands (host↔chip commands) │
│ (src/ZWave.Serial/Commands/) │
├─────────────────────────────────────────────────────┤
│ Serial Frame Layer (SOF/ACK/NAK framing) │
│ (src/ZWave.Serial/) │
├─────────────────────────────────────────────────────┤
│ System.IO.Ports (serial port I/O) │
└─────────────────────────────────────────────────────┘
│ ▲
▼ │
┌──────────────────────────────────────┐
│ Z-Wave USB Controller │
│ (e.g. Aeotec Z-Stick, UZB-7) │
└──────────────────────────────────────┘
Shared Protocol Types (src/ZWave.Protocol/) are used by all layers.
Note: The protocol layer diagram above reflects the Z-Wave specification's logical layers. The project dependency graph below reflects the code structure, where Command Classes and Serial are independent projects that both depend on Protocol but not on each other - the Driver project bridges them.
ZWave.Protocol (no dependencies)
↑ ↑
ZWave.Serial ZWave.CommandClasses (independent of each other)
↑ ↑
ZWave (Driver)
↑
ZWave.Server
ZWave.BuildTools (source generators) is referenced as an analyzer by ZWave.CommandClasses.
The foundational layer contains Z-Wave domain types shared across all other projects. These are pure data types with no dependencies beyond .NET itself. All types are in namespace ZWave;.
| File | Purpose |
|---|---|
CommandClassId.cs |
Enum of all Z-Wave command class IDs (e.g. BinarySwitch = 0x25) |
CommandClassInfo.cs |
Record struct with CC ID, supported/controlled flags |
ZWaveException.cs |
Exception type for Z-Wave-specific errors |
ZWaveErrorCode.cs |
Error code categories (initialization, command, CC errors) |
FrequentListeningMode.cs |
FLiRS node listening patterns (None, 250ms, 1000ms) |
NodeType.cs |
Node classification (Controller, EndNode, Unknown) |
The lowest layer handles raw byte framing over the serial port per the Z-Wave Host API Specification (referred to as INS12350 in some source code comments).
| File | Purpose |
|---|---|
FrameHeader.cs |
Constants for frame header bytes: SOF (0x01), ACK (0x06), NAK (0x15), CAN (0x18) |
Frame.cs |
Represents a single-byte frame (ACK/NAK/CAN) or a multi-byte data frame (SOF). Immutable struct. |
DataFrame.cs |
SOF data frame: [SOF][Length][Type][CommandId][Parameters...][Checksum]. Computes and validates XOR checksums. |
DataFrameType.cs |
REQ (request) or RES (response) |
FrameType.cs |
ACK, NAK, CAN, or Data |
FrameParser.cs |
Stateless parser that extracts Frame instances from a ReadOnlySequence<byte> using System.IO.Pipelines. Skips invalid data gracefully. |
ZWaveSerialPortCoordinator.cs |
Manages the serial port lifecycle, read/write loops, ACK handshake, retransmission (up to 3 retries per the Host API Specification §6.3), and port re-opening on failure. Uses System.Threading.Channels for decoupled send/receive. |
Data flow:
ZWaveSerialPortCoordinatorreads bytes from the serial port into aPipeReaderFrameParser.TryParseDataextracts complete frames- Data frames with valid checksums are ACK'd and written to a receive channel
- The write loop reads from a send channel, transmits frames, and waits for ACK/NAK/CAN delivery confirmation with a 1600ms timeout
Each Z-Wave Serial API function is represented as a struct. These map to the function IDs in the Z-Wave Host API Specification. All function IDs defined in the CommandId enum have corresponding implementation structs.
Key interfaces:
ICommand<T>- A serial API command with aType(REQ/RES),CommandId, factoryCreate(DataFrame)method, and aFramepropertyIRequestWithCallback<T>- ExtendsICommand<T>for commands that trigger an asynchronous callback with aSessionIdand anExpectsResponseStatusflag
CommandId enum - All Serial API function IDs (e.g. SendData = 0x13, GetInitData = 0x02).
CommandDataParsingHelpers (Commands/CommandDataParsingHelpers.cs) - Internal shared utilities for parsing common serial API response patterns:
ParseCommandClasses— Parses a CC byte list with the support/control mark intoCommandClassInforecords.ParseNodeBitmask— Parses a node ID bitmask (each bit = one node) into aHashSet<ushort>. Takes abaseNodeIdparameter (1 for classic nodes, 256+ for LR nodes).
Communication patterns:
- Fire-and-forget: Host sends a REQ with no response expected (e.g.
SoftReset,SendDataAbort,WatchdogKick) - Request → Response: Host sends a REQ, chip replies with a RES (e.g.
GetNetworkIds,GetLibraryVersion,GetRoutingInfo,IsNodeFailed) - Request → Response + Callback: Host sends a REQ, chip replies with a RES (status), then later sends an unsolicited REQ as a callback (e.g.
SendData,SetSucNodeId,RemoveFailedNode). The callback is correlated bySessionId. - Request → Callback (no response): Host sends a REQ, chip later sends an unsolicited REQ as a callback without an initial RES (e.g.
SetDefault,SetLearnMode,RequestNodeNeighborDiscovery) - Set only: Host sends a REQ to configure the chip with no response (e.g.
ApplicationNodeInformation,SetPromiscuousMode) - Unsolicited request: Chip sends a REQ without the host asking (e.g.
ApplicationCommandHandler,ApplicationUpdate,ApplicationCommandHandlerBridge,SerialApiStarted)
Node ID encoding: All node ID parameters in the C# API are ushort to support both classic (1–232) and Long Range (256+) nodes. On the wire, node IDs are currently serialized as a single byte (8-bit mode, the default). The Create() methods cast (byte)nodeId when writing to the buffer.
Implements Z-Wave Command Classes per the Z-Wave Application Specification. These are the application-level messages exchanged between Z-Wave nodes. This project references ZWave.Protocol but not ZWave.Serial, enabling mock driver implementations without a serial dependency.
Base classes:
CommandClass- Abstract base with interview lifecycle, version tracking, awaited report management, and dependency declaration. TakesIDriverandINodeinterfaces (not concrete types) to enable mock implementations.CommandClass<TCommand>- Generic version that addsIsCommandSupported(TCommand)whereTCommandis a byte-backed enum
Key types:
CommandClassIdenum - All Z-Wave CC IDs (e.g.BinarySwitch = 0x25). Defined inZWave.Protocol.CommandClassInfo- Record struct with CC ID, supported/controlled flags. Defined inZWave.Protocol.CommandClassFrame- Wraps CC payload:[CommandClassId][CommandId][Parameters...]ICommand- Interface for CC-level commands (distinct from the Serial APIICommand<T>)IDriver- Interface for the driver operations command classes need (sending commands, looking up nodes)INode- Interface for the node operations command classes need (ID, command class access, properties)
Each CC file contains:
- A byte-backed command enum (e.g.
BinarySwitchCommand) - State types (e.g.
BinarySwitchState) - The CC class with
[CommandClass(...)]attribute, inheritingCommandClass<TEnum> - Private inner command structs implementing
ICommandfor Set/Get/Report
Registration: The [CommandClass(CommandClassId.X)] attribute is scanned by the CommandClassFactoryGenerator source generator, which generates CommandClassFactory - a mapping from CommandClassId to constructor delegate. Unrecognized CCs fall back to NotImplementedCommandClass.
Driver (Driver.cs) - Entry point for the library. Created via Driver.CreateAsync(). Implements IDriver.
- Opens the serial port via
ZWaveSerialPortCoordinator - Runs the initialization sequence per the Host API Specification §6.1 (NAK → soft reset → wait for SerialApiStarted)
- Manages request-response flow (only one REQ→RES session at a time per the Host API Specification §6.5.2)
- Tracks pending callbacks by
(CommandId, SessionId)key - Routes unsolicited requests (
ApplicationCommandHandler,ApplicationUpdate) to the correctNode
Controller (Controller.cs) - Represents the USB controller.
IdentifyAsync()queries the chip: home/node ID, serial API capabilities, library version, controller capabilities, supported setup subcommands, SUC node ID, and init data (node list)- Promotes itself to SUC/SIS if none exists on the network
- Creates
Nodeinstances for every node in the network - Stores nodes in a
Dictionary<ushort, Node>keyed by node ID
Node (Node.cs) - Represents a Z-Wave network node. Implements INode.
InterviewAsync()queries protocol info, requests node info, discovers command classes, then interviews each CC in topological dependency order- Uses copy-on-write dictionary for thread-safe command class access
- Exposes
GetCommandClass<T>()for typed access to a node's command classes
The ZWave.BuildTools project contains Roslyn incremental source generators that target netstandard2.0 (required by the analyzer infrastructure). It is referenced as an analyzer in ZWave.CommandClasses.csproj.
| Generator | Input | Output |
|---|---|---|
CommandClassFactoryGenerator |
[CommandClass] attributes on CC classes |
CommandClassFactory with ID→constructor mapping and type→ID reverse mapping |
MultilevelSensorTypeGenerator |
Config/MultilevelSensorTypes.json |
Sensor type enum and lookup |
MultilevelSensorScaleGenerator |
Config/MultilevelSensorScales.json |
Sensor scale enum and lookup |
src/Shared/BinaryExtensions.cs contains big-endian binary helpers (ToUInt16BE, WriteBytesBE, etc.) and is compiled into both ZWave.Serial and ZWave.CommandClasses as an internal class via <Compile Include> links. This avoids duplication without adding a public API surface.
All log messages are defined using source-generated [LoggerMessage] partial methods on ILogger extension classes.
src/ZWave.Serial/Logging.cs- Serial API layer messagessrc/ZWave/Logging.cs- Driver and controller messages
Event ID ranges:
- 100–199: Serial API layer (port open/close, frame send/receive, errors)
- 200–299: Driver and controller (initialization, identity, capabilities)
ZWaveException is thrown for Z-Wave-specific errors, categorized by ZWaveErrorCode:
DriverInitializationFailed/ControllerInitializationFailed- Startup failuresCommandSendFailed- Frame delivery failed after retransmissionsCommandFailed- Response indicated failureCommandClassNotImplemented/CommandNotSupported- CC or command not available on a nodeSerialApiCommandNotSupported- Serial API command not supported by the controllerCommandNotReady- CC not yet interviewedCommandInvalidArgument- Invalid parameter
The implementation references these official specifications:
The official Z-Wave specification package can be downloaded from the Z-Wave Alliance. The most relevant specs from the package are:
- Z-Wave Host API Specification — Serial API frame format, handshake, initialization sequence, command definitions. Replaces the old INS12350 document; existing source code comments still reference INS12350 section numbers which map to this spec.
- Z-Wave Application Specification — Command Class message formats, versioning, required fields
- Z-Wave and Z-Wave Long Range Network Layer Specification — Network protocol overview, routing, security framing
- Z-Wave Long Range PHY and MAC Layer Specification — PHY/MAC layer (supersedes ITU-T G.9959 references)
Additional resources:
- Silicon Labs Serial API Reference
- zwave-js/specs — Community-maintained specification collection