UNPKG

esphome-client

Version:

A nearly complete implementation of the ESPHome client protocol with encryption support.

559 lines (558 loc) 23.3 kB
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 {};