@webarray/esphome-native-api
Version:
TypeScript/Node.js client for ESPHome native API with encryption and deep sleep support
877 lines • 33.6 kB
JavaScript
"use strict";
/**
* ESPHome Native API Client
* High-level client for interacting with ESPHome devices
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ESPHomeClient = void 0;
const eventemitter3_1 = require("eventemitter3");
const debug_1 = __importDefault(require("debug"));
const encrypted_connection_1 = require("../connection/encrypted-connection");
const api = __importStar(require("../proto/api"));
const types_1 = require("../types");
const debug = (0, debug_1.default)('esphome:client');
class ESPHomeClient extends eventemitter3_1.EventEmitter {
constructor(options) {
super();
this.entities = new Map();
this.stateSubscriptions = new Set();
this.logSubscriptions = new Set();
this.isAuthenticating = false;
this.options = options;
// TEMPORARY: Always use EncryptedConnection (works for both encrypted and unencrypted)
// There's a bug in the Connection class that causes device to close connection on ListEntitiesRequest
debug(options.encryptionKey ? 'Using encrypted connection' : 'Using unencrypted connection (via EncryptedConnection)');
this.connection = new encrypted_connection_1.EncryptedConnection(options);
this.setupConnectionHandlers();
debug('Client initialized for %s:%d', options.host, options.port || 6053);
}
/**
* Setup connection event handlers
*/
setupConnectionHandlers() {
const conn = this.connection; // Type workaround for union type
conn.on('connect', () => {
debug('Connection established, starting handshake');
this.performHandshake().catch((error) => {
debug('Handshake failed: %s', error);
this.connection.disconnect();
});
});
conn.on('disconnect', (error) => {
debug('Disconnected: %s', error?.message || 'No error');
this.emit('disconnected', error);
});
conn.on('message', (message) => {
this.handleMessage(message).catch((error) => {
debug('Message handling error: %s', error);
this.emit('error', error instanceof Error ? error : new Error(String(error)));
});
});
conn.on('error', (error) => {
this.emit('error', error);
});
}
/**
* Connect to the ESPHome device
*/
async connect() {
debug('Connecting to device');
await this.connection.connect();
// Wait for the handshake to complete
// The handshake is performed asynchronously after the connection is established
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.off('connected', handleConnected);
this.off('error', handleError);
reject(new Error('Handshake timeout'));
}, 10000);
// Allow Node.js to exit if this is the only timer left
timeout.unref();
const handleConnected = () => {
clearTimeout(timeout);
this.off('error', handleError);
resolve();
};
const handleError = (error) => {
clearTimeout(timeout);
this.off('connected', handleConnected);
reject(error);
};
this.once('connected', handleConnected);
this.once('error', handleError);
});
}
/**
* Disconnect from the device
*/
disconnect() {
debug('Disconnecting from device');
this.connection.disconnect();
}
/**
* Perform the initial handshake
*/
async performHandshake() {
// Send Hello request
await this.sendHelloRequest();
// Wait for Hello response
const helloResponse = await this.waitForMessage(types_1.MessageType.HelloResponse, 5000);
this.handleHelloResponse(helloResponse);
// Always send authentication (like Python's login=True)
// For devices without password: they may not respond, which is fine
// For devices with password: they will respond with AuthenticationResponse
await this.authenticate();
// Mark as authenticated
this.connection.setAuthenticated(true);
// Request device info
await this.requestDeviceInfo();
// Emit connected event
this.emit('connected');
}
/**
* Send Hello request
*/
async sendHelloRequest() {
const message = {
clientInfo: this.options.clientInfo || 'ESPHome TypeScript Client',
apiVersionMajor: 1,
apiVersionMinor: 13, // Match Python aioesphomeapi client version
};
const data = this.encodeMessage('HelloRequest', message);
this.connection.sendMessage(types_1.MessageType.HelloRequest, data);
debug('Sent Hello request');
}
/**
* Handle Hello response
*/
handleHelloResponse(message) {
const response = this.decodeMessage('HelloResponse', message.data);
debug('Hello response: %o', response);
this.connection.setApiVersion(response.apiVersionMajor || 1, response.apiVersionMinor || 9);
if (response.serverInfo) {
this.connection.setServerInfo(response.serverInfo);
}
}
/**
* Authenticate with the device
*/
async authenticate() {
if (this.isAuthenticating) {
throw new types_1.AuthenticationError('Already authenticating');
}
this.isAuthenticating = true;
try {
debug('Sending authentication request%s', this.options.password ? ' with password' : '');
const message = {
password: this.options.password || '',
};
const data = this.encodeMessage('AuthenticationRequest', message);
this.connection.sendMessage(types_1.MessageType.ConnectRequest, data);
// Only wait for response if password is set
// Devices without password authentication won't send AuthenticationResponse
if (this.options.password) {
const response = await this.waitForMessage(types_1.MessageType.ConnectResponse, 5000);
const connectResponse = this.decodeMessage('AuthenticationResponse', response.data);
if (connectResponse.invalidPassword) {
throw new types_1.AuthenticationError('Invalid password');
}
debug('Authentication successful');
}
else {
debug('Authentication request sent (no response expected without password)');
}
}
finally {
this.isAuthenticating = false;
}
}
/**
* Request device information
*/
async requestDeviceInfo() {
debug('Requesting device info');
this.connection.sendMessage(types_1.MessageType.DeviceInfoRequest, Buffer.alloc(0));
const response = await this.waitForMessage(types_1.MessageType.DeviceInfoResponse, 5000);
const info = this.decodeMessage('DeviceInfoResponse', response.data);
this.deviceInfo = {
usesPassword: info.uses_password || false,
name: info.name || '',
macAddress: info.mac_address || '',
esphomeVersion: info.esphome_version || '',
compilationTime: info.compilation_time || '',
model: info.model || '',
hasDeepSleep: info.has_deep_sleep || false,
projectName: info.project_name,
projectVersion: info.project_version,
webserverPort: info.webserver_port,
bluetoothProxyVersion: info.bluetooth_proxy_version,
manufacturer: info.manufacturer,
friendlyName: info.friendly_name,
voiceAssistantVersion: info.voice_assistant_version,
suggestedArea: info.suggested_area,
};
debug('Device info: %o', this.deviceInfo);
// Enable deep sleep mode on connection if device supports it
if (this.deviceInfo.hasDeepSleep) {
debug('Device has deep sleep - configuring connection');
if ('setDeepSleepMode' in this.connection) {
this.connection.setDeepSleepMode(true);
}
}
this.emit('deviceInfo', this.deviceInfo);
}
/**
* Get device information
*/
getDeviceInfo() {
return this.deviceInfo;
}
/**
* List all entities on the device
*/
async listEntities() {
debug('Listing entities');
this.entities.clear();
this.connection.sendMessage(types_1.MessageType.ListEntitiesRequest, Buffer.alloc(0));
// Wait for ListEntitiesDone message
await this.waitForMessage(types_1.MessageType.ListEntitiesDoneResponse, 10000);
return Array.from(this.entities.values());
}
/**
* Subscribe to state changes
*/
subscribeStates(callback) {
debug('Subscribing to states');
if (callback) {
this.stateSubscriptions.add(callback);
}
this.connection.sendMessage(types_1.MessageType.SubscribeStatesRequest, Buffer.alloc(0));
}
/**
* Unsubscribe from state changes
*/
unsubscribeStates(callback) {
if (callback) {
this.stateSubscriptions.delete(callback);
}
}
/**
* Subscribe to logs
*/
subscribeLogs(level = 3, callback) {
debug('Subscribing to logs with level %d', level);
if (callback) {
this.logSubscriptions.add(callback);
}
const message = {
level,
dumpConfig: false,
};
const data = this.encodeMessage('SubscribeLogsRequest', message);
this.connection.sendMessage(types_1.MessageType.SubscribeLogsRequest, data);
}
/**
* Unsubscribe from logs
*/
unsubscribeLogs(callback) {
if (callback) {
this.logSubscriptions.delete(callback);
}
}
/**
* Get all entities of a specific type
* @param type - Entity type filter (e.g., 'sensor', 'binary_sensor', 'switch')
* @returns Array of entities matching the type
*/
getEntitiesByType(type) {
const entities = Array.from(this.entities.values());
return entities.filter((entity) => {
// Prefer explicit type field when available
if (entity.type) {
if (typeof type === 'string') {
return entity.type === type;
}
return entity.type === type;
}
// Fallback: match by constructor name or inferred type
const constructorName = entity.constructor?.name?.toLowerCase() || '';
return constructorName.includes(String(type).toLowerCase().replace('_', ''));
});
}
/**
* Find entity by name or object ID
* @param nameOrId - Entity name or object ID to search for
* @returns First matching entity or undefined
*/
findEntity(nameOrId) {
const entities = Array.from(this.entities.values());
const search = nameOrId.toLowerCase();
return entities.find((entity) => entity.name.toLowerCase().includes(search) ||
entity.objectId.toLowerCase().includes(search) ||
entity.uniqueId?.toLowerCase().includes(search));
}
/**
* Find all entities matching a search term
* @param searchTerm - Search term to match against name, object ID, or unique ID
* @returns Array of matching entities
*/
findEntities(searchTerm) {
const entities = Array.from(this.entities.values());
const search = searchTerm.toLowerCase();
return entities.filter((entity) => entity.name.toLowerCase().includes(search) ||
entity.objectId.toLowerCase().includes(search) ||
entity.uniqueId?.toLowerCase().includes(search));
}
/**
* Get entity by key
* @param key - Entity key
* @returns Entity info or undefined if not found
*/
getEntityByKey(key) {
return this.entities.get(key);
}
/**
* Get entity state by key (requires state subscription to be active)
* Note: This returns the last known state. Subscribe to states to receive updates.
* @param key - Entity key
* @returns Entity info for the key, or undefined if not found
*/
getEntityInfo(key) {
return this.entities.get(key);
}
/**
* Wait for an entity to appear during discovery
* @param nameOrId - Entity name or object ID to wait for
* @param timeout - Timeout in milliseconds (default: 30000)
* @returns Promise that resolves with the entity info
*/
async waitForEntity(nameOrId, timeout = 30000) {
const search = nameOrId.toLowerCase();
// Check if entity already exists
const existing = this.findEntity(search);
if (existing) {
return existing;
}
// Wait for entity to appear
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.off('entity', handler);
reject(new types_1.ESPHomeError(`Timeout waiting for entity: ${nameOrId}`, 'TIMEOUT', 'Ensure the entity exists on the device and listEntities() has been called', { nameOrId, timeout }));
}, timeout);
const handler = (entity) => {
const matches = entity.name.toLowerCase().includes(search) ||
entity.objectId.toLowerCase().includes(search) ||
entity.uniqueId?.toLowerCase().includes(search);
if (matches) {
clearTimeout(timer);
this.off('entity', handler);
resolve(entity);
}
};
this.on('entity', handler);
});
}
/**
* Get all entities
* @returns Array of all entities
*/
getAllEntities() {
return Array.from(this.entities.values());
}
/**
* Check if an entity exists
* @param key - Entity key or name
* @returns True if entity exists
*/
hasEntity(key) {
if (typeof key === 'number') {
return this.entities.has(key);
}
return this.findEntity(key) !== undefined;
}
/**
* Get entity count
* @returns Total number of entities
*/
getEntityCount() {
return this.entities.size;
}
/**
* Get entities by category
* @param category - Entity category (EntityCategory.NONE, EntityCategory.CONFIG, or EntityCategory.DIAGNOSTIC)
* @returns Array of entities in the specified category
*/
getEntitiesByCategory(category) {
const entities = Array.from(this.entities.values());
return entities.filter((entity) => entity.entityCategory === category);
}
/**
* Switch command
*/
async switchCommand(key, state) {
debug('Switch command: key=%d, state=%s', key, state);
const message = {
key,
state,
};
const data = this.encodeMessage('SwitchCommandRequest', message);
this.connection.sendMessage(types_1.MessageType.SwitchCommandRequest, data);
}
/**
* Light command
*/
async lightCommand(key, options) {
debug('Light command: key=%d, options=%o', key, options);
const message = {
key,
...options,
};
const data = this.encodeMessage('LightCommandRequest', message);
this.connection.sendMessage(types_1.MessageType.LightCommandRequest, data);
}
/**
* Fan command
*/
async fanCommand(key, options) {
debug('Fan command: key=%d, options=%o', key, options);
const message = {
key,
};
if (options.state !== undefined) {
message.state = options.state;
message.has_state = true;
}
if (options.speed !== undefined) {
message.speed = options.speed;
message.has_speed = true;
}
if (options.speedLevel !== undefined) {
message.speed_level = options.speedLevel;
message.has_speed_level = true;
}
if (options.oscillating !== undefined) {
message.oscillating = options.oscillating;
message.has_oscillating = true;
}
const data = this.encodeMessage('FanCommandRequest', message);
this.connection.sendMessage(types_1.MessageType.FanCommandRequest, data);
}
async numberCommand(key, state) {
debug('Number command: key=%d, state=%d', key, state);
const message = {
key,
state,
};
const data = this.encodeMessage('NumberCommandRequest', message);
this.connection.sendMessage(types_1.MessageType.NumberCommandRequest, data);
}
async selectCommand(key, state) {
debug('Select command: key=%d, state=%s', key, state);
const message = {
key,
state,
};
const data = this.encodeMessage('SelectCommandRequest', message);
this.connection.sendMessage(types_1.MessageType.SelectCommandRequest, data);
}
async buttonCommand(key) {
debug('Button command: key=%d', key);
const message = {
key,
};
const data = this.encodeMessage('ButtonCommandRequest', message);
this.connection.sendMessage(types_1.MessageType.ButtonCommandRequest, data);
}
/**
* Handle incoming messages
*/
async handleMessage(message) {
switch (message.type) {
case types_1.MessageType.DeviceInfoResponse:
this.handleDeviceInfoResponse(message);
break;
case types_1.MessageType.ListEntitiesBinarySensorResponse:
case types_1.MessageType.ListEntitiesSensorResponse:
case types_1.MessageType.ListEntitiesSwitchResponse:
case types_1.MessageType.ListEntitiesLightResponse:
case types_1.MessageType.ListEntitiesTextSensorResponse:
case types_1.MessageType.ListEntitiesFanResponse:
case types_1.MessageType.ListEntitiesCoverResponse:
case types_1.MessageType.ListEntitiesNumberResponse:
case types_1.MessageType.ListEntitiesSelectResponse:
case types_1.MessageType.ListEntitiesButtonResponse:
this.handleEntityResponse(message);
break;
case types_1.MessageType.BinarySensorStateResponse:
case types_1.MessageType.SensorStateResponse:
case types_1.MessageType.SwitchStateResponse:
case types_1.MessageType.TextSensorStateResponse:
case types_1.MessageType.LightStateResponse:
case types_1.MessageType.FanStateResponse:
case types_1.MessageType.CoverStateResponse:
case types_1.MessageType.NumberStateResponse:
case types_1.MessageType.SelectStateResponse:
this.handleStateResponse(message);
break;
case types_1.MessageType.SubscribeLogsResponse:
this.handleLogResponse(message);
break;
case types_1.MessageType.ListEntitiesDoneResponse:
// This message signals the end of the entity list
// waitForMessage() will handle it
debug('Received ListEntitiesDoneResponse - entity list complete');
break;
case types_1.MessageType.GetTimeRequest:
// Device is requesting current time - respond if enabled
if (this.options.respondToTimeRequests !== false) {
this.handleGetTimeRequest();
}
else {
debug('Received GetTimeRequest (ignoring - respondToTimeRequests disabled)');
}
break;
default:
debug('Unhandled message type: %d', message.type);
}
}
/**
* Handle GetTimeRequest from device
*/
handleGetTimeRequest() {
debug('Received GetTimeRequest from device');
// Send current Unix timestamp (seconds since epoch)
const epochSeconds = Math.floor(Date.now() / 1000);
const message = {
epochSeconds,
};
const data = this.encodeMessage('GetTimeResponse', message);
this.connection.sendMessage(types_1.MessageType.GetTimeResponse, data);
debug('Sent GetTimeResponse with timestamp %d', epochSeconds);
}
/**
* Handle device info response
*/
handleDeviceInfoResponse(message) {
const info = this.decodeMessage('DeviceInfoResponse', message.data);
debug('Device info response: %o', info);
}
/**
* Handle entity response
*/
handleEntityResponse(message) {
let entity = null;
let entityType = null;
switch (message.type) {
case types_1.MessageType.ListEntitiesBinarySensorResponse:
entity = this.decodeMessage('ListEntitiesBinarySensorResponse', message.data);
entityType = types_1.EntityDomain.BINARY_SENSOR;
break;
case types_1.MessageType.ListEntitiesSensorResponse:
entity = this.decodeMessage('ListEntitiesSensorResponse', message.data);
entityType = types_1.EntityDomain.SENSOR;
break;
case types_1.MessageType.ListEntitiesSwitchResponse:
entity = this.decodeMessage('ListEntitiesSwitchResponse', message.data);
entityType = types_1.EntityDomain.SWITCH;
break;
case types_1.MessageType.ListEntitiesLightResponse:
entity = this.decodeMessage('ListEntitiesLightResponse', message.data);
entityType = types_1.EntityDomain.LIGHT;
break;
case types_1.MessageType.ListEntitiesTextSensorResponse:
entity = this.decodeMessage('ListEntitiesTextSensorResponse', message.data);
entityType = types_1.EntityDomain.TEXT_SENSOR;
break;
case types_1.MessageType.ListEntitiesFanResponse:
entity = this.decodeMessage('ListEntitiesFanResponse', message.data);
entityType = types_1.EntityDomain.FAN;
break;
case types_1.MessageType.ListEntitiesCoverResponse:
entity = this.decodeMessage('ListEntitiesCoverResponse', message.data);
entityType = types_1.EntityDomain.COVER;
break;
case types_1.MessageType.ListEntitiesNumberResponse:
entity = this.decodeMessage('ListEntitiesNumberResponse', message.data);
entityType = types_1.EntityDomain.NUMBER;
break;
case types_1.MessageType.ListEntitiesSelectResponse:
entity = this.decodeMessage('ListEntitiesSelectResponse', message.data);
entityType = types_1.EntityDomain.SELECT;
break;
case types_1.MessageType.ListEntitiesButtonResponse:
entity = this.decodeMessage('ListEntitiesButtonResponse', message.data);
entityType = types_1.EntityDomain.BUTTON;
break;
}
if (entity && entity.key !== undefined) {
const entityWithType = {
...entity,
type: entity.type ?? entityType,
};
this.entities.set(entityWithType.key, entityWithType);
this.emit('entity', entityWithType);
debug('Entity registered: %o', entityWithType);
}
}
/**
* Handle state response
*/
handleStateResponse(message) {
let state = null;
switch (message.type) {
case types_1.MessageType.BinarySensorStateResponse:
state = this.decodeMessage('BinarySensorStateResponse', message.data);
this.emit('binarySensorState', state);
break;
case types_1.MessageType.SensorStateResponse:
state = this.decodeMessage('SensorStateResponse', message.data);
this.emit('sensorState', state);
break;
case types_1.MessageType.SwitchStateResponse:
state = this.decodeMessage('SwitchStateResponse', message.data);
this.emit('switchState', state);
break;
case types_1.MessageType.TextSensorStateResponse:
state = this.decodeMessage('TextSensorStateResponse', message.data);
this.emit('textSensorState', state);
break;
case types_1.MessageType.FanStateResponse:
state = this.decodeMessage('FanStateResponse', message.data);
this.emit('fanState', state);
break;
case types_1.MessageType.CoverStateResponse:
state = this.decodeMessage('CoverStateResponse', message.data);
this.emit('coverState', state);
break;
case types_1.MessageType.LightStateResponse:
state = this.decodeMessage('LightStateResponse', message.data);
this.emit('lightState', state);
break;
case types_1.MessageType.NumberStateResponse:
state = this.decodeMessage('NumberStateResponse', message.data);
this.emit('numberState', state);
break;
case types_1.MessageType.SelectStateResponse:
state = this.decodeMessage('SelectStateResponse', message.data);
this.emit('selectState', state);
break;
}
if (state) {
this.emit('state', state);
// Notify subscriptions
for (const callback of this.stateSubscriptions) {
try {
callback(state);
}
catch (error) {
debug('State subscription callback error: %s', error);
}
}
debug('State update: %o', state);
}
}
/**
* Handle log response
*/
handleLogResponse(message) {
const log = this.decodeMessage('SubscribeLogsResponse', message.data);
const entry = {
level: log.level || 0,
message: log.message || '',
sendFailed: log.sendFailed || false,
};
this.emit('logs', entry);
// Notify subscriptions
for (const callback of this.logSubscriptions) {
try {
callback(entry);
}
catch (error) {
debug('Log subscription callback error: %s', error);
}
}
debug('Log: [%d] %s', entry.level, entry.message);
}
/**
* Wait for a specific message type
*/
waitForMessage(type, timeout) {
return new Promise((resolve, reject) => {
const conn = this.connection; // Type workaround
const timer = setTimeout(() => {
conn.off('message', handler);
reject(new types_1.ESPHomeError(`Timeout waiting for message type ${type}`));
}, timeout);
// Allow Node.js to exit if this is the only timer left
timer.unref();
const handler = (message) => {
if (message.type === type) {
clearTimeout(timer);
conn.off('message', handler);
resolve(message);
}
};
conn.on('message', handler);
});
}
/**
* Encode a message using protobuf
*/
encodeMessage(type, data) {
try {
const MessageClass = api[type];
if (!MessageClass || !MessageClass.encode) {
debug('Unknown message type: %s', type);
return Buffer.alloc(0);
}
const message = MessageClass.create(data);
const encoded = MessageClass.encode(message).finish();
return Buffer.from(encoded);
}
catch (error) {
debug('Failed to encode message type %s: %s', type, error);
return Buffer.alloc(0);
}
}
/**
* Decode a message using protobuf
*/
decodeMessage(type, data) {
try {
const MessageClass = api[type];
if (!MessageClass || !MessageClass.decode) {
debug('Unknown message type: %s', type);
return {};
}
const decoded = MessageClass.decode(data);
return decoded;
}
catch (error) {
debug('Failed to decode message type %s: %s', type, error);
return {};
}
}
/**
* Destroy the client
*/
destroy() {
debug('Destroying client');
this.connection.destroy();
this.entities.clear();
this.stateSubscriptions.clear();
this.logSubscriptions.clear();
this.removeAllListeners();
}
/**
* Check if connected
*/
isConnected() {
return this.connection.isConnected();
}
/**
* Check if authenticated
*/
isAuthenticated() {
return this.connection.isAuthenticated();
}
/**
* Get connection health metrics
* @returns Health metrics including latency, uptime, and message counts
*/
getHealthMetrics() {
if ('getHealthMetrics' in this.connection) {
return this.connection.getHealthMetrics();
}
return undefined;
}
/**
* Get connection health status with analysis
* @returns Health status with metrics, status indicator, and identified issues
*/
getConnectionHealth() {
if ('getConnectionHealth' in this.connection) {
return this.connection.getConnectionHealth();
}
return undefined;
}
/**
* Reset health metrics counters
*/
resetHealthMetrics() {
if ('resetHealthMetrics' in this.connection) {
this.connection.resetHealthMetrics();
}
}
/**
* Enable detailed logging
* Sets DEBUG environment variable for this instance
*/
enableDetailedLogging() {
// Store original debug value
if (typeof process !== 'undefined' && process.env) {
process.env['DEBUG'] = 'esphome:*';
debug('Detailed logging enabled');
}
else {
debug('Detailed logging could not be enabled');
}
}
/**
* Get connection metrics for debugging
* @returns Object containing connection statistics
*/
getConnectionMetrics() {
const health = this.getHealthMetrics();
const state = this.connection.getState();
return {
state,
health,
entityCount: this.entities.size,
subscriptions: {
states: this.stateSubscriptions.size,
logs: this.logSubscriptions.size,
},
};
}
/**
* Capture protocol dump to console
* Useful for debugging protocol issues
* @param enable - Enable or disable protocol dumping
*/
captureProtocolDump(enable = true) {
if (enable) {
debug('Protocol dump enabled - all messages will be logged');
// Hook into message events
const conn = this.connection;
conn.on('message', (msg) => {
console.log('PROTOCOL DUMP:', {
type: msg.type,
data: msg.data,
timestamp: Date.now(),
});
});
}
else {
debug('Protocol dump disabled');
}
}
}
exports.ESPHomeClient = ESPHomeClient;
//# sourceMappingURL=client.js.map