zigbee-herdsman is an open source Zigbee gateway solution built with Node.js. It provides a TypeScript library for communicating with Zigbee devices through various adapter types (Z-Stack, EZSP/EmberZNet, deCONZ, Zigate, ZBOSS, ZOH).
Key Technologies:
- TypeScript 5.9.2 (target ES2022, module NodeNext, strict mode)
- Node.js with CommonJS modules
- Package Manager: pnpm 10.12.1 (enforced)
- Testing: Vitest 3.2.4
- Code Quality: Biome 2.2.5 (linting + formatting)
- Serial Communication: @serialport packages
Architecture:
- Layered architecture with clear separation between adapter (hardware), controller (business logic), and model layers
- Event-driven design using Node.js EventEmitter
- Static factory pattern for entities (Device, Group)
- Database abstraction layer for persistence
Install dependencies (pnpm is required):
pnpm installBuild TypeScript to JavaScript:
pnpm run buildClean build artifacts:
pnpm run cleanBuild TypeScript in watch mode (auto-recompile on changes):
pnpm run build:watchRun Biome linting and formatting checks:
pnpm run checkAuto-fix linting and formatting issues:
pnpm run check:wImportant: Always run pnpm run check before committing. The CI will fail if there are any Biome errors or warnings.
src/
├── index.ts # Public API exports
├── adapter/ # Hardware adapter implementations
│ ├── z-stack/ # Texas Instruments Z-Stack
│ ├── ember/ # Silicon Labs EmberZNet
│ ├── ezsp/ # EZSP protocol
│ ├── deconz/ # deCONZ adapter
│ ├── zigate/ # Zigate adapter
│ ├── zboss/ # ZBOSS adapter
│ └── zoh/ # Zigbee-on-Host adapter
├── buffalo/ # Binary serialization/deserialization
├── controller/ # Core business logic
│ ├── controller.ts # Main controller class
│ ├── database.ts # Persistence layer
│ ├── helpers/ # Shared utilities
│ └── model/ # Domain models (Device, Endpoint, Group)
├── models/ # Backup and configuration models
├── utils/ # Cross-cutting utilities
└── zspec/ # Zigbee specification
├── zcl/ # Zigbee Cluster Library
└── zdo/ # Zigbee Device Objects
- Compiled JavaScript goes to
dist/directory - Type definitions (.d.ts) are generated alongside JavaScript
- Source maps are generated for debugging
- The package exports
dist/index.jsas the main entry point
Run all tests:
pnpm run testRun tests with coverage report:
pnpm run test:coverageRun tests in watch mode:
pnpm run test:watchRun benchmarks:
pnpm run benchTest Configuration:
- Tests are in the
test/directory - Test files use
.test.tssuffix - Configuration:
test/vitest.config.mts - Coverage target: 100% (enforced)
- Default timeout: 10000ms (configured in CI)
Running Specific Tests:
Focus on a specific test file:
pnpm vitest run test/controller.test.ts --config ./test/vitest.config.mtsFocus on a specific test by name:
pnpm vitest run -t "test name pattern" --config ./test/vitest.config.mtsTest Patterns:
- Use Vitest's
describe,it,expect,beforeEach,afterAll,vi(for mocking) - Mock adapters and logger in tests (see
test/mockAdapters.ts,test/mockDevices.ts) - Tests for model classes should test CRUD operations, static factories, and business logic
- Always mock external dependencies (serial ports, adapters, file system)
TypeScript:
- Strict mode enabled (
strict: true,noImplicitAny: true,noImplicitThis: true) - Target ES2022, use modern JavaScript features
- Use
importwith Node.jsnode:prefix for built-ins:import assert from "node:assert" - Always provide explicit return types for public methods
- Use
readonlyfor immutable properties - Prefer
constoverlet, never usevar
Naming Conventions:
- Private fields: Use
#prefix (e.g.,#customClusters,#genBasic) - Internal properties: Use
_prefix (e.g.,_members,_ieeeAddr) - Public properties/methods: camelCase (e.g.,
groupID,addMember()) - Classes/Interfaces: PascalCase (e.g.,
Controller,Device,Options) - Constants: SCREAMING_SNAKE_CASE (e.g.,
NS = "zh:controller:group") - Static factory methods: camelCase (e.g.,
byGroupID(),byIeeeAddr()) - Iterators: Suffix with
Iterator(e.g.,allIterator())
Import Organization:
- Node.js built-ins (with
node:prefix) - External dependencies
- Internal imports (grouped by layer)
- Relative imports from same layer
Example:
import assert from "node:assert";
import events from "node:events";
import mixinDeep from "mixin-deep";
import {Adapter, type Events as AdapterEvents} from "../adapter";
import {logger} from "../utils/logger";
import * as Zcl from "../zspec/zcl";
import Database from "./database";- Indentation: 4 spaces
- Line width: 150 characters
- Bracket spacing: false (
{foo}not{ foo }) - No semicolons are automatically enforced
- Use double quotes for strings
- Trailing commas where valid
Key enforced rules:
noUnusedImports: errornoUnusedVariables: warninguseThrowNewError: error - always usenew Error()useAwait: error - don't mark functions async if they don't awaitnoNonNullAssertion: off in tests, discouraged in source (use with biome-ignore comment)
Naming flexibility:
- Object properties, const, and type properties can use: camelCase, PascalCase, snake_case, or CONSTANT_CASE
- Enum members: CONSTANT_CASE or PascalCase
Always cast errors to Error type:
try {
await operation();
} catch (error) {
const err = error as Error;
err.message = `Operation failed (${err.message})`;
logger.debug(err.stack!, NS);
throw error;
}Use assertions for preconditions:
import assert from "node:assert";
assert(typeof groupID === "number", "GroupID must be a number");
assert(groupID >= 1, "GroupID must be at least 1");Use the logger utility with namespace constants:
import {logger} from "../../utils/logger";
const NS = "zh:controller:group";
logger.debug(`Message with ${interpolation}`, NS);
logger.info("Important state change", NS);
logger.warning("Recoverable issue", NS);
logger.error("Failure", NS);
// For expensive operations, use lambda
logger.debug(() => `Expensive: ${JSON.stringify(large)}`, NS);- Always return
Promise<Type>explicitly for async methods - Use sequential awaits when order matters (most operations)
- Use
Promise.all()sparingly when operations are truly independent - Create a
createLogMessage()function for logging before try/catch blocks
Compile TypeScript:
pnpm run buildThis runs tsc which:
- Compiles
src/**/*.tstodist/ - Generates declaration files (.d.ts)
- Generates source maps
- Uses incremental compilation (tsconfig.tsbuildinfo)
The prepack script runs automatically before publishing:
pnpm run prepackThis cleans and rebuilds everything:
pnpm run clean- removes temp, coverage, dist, tsconfig.tsbuildinfopnpm run build- fresh TypeScript compilation
Only these files are included in npm package (see files in package.json):
./dist- compiled JavaScript and type definitions./CHANGELOG.md- version history
- Node.js version: 24 (used in CI)
- Package manager: pnpm 10.12.1 (strictly enforced via
packageManagerfield) - Module system: CommonJS (
type: "commonjs")
The CI workflow (.github/workflows/ci.yml) runs on:
- Push to
masterbranch - Push of version tags (
v*.*.*) - All pull requests
CI Steps:
- Checkout code
- Setup pnpm and Node.js 24
pnpm i --frozen-lockfile- install with exact versionspnpm run check- Biome linting (fails on warnings)pnpm run build- TypeScript compilationpnpm run test:coverage -- --testTimeout=10000- run tests with coveragepnpm run bench- run benchmarks (on master and PRs, not on release-please branches)- Publish to npm (only on version tags)
Before creating a PR, ensure:
pnpm run check # No errors or warnings
pnpm run build # Successful compilation
pnpm run test # All tests passTitle Format: Use descriptive titles that clearly indicate the change. No strict format required, but be specific.
Required Checks Before Submission:
pnpm run check # Must pass with no errors or warnings
pnpm run build # Must compile successfully
pnpm run test # All tests must passCode Review Requirements:
- Follow existing code patterns (see
.github/copilot-instructions.mdfor detailed patterns) - Maintain 100% test coverage
- Add tests for new code
- Update tests when modifying existing code
- Use TypeScript strict mode - no
anytypes - Follow the established architecture (adapter/controller/model layers)
The project uses an entity hierarchy:
Entity<EventMap>- base classZigbeeEntity- extends Entity with Zigbee-specific logicDevice,Group- concrete implementations
Entities use static factories with caching:
Device.byIeeeAddr(ieeeAddr) // Find device by IEEE address
Device.byNetworkAddress(address) // Find device by network address
Group.byGroupID(groupID) // Find group by ID
Device.create(...) // Create new device
Group.create(groupID) // Create new groupPrefer generators over arrays:
Device.allIterator() // Generator for all devices
Group.allIterator(predicate?) // Generator for groups with optional filterAll persistence goes through controller/database.ts:
Entity.database.insert(record)- createEntity.database.update(record)- updateEntity.database.remove(id)- deleteEntity.database.getEntriesIterator(types?)- read
Entities implement:
toDatabaseRecord()- serialize to database formatfromDatabaseEntry(entry)- deserialize from database
Issue: TypeScript compilation errors
- Solution: Ensure you're using TypeScript 5.9.2:
pnpm list typescript - Solution: Clean and rebuild:
pnpm run clean && pnpm run build
Issue: Incremental build issues
- Solution: Delete
tsconfig.tsbuildinfoand rebuild
Issue: Tests timeout
- Solution: Default timeout is 10000ms. Increase in specific tests if needed.
- Solution: Check for missing mocks or unresolved promises
Issue: Coverage not at 100%
- Solution: Add tests for uncovered lines
- Solution: Test files themselves don't need coverage (in
test/directory)
Issue: Biome check fails
- Solution: Run
pnpm run check:wto auto-fix formatting issues - Solution: For unfixable issues, address linting errors manually
- Solution: Use
// biome-ignore lint/rule/name: reasonfor justified exceptions
Issue: Non-null assertion errors
- Solution: Avoid using
!except in tests - Solution: If necessary in source, add
// biome-ignore lint/style/noNonNullAssertion: justification
Issue: Module resolution errors
- Solution: Use
node:prefix for Node.js built-ins - Solution: Check tsconfig.json
module: "NodeNext"andmoduleResolution: "NodeNext" - Solution: Use relative paths for internal imports
When working with adapters:
- Each adapter is in its own subdirectory under
src/adapter/ - All adapters extend the base
Adapterclass - Adapters handle hardware-specific communication
- Controller layer should not contain adapter-specific logic
Related Projects:
- Zigbee2MQTT - Uses zigbee-herdsman as its core Zigbee communication library
- ioBroker - Home automation platform using zigbee-herdsman
Documentation:
- API Documentation: https://koenkk.github.io/zigbee-herdsman
- GitHub Copilot Instructions:
.github/copilot-instructions.md(comprehensive coding patterns)
Version Management:
- Follows semantic versioning (currently 6.2.0)
- Breaking changes are documented in CHANGELOG.md
- Release Please manages releases automatically
Package Dependencies:
- Serial communication via
@serialport/*packages - Network discovery via
bonjour-service - Utilities:
fast-deep-equal,debounce,mixin-deep - All dependencies use exact or caret versions
Development Tips:
- Use the example in
examples/join-and-log.jsto understand basic usage - Run tests in watch mode while developing:
pnpm run test:watch - Keep build in watch mode for faster iteration:
pnpm run build:watch - Check coverage locally before pushing:
pnpm run test:coverage - Reference
.github/copilot-instructions.mdfor detailed coding patterns and conventions