mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-06-21 12:41:47 +00:00
17 KiB
17 KiB
GitHub Copilot Instructions
Priority Guidelines
When generating code for this repository:
- Version Compatibility: Always detect and respect the exact versions of languages, frameworks, and libraries used in this project
- Context Files: Prioritize patterns and standards defined in the .github/copilot directory
- Codebase Patterns: When context files don't provide specific guidance, scan the codebase for established patterns
- Architectural Consistency: Maintain our layered architectural style with clear separation between controller, extensions, models, and utilities
- Code Quality: Prioritize maintainability, performance, security, and testability in all generated code
Technology Stack
Core Technologies
- Language: TypeScript 5.9.3 with target
esnextand moduleNodeNext - 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
noImplicitAnyandnoImplicitThis - 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):
- Node.js built-in modules (use
node:prefix:import fs from "node:fs") - Third-party libraries (e.g.,
bind-decorator,mqtt) - Type-only imports from external packages (using
typekeyword) - Internal absolute imports from project root
- 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
typeimports for TypeScript types:import type * as zhc from "zigbee-herdsman-converters" - Explicitly type function parameters and return types
- Use
KeyValuetype 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/awaitfor 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
rimrafSyncfor 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.tspattern) - Sanitize file paths using
path.joinfrom 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,afterAllfor test setup/teardown - Place test files in
test/directory with.test.tsextension - 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
afterEachor between tests - Mock external libraries like
mqtt,zigbee-herdsmanconsistently
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
@paramtags 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/exportsyntax - 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
.jsextension 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
unknownoveranywhen type is truly unknown - Use
// biome-ignore lint/suspicious/noExplicitAny: APIwhenanyis 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
typefor aliases,interfacefor object shapes
Version Control and Releases
Versioning Strategy
- Follow Semantic Versioning (MAJOR.MINOR.PATCH)
- Current version managed in
package.json - Use
-devsuffix 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
devbranch - Production releases from
masterbranch - 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-herdsmanentities - Access underlying entity via
.zhproperty - Expose computed properties as getters
- Include definition from
zigbee-herdsman-converters - Handle coordinator devices specially (type checking)
MQTT Integration
- MQTT client wrapped in
Mqttclass - Publish options:
retain,qosproperties - 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
- Never use language features beyond TypeScript 5.9.3 or ES2024
- Always respect exact versions of zigbee-herdsman and zigbee-herdsman-converters - these are critical for device compatibility
- Use the EventBus for all component communication - avoid direct coupling
- Follow the Extension pattern for new features - don't add logic directly to Controller
- Log appropriately - info for user-relevant events, debug for developer info, error for failures
- Test with real Zigbee scenarios - many edge cases exist with different device types
- Handle coordinator specially - coordinator is a device but with unique behavior
- Validate all external input - MQTT messages, configuration files, device data
- Use the bind decorator for event handlers to preserve
thiscontext - Match the exact code formatting - Biome enforces 4 spaces, 150 line width, no bracket spacing
Common Patterns to Follow
Creating a New Extension
- Extend
Extensionabstract class - Accept all dependencies in constructor
- Implement
start()method for initialization - Subscribe to EventBus events in
start() - Implement
stop()method for cleanup - 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-herdsmanAPI - 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
mqttMessageevent
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
- Node.js: Only versions 20, 22, and 24 are supported
- TypeScript: Features must be compatible with 5.9.3
- Zigbee Libraries: Exact versions are critical - do not suggest upgrades without testing
- MQTT Protocol: Uses MQTT 3.1.1 and 5.0 features
- ES Modules: Project uses ESM with NodeNext resolution
- Experimental Decorators: Required for
@binddecorator support
When in Doubt
- Search for similar patterns in the existing codebase
- Check existing extensions for implementation examples
- Follow the controller and extension architecture - don't bypass it
- Consult the test files for usage examples
- Match the exact style - run
pnpm checkto verify - Prioritize consistency over external best practices
- Test thoroughly - this project controls real hardware
Resources
- Repository: https://github.com/Koenkk/zigbee2mqtt
- Documentation: https://koenkk.github.io/zigbee2mqtt
- License: GPL-3.0
- Issue Tracker: https://github.com/Koenkk/zigbee2mqtt/issues