@turquoisebay/mqtt 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.env.example ADDED
@@ -0,0 +1,10 @@
1
+ # MQTT Configuration (optional - can also use openclaw.json)
2
+ # These override config file values
3
+
4
+ MQTT_BROKER_URL=mqtt://localhost:1883
5
+ MQTT_USERNAME=openclaw
6
+ MQTT_PASSWORD=your-secret-password
7
+ MQTT_CLIENT_ID=openclaw-agent
8
+
9
+ # TLS (optional)
10
+ # MQTT_CA_PATH=/path/to/ca.crt
package/CHANGELOG.md ADDED
@@ -0,0 +1,103 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.13] - 2026-02-03
9
+
10
+ ### Fixed
11
+ - Rename npm package to `@turquoisebay/mqtt` so install id matches `mqtt`
12
+
13
+ ## [0.1.12] - 2026-02-03
14
+
15
+ ### Fixed
16
+ - Set plugin id to `mqtt` to align with channel id and auto-enable/doctor checks
17
+
18
+ ## [0.1.11] - 2026-02-03
19
+
20
+ ### Changed
21
+ - Simplified MQTT inbound handling and reduced debug noise
22
+ - Publish agent replies to outbound as JSON payloads
23
+ - Align plugin id with manifest/config (`openclaw-mqtt`)
24
+
25
+ ## [0.1.10] - 2026-02-03
26
+
27
+ ### Fixed
28
+ - Align plugin id with channel id (`mqtt`) to satisfy OpenClaw doctor auto-enable checks
29
+
30
+ ## [0.1.9] - 2026-02-01
31
+
32
+ ### Changed
33
+ - Complete rewrite based on Telegram channel architecture analysis
34
+ - Use `finalizeInboundContext` for proper inbound message formatting
35
+ - Use `dispatchReplyWithBufferedBlockDispatcher` for full agent processing
36
+ - Replies are sent back via MQTT outbound topic
37
+ - Each MQTT sender gets their own session (mqtt:{senderId})
38
+
39
+ ## [0.1.8] - 2026-02-01
40
+
41
+ ### Fixed
42
+ - Use `enqueueSystemEvent` for message injection (simpler, more reliable)
43
+ - Removed complex debouncer that required additional params
44
+
45
+ ## [0.1.7] - 2026-02-01
46
+
47
+ ### Fixed
48
+ - Use `gateway.startAccount` instead of `gateway.start` (correct OpenClaw plugin API)
49
+ - Use `createInboundDebouncer` for proper message injection
50
+ - Add `isEnabled` and `isConfigured` config methods
51
+ - Return promise that resolves on abort for clean shutdown
52
+
53
+ ## [0.1.6] - 2026-02-01
54
+
55
+ ### Fixed
56
+ - Plugin id changed from `mqtt` to `openclaw-mqtt` to match install directory name
57
+ - Fixes "plugin not found" error during install
58
+
59
+ ## [0.1.5] - 2026-02-01
60
+
61
+ ### Added
62
+ - Onboarding adapter for `openclaw configure channels` support
63
+ - Interactive setup prompts for broker URL, auth, topics, QoS, TLS
64
+
65
+ ## [0.1.4] - 2026-02-01
66
+
67
+ ### Fixed
68
+ - Package now ships pre-built JS files (consumers no longer need to compile)
69
+ - Added `files` field to package.json for clean npm publishing
70
+ - Added type declarations for openclaw/plugin-sdk (SDK doesn't ship .d.ts yet)
71
+ - Fixed implicit `any` type errors in channel.ts
72
+
73
+ ### Changed
74
+ - `main` now points to `dist/index.js` instead of `index.ts`
75
+ - `openclaw.extensions` updated to reference compiled output
76
+ - Added `prepublishOnly` script to ensure build before publish
77
+
78
+ ## [Unreleased]
79
+
80
+ ### TODO
81
+ - [ ] TLS certificate file loading
82
+ - [ ] Integration tests with Mosquitto container
83
+ - [ ] GitHub Actions CI
84
+ - [ ] Last Will and Testament support
85
+ - [ ] Multiple topic subscriptions
86
+
87
+ ## [0.1.0] - Unreleased
88
+
89
+ ### Added
90
+ - Initial project scaffold
91
+ - Plugin manifest (`openclaw.plugin.json`)
92
+ - Config schema with Zod validation
93
+ - Channel plugin structure following OpenClaw conventions
94
+ - README with installation and usage docs
95
+ - TypeScript configuration
96
+ - MQTT client manager with connection lifecycle
97
+ - Subscribe to inbound topic, inject messages to OpenClaw
98
+ - Publish to outbound topic via sendText
99
+ - Reconnection with exponential backoff
100
+ - MQTT wildcard support (+ and #)
101
+ - Environment variable support for secrets
102
+ - JSON message parsing for structured alerts
103
+ - Unit tests for topic matching and config merge
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @turquoisebay/mqtt
2
+
3
+ [![CI](https://github.com/hughmadden/openclaw-mqtt/actions/workflows/ci.yml/badge.svg)](https://github.com/hughmadden/openclaw-mqtt/actions)
4
+ [![npm](https://img.shields.io/npm/v/@turquoisebay/mqtt)](https://www.npmjs.com/package/@turquoisebay/mqtt)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ MQTT channel plugin for [OpenClaw](https://github.com/openclaw/openclaw) — bidirectional messaging via MQTT brokers.
8
+
9
+ ## Features
10
+
11
+ - 🔌 **Bidirectional messaging** — subscribe and publish to MQTT topics
12
+ - 🏠 **Home automation ready** — integrates with Home Assistant, Mosquitto, EMQX
13
+ - 🔒 **TLS support** — secure connections to cloud brokers
14
+ - 📊 **Service monitoring** — receive alerts from Uptime Kuma, healthchecks, etc.
15
+ - ⚡ **QoS levels** — configurable delivery guarantees (0, 1, 2)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ openclaw plugins install @turquoisebay/mqtt
21
+ ```
22
+
23
+ Or manually:
24
+
25
+ ```bash
26
+ git clone https://github.com/hughmadden/openclaw-mqtt ~/.openclaw/extensions/mqtt
27
+ cd ~/.openclaw/extensions/mqtt && npm install
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ Add to `~/.openclaw/openclaw.json`:
33
+
34
+ ```json5
35
+ {
36
+ channels: {
37
+ mqtt: {
38
+ brokerUrl: "mqtt://localhost:1883",
39
+ // Optional auth
40
+ username: "openclaw",
41
+ password: "secret",
42
+ // Topics
43
+ topics: {
44
+ inbound: "openclaw/inbound", // Subscribe to this
45
+ outbound: "openclaw/outbound" // Publish responses here
46
+ },
47
+ // Quality of Service (0=fire-and-forget, 1=at-least-once, 2=exactly-once)
48
+ qos: 1
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ Then restart the gateway:
55
+
56
+ ```bash
57
+ openclaw gateway restart
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ ### Receiving messages (inbound)
63
+
64
+ Messages published to your `inbound` topic will be processed by OpenClaw.
65
+ You can send either plain text or JSON (recommended):
66
+
67
+ ```bash
68
+ # Plain text
69
+ mosquitto_pub -t "openclaw/inbound" -m "Alert: Service down on playground"
70
+
71
+ # JSON (recommended)
72
+ mosquitto_pub -t "openclaw/inbound" -m '{"senderId":"pg-cli","text":"hello"}'
73
+ ```
74
+
75
+ ### Sending messages (outbound)
76
+
77
+ Agent replies are published to the `outbound` topic as JSON:
78
+
79
+ ```json
80
+ {"senderId":"openclaw","text":"...","kind":"final","ts":1700000000000}
81
+ ```
82
+
83
+ If you want to publish custom text via CLI, use the `message` tool:
84
+
85
+ ```bash
86
+ openclaw agent --message "Send MQTT: Temperature is 23°C"
87
+ ```
88
+
89
+ ## Use Cases
90
+
91
+ ### Service Monitoring
92
+
93
+ Pair with [Uptime Kuma](https://github.com/louislam/uptime-kuma) to receive alerts:
94
+
95
+ 1. Configure Uptime Kuma notification → MQTT
96
+ 2. Set topic to `openclaw/inbound`
97
+ 3. OpenClaw receives and can act on alerts
98
+
99
+ ### Home Assistant Integration
100
+
101
+ ```yaml
102
+ # Home Assistant configuration.yaml
103
+ mqtt:
104
+ sensor:
105
+ - name: "OpenClaw Status"
106
+ state_topic: "openclaw/outbound"
107
+
108
+ automation:
109
+ - trigger:
110
+ platform: mqtt
111
+ topic: "home/alerts"
112
+ action:
113
+ service: mqtt.publish
114
+ data:
115
+ topic: "openclaw/inbound"
116
+ payload: "{{ trigger.payload }}"
117
+ ```
118
+
119
+ ## Development
120
+
121
+ ```bash
122
+ # Clone
123
+ git clone turq@10.0.20.9:/opt/git/openclaw-mqtt.git
124
+ cd openclaw-mqtt
125
+
126
+ # Install deps
127
+ npm install
128
+
129
+ # Run tests
130
+ npm test
131
+
132
+ # Type check
133
+ npm run typecheck
134
+
135
+ # Build
136
+ npm run build
137
+ ```
138
+
139
+ ## Architecture
140
+
141
+ ```
142
+ MQTT Broker (Mosquitto/EMQX)
143
+
144
+ ├─► inbound topic ──► OpenClaw Gateway ──► Agent
145
+
146
+ └─◄ outbound topic ◄── OpenClaw Gateway ◄── Agent
147
+ ```
148
+
149
+ ## License
150
+
151
+ MIT © Hugh Madden
152
+
153
+ ## See Also
154
+
155
+ - [OpenClaw](https://github.com/openclaw/openclaw) — The AI assistant platform
156
+ - [MQTT.js](https://github.com/mqttjs/MQTT.js) — MQTT client library
157
+ - [Mosquitto](https://mosquitto.org/) — Popular MQTT broker
@@ -0,0 +1,18 @@
1
+ {
2
+ "channels": {
3
+ "mqtt": {
4
+ "brokerUrl": "mqtt://localhost:1883",
5
+ "username": "your-username",
6
+ "password": "YOUR_PASSWORD_HERE",
7
+ "clientId": "openclaw-agent",
8
+ "topics": {
9
+ "inbound": "openclaw/inbound",
10
+ "outbound": "openclaw/outbound"
11
+ },
12
+ "qos": 1,
13
+ "tls": {
14
+ "enabled": false
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,10 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ declare const plugin: {
3
+ id: string;
4
+ name: string;
5
+ description: string;
6
+ configSchema: any;
7
+ register(api: OpenClawPluginApi): void;
8
+ };
9
+ export default plugin;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAM7D,QAAA,MAAM,MAAM;;;;;kBAKI,iBAAiB;CAIhC,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
+ import { mqttPlugin } from "./src/channel.js";
3
+ import { setMqttRuntime } from "./src/runtime.js";
4
+ const plugin = {
5
+ id: "mqtt",
6
+ name: "MQTT",
7
+ description: "MQTT channel plugin for IoT and home automation integration",
8
+ configSchema: emptyPluginConfigSchema(),
9
+ register(api) {
10
+ setMqttRuntime(api.runtime);
11
+ api.registerChannel({ plugin: mqttPlugin });
12
+ },
13
+ };
14
+ export default plugin;
@@ -0,0 +1,35 @@
1
+ import { EventEmitter } from "events";
2
+ /**
3
+ * Mock MQTT Client for testing
4
+ */
5
+ export declare class MockMqttClient extends EventEmitter {
6
+ connected: boolean;
7
+ subscriptions: Map<string, {
8
+ qos: number;
9
+ }>;
10
+ published: Array<{
11
+ topic: string;
12
+ message: string;
13
+ opts: unknown;
14
+ }>;
15
+ subscribe(topic: string, opts: {
16
+ qos: number;
17
+ }, callback?: (err: Error | null) => void): this;
18
+ publish(topic: string, message: string, opts: unknown, callback?: (err: Error | null) => void): this;
19
+ end(force: boolean, opts: unknown, callback?: () => void): this;
20
+ simulateConnect(): void;
21
+ simulateMessage(topic: string, payload: Buffer | string): void;
22
+ simulateError(err: Error): void;
23
+ simulateDisconnect(): void;
24
+ simulateReconnect(): void;
25
+ }
26
+ export declare function connect(url: string, opts?: unknown): MockMqttClient;
27
+ export declare function getMockClient(): MockMqttClient | null;
28
+ export declare function resetMock(): void;
29
+ declare const _default: {
30
+ connect: typeof connect;
31
+ getMockClient: typeof getMockClient;
32
+ resetMock: typeof resetMock;
33
+ };
34
+ export default _default;
35
+ //# sourceMappingURL=mqtt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mqtt.d.ts","sourceRoot":"","sources":["../../../src/__mocks__/mqtt.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC;;GAEG;AACH,qBAAa,cAAe,SAAQ,YAAY;IAC9C,SAAS,UAAS;IAClB,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAa;IACxD,SAAS,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,CAAC,CAAM;IAEzE,SAAS,CACP,KAAK,EAAE,MAAM,EACb,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,EACrB,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI;IAOxC,OAAO,CACL,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,OAAO,EACb,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI;IAOxC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI;IAQxD,eAAe;IAKf,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAKvD,aAAa,CAAC,GAAG,EAAE,KAAK;IAIxB,kBAAkB;IAKlB,iBAAiB;CAGlB;AAKD,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,cAAc,CAKnE;AAGD,wBAAgB,aAAa,IAAI,cAAc,GAAG,IAAI,CAErD;AAGD,wBAAgB,SAAS,SAExB;;;;;;AAED,wBAAqD"}
@@ -0,0 +1,61 @@
1
+ import { EventEmitter } from "events";
2
+ /**
3
+ * Mock MQTT Client for testing
4
+ */
5
+ export class MockMqttClient extends EventEmitter {
6
+ connected = false;
7
+ subscriptions = new Map();
8
+ published = [];
9
+ subscribe(topic, opts, callback) {
10
+ this.subscriptions.set(topic, opts);
11
+ callback?.(null);
12
+ return this;
13
+ }
14
+ publish(topic, message, opts, callback) {
15
+ this.published.push({ topic, message, opts });
16
+ callback?.(null);
17
+ return this;
18
+ }
19
+ end(force, opts, callback) {
20
+ this.connected = false;
21
+ this.subscriptions.clear();
22
+ callback?.();
23
+ return this;
24
+ }
25
+ // Test helpers
26
+ simulateConnect() {
27
+ this.connected = true;
28
+ this.emit("connect");
29
+ }
30
+ simulateMessage(topic, payload) {
31
+ const buf = typeof payload === "string" ? Buffer.from(payload) : payload;
32
+ this.emit("message", topic, buf);
33
+ }
34
+ simulateError(err) {
35
+ this.emit("error", err);
36
+ }
37
+ simulateDisconnect() {
38
+ this.connected = false;
39
+ this.emit("close");
40
+ }
41
+ simulateReconnect() {
42
+ this.emit("reconnect");
43
+ }
44
+ }
45
+ // Factory function that returns mock client
46
+ let mockClient = null;
47
+ export function connect(url, opts) {
48
+ mockClient = new MockMqttClient();
49
+ // Auto-connect after short delay to simulate async connection
50
+ setTimeout(() => mockClient?.simulateConnect(), 10);
51
+ return mockClient;
52
+ }
53
+ // Test helper to get current mock client
54
+ export function getMockClient() {
55
+ return mockClient;
56
+ }
57
+ // Reset between tests
58
+ export function resetMock() {
59
+ mockClient = null;
60
+ }
61
+ export default { connect, getMockClient, resetMock };
@@ -0,0 +1,10 @@
1
+ import type { ChannelPlugin } from "openclaw/plugin-sdk";
2
+ import type { MqttCoreConfig } from "./types.js";
3
+ /**
4
+ * MQTT Channel Plugin for OpenClaw
5
+ *
6
+ * Provides bidirectional messaging via MQTT brokers (Mosquitto, EMQX, etc.)
7
+ * Useful for IoT integration, home automation alerts, and service monitoring.
8
+ */
9
+ export declare const mqttPlugin: ChannelPlugin<MqttCoreConfig>;
10
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAQjD;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,aAAa,CAAC,cAAc,CAoIpD,CAAC"}
@@ -0,0 +1,235 @@
1
+ import { createMqttClient } from "./client.js";
2
+ import { mqttOnboardingAdapter } from "./onboarding.js";
3
+ import { getMqttRuntime } from "./runtime.js";
4
+ // Global client instance (one per gateway lifecycle)
5
+ let mqttClient = null;
6
+ /**
7
+ * MQTT Channel Plugin for OpenClaw
8
+ *
9
+ * Provides bidirectional messaging via MQTT brokers (Mosquitto, EMQX, etc.)
10
+ * Useful for IoT integration, home automation alerts, and service monitoring.
11
+ */
12
+ export const mqttPlugin = {
13
+ id: "mqtt",
14
+ meta: {
15
+ id: "mqtt",
16
+ label: "MQTT",
17
+ selectionLabel: "MQTT (IoT/Home Automation)",
18
+ docsPath: "/channels/mqtt",
19
+ blurb: "Bidirectional messaging via MQTT brokers",
20
+ aliases: ["mosquitto"],
21
+ },
22
+ capabilities: {
23
+ chatTypes: ["direct"],
24
+ supportsMedia: false,
25
+ supportsReactions: false,
26
+ supportsThreads: false,
27
+ },
28
+ config: {
29
+ listAccountIds: (cfg) => {
30
+ return cfg.channels?.mqtt?.brokerUrl ? ["default"] : [];
31
+ },
32
+ resolveAccount: (cfg, accountId) => {
33
+ const mqtt = cfg.channels?.mqtt;
34
+ if (!mqtt)
35
+ return { accountId: accountId ?? "default", enabled: false };
36
+ return {
37
+ accountId: accountId ?? "default",
38
+ enabled: mqtt.enabled !== false,
39
+ brokerUrl: mqtt.brokerUrl,
40
+ config: mqtt,
41
+ };
42
+ },
43
+ isEnabled: (account) => account.enabled !== false,
44
+ isConfigured: (account) => Boolean(account.brokerUrl),
45
+ },
46
+ outbound: {
47
+ deliveryMode: "direct",
48
+ async sendText({ text, cfg }) {
49
+ const mqtt = cfg.channels?.mqtt;
50
+ if (!mqtt?.brokerUrl) {
51
+ return { ok: false, error: "MQTT not configured" };
52
+ }
53
+ if (!mqttClient || !mqttClient.isConnected()) {
54
+ return { ok: false, error: "MQTT not connected" };
55
+ }
56
+ try {
57
+ const topic = mqtt.topics?.outbound ?? "openclaw/outbound";
58
+ await mqttClient.publish(topic, text, mqtt.qos);
59
+ return { ok: true };
60
+ }
61
+ catch (err) {
62
+ const error = err instanceof Error ? err.message : String(err);
63
+ return { ok: false, error };
64
+ }
65
+ },
66
+ },
67
+ gateway: {
68
+ startAccount: async (ctx) => {
69
+ const { cfg, account, accountId, abortSignal, log } = ctx;
70
+ const runtime = getMqttRuntime();
71
+ const mqtt = cfg.channels?.mqtt;
72
+ if (!mqtt?.brokerUrl) {
73
+ log?.debug?.("MQTT channel not configured, skipping");
74
+ return;
75
+ }
76
+ log?.info?.(`[${accountId}] starting MQTT provider (${mqtt.brokerUrl})`);
77
+ // Create and connect client
78
+ mqttClient = createMqttClient(mqtt, {
79
+ debug: (msg) => log?.debug?.(`[MQTT] ${msg}`),
80
+ info: (msg) => log?.info?.(`[MQTT] ${msg}`),
81
+ warn: (msg) => log?.warn?.(`[MQTT] ${msg}`),
82
+ error: (msg) => log?.error?.(`[MQTT] ${msg}`),
83
+ });
84
+ try {
85
+ await mqttClient.connect();
86
+ }
87
+ catch (err) {
88
+ log?.error?.(`MQTT connection failed: ${err}`);
89
+ throw err;
90
+ }
91
+ // Subscribe to inbound topic
92
+ const inboundTopic = mqtt.topics?.inbound ?? "openclaw/inbound";
93
+ const outboundTopic = mqtt.topics?.outbound ?? "openclaw/outbound";
94
+ mqttClient.subscribe(inboundTopic, async (topic, payload) => {
95
+ await handleInboundMessage({
96
+ topic,
97
+ payload,
98
+ runtime,
99
+ cfg,
100
+ accountId,
101
+ log,
102
+ outboundTopic,
103
+ qos: mqtt.qos,
104
+ });
105
+ });
106
+ log?.info?.(`[${accountId}] MQTT channel ready, subscribed to ${inboundTopic}`);
107
+ // Return a promise that resolves when aborted
108
+ return new Promise((resolve) => {
109
+ const cleanup = () => {
110
+ if (mqttClient) {
111
+ log?.info?.(`[${accountId}] MQTT channel stopping`);
112
+ mqttClient.disconnect().finally(() => {
113
+ mqttClient = null;
114
+ resolve();
115
+ });
116
+ }
117
+ else {
118
+ resolve();
119
+ }
120
+ };
121
+ if (abortSignal) {
122
+ abortSignal.addEventListener("abort", cleanup, { once: true });
123
+ }
124
+ });
125
+ },
126
+ },
127
+ onboarding: mqttOnboardingAdapter,
128
+ };
129
+ /**
130
+ * Handle inbound MQTT message - process through OpenClaw agent and deliver reply
131
+ */
132
+ async function handleInboundMessage(opts) {
133
+ const { topic, payload, runtime, cfg, accountId, log, outboundTopic, qos } = opts;
134
+ try {
135
+ const text = payload.toString("utf-8");
136
+ log?.info?.(`Inbound MQTT message on ${topic}: ${text.slice(0, 200)}${text.length > 200 ? "..." : ""}`);
137
+ // Parse JSON if possible to extract structured data
138
+ let parsedPayload = null;
139
+ try {
140
+ parsedPayload = JSON.parse(text);
141
+ }
142
+ catch {
143
+ parsedPayload = null;
144
+ }
145
+ // Extract message body and sender from payload
146
+ let messageBody;
147
+ let senderId;
148
+ if (parsedPayload && typeof parsedPayload === "object") {
149
+ messageBody =
150
+ parsedPayload.message ??
151
+ parsedPayload.text ??
152
+ parsedPayload.msg ??
153
+ parsedPayload.alert ??
154
+ parsedPayload.body ??
155
+ text;
156
+ senderId =
157
+ parsedPayload.senderId ??
158
+ parsedPayload.source ??
159
+ parsedPayload.sender ??
160
+ parsedPayload.from ??
161
+ parsedPayload.service ??
162
+ topic.replace(/\//g, "-");
163
+ }
164
+ else {
165
+ messageBody = text;
166
+ senderId = topic.replace(/\//g, "-");
167
+ }
168
+ // Build the inbound context using OpenClaw's standard format
169
+ const ctxPayload = runtime.channel.reply.finalizeInboundContext({
170
+ Body: messageBody,
171
+ RawBody: text,
172
+ CommandBody: messageBody,
173
+ CommandAuthorized: true,
174
+ From: `mqtt:${senderId}`,
175
+ To: `mqtt:${accountId}`,
176
+ SessionKey: `agent:main:mqtt:${senderId}`,
177
+ AccountId: accountId,
178
+ ChatType: "direct",
179
+ ConversationLabel: `mqtt:${senderId}`,
180
+ SenderName: senderId,
181
+ SenderId: senderId,
182
+ Provider: "mqtt",
183
+ Surface: "mqtt",
184
+ MessageSid: `mqtt-${Date.now()}-${Math.random().toString(36).slice(2)}`,
185
+ Timestamp: Date.now(),
186
+ });
187
+ // inbound context logging removed
188
+ // Dispatch through OpenClaw's reply system and publish replies
189
+ await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
190
+ ctx: ctxPayload,
191
+ cfg,
192
+ dispatcherOptions: {
193
+ deliver: async (payload, info) => {
194
+ if (!payload.text) {
195
+ log?.debug?.(`MQTT: skipping empty ${info.kind} reply`);
196
+ return;
197
+ }
198
+ log?.info?.(`MQTT reply (${info.kind}) [${payload.text.length} chars]`);
199
+ if (mqttClient?.isConnected()) {
200
+ try {
201
+ const outboundPayload = JSON.stringify({
202
+ senderId: "openclaw",
203
+ text: payload.text,
204
+ kind: info.kind,
205
+ ts: Date.now(),
206
+ });
207
+ await mqttClient.publish(outboundTopic, outboundPayload, qos);
208
+ log?.info?.(`MQTT: sent reply to ${outboundTopic}`);
209
+ }
210
+ catch (err) {
211
+ log?.error?.(`MQTT: failed to send reply: ${err}`);
212
+ }
213
+ }
214
+ else {
215
+ log?.warn?.(`MQTT: not connected, cannot send reply`);
216
+ }
217
+ },
218
+ onSkip: (_payload, info) => {
219
+ log?.debug?.(`MQTT: skipped reply (${info.reason})`);
220
+ },
221
+ onError: (err, info) => {
222
+ log?.error?.(`MQTT: ${info.kind} reply error: ${err}`);
223
+ },
224
+ },
225
+ replyOptions: {
226
+ disableBlockStreaming: true,
227
+ },
228
+ });
229
+ // dispatch complete
230
+ log?.info?.(`MQTT message processed from ${senderId}`);
231
+ }
232
+ catch (err) {
233
+ log?.error?.(`Failed to process MQTT message: ${err}`);
234
+ }
235
+ }