@webarray/esphome-native-api
v1.0.5
Published
TypeScript/Node.js client for ESPHome native API with encryption and deep sleep support
Downloads
326
Readme
ESPHome Native API for Node.js
A TypeScript/Node.js implementation of the ESPHome native API protocol. This library allows you to interact with ESPHome devices directly from Node.js applications.
Features
- Full ESPHome Native API Support - Complete implementation of the ESPHome native API protocol
- Automatic Reconnection - Robust connection handling with automatic reconnection on network issues
- Event-Driven Architecture - Uses EventEmitter pattern for real-time state updates
- Device Discovery - Automatic discovery of ESPHome devices on your network using mDNS
- TypeScript Support - Full TypeScript support with comprehensive type definitions and utility types
- High Performance - Efficient binary protocol handling with protobuf
- Well Tested - Comprehensive test suite ensuring reliability
- Flexible Logging - Built-in debug logging with optional custom logger integration (Winston, Pino, etc.)
- Custom Timer Support - Optional timer factory for platforms like Homey that require custom timer handling
- Connection Health Monitoring - Built-in health metrics and connection diagnostics
- Enhanced Error Handling - Detailed error messages with codes and helpful suggestions
- Entity Helper Methods - Convenient methods to search, filter, and manage entities
- Debug Utilities - Built-in debugging tools for troubleshooting
Requirements
- Node.js >= 18.0.0
- ESPHome device with Native API component enabled
Installation
npm install @webarray/esphome-native-apiQuick Start
Basic Usage
import { ESPHomeClient } from '@webarray/esphome-native-api';
// Create a client and connect to an ESPHome device
const client = new ESPHomeClient({
host: '192.168.1.100',
port: 6053,
encryptionKey: 'your-base64-encryption-key' // Recommended: secure encryption
// password: 'your-api-password' // Deprecated: use encryptionKey instead
});
// Connect to the device
await client.connect();
// Get device information
const deviceInfo = client.getDeviceInfo();
console.log('Device:', deviceInfo?.name);
console.log('Version:', deviceInfo?.esphomeVersion);
// Subscribe to state changes
client.subscribeStates((state) => {
console.log('State update:', state);
});
// Control a switch
await client.switchCommand(switchKey, true);
// Disconnect when done
client.disconnect();💡 Tip: Enable debug logging to see what's happening:
DEBUG=esphome:* node your-app.jsDevice Discovery
import { discover } from '@webarray/esphome-native-api';
// Discover ESPHome devices on the network
const devices = await discover(5000); // Search for 5 seconds
for (const device of devices) {
console.log(`Found device: ${device.name} at ${device.host}:${device.port}`);
}Event-Based State Monitoring
import { ESPHomeClient } from '@webarray/esphome-native-api';
const client = new ESPHomeClient({
host: 'device.local',
encryptionKey: 'your-base64-encryption-key' // Recommended
});
// Listen for specific entity types
client.on('binarySensorState', (state) => {
console.log(`Binary sensor ${state.key}: ${state.state}`);
});
client.on('sensorState', (state) => {
console.log(`Sensor ${state.key}: ${state.state}`);
});
client.on('switchState', (state) => {
console.log(`Switch ${state.key}: ${state.state}`);
});
// Listen for log messages
client.on('logs', (log) => {
console.log(`[${log.level}] ${log.message}`);
});
await client.connect();Logging
Subscribe to device logs using log level constants:
import { ESPHomeClient, LogLevel, LOG_LEVEL_INFO, LOG_LEVEL_DEBUG } from '@webarray/esphome-native-api';
const client = new ESPHomeClient({ host: 'device.local' });
// Using enum
client.subscribeLogs(LogLevel.INFO);
// Using constants (easier to import)
client.subscribeLogs(LOG_LEVEL_DEBUG);
// Using numeric value (also supported)
client.subscribeLogs(3); // INFO level
// Available log levels:
// LOG_LEVEL_NONE (0) - No logging
// LOG_LEVEL_ERROR (1) - Errors only
// LOG_LEVEL_WARN (2) - Warnings and errors
// LOG_LEVEL_INFO (3) - Info, warnings, and errors (default)
// LOG_LEVEL_CONFIG (4) - Configuration messages
// LOG_LEVEL_DEBUG (5) - Debug messages
// LOG_LEVEL_VERBOSE (6) - Verbose debug
// LOG_LEVEL_VERY_VERBOSE (7) - All messagesList All Entities
const client = new ESPHomeClient({
host: 'device.local',
encryptionKey: 'your-base64-encryption-key' // Recommended
});
await client.connect();
// Get all entities
const entities = await client.listEntities();
for (const entity of entities) {
console.log(`Entity: ${entity.name} (${entity.objectId})`);
console.log(` Key: ${entity.key}`);
console.log(` Type: ${entity.constructor.name}`);
}Connection with Reconnection
const client = new ESPHomeClient({
host: 'device.local',
encryptionKey: 'your-base64-encryption-key', // Recommended
reconnect: true, // Enable automatic reconnection
reconnectInterval: 5000, // Reconnect every 5 seconds
pingInterval: 20000, // Send ping every 20 seconds
pingTimeout: 5000, // Ping timeout after 5 seconds
connectTimeout: 10000 // Connection timeout after 10 seconds
});
// Monitor connection state
client.on('connected', () => {
console.log('Connected to device');
});
client.on('disconnected', (error) => {
if (error) {
console.error('Disconnected with error:', error);
} else {
console.log('Disconnected');
}
});
await client.connect();Advanced Discovery with Event Monitoring
import { Discovery } from '@webarray/esphome-native-api';
const discovery = new Discovery();
// Listen for discovered devices
discovery.on('device', (device) => {
console.log(`Discovered: ${device.name} at ${device.host}`);
if (device.macAddress) {
console.log(` MAC: ${device.macAddress}`);
}
if (device.version) {
console.log(` Version: ${device.version}`);
}
});
// Start discovery
discovery.start();
// Stop after 10 seconds
setTimeout(() => {
const devices = discovery.getDevices();
console.log(`Found ${devices.length} devices`);
discovery.stop();
}, 10000);API Reference
ESPHomeClient
The main client for connecting to ESPHome devices.
Constructor Options
interface ConnectionOptions {
host: string; // Device hostname or IP address
port?: number; // Port number (default: 6053)
encryptionKey?: string; // Base64 encryption key (recommended for secure communication)
password?: string; // API password (deprecated, use encryptionKey instead)
clientInfo?: string; // Client identification string
reconnect?: boolean; // Enable auto-reconnection (default: true)
reconnectInterval?: number; // Reconnection interval in ms (default: 5000)
pingInterval?: number; // Ping interval in ms (default: 20000)
pingTimeout?: number; // Ping timeout in ms (default: 90000)
connectTimeout?: number; // Connection timeout in ms (default: 10000)
expectedServerName?: string; // Expected server name for additional security (optional)
respondToTimeRequests?: boolean; // Respond to GetTimeRequest from device (default: true)
logger?: Logger; // Custom logger function
timerFactory?: TimerFactory; // Custom timer implementation
}Note on respondToTimeRequests:
By default, the client responds to GetTimeRequest messages from the device with the current system time. This is useful for devices that need to know the current time (e.g., for scheduling or logging). However, responding to these requests is not required for the connection to work properly.
You may want to disable this if you don't want to affect the device's timekeeping.
const client = new ESPHomeClient({
host: 'device.local',
respondToTimeRequests: false, // Don't respond to time requests
});Events
connected- Emitted when connected to the devicedisconnected- Emitted when disconnected from the deviceerror- Emitted on errordeviceInfo- Emitted when device info is receivedlogs- Emitted when a log message is receivedstate- Emitted on any state changebinarySensorState- Emitted on binary sensor state changesensorState- Emitted on sensor state changeswitchState- Emitted on switch state changetextSensorState- Emitted on text sensor state changefanState- Emitted on fan state changecoverState- Emitted on cover state changelightState- Emitted on light state changeentity- Emitted when an entity is discovered
Discovery
Service discovery for finding ESPHome devices on the network.
Methods
start(duration?: number): void- Start discoverystop(): void- Stop discoverygetDevices(): DiscoveredDevice[]- Get all discovered devicesclear(): void- Clear discovered devicesdestroy(): void- Clean up resources
Events
device- Emitted when a device is discoverederror- Emitted on error
Helper Functions
createClient(options: ConnectionOptions): ESPHomeClient- Create a new clientdiscover(duration?: number): Promise<DiscoveredDevice[]>- Discover devicesconnectToFirstDevice(encryptionKey?: string, duration?: number): Promise<ESPHomeClient | null>- Connect to first discovered device
ESPHome Configuration
To use this library, you need to enable the Native API component in your ESPHome configuration:
# Example ESPHome configuration
esphome:
name: my-device
# Recommended: Use encryption for secure communication
api:
encryption:
key: "YOUR_BASE64_ENCRYPTION_KEY" # Generate with: openssl rand -base64 32
# Legacy (deprecated): Password-based authentication
# api:
# password: "your-secure-password" # Not recommended, use encryption instead
# Optional: mDNS for device discovery
mdns:
disabled: falseAuthentication Methods:
- Encryption Key (Recommended): Provides end-to-end encryption using the Noise protocol with ChaCha20-Poly1305. This is the modern, secure approach.
- Password (Deprecated): Simple password authentication without encryption. Use only for legacy devices or testing.
Generating an encryption key:
# Generate a new encryption key
openssl rand -base64 32
# Or let ESPHome generate one for you:
esphome run your-device.yaml
# The key will be shown in the outputError Handling
The library provides specific error types for different scenarios:
import {
ESPHomeError,
ConnectionError,
AuthenticationError,
ProtocolError
} from '@webarray/esphome-native-api';
try {
await client.connect();
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Invalid password');
} else if (error instanceof ConnectionError) {
console.error('Connection failed:', error.message);
} else if (error instanceof ProtocolError) {
console.error('Protocol error:', error.message);
}
}Logging
The library uses the debug package for logging. By default, logging works automatically without any configuration.
Default Logging (No Setup Required)
Simply set the DEBUG environment variable to enable logs:
# Enable all ESPHome logs
DEBUG=esphome:* node your-app.js
# Enable specific namespaces
DEBUG=esphome:client node your-app.js
DEBUG=esphome:connection node your-app.js
DEBUG=esphome:discovery node your-app.js
# Multiple namespaces
DEBUG=esphome:client,esphome:connection node your-app.jsAvailable namespaces:
esphome:client- Client operations and eventsesphome:connection- Connection managementesphome:encrypted-connection- Encrypted connectionsesphome:discovery- Device discoveryesphome:protocol- Protocol messagesesphome:noise- Encryption details
Custom Logging
For integration with custom logging systems (Winston, Pino, Bunyan, Homey, etc.), provide a custom logger function:
import { ESPHomeClient } from '@webarray/esphome-native-api';
const client = new ESPHomeClient({
host: '192.168.1.100',
// Custom logger function
logger: (namespace, message, ...args) => {
// Use your logging system
winston.info({ namespace, message, args });
// Or: pino.info({ namespace, args }, message);
// Or: console.log(`[${namespace}] ${message}`, ...args);
}
});Global Logger Setup
To redirect all library logs to a custom logger:
import { setupGlobalLogger } from '@webarray/esphome-native-api';
setupGlobalLogger((message) => {
myLogger.info(message);
});Example: Integration with Winston
import winston from 'winston';
import { ESPHomeClient } from '@webarray/esphome-native-api';
const logger = winston.createLogger({
transports: [new winston.transports.Console()]
});
const client = new ESPHomeClient({
host: '192.168.1.100',
logger: (namespace, message, ...args) => {
logger.info({ namespace, args }, message);
}
});See examples/custom-logging-example.js for more integration examples.
Connection Health Monitoring
Monitor connection health and performance metrics:
import { ESPHomeClient } from '@webarray/esphome-native-api';
const client = new ESPHomeClient({
host: '192.168.1.100',
});
await client.connect();
// Get health metrics
const metrics = client.getHealthMetrics();
console.log('Connection uptime:', metrics.connectionUptime);
console.log('Messages sent:', metrics.messagesSent);
console.log('Average ping latency:', metrics.averagePingLatency);
// Get health status with analysis
const health = client.getConnectionHealth();
console.log('Status:', health.status); // 'healthy', 'degraded', 'unhealthy', or 'disconnected'
console.log('Issues:', health.issues); // Array of identified issues
// Reset metrics
client.resetHealthMetrics();Entity Helper Methods
Convenient methods for working with entities:
// Find entities by name
const tempSensor = client.findEntity('temperature');
// Find all entities matching a search
const sensors = client.findEntities('sensor');
// Get entities by type
const allSwitches = client.getEntitiesByType('switch');
// Check if entity exists
if (client.hasEntity('light_living_room')) {
// ...
}
// Wait for an entity to appear
const entity = await client.waitForEntity('new_sensor', 30000);
// Get entity count
console.log('Total entities:', client.getEntityCount());Debug Utilities
Built-in debugging tools:
import { ESPHomeClient } from '@webarray/esphome-native-api';
const client = new ESPHomeClient({
host: '192.168.1.100',
});
// Enable detailed logging
client.enableDetailedLogging();
// Get connection metrics for debugging
const metrics = client.getConnectionMetrics();
console.log(metrics);
// Capture protocol messages
client.captureProtocolDump(true);Enhanced Error Handling
Detailed error messages with codes and suggestions:
import {
ESPHomeClient,
ErrorCode,
ConnectionError,
AuthenticationError
} from '@webarray/esphome-native-api';
try {
await client.connect();
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Error code:', error.code);
console.error('Suggestion:', error.suggestion);
console.error('Context:', error.context);
} else if (error instanceof ConnectionError) {
switch (error.code) {
case ErrorCode.CONNECTION_TIMEOUT:
console.error('Connection timed out');
break;
case ErrorCode.CONNECTION_REFUSED:
console.error('Connection refused');
break;
}
}
}Custom Timer Implementations
For environments that require custom timer handling (like Athom Homey), you can provide a timerFactory:
import { ESPHomeClient, TimerFactory } from '@webarray/esphome-native-api';
// Example: Homey timer factory
const homeyTimers: TimerFactory = {
setTimeout: (callback, ms) => this.homey.setTimeout(callback, ms),
setInterval: (callback, ms) => this.homey.setInterval(callback, ms),
clearTimeout: (id) => this.homey.clearTimeout(id),
clearInterval: (id) => this.homey.clearInterval(id),
};
const client = new ESPHomeClient({
host: '192.168.1.100',
timerFactory: homeyTimers, // Use Homey's timer system
});This is useful for platforms where:
- Timers need to be managed by a specific global object
- You want to track or control all timers centrally
- The environment has custom timer lifecycle requirements
If not provided, the library uses Node.js's standard setTimeout/setInterval.
Development
Building
npm run buildTesting
npm test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverageProtocol Buffer Files
The library uses protocol buffers for communication. The proto files are pre-generated and committed to the repository.
To update the proto files from ESPHome:
npm run proto:generateThis automatically:
- Downloads the latest proto files from ESPHome
- Generates JavaScript and TypeScript code
- Fixes the
voidkeyword conflict (ESPHome usesvoidas a message name) - Formats the generated files with Prettier
The generated files (src/proto/api.js and src/proto/api.d.ts) are:
- Excluded from ESLint checks (in
.eslintignore) - Excluded from Prettier checks (in
.prettierignore) - Automatically formatted during generation
This ensures automated proto updates (via GitHub Actions) won't cause linter failures.
See proto/README.md for more details.
Auto-Generated Entity Types
import { ALL_ENTITY_TYPES, isValidEntityType, EntityType } from '@webarray/esphome-native-api';
// All entity types from proto files
console.log(ALL_ENTITY_TYPES);
// ['binary_sensor', 'sensor', 'switch', 'light', ...]
// Type guard for runtime validation
if (isValidEntityType(someString)) {
// TypeScript knows someString is EntityType
const type: EntityType = someString;
}Regenerating entity types:
When ESPHome updates its proto files, run:
npm run proto:generate # Regenerates proto files AND entity types
# or
npm run generate:entity-types # Only regenerates entity typesThe entity types in src/types/generated-entity-types.ts are automatically generated from proto/api.proto by parsing all ListEntities*Response messages.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- ESPHome - The amazing platform that this library interfaces with
- aioesphomeapi - The Python implementation that inspired this library
Support
For issues, questions, or contributions, please visit the GitHub repository.
