esphome-client
Version:
A nearly complete implementation of the ESPHome client protocol with encryption support.
559 lines (558 loc) • 23.3 kB
TypeScript
import type { EspHomeLogging, Nullable } from "./types.js";
import { EventEmitter } from "node:events";
/**
* Represents one entity from the ESPHome device. An entity is any controllable or observable component on the device.
*
* @property key - The numeric key identifier for the entity.
* @property name - The human-readable name of the entity.
* @property type - The type of entity (e.g., "switch", "light", "cover").
*/
interface Entity {
key: number;
name: string;
type: string;
}
/**
* Device information to send when requested by the ESPHome device. This structure contains metadata about the connected ESPHome device.
*
* @property bluetoothProxyFeatureFlags - Bluetooth proxy feature flags.
* @property compilationTime - When the client was compiled/started.
* @property esphomeVersion - Version of ESPHome protocol being used.
* @property hasDeepSleep - Whether the client supports deep sleep.
* @property legacyBluetoothProxyVersion - Legacy Bluetooth proxy version.
* @property macAddress - MAC address of the client (format: "AA:BB:CC:DD:EE:FF").
* @property model - Model or type of the client.
* @property name - Friendly name of the client.
* @property projectName - Name of the project/plugin.
* @property projectVersion - Version of the project/plugin.
* @property usesPassword - Whether the client uses password authentication.
* @property webserverPort - Port number of any web server.
*/
export interface DeviceInfo {
bluetoothProxyFeatureFlags?: number;
compilationTime?: string;
esphomeVersion?: string;
hasDeepSleep?: boolean;
legacyBluetoothProxyVersion?: number;
macAddress?: string;
model?: string;
name?: string;
projectName?: string;
projectVersion?: string;
usesPassword?: boolean;
webserverPort?: number;
}
/**
* Configuration options for creating an ESPHome client instance. These options control how the client connects to and communicates with ESPHome devices.
*
* @property clientId - Optional client identifier to announce when connecting (default: "esphome-client").
* @property host - The hostname or IP address of the ESPHome device.
* @property logger - Optional logging interface for debug and error messages.
* @property port - The port number for the ESPHome API (default: 6053).
* @property psk - Optional base64 encoded pre-shared key for Noise encryption.
* @property serverName - Optional expected server name for validation during encrypted connections.
*/
export interface EspHomeClientOptions {
clientId?: Nullable<string>;
host: string;
logger?: EspHomeLogging;
port?: number;
psk?: Nullable<string>;
serverName?: Nullable<string>;
}
/**
* ESPHome API client for communicating with ESPHome devices.
* Implements the ESPHome native API protocol over TCP with optional Noise encryption.
*
* This client automatically handles encryption based on the presence of a pre-shared key (PSK). When a PSK is provided, the client will attempt an encrypted connection
* first and fall back to plaintext if the device doesn't support encryption. Without a PSK, only plaintext connections are attempted.
*
* @extends EventEmitter
* @emits connect - Connected to device with encryption status (boolean).
* @emits disconnect - Disconnected from device with optional reason string.
* @emits message - Raw message received with type and payload in MessageEventData format.
* @emits entities - List of discovered entities after enumeration completes.
* @emits telemetry - Generic telemetry update for any entity with TelemetryData.
* @emits heartbeat - Heartbeat response received (ping/pong).
* @emits time - Time response received with epoch seconds as number.
* @emits deviceInfo - Device information received with DeviceInfo and encryption status.
* @emits {entityType} - Type-specific telemetry events (e.g., "cover", "light", "switch", "binary_sensor", "sensor", "text_sensor", "number", "lock").
*
* @example
* ```typescript
* // Create a client without encryption for devices that don't require it.
* const client = new EspHomeClient({
* host: "192.168.1.100",
* logger: log
* });
* client.connect();
*
* // Create a client with encryption - will try encrypted first, then plaintext.
* const encryptedClient = new EspHomeClient({
* host: "192.168.1.100",
* port: 6053,
* psk: "base64encodedkey",
* logger: log
* });
* encryptedClient.connect();
*
* // Create a client with custom client ID and server name validation.
* const customClient = new EspHomeClient({
* host: "192.168.1.100",
* clientId: "my-custom-client",
* serverName: "garage-controller",
* psk: "base64encodedkey",
* logger: log
* });
* customClient.connect();
*
* // Listen for connection events to know when the device is ready.
* client.on("connect", (usingEncryption) => {
* console.log(`Connected ${usingEncryption ? 'with' : 'without'} encryption`);
* });
*
* // Listen for discovered entities to see what's available.
* client.on("entities", (entities) => {
* // Log all available entity IDs for reference.
* client.logAllEntityIds();
* });
*
* // Send commands using entity IDs once entities are discovered.
* await client.sendSwitchCommand("switch-garagedoor", true);
* await client.sendLightCommand("light-light", { state: true, brightness: 0.8 });
* await client.sendCoverCommand("cover-door", { command: "open" });
* ```
*/
export declare class EspHomeClient extends EventEmitter {
private clientId;
private clientSocket;
private dataListener;
private host;
private log;
private port;
private recvBuffer;
private remoteDeviceInfo;
private discoveredEntities;
private entityKeys;
private entityNames;
private entityTypes;
private encryptionKey;
private expectedServerName;
private noiseClient;
private handshakeState;
private connectionState;
private connectionTimer;
private usingEncryption;
/**
* Creates a new ESPHome client instance. The client can be configured for both encrypted and unencrypted connections depending on the provided options. When a PSK
* is provided, the client will automatically attempt encryption first and fall back to plaintext if the device doesn't support it.
*
* @param options - Configuration options for the client connection.
* @param options.clientId - Optional client identifier to announce when connecting (default: "esphome-client").
* @param options.host - The hostname or IP address of the ESPHome device.
* @param options.logger - Optional logging interface for debug and error messages. If not provided, defaults to console methods.
* @param options.port - The port number for the ESPHome API (default: 6053).
* @param options.psk - Optional base64 encoded pre-shared key for Noise encryption. Must be exactly 32 bytes when decoded.
* @param options.serverName - Optional expected server name for validation during encrypted connections.
*
* @example
* ```typescript
* // Minimal configuration for unencrypted connection.
* const client = new EspHomeClient({ host: "192.168.1.100" });
*
* // Full configuration with all options except serverName.
* const client = new EspHomeClient({
* host: "192.168.1.100",
* port: 6053,
* clientId: "homebridge-ratgdo",
* psk: "base64encodedkey",
* logger: myLogger
* });
* ```
*/
constructor(options: EspHomeClientOptions);
/**
* Connect to the ESPHome device and start communication. This method initializes a new connection. If an encryption key is provided, it will attempt an encrypted
* connection first and fall back to plaintext if the device doesn't support encryption. Without an encryption key, only plaintext connections are attempted.
*/
connect(): void;
/**
* Create a new TCP connection to the ESPHome device. This is a separate method to allow reconnection with different protocols when falling back from encrypted to
* plaintext connections.
*/
private createConnection;
/**
* Internal disconnect method that cleans up resources and emits the disconnect event.
*
* @param reason - Optional reason for the disconnection.
*/
private _disconnect;
/**
* Disconnect from the ESPHome device and cleanup resources. This method should be called when you're done communicating with the device.
*/
disconnect(): void;
/**
* Clean up Noise encryption resources. This ensures we don't leak memory from the WebAssembly Noise library.
*/
private cleanupNoiseResources;
/**
* Clear the connection timer if it exists. This prevents timeout callbacks from firing after they're no longer needed.
*/
private clearConnectionTimer;
/**
* Set a connection timer for timeout detection. This helps detect when a connection attempt has stalled.
*
* @param timeout - Timeout duration in milliseconds (default: 5000).
*/
private setConnectionTimer;
/**
* Handle connection timeout based on the current connection state. This method determines what to do when a connection attempt times out.
*/
private handleConnectionTimeout;
/**
* Handle a newly connected socket. This method is called when the TCP connection is established.
*/
private handleConnect;
/**
* Initialize the Noise handshake for encrypted connections. This sets up the Noise protocol state and sends the initial handshake message.
*/
private initializeNoiseHandshake;
/**
* Send a hello request to let ESPHome know who we are. This is the initial message sent to establish communication when unencrypted. When encrypted, this is sent
* after we've established a secure connection.
*/
private sendHello;
/**
* Handle socket errors by logging appropriate messages and disconnecting.
*
* @param err - The socket error that occurred.
*/
private handleSocketError;
/**
* Handle socket closure. If we were trying encryption and the socket closed, it might be because the device doesn't support encryption.
*/
private handleSocketClose;
/**
* Clean up the data listener if it exists.
*/
private cleanupDataListener;
/**
* Handle incoming raw data, frame messages, and dispatch. This method accumulates data and processes complete frames.
*
* @param chunk - The incoming data chunk from the socket.
*/
private handleData;
/**
* Process Noise protocol frames. This handles the Noise handshake and encrypted message processing.
*/
private processNoiseFrames;
/**
* Extract a Noise frame from the receive buffer. Noise frames have a specific format: [0x01][size_high][size_low][data...].
*
* @returns The frame data or null if incomplete.
*/
private extractNoiseFrame;
/**
* Handle the Noise hello response. This processes the server's protocol selection and validates the server name if configured.
*
* @param serverHello - The server hello data.
*/
private handleNoiseHello;
/**
* Handle the Noise handshake response. This completes the Noise handshake and establishes the encrypted channel.
*
* @param serverHandshake - The server handshake data.
*/
private handleNoiseHandshake;
/**
* Write a Noise protocol frame. Frames are sent with a specific header format for the Noise protocol.
*
* @param frame - The frame data to send.
*/
private writeNoiseFrame;
/**
* Serialize a message for Noise protocol. This creates the message format used within encrypted frames.
*
* @param type - The message type.
* @param payload - The message payload.
*
* @returns The serialized message buffer.
*/
private serializeNoiseMessage;
/**
* Deserialize a Noise protocol message. This extracts the message type and payload from the decrypted data.
*
* @param buffer - The buffer to deserialize.
*
* @returns The message type and payload, or null if invalid.
*/
private deserializeNoiseMessage;
/**
* Process plaintext frames during the handshake phase. This handles unencrypted message processing for devices that don't require encryption.
*/
private processPlaintextFrames;
/**
* Dispatch based on message type. This is the main message router that handles all protocol messages.
*
* @param type - The message type identifier.
* @param payload - The message payload data.
*/
private handleMessage;
/**
* Handle device info response from the ESPHome device. This extracts all the device metadata from the response message.
*
* @param payload - The device info response payload.
*/
private handleDeviceInfoResponse;
/**
* Return the device information of the connected ESPHome device if available.
*
* @returns The device information if available, or `null`.
*/
deviceInfo(): Nullable<DeviceInfo>;
/**
* Check if a message type is a list entities response. These messages contain entity discovery information.
*
* @param type - The message type to check.
* @returns `true` if this is a list entities response, `false` otherwise.
*/
private isListEntitiesResponse;
/**
* Check if a message type is a state update. These messages contain current state information for entities.
*
* @param type - The message type to check.
* @returns `true` if this is a state update message, `false` otherwise.
*/
private isStateUpdate;
/**
* Extract entity type label from message type. This converts the message type enum to a lowercase string identifier.
*
* @param type - The message type enum value.
* @returns The entity type label string.
*/
private getEntityTypeLabel;
/**
* Parses a single ListEntities*Response, logs it, and stores it. This registers a discovered entity in our internal maps for later reference.
*
* @param type - The message type indicating the entity type.
* @param payload - The entity description payload.
*/
private handleListEntity;
/**
* Decodes a state update, looks up entity info, and emits events. This processes telemetry data from entities and emits appropriate events.
*
* @param type - The message type indicating the entity type.
* @param payload - The state update payload.
*/
private handleTelemetry;
/**
* Decode cover state telemetry. Cover entities have complex state with position, tilt, and operation status.
*
* @param fields - The decoded protobuf fields.
* @param eventType - The event type string.
* @param name - The entity name.
*/
private decodeCoverState;
/**
* Extract entity key from protobuf fields. Entity keys can be encoded as either Buffer or number types.
*
* @param fields - The decoded protobuf fields.
* @param fieldNum - The field number to extract.
* @returns The entity key or undefined if not found.
*/
private extractEntityKey;
/**
* Extract fixed32 field from protobuf fields. Fixed32 fields are always 4 bytes and represent 32-bit values.
*
* @param fields - The decoded protobuf fields.
* @param fieldNum - The field number to extract.
* @returns The numeric value or undefined if not found.
*/
private extractFixed32Field;
/**
* Extract string field from protobuf fields. String fields are encoded as UTF-8 bytes.
*
* @param fields - The decoded protobuf fields.
* @param fieldNum - The field number to extract.
* @returns The string value or undefined if not found.
*/
private extractStringField;
/**
* Extract number field from protobuf fields. Number fields are encoded as varints.
*
* @param fields - The decoded protobuf fields.
* @param fieldNum - The field number to extract.
* @returns The numeric value or undefined if not found.
*/
private extractNumberField;
/**
* Extract telemetry value from protobuf fields. Telemetry values can be numbers, floats, or strings depending on the entity type.
*
* @param fields - The decoded protobuf fields.
* @param fieldNum - The field number to extract.
* @returns The telemetry value or undefined if not found.
*/
private extractTelemetryValue;
/**
* Frames a raw protobuf payload with the appropriate framing based on encryption state. This method automatically chooses between encrypted and plaintext framing.
*
* @param type - The message type.
* @param payload - The message payload.
*/
private frameAndSend;
/**
* Send a plaintext message with standard framing. Plaintext messages use a simple length-prefixed format.
*
* @param type - The message type.
* @param payload - The message payload.
*/
private sendPlaintextMessage;
/**
* Encode protobuf fields into a buffer. This creates a protobuf message from field definitions.
*
* @param fields - The fields to encode.
* @returns The encoded protobuf message.
*/
private encodeProtoFields;
/**
* Build key field as fixed32 for command requests. Entity keys are always sent as fixed32 fields in command messages.
*
* @param key - The entity key.
* @returns The field definition.
*/
private buildKeyField;
/**
* Get entity key by ID. This looks up the numeric key for an entity given its string ID.
*
* @param id - The entity ID to look up.
*
* @returns The entity key or `null` if not found.
*/
getEntityKey(id: string): Nullable<number>;
/**
* Log all registered entity IDs for debugging. Logs entities grouped by type with their names and keys. This is primarily a debugging and development tool.
*/
logAllEntityIds(): void;
/**
* Get entity information by ID. This retrieves full entity details given its string ID.
*
* @param id - The entity ID to look up.
*
* @returns The entity information or `null` if not found.
*/
getEntityById(id: string): Nullable<Entity>;
/**
* Check if an entity ID exists. This is useful for validating entity IDs before sending commands.
*
* @param id - The entity ID to check.
*
* @returns `true` if the entity exists, `false` otherwise.
*/
hasEntity(id: string): boolean;
/**
* Get all available entity IDs grouped by type. This provides a structured view of all discovered entities.
*
* @returns Object with entity types as keys and arrays of IDs as values.
*/
getAvailableEntityIds(): Record<string, string[]>;
/**
* Get all entities with their IDs. This returns the complete list of entities with their string IDs included.
*
* @returns Array of entities with their corresponding IDs.
*/
getEntitiesWithIds(): Array<Entity & {
id: string;
}>;
/**
* Send a ping request to the device to heartbeat the connection. This can be used to keep the connection alive and verify connectivity.
*/
sendPing(): void;
/**
* Sends a SwitchCommandRequest for the given entity ID and on/off state. This controls binary switch entities like garage door openers.
*
* @param id - The entity ID (format: "switch-entityname").
* @param state - `true` for on, `false` for off.
*/
sendSwitchCommand(id: string, state: boolean): void;
/**
* Sends a ButtonCommandRequest to press a button entity. Button entities trigger one-time actions when pressed.
*
* @param id - The entity ID (format: "button-entityname").
*/
sendButtonCommand(id: string): void;
/**
* Sends a CoverCommandRequest for the given entity ID. Cover entities represent things like garage doors, blinds, or shades.
*
* @param id - The entity ID (format: "cover-entityname").
* @param options - Command options (at least one option must be provided).
* @param options.command - The command: "open", "close", or "stop" (optional).
* @param options.position - Target position 0.0-1.0 where 0 is closed, 1 is open (optional).
* @param options.tilt - Target tilt 0.0-1.0 where 0 is closed, 1 is open (optional).
*
* @example
* ```typescript
* // Send a simple command
* await client.sendCoverCommand("cover-garagedoor", { command: "open" });
*
* // Set to specific position
* await client.sendCoverCommand("cover-garagedoor", { position: 0.5 }); // 50% open
*
* // Set position and tilt for blinds
* await client.sendCoverCommand("cover-blinds", { position: 1.0, tilt: 0.25 });
* ```
*/
sendCoverCommand(id: string, options: {
command?: "open" | "close" | "stop";
position?: number;
tilt?: number;
}): void;
/**
* Sends a LightCommandRequest to turn on/off and optionally set brightness. Light entities represent controllable lights with optional dimming.
*
* @param id - The entity ID (format: "light-entityname").
* @param options - Command options.
* @param options.state - `true` for on, `false` for off (optional).
* @param options.brightness - Brightness level 0.0-1.0 (optional).
*/
sendLightCommand(id: string, options: {
state?: boolean;
brightness?: number;
}): void;
/**
* Sends a LockCommandRequest to lock or unlock the given entity ID. Lock entities represent controllable locks with optional code support.
*
* @param id - The entity ID (format: "lock-entityname").
* @param command - The command to send: "lock" or "unlock".
* @param code - Optional unlock code.
*/
sendLockCommand(id: string, command: "lock" | "unlock", code?: string): void;
/**
* Encode an integer as a VarInt (protobuf-style). VarInts use 7 bits per byte with a continuation bit in the MSB.
*
* @param value - The value to encode.
* @returns The encoded varint as a Buffer.
*/
private encodeVarint;
/**
* Read a VarInt from buffer at offset; returns [value, bytesRead]. This decodes protobuf-style variable-length integers.
*
* @param buffer - The buffer to read from.
* @param offset - The offset to start reading at.
* @returns A tuple of [decoded value, number of bytes consumed].
*/
private readVarint;
/**
* Decode a simple protobuf message into a map of field numbers to values. This implements basic protobuf decoding for the ESPHome protocol.
*
* @param buffer - The protobuf message to decode.
* @returns A map from field numbers to arrays of decoded values.
*/
private decodeProtobuf;
/**
* Return whether we are on an encrypted connection or not.
*
* @returns `true` if we are on an encrypted connection, `false` otherwise.
*/
get isEncrypted(): boolean;
}
export {};