Files
zigbee2mqtt/.github/copilot-instructions.md

17 KiB

GitHub Copilot Instructions

Priority Guidelines

When generating code for this repository:

  1. Version Compatibility: Always detect and respect the exact versions of languages, frameworks, and libraries used in this project
  2. Context Files: Prioritize patterns and standards defined in the .github/copilot directory
  3. Codebase Patterns: When context files don't provide specific guidance, scan the codebase for established patterns
  4. Architectural Consistency: Maintain our layered architectural style with clear separation between controller, extensions, models, and utilities
  5. Code Quality: Prioritize maintainability, performance, security, and testability in all generated code

Technology Stack

Core Technologies

  • Language: TypeScript 5.9.3 with target esnext and module NodeNext
  • Runtime: Node.js ^20 || ^22 || ^24
  • Package Manager: pnpm 10.12.1
  • Testing: Vitest 3.1.1 with @vitest/coverage-v8
  • Linting/Formatting: Biome 2.2.5 (configured with 4-space indents, 150 line width, no bracket spacing)

Key Dependencies

  • zigbee-herdsman: 6.2.0 (exact version - critical for Zigbee protocol compatibility)
  • zigbee-herdsman-converters: 25.42.0 (exact version - device definitions)
  • MQTT: mqtt 5.14.1
  • Logging: winston 3.18.3
  • YAML: js-yaml 4.1.0
  • Decorators: bind-decorator 1.0.11
  • WebSocket: ws 8.18.1

TypeScript Configuration

  • Strict Mode: Enabled with noImplicitAny and noImplicitThis
  • Module System: NodeNext with ESM interop
  • Decorators: Experimental decorators enabled
  • Composite: True (for project references)
  • Source Maps: Inline source maps enabled
  • Output: Compiled to dist/ directory

Project Architecture

Directory Structure

lib/                    # Source TypeScript files
├── controller.ts       # Main controller orchestrating all components
├── mqtt.ts            # MQTT client management
├── zigbee.ts          # Zigbee network management
├── state.ts           # State management
├── eventBus.ts        # Event-driven communication
├── extension/         # Extension system (plugins)
│   ├── extension.ts   # Abstract base class
│   ├── availability.ts
│   ├── bind.ts
│   ├── bridge.ts
│   ├── configure.ts
│   └── ...
├── model/             # Domain models
│   ├── device.ts
│   └── group.ts
├── util/              # Utility functions
│   ├── logger.ts
│   ├── settings.ts
│   ├── utils.ts
│   └── ...
└── types/             # TypeScript type definitions
    └── api.ts
test/                  # Vitest test files
data/                  # Runtime configuration and data

Architectural Patterns

Extension Pattern

All extensions inherit from the abstract Extension base class:

abstract class Extension {
    protected zigbee: Zigbee;
    protected mqtt: Mqtt;
    protected state: State;
    protected publishEntityState: PublishEntityState;
    protected eventBus: EventBus;
    
    async start(): Promise<void> {}
    async stop(): Promise<void> {}
}

Event-Driven Architecture

Use the EventBus for component communication. Events are strongly typed:

interface EventBusMap {
    deviceMessage: [data: eventdata.DeviceMessage];
    mqttMessage: [data: eventdata.MQTTMessage];
    publishEntityState: [data: eventdata.PublishEntityState];
    // ... other events
}

Dependency Injection

The Controller class instantiates and injects dependencies into extensions. Follow this pattern when creating new extensions.

Code Style and Conventions

Naming Conventions

  • Classes: PascalCase (e.g., Extension, Device, EventBus)
  • Interfaces/Types: PascalCase (e.g., MqttPublishOptions, DeviceOptions)
  • Functions/Methods: camelCase (e.g., publishEntityState, enableDisableExtension)
  • Constants: SCREAMING_SNAKE_CASE for top-level constants (e.g., CURRENT_VERSION, LOG_LEVELS)
  • Private members: Prefix with underscore for private class fields only when needed to distinguish from public properties (e.g., _definitionModelID)
  • Files: camelCase for TypeScript files (e.g., eventBus.ts, externalJS.ts)

Import Organization

Follow this import order (separated by blank lines):

  1. Node.js built-in modules (use node: prefix: import fs from "node:fs")
  2. Third-party libraries (e.g., bind-decorator, mqtt)
  3. Type-only imports from external packages (using type keyword)
  4. Internal absolute imports from project root
  5. Type-only imports from internal modules

Example:

import fs from "node:fs";
import bind from "bind-decorator";
import type {IClientOptions} from "mqtt";
import {connectAsync} from "mqtt";
import type {Zigbee2MQTTAPI} from "./types/api";
import logger from "./util/logger";
import * as settings from "./util/settings";

Type Annotations

  • Use type imports for TypeScript types: import type * as zhc from "zigbee-herdsman-converters"
  • Explicitly type function parameters and return types
  • Use KeyValue type for generic object payloads: type KeyValue = Record<string, any>
  • Prefer interfaces for object shapes, type aliases for unions/intersections
  • Use namespace exports for related types: export type * as ZSpec from "zigbee-herdsman/dist/zspec"

Async/Await Patterns

  • Always use async/await for asynchronous operations
  • Return types should be explicitly Promise<Type>
  • Methods that don't return values should be Promise<void>
  • Use Awaited<ReturnType<typeof fn>> for inferring async function return types

Decorators

Use @bind decorator from bind-decorator for methods that need this binding:

@bind async onMQTTMessage(data: eventdata.MQTTMessage): Promise<void> {
    // Implementation
}

Error Handling

  • Use throw new Error("message") for explicit errors
  • Include descriptive error messages
  • Log errors using the logger: logger.error("message")
  • For Zigbee-herdsman errors, log the stack trace: logger.error((error as Error).stack!)
  • Catch and handle errors at appropriate boundaries (controller level)

Logging

Use the centralized logger (winston-based):

import logger from "./util/logger";

logger.info("message");
logger.warning("message");
logger.error("message");
logger.debug("message");
  • Use namespaced loggers for specific modules (created internally by logger)
  • Log levels: error, warning, info, debug (from most to least critical)
  • Include relevant context in log messages (device names, IEEE addresses, etc.)

Code Quality Standards

Maintainability

  • Write self-documenting code with clear, descriptive names
  • Keep methods focused on single responsibilities
  • Abstract classes should define clear contracts with protected members for subclasses
  • Use constructor dependency injection for required dependencies
  • Limit function complexity - methods should be concise and focused
  • Use TypeScript's strict mode features (noImplicitAny, noImplicitThis)

Performance

  • Use rimrafSync for synchronous file deletion when appropriate
  • Leverage async/await for I/O operations to avoid blocking
  • Use JSON stable stringify for consistent object serialization: json-stable-stringify-without-jsonify
  • Cache computed values when appropriate (see device model patterns)
  • Use getter methods for computed properties that should be cached

Security

  • Validate input using Ajv JSON schema validation (see settings.ts pattern)
  • Sanitize file paths using path.join from Node.js
  • Use YAML safe loading: yaml.safeLoad()
  • Handle sensitive data (credentials, tokens) through settings with proper defaults
  • Never log sensitive information (passwords, tokens)

Testability

  • Write tests using Vitest with describe/it/expect patterns
  • Mock external dependencies using Vitest's vi.mock()
  • Use beforeEach, afterEach, beforeAll, afterAll for test setup/teardown
  • Place test files in test/ directory with .test.ts extension
  • Mock constructors and modules in the pattern shown in test/controller.test.ts
  • Use flushPromises() utility for async test synchronization
  • Target 100% code coverage (configured in vitest.config.mts)

Testing Standards

Unit Testing Structure

import {afterAll, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";

describe("ComponentName", () => {
    beforeEach(() => {
        // Setup
    });

    it("Should do something specific", async () => {
        // Arrange
        const input = {};
        
        // Act
        const result = await someFunction(input);
        
        // Assert
        expect(result).toBe(expected);
    });
});

Mocking Patterns

  • Create mock modules in test/mocks/ directory
  • Use vi.fn() for function mocks
  • Use vi.mock() for module mocks
  • Clear mocks in afterEach or between tests
  • Mock external libraries like mqtt, zigbee-herdsman consistently

Test Coverage

  • All code in lib/** should be covered
  • Use coverage reports: pnpm test:coverage
  • Thresholds set to 100% (can be adjusted per project needs)
  • Tests should cover both success and failure paths

Documentation Standards

JSDoc Comments

Use JSDoc-style comments for classes and public methods:

/**
 * Besides initializing variables, the constructor should do nothing!
 *
 * @param {Zigbee} zigbee Zigbee controller
 * @param {Mqtt} mqtt MQTT controller
 * @param {State} state State controller
 * @param {Function} publishEntityState Method to publish device state to MQTT.
 * @param {EventBus} eventBus The event bus
 */
constructor(zigbee: Zigbee, mqtt: Mqtt, state: State, ...) {

Comment Style

  • Use single-line comments (//) for implementation notes
  • Use JSDoc (/** */) for public APIs and class/method documentation
  • Include context for non-obvious logic
  • Document parameters with their types and purposes
  • Use @param tags with TypeScript types in braces
  • Use biome-ignore comments when necessary: // biome-ignore lint/rule: reason

Code Documentation

  • Document complex algorithms or business logic
  • Explain "why" not just "what" when logic is non-trivial
  • Include links to relevant issues or documentation when applicable
  • Document deprecations and breaking changes

TypeScript-Specific Guidelines

Module System

  • Use ES modules with import/export syntax
  • Default exports for main classes: export default class Device {}
  • Named exports for utilities and types: export const LOG_LEVELS = ...
  • Namespace exports for related types: export type * as ZSpec from ...
  • Use .js extension in imports for local modules when using dynamic imports: await import("./extension/frontend.js")

Type Safety

  • Enable all strict type checking options
  • Use type guards and assertions when necessary: asserts expose is zhc.Numeric
  • Prefer unknown over any when type is truly unknown
  • Use // biome-ignore lint/suspicious/noExplicitAny: API when any is necessary
  • Define proper interfaces for external module types (e.g., unix-dgram.d.ts)

Generic Types

  • Use generics for reusable, type-safe abstractions
  • Example: abstract class ExternalJSExtension<M> extends Extension
  • Constrain generics when appropriate
  • Document generic type parameters

Utility Types

  • Use built-in utility types: Partial, Required, Pick, Omit, Record
  • Use Awaited<ReturnType<typeof fn>> for async function return types
  • Define custom utility types when patterns emerge
  • Use type for aliases, interface for object shapes

Version Control and Releases

Versioning Strategy

  • Follow Semantic Versioning (MAJOR.MINOR.PATCH)
  • Current version managed in package.json
  • Use -dev suffix for development versions (e.g., 2.6.2-dev)
  • Configuration version tracked separately: CURRENT_VERSION = 4

Changelog

  • Maintain CHANGELOG.md with all changes
  • Group changes by type: Bug Fixes, Features, Breaking Changes
  • Include issue/PR references: ([#28583](url))
  • Include commit references: ([09f33b3](url))
  • Use conventional commits format

Git Workflow

  • Development on dev branch
  • Production releases from master branch
  • Use meaningful commit messages
  • Reference issues in commits

Project-Specific Patterns

Settings Management

  • All configuration loaded through util/settings.ts
  • Validate settings using Ajv with JSON schema
  • Schema defined in settings.schema.json
  • Support runtime setting changes with restart detection
  • Use settings.get() to access current configuration
  • Use settings.getDevice(ieeeAddr) for device-specific config

Device and Group Models

  • Devices and groups are domain models wrapping zigbee-herdsman entities
  • Access underlying entity via .zh property
  • Expose computed properties as getters
  • Include definition from zigbee-herdsman-converters
  • Handle coordinator devices specially (type checking)

MQTT Integration

  • MQTT client wrapped in Mqtt class
  • Publish options: retain, qos properties
  • Topics follow pattern: {base_topic}/{device}/{attribute}
  • Event-based message handling via EventBus
  • Clean disconnect handling with retry logic

Extension System

  • Extensions are loosely coupled plugins
  • Lifecycle: constructor → start() → stop()
  • Constructor should only assign properties (no side effects)
  • Use EventBus for inter-extension communication
  • Extensions can be enabled/disabled at runtime
  • External extensions loaded from data/external_extensions/

State Management

  • State persisted to state.json
  • Cached in memory for performance
  • Device states include all exposed attributes
  • State changes trigger events via EventBus

Best Practices Specific to This Project

  1. Never use language features beyond TypeScript 5.9.3 or ES2024
  2. Always respect exact versions of zigbee-herdsman and zigbee-herdsman-converters - these are critical for device compatibility
  3. Use the EventBus for all component communication - avoid direct coupling
  4. Follow the Extension pattern for new features - don't add logic directly to Controller
  5. Log appropriately - info for user-relevant events, debug for developer info, error for failures
  6. Test with real Zigbee scenarios - many edge cases exist with different device types
  7. Handle coordinator specially - coordinator is a device but with unique behavior
  8. Validate all external input - MQTT messages, configuration files, device data
  9. Use the bind decorator for event handlers to preserve this context
  10. Match the exact code formatting - Biome enforces 4 spaces, 150 line width, no bracket spacing

Common Patterns to Follow

Creating a New Extension

  1. Extend Extension abstract class
  2. Accept all dependencies in constructor
  3. Implement start() method for initialization
  4. Subscribe to EventBus events in start()
  5. Implement stop() method for cleanup
  6. Export as default: export default class MyExtension extends Extension

Accessing Device Information

const device: Device; // Our wrapper
device.ieeeAddr;      // IEEE address
device.name;          // Friendly name
device.zh;            // Underlying zigbee-herdsman device
device.definition;    // zigbee-herdsman-converters definition
device.options;       // User configuration

Publishing MQTT Messages

await this.mqtt.publish(topic, message, {retain: true, qos: 0});

Emitting Events

this.eventBus.emit('deviceMessage', {device, message});

Listening to Events

this.eventBus.on('deviceMessage', this.onDeviceMessage, this);

Integration Points

Zigbee-Herdsman Integration

  • Start controller: await this.zigbee.start()
  • Access coordinator: this.zigbee.coordinator()
  • Device operations through zigbee-herdsman API
  • Event handling through EventBus wrappers

MQTT Integration

  • Connect: await this.mqtt.connect()
  • Subscribe: await this.mqtt.subscribe(topic)
  • Publish: await this.mqtt.publish(topic, message, options)
  • Handle messages via EventBus mqttMessage event

Frontend Integration

  • Optional extension loaded dynamically
  • Serves static files with compression
  • WebSocket support for real-time updates
  • Configurable port and base URL

Home Assistant Integration

  • Optional extension for discovery
  • Publishes discovery messages to MQTT
  • Supports entities, sensors, and devices
  • Configurable discovery topic

Critical Compatibility Notes

  1. Node.js: Only versions 20, 22, and 24 are supported
  2. TypeScript: Features must be compatible with 5.9.3
  3. Zigbee Libraries: Exact versions are critical - do not suggest upgrades without testing
  4. MQTT Protocol: Uses MQTT 3.1.1 and 5.0 features
  5. ES Modules: Project uses ESM with NodeNext resolution
  6. Experimental Decorators: Required for @bind decorator support

When in Doubt

  1. Search for similar patterns in the existing codebase
  2. Check existing extensions for implementation examples
  3. Follow the controller and extension architecture - don't bypass it
  4. Consult the test files for usage examples
  5. Match the exact style - run pnpm check to verify
  6. Prioritize consistency over external best practices
  7. Test thoroughly - this project controls real hardware

Resources