/** * MQTT Client Wrapper for Dyson Devices * Provides a clean interface for MQTT communication with Dyson devices */ import { EventEmitter } from 'events'; import type { MqttClient as MqttClientType, IClientOptions } from 'mqtt'; /** * MQTT Client configuration options */ export interface MqttClientOptions { /** Device IP address */ host: string; /** Device serial number (used as username) */ serial: string; /** Device local credentials (used as password) */ credentials: string; /** Device product type (e.g., '438') */ productType: string; /** Connection timeout in milliseconds (default: 10000) */ timeout?: number; /** Keep-alive interval in seconds (default: 30) */ keepalive?: number; /** Enable automatic reconnection (default: true) */ autoReconnect?: boolean; /** Maximum reconnection attempts (default: 5) */ maxReconnectAttempts?: number; } /** MQTT connect function type for dependency injection */ export type MqttConnectFn = (brokerUrl: string, options: IClientOptions) => MqttClientType; /** * MQTT message received from device */ export interface MqttMessage { /** Topic the message was received on */ topic: string; /** Raw message payload */ payload: Buffer; /** Parsed JSON payload (if valid JSON) */ data?: unknown; } /** * Events emitted by MqttClient */ export interface MqttClientEvents { /** Emitted when connected to device */ connect: []; /** Emitted when disconnected from device */ disconnect: []; /** Emitted when a message is received */ message: [MqttMessage]; /** Emitted on connection or communication error */ error: [Error]; /** Emitted when connection is closed */ close: []; /** Emitted when attempting to reconnect (includes attempt number) */ reconnect: [number]; /** Emitted when all reconnection attempts have failed */ reconnectFailed: []; /** Emitted when device goes offline (during reconnection) */ offline: []; } /** * A single CONNECT-time configuration the client may try. * * Some Dyson firmware (notably the Big+Quiet BP02/BP03/BP04 series) rejects * the historical clientId / protocol-version / clean-session combination at * CONNACK with `Connection refused: Identifier rejected`. The fallback ladder * lets the client try alternate combinations on those specific recoverable * errors without changing behavior for devices that already work. */ export interface ConnectVariant { /** Human-readable label, used by `getActiveVariant()` and logs */ label: string; /** How to build the MQTT clientId */ clientIdStrategy: 'default' | 'short'; /** MQTT protocol version: 3 = MQTT 3.1, 4 = MQTT 3.1.1, 5 = MQTT 5 */ protocolVersion: 3 | 4 | 5; /** clean session flag */ clean: boolean; } /** * Connect-variant ladder, tried in order on CONNACK-level recoverable errors. * * Variant 0 is the historical default — devices that already work see no change. * Later variants address common Big+Quiet firmware quirks: oversized clientIds, * MQTT-5-only brokers, brokers that refuse clean sessions, or legacy 3.1 brokers. */ export declare const MQTT_CONNECT_VARIANTS: readonly ConnectVariant[]; /** * @internal exported for tests */ export declare function isRecoverableConnackError(error: Error): boolean; /** * @internal exported for tests */ export declare function buildClientId(strategy: ConnectVariant['clientIdStrategy'], serial: string): string; /** * MQTT Client wrapper for Dyson device communication * * Provides event-driven MQTT communication with Dyson devices. * Uses the device serial as username and local credentials as password. * * @example * ```typescript * const client = new DysonMqttClient({ * host: '192.168.1.100', * serial: 'ABC-AB-12345678', * credentials: 'localPassword', * productType: '438', * }); * * client.on('message', (msg) => console.log(msg.data)); * await client.connect(); * await client.subscribe(`438/ABC-AB-12345678/status/current`); * ``` */ export declare class DysonMqttClient extends EventEmitter { private client; private readonly options; private readonly mqttConnect; private connected; private subscribedTopics; private reconnectAttempts; private isReconnecting; private intentionalDisconnect; private reconnectAbortController?; private lastSuccessfulVariantIndex; private activeVariant; constructor(options: MqttClientOptions, mqttConnect?: MqttConnectFn); /** * Connect to the Dyson device MQTT broker. * * Tries the variants in `MQTT_CONNECT_VARIANTS` in order, starting from the * variant that worked last time (if any) so reconnects skip the ladder. * Only CONNACK-level recoverable rejections (see `isRecoverableConnackError`) * escalate to the next variant; transient errors (timeout, network) reject * immediately as before. * * @throws {Error} If all variants are exhausted or a non-recoverable error occurs */ connect(): Promise; /** * Attempt a single CONNECT with the given variant. Returns once the broker * sends CONNACK with success, or rejects with the CONNACK / timeout / network * error from this attempt only. */ private connectWithVariant; /** * Return the variant that produced the active connection, or `null` if not * connected. Useful for diagnostics: callers can log this to record which * fallback combination worked for a given device firmware. */ getActiveVariant(): ConnectVariant | null; /** * Disconnect from the device * * @param intentional - Whether this is an intentional disconnect (prevents auto-reconnect) */ disconnect(intentional?: boolean): Promise; /** * Subscribe to an MQTT topic * * @param topic - Topic to subscribe to * @throws {Error} If not connected or subscription fails */ subscribe(topic: string): Promise; /** * Unsubscribe from an MQTT topic * * @param topic - Topic to unsubscribe from * @throws {Error} If not connected or unsubscription fails */ unsubscribe(topic: string): Promise; /** * Publish a message to an MQTT topic * * @param topic - Topic to publish to * @param message - Message to publish (string or object to be JSON serialized) * @throws {Error} If not connected or publish fails */ publish(topic: string, message: string | object): Promise; /** * Subscribe to the device status topic * * Convenience method to subscribe to the standard status topic: * `{productType}/{serial}/status/current` */ subscribeToStatus(): Promise; /** * Publish a command to the device * * Convenience method to publish to the standard command topic: * `{productType}/{serial}/command` * * @param command - Command object to send */ publishCommand(command: object): Promise; /** * Request current state from the device * * Sends a REQUEST-CURRENT-STATE message to get the device's current state */ requestCurrentState(): Promise; /** * Get the status topic for this device */ getStatusTopic(): string; /** * Get the command topic for this device */ getCommandTopic(): string; /** * Check if connected to the device */ isConnected(): boolean; /** * Get list of currently subscribed topics */ getSubscribedTopics(): string[]; /** * Get the device serial number */ getSerial(): string; /** * Get the device product type */ getProductType(): string; /** * Get the current reconnection attempt count */ getReconnectAttempts(): number; /** * Check if currently in reconnection state */ isReconnectingState(): boolean; /** * Ensure client is connected before operations */ private ensureConnected; /** * Handle automatic reconnection with exponential backoff */ private handleReconnection; /** * Re-subscribe to all previously subscribed topics */ private resubscribeToTopics; /** * Clean up resources */ private cleanup; } export type { MqttClientType }; //# sourceMappingURL=mqttClient.d.ts.map