TypeScript/Node.js client for ESPHome native API with encryption and deep sleep support
npm install @webarray/esphome-native-api






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.
- 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
- Node.js >= 18.0.0
- ESPHome device with Native API component enabled
``bash`
npm install @webarray/esphome-native-api
`typescript
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:
`bash`
DEBUG=esphome:* node your-app.js
`typescript
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});`
}
`typescript
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();
`
Subscribe to device logs using log level constants:
`typescript
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 messages
`
`typescript
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})); Key: ${entity.key}
console.log(); Type: ${entity.constructor.name}
console.log();`
}
`typescript
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();
`
`typescript
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}); MAC: ${device.macAddress}
if (device.macAddress) {
console.log(); Version: ${device.version}
}
if (device.version) {
console.log();
}
});
// Start discovery
discovery.start();
// Stop after 10 seconds
setTimeout(() => {
const devices = discovery.getDevices();
console.log(Found ${devices.length} devices);`
discovery.stop();
}, 10000);
The main client for connecting to ESPHome devices.
#### Constructor Options
`typescript`
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.
`typescript`
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
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
- createClient(options: ConnectionOptions): ESPHomeClient - Create a new clientdiscover(duration?: number): Promise
- - Discover devicesconnectToFirstDevice(encryptionKey?: string, duration?: number): Promise
- - Connect to first discovered device
To use this library, you need to enable the Native API component in your ESPHome configuration:
`yamlExample ESPHome configuration
esphome:
name: my-device
Authentication 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:
`bash
Generate a new encryption key
openssl rand -base64 32Or let ESPHome generate one for you:
esphome run your-device.yaml
The key will be shown in the output
`Error Handling
The library provides specific error types for different scenarios:
`typescript
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.
$3
Simply set the
DEBUG environment variable to enable logs:`bash
Enable all ESPHome logs
DEBUG=esphome:* node your-app.jsEnable specific namespaces
DEBUG=esphome:client node your-app.js
DEBUG=esphome:connection node your-app.js
DEBUG=esphome:discovery node your-app.jsMultiple namespaces
DEBUG=esphome:client,esphome:connection node your-app.js
`Available namespaces:
-
esphome:client - Client operations and events
- esphome:connection - Connection management
- esphome:encrypted-connection - Encrypted connections
- esphome:discovery - Device discovery
- esphome:protocol - Protocol messages
- esphome:noise - Encryption details$3
For integration with custom logging systems (Winston, Pino, Bunyan, Homey, etc.), provide a custom logger function:
`typescript
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);
}
});
`$3
To redirect all library logs to a custom logger:
`typescript
import { setupGlobalLogger } from '@webarray/esphome-native-api';setupGlobalLogger((message) => {
myLogger.info(message);
});
`$3
`typescript
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:
`typescript
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:
`typescript
// 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:
`typescript
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:
`typescript
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:`typescript
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
$3
`bash
npm run build
`$3
`bash
npm test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
`$3
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:
`bash
npm run proto:generate
`This automatically:
- Downloads the latest proto files from ESPHome
- Generates JavaScript and TypeScript code
- Fixes the
void keyword conflict (ESPHome uses void as a message name)
- Formats the generated files with PrettierThe 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 generationThis ensures automated proto updates (via GitHub Actions) won't cause linter failures.
See
proto/README.md for more details.Auto-Generated Entity Types
`typescript
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:
`bash
npm run proto:generate # Regenerates proto files AND entity types
or
npm run generate:entity-types # Only regenerates entity types
`The 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.
1. Fork the repository
2. Create your feature branch (
git checkout -b feature/AmazingFeature)
3. Commit your changes (git commit -m 'Add some AmazingFeature')
4. Push to the branch (git push origin feature/AmazingFeature`)This project is licensed under the MIT License - see the LICENSE file for details.
- ESPHome - The amazing platform that this library interfaces with
- aioesphomeapi - The Python implementation that inspired this library
For issues, questions, or contributions, please visit the GitHub repository.