import type { HomebridgePluginLogging } from "./util.ts"; /** * A handler for raw MQTT messages delivered on a subscribed topic. * * @param payload - The message payload as received from the broker. * * @category Utilities */ export type MqttHandler = (payload: Buffer) => Promise | void; /** * A handler invoked by {@link MqttClient.subscribeGet} when a "true" message arrives on the `/get` topic. Returns the current value as a string that will be published * on the parent topic as the response. * * @category Utilities */ export type MqttGetHandler = () => string; /** * A handler invoked by {@link MqttClient.subscribeSet} when a value is received on the `/set` topic. * * Receives three arguments: * * - `value` - the lowercased normalized form, convenient for comparisons against fixtures like `"true"` / `"on"`. * - `rawValue` - the raw message string, for cases where case or surrounding whitespace matters. * - `signal` - an {@link AbortSignal} that aborts when the subscription's connection-level signal fires or (if configured) the per-invocation timeout elapses. * Signal-aware setters forward this to any cancellation-capable API they call (`fetch`, `events.once`, `node:timers/promises`, etc.) so the setter's work * actually stops when the wrapper times out. Setters that ignore the signal continue to run after timeout, but the subscription slot is released either * way; nothing is structurally blocked by a hanging setter. * * **Log-routing contract.** How the setter settles determines which log line `subscribeSet` emits: * * - **Return normally** (work completed successfully) - logs INFO `"MQTT: set message received for X: value."`. * - **Throw a non-abort error** (work failed for a reason unrelated to cancellation) - logs ERROR `"MQTT: error setting X to value: message."`. * - **Throw while the composed signal is already aborted** (the connection-level abort or the per-invocation timeout, whichever fired first) - logs WARN * `"MQTT: set handler for X was cancelled before completion."`, regardless of what value is thrown. A setter that catches its own abort and returns normally * is indistinguishable to the wrapper from a successful completion, which is why a signal-aware setter that wants cancellation reflected in the log stream * should rethrow once it observes the signal has aborted, rather than swallow it. * * @category Utilities */ export type MqttSetHandler = (value: string, rawValue: string, signal: AbortSignal) => Promise | void; /** * Static configuration for an {@link MqttClient}. Captures the broker connection parameters and the topic-prefix convention the client applies to every topic it * touches. * * @property brokerUrl - The MQTT broker URL (for example, `"mqtt://localhost:1883"`). * @property log - Logger used for connection and publish/subscribe tracing. * @property reconnectInterval - Seconds to wait between transient reconnect attempts. Defaults to 60. * @property topicPrefix - Prefix prepended to every topic the client publishes or subscribes to. The caller is responsible for the remaining path structure; * this class never reinterprets the topic beyond concatenation. * * @category Utilities */ export interface MqttConfig { brokerUrl: string; log: HomebridgePluginLogging; reconnectInterval?: number; topicPrefix: string; } /** * Construction-time options for {@link MqttClient}. * * @property signal - Optional parent {@link AbortSignal} composed with the client's internal controller. When the parent aborts, the MQTT connection ends permanently. * * @category Utilities */ export interface MqttClientInit { signal?: AbortSignal; } /** * Result of routing a transport-level MQTT error through {@link routeMqttBrokerError}. Returned to the caller so the wiring layer (the `client.on("error", ...)` * handler in {@link MqttClient}) can apply the side effect on its own state. Keeping the routing pure - logging plus a flag - lets the routing logic be tested in * isolation against synthetic error inputs, without standing up a broker or injecting events into the underlying mqtt.js client. * * @property endTransport - `true` when the error code requires forcibly ending the underlying transport (the mqtt.js client should not attempt reconnect). Mirrors * the architectural rule that ENOTFOUND is the one transport error reconnect cannot recover from. * * @category Utilities */ export interface MqttBrokerErrorResult { endTransport: boolean; } /** * Route a transport-level MQTT error to the appropriate log line and signal whether the underlying transport should be ended. Pure function: no class state, no * mqtt.js handles, no closure over the live client. The wiring layer in {@link MqttClient} forwards every `client.on("error", ...)` invocation through here and acts * on the returned `endTransport` flag. * * The routing paths mirror the transport-error categories HBPU distinguishes: * * - `ECONNREFUSED` - the broker host is up but no listener accepts the connection. Recoverable; auto-reconnect handles it. * - `ECONNRESET` - the broker accepted then dropped the connection. Recoverable; auto-reconnect handles it. * - `ENOTFOUND` - DNS could not resolve the broker hostname. Non-recoverable - retrying the same hostname will keep failing - so we end the transport permanently * and emit a standalone log line without the retry-cadence suffix. * - default - any other error code (or none). Logged through the retry-cadence formatter with `util.inspect` of the error, so a future mqtt.js error code we * did not anticipate still surfaces in the log stream rather than being silently swallowed. * * @param error - The error event payload from the underlying mqtt.js client. * @param log - Logger used to emit the routed message. * @param reconnectInterval - Configured reconnect interval (in seconds) used to format the retry-cadence suffix. * * @returns A {@link MqttBrokerErrorResult} indicating whether the wiring layer should end the transport. * * @category Utilities */ export declare function routeMqttBrokerError(error: NodeJS.ErrnoException, log: HomebridgePluginLogging, reconnectInterval: number): MqttBrokerErrorResult; /** * Outcome of a getter-driven response publish issued by {@link MqttClient.subscribeGet}. Discriminated union: `ok: true` after a successful publish, `ok: false` with * the captured error after a failure. Passed to {@link logGetterPublishOutcome} so the routing logic between the success and failure log lines is testable in * isolation against synthetic outcomes - the same architectural pattern {@link routeMqttBrokerError} uses for transport-error log routing. * * @category Utilities */ export type GetterPublishOutcome = { readonly ok: true; } | { readonly error: unknown; readonly ok: false; }; /** * Route the outcome of a `subscribeGet` response publish to the appropriate log line. Pure function: no class state, no mqtt.js handles, no closure over the live * client. The wiring in {@link MqttClient.subscribeGet} forwards each `.then` / `.catch` settlement here, and tests cover both branches by calling the function * directly with synthetic `{ ok: true }` and `{ ok: false, error: ... }` outcomes - bypassing the real-broker substrate where a forced QoS-0 publish failure would * require contrived socket-level setup that is not worth the test-architecture complexity. * * @param log - Logger that receives the routed message. * @param type - Human-readable label (the `type` argument the caller passed to `subscribeGet`). * @param outcome - The publish outcome. See {@link GetterPublishOutcome}. * * @category Utilities */ export declare function logGetterPublishOutcome(log: HomebridgePluginLogging, type: string, outcome: GetterPublishOutcome): void; /** * Per-subscription options accepted by {@link MqttClient.subscribe}, {@link MqttClient.subscribeGet}, and {@link MqttClient.subscribeSet}. * * @property signal - Optional {@link AbortSignal}. When it aborts, the specific handler is removed and (if it was the last handler on the topic) the underlying MQTT * subscription is dropped. Composes with the connection-level signal: a client-level abort removes every handler regardless of per-subscription * state. * * @category Utilities */ export interface MqttSubscribeInit { signal?: AbortSignal; } /** * Per-subscription options accepted by {@link MqttClient.subscribeSet}. * * @property signal - Optional {@link AbortSignal} that auto-unsubscribes this handler. See {@link MqttSubscribeInit}. * @property timeout - Optional timeout, in milliseconds, applied to each invocation of the user-supplied setter. When the setter takes longer than this, the * invocation is cancelled and a warning is logged. Omit for no timeout - the setter still unwinds if the connection-level signal aborts, via * {@link runWithAbort}. * * @category Utilities */ export interface MqttSubscribeSetInit { signal?: AbortSignal; timeout?: number; } /** * Per-publish options accepted by {@link MqttClient.publish}. * * @property signal - Optional {@link AbortSignal}. When it aborts before the broker acknowledges the publish, the returned promise rejects with `signal.reason`. * Composes with the connection-level signal. * * @category Utilities */ export interface MqttPublishInit { signal?: AbortSignal; } /** * Signal-driven MQTT client with automatic topic-prefix management, composed connection lifetime, and per-operation signal support. * * @example * * ```ts * import { MqttClient } from "homebridge-plugin-utils"; * * await using mqtt = new MqttClient({ brokerUrl: "mqtt://localhost:1883", log, topicPrefix: "homebridge" }, { signal: platform.signal }); * * // A subscription that auto-unsubscribes on the per-feature signal. * const feature = new AbortController(); * * mqtt.subscribe("device1/status", (payload) => log.info("Status: %s.", payload.toString()), { signal: feature.signal }); * * // Abort-aware publish. * await mqtt.publish("device1/status", "on"); * ``` * * @category Utilities */ export declare class MqttClient implements AsyncDisposable { #private; /** * The composed abort signal representing this client's lifetime. Aborts exactly once when {@link MqttClient.abort} is called or when the parent signal fires. */ readonly signal: AbortSignal; /** * Construct and start a new MQTT client. * * Connection is initiated synchronously as part of construction; there is no separate `connect()` step. A synchronous failure from mqtt.js (typically an invalid * broker URL) surfaces as an `Error` wrapping the underlying cause, so a misconfigured plugin fails loudly instead of living in a zombie state. Network-level * failures (an unreachable broker reachable by a valid URL) do not throw - they surface asynchronously through the client's `error` event, are logged, and trigger * mqtt.js's built-in auto-reconnect until {@link MqttClient.abort} or a parent signal ends the client for good. A pre-aborted parent signal still constructs * a client (so `#mqtt` stays non-null) and then immediately runs the regular teardown path. * * @param config - Static broker / topic configuration. See {@link MqttConfig}. * @param init - Optional init options. See {@link MqttClientInit}. * * @throws `Error` (with the underlying mqtt.js error attached as `cause`) when mqtt.js's `connect()` fails synchronously. */ constructor(config: MqttConfig, init?: MqttClientInit); /** * Publish `payload` to `topic`, returning a promise that resolves when the broker acknowledges the publish, or rejects on failure or abort. * * The topic is prefixed with the configured {@link MqttConfig.topicPrefix} before being sent; callers supply the topic tail (for example, `"device1/status"`). * * @param topic - The relative topic (tail) to publish to. * @param payload - The payload to publish. Buffers and strings are passed through unchanged. * @param init - Optional per-publish options. See {@link MqttPublishInit}. * * @returns A promise that resolves once the broker acknowledges, or rejects on error or abort. */ publish(topic: string, payload: Buffer | string, init?: MqttPublishInit): Promise; /** * Subscribe to `topic` with the given handler. The topic is prefixed with the configured {@link MqttConfig.topicPrefix} before being registered with the broker. * Multiple handlers may subscribe to the same topic; each gets independent delivery. * * @param topic - The relative topic (tail) to subscribe to. * @param handler - Callback invoked with each received payload. * @param init - Optional per-subscription options. See {@link MqttSubscribeInit}. */ subscribe(topic: string, handler: MqttHandler, init?: MqttSubscribeInit): void; /** * Subscribe to the `/get` child of `topic`. When a `"true"` message arrives on the get topic, the provided `getValue` callback runs and its return value is * published back on the parent topic. The classic HomeKit "get" pattern, wrapped once. * * @param topic - The relative topic (tail); the `/get` suffix is appended automatically. * @param type - Human-readable label used in log messages (for example, `"Temperature"`). * @param getValue - Callback returning the current value as a string, invoked on each incoming `"true"` message. * @param init - Optional per-subscription options. See {@link MqttSubscribeInit}. */ subscribeGet(topic: string, type: string, getValue: MqttGetHandler, init?: MqttSubscribeInit): void; /** * Subscribe to the `/set` child of `topic`. Each incoming message invokes `setValue` with the lowercased normalized value, the raw message string, and an * {@link AbortSignal} that composes the connection-level signal with the optional per-invocation `timeout`. Signal-aware setters forward that signal to cancellation- * capable APIs so the setter's work actually stops when the timeout elapses or the client aborts; signal-unaware setters continue to run but the subscription slot * is released either way, so a hanging setter cannot tie up the slot indefinitely. * * @param topic - The relative topic (tail); the `/set` suffix is appended automatically. * @param type - Human-readable label used in log messages. * @param setValue - Callback invoked with each received value. Receives three arguments: `(value, rawValue, signal)`. See {@link MqttSetHandler}. * @param init - Optional per-subscription options including a handler-invocation `timeout`. See {@link MqttSubscribeSetInit}. */ subscribeSet(topic: string, type: string, setValue: MqttSetHandler, init?: MqttSubscribeSetInit): void; /** * Unsubscribe all handlers for the specified `(id, topic)` tuple. Reconstructs the topic using the configured {@link MqttConfig.topicPrefix}, removes the * subscription from the internal map, and issues the wire-level unsubscribe. Preserved as a separate imperative verb for the mid-session feature-toggle pattern * where the caller has the `(id, topic)` tuple but never retained a dedicated controller. * * Deliberately does not accept a `{ signal }` option: unsubscribe is synchronous and has nothing to cancel, and exposing a vestigial signal would suggest a * cancellation semantic the method cannot deliver. Callers composing teardown through a signal remove handlers by aborting the per-subscription signal they passed * to `subscribe*` instead. * * @param id - The device or accessory identifier portion of the topic. An empty string short-circuits the whole call. * @param topic - The topic tail relative to the id. */ unsubscribe(id: string, topic: string): void; /** * Abort the client and tear the connection down. Defaults to `HbpuAbortError("shutdown")` when no reason is supplied; explicit reasons pass through unchanged. * * Safe to call more than once. After this runs, every subsequent `publish`, `subscribe*`, or `unsubscribe` call is a no-op. * * @param reason - Optional abort reason. Typically an {@link HbpuAbortError}; platform errors also interoperate by convention. */ abort(reason?: unknown): void; /** * `AsyncDisposable` implementation. Aborts the client (defaulting to `"shutdown"`), which tears the MQTT connection down and rejects any pending publishes through * the regular teardown path. * * @returns A promise that resolves once teardown has been scheduled. MQTT.js's `end(true)` completes synchronously for userland purposes, so the awaited microtask * is all the ordering the caller needs. */ [Symbol.asyncDispose](): Promise; /** * `true` once `this.signal` has aborted. Derived from the signal; no independent state. */ get aborted(): boolean; } //# sourceMappingURL=mqttClient.d.ts.map