@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 +10 -0
- package/CHANGELOG.md +103 -0
- package/README.md +157 -0
- package/config.example.json +18 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/src/__mocks__/mqtt.d.ts +35 -0
- package/dist/src/__mocks__/mqtt.d.ts.map +1 -0
- package/dist/src/__mocks__/mqtt.js +61 -0
- package/dist/src/channel.d.ts +10 -0
- package/dist/src/channel.d.ts.map +1 -0
- package/dist/src/channel.js +235 -0
- package/dist/src/client.d.ts +23 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +192 -0
- package/dist/src/config-schema.d.ts +73 -0
- package/dist/src/config-schema.d.ts.map +1 -0
- package/dist/src/config-schema.js +40 -0
- package/dist/src/env.d.ts +18 -0
- package/dist/src/env.d.ts.map +1 -0
- package/dist/src/env.js +34 -0
- package/dist/src/onboarding.d.ts +73 -0
- package/dist/src/onboarding.d.ts.map +1 -0
- package/dist/src/onboarding.js +138 -0
- package/dist/src/runtime.d.ts +4 -0
- package/dist/src/runtime.d.ts.map +1 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/types.d.ts +22 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +1 -0
- package/openclaw.plugin.json +11 -0
- package/package.json +76 -0
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
|
+
[](https://github.com/hughmadden/openclaw-mqtt/actions)
|
|
4
|
+
[](https://www.npmjs.com/package/@turquoisebay/mqtt)
|
|
5
|
+
[](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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|