Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,8 @@ switchbot config show
| `--no-retry` | Disable automatic 429 retries |
| `--backoff <strategy>` | Retry backoff: `exponential` (default) or `linear` |
| `--no-quota` | Disable local request-quota tracking |
| `--audit-log [path]` | Append mutating commands to a JSONL audit log (default path: `~/.switchbot/audit.log`) |
| `--audit-log` | Append mutating commands to a JSONL audit log (default path: `~/.switchbot/audit.log`) |
| `--audit-log-path <path>` | Custom audit log path; use together with `--audit-log` |
| `-V`, `--version` | Print the CLI version |
| `-h`, `--help` | Show help for any command or subcommand |

Expand Down Expand Up @@ -212,6 +213,11 @@ switchbot devices list --json | jq '.deviceList[].deviceId'
# Physical: category = "physical"
switchbot devices list --format=tsv --fields=deviceId,type,category

# Filter devices by type / name / category / room (server-side filter keys)
switchbot devices list --filter category=physical
switchbot devices list --filter type=Bot
switchbot devices list --filter name=living,category=physical

# Filter by family / room (family & room info requires the 'src: OpenClaw'
# header, which this CLI sends on every request)
switchbot devices list --json | jq '.deviceList[] | select(.familyName == "Home")'
Expand All @@ -221,6 +227,16 @@ switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | gro
switchbot devices status <deviceId>
switchbot devices status <deviceId> --json

# Resolve device by fuzzy name instead of ID (status, command, describe, expand, watch)
switchbot devices status --name "客厅空调"
switchbot devices command --name "Office Light" turnOn
switchbot devices describe --name "Kitchen Bot"

# Batch status across multiple devices
switchbot devices status --ids ABC,DEF,GHI
switchbot devices status --ids ABC,DEF --fields power,battery # only show specific fields
switchbot devices status --ids ABC,DEF --format jsonl # one JSON line per device

# Send a control command
switchbot devices command <deviceId> <cmd> [parameter] [--type command|customize]

Expand All @@ -229,7 +245,7 @@ switchbot devices describe <deviceId>
switchbot devices describe <deviceId> --json

# Discover what's supported (offline reference, no API call)
switchbot devices types # List all device types + IR remote types
switchbot devices types # List all device types + IR remote types (incl. role column)
switchbot devices commands <type> # Show commands, parameter formats, and status fields
switchbot devices commands Bot
switchbot devices commands "Smart Lock"
Expand Down Expand Up @@ -268,6 +284,8 @@ Some commands require a packed string like `"26,2,2,on"`. `devices expand` build
```bash
# Air Conditioner — setAll
switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
# Resolve by name
switchbot devices expand --name "客厅空调" setAll --temp 26 --mode cool --fan low --power on

# Curtain / Roller Shade — setPosition
switchbot devices expand <curtainId> setPosition --position 50 --mode silent
Expand Down Expand Up @@ -412,6 +430,40 @@ nohup switchbot events mqtt-tail --json >> ~/switchbot-events.log 2>&1 &

Run `switchbot doctor` to verify MQTT credentials are configured correctly before connecting.

#### `mqtt-tail` sinks — route events to external services

By default `mqtt-tail` prints JSONL to stdout. Use `--sink` (repeatable) to route events to one or more destinations instead:

| Sink | Required flags |
|---|---|
| `stdout` | (default when no `--sink` given) |
| `file` | `--sink-file <path>` — append JSONL |
| `webhook` | `--webhook-url <url>` — HTTP POST each event |
| `openclaw` | `--openclaw-url`, `--openclaw-token` (or `$OPENCLAW_TOKEN`), `--openclaw-model` |
| `telegram` | `--telegram-token` (or `$TELEGRAM_TOKEN`), `--telegram-chat <chatId>` |
| `homeassistant` | `--ha-url <url>` + `--ha-webhook-id` (no auth) or `--ha-token` (REST event API) |

```bash
# Push events to an OpenClaw agent (replaces the SwitchBot channel plugin)
switchbot events mqtt-tail \
--sink openclaw \
--openclaw-token <token> \
--openclaw-model my-home-agent

# Write to file + push to OpenClaw simultaneously
switchbot events mqtt-tail \
--sink file --sink-file ~/.switchbot/events.jsonl \
--sink openclaw --openclaw-token <token> --openclaw-model home

# Generic webhook (n8n, Make, etc.)
switchbot events mqtt-tail --sink webhook --webhook-url https://n8n.local/hook/abc

# Forward to Home Assistant via webhook trigger
switchbot events mqtt-tail --sink homeassistant --ha-url http://homeassistant.local:8123 --ha-webhook-id switchbot
```

Device state is also persisted to `~/.switchbot/device-history/<deviceId>.json` (latest + 100-entry ring buffer) regardless of sink configuration. This enables the `get_device_history` MCP tool to answer state queries without an API call.

### `completion` — shell tab-completion

```bash
Expand Down Expand Up @@ -498,7 +550,7 @@ switchbot history replay 7 # re-run entry #7
switchbot --json history show --limit 50 | jq '.entries[] | select(.result=="error")'
```

Reads the JSONL audit log (`~/.switchbot/audit.log` by default; override with `--audit-log`). Each entry records the timestamp, command, device ID, result, and dry-run flag. `replay` re-runs the original command with the original arguments.
Reads the JSONL audit log (`~/.switchbot/audit.log` by default; override with `--audit-log --audit-log-path <path>`). Each entry records the timestamp, command, device ID, result, and dry-run flag. `replay` re-runs the original command with the original arguments.

### `catalog` — device type catalog

Expand Down
61 changes: 48 additions & 13 deletions docs/agent-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,21 +57,38 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
}
```

### Available tools (8)

| Tool | Purpose | Destructive-guard? |
|---------------------|-------------------------------------------------------------------|--------------------------|
| `list_devices` | Enumerate physical devices + IR remotes | — |
| `get_device_status` | Live status for one device | — |
| `send_command` | Dispatch a built-in or customize command | yes (`confirm: true` required) |
| `list_scenes` | Enumerate saved manual scenes | — |
| `run_scene` | Execute a saved manual scene | — |
| `search_catalog` | Look up device type by name/alias | — |
| `describe_device` | Live status **plus** catalog-derived commands + suggested actions | — |
| `account_overview` | Single cold-start snapshot — devices, scenes, quota, cache, MQTT state. Call this first in a new agent session to avoid multiple round-trips. | — |
### Available tools (9)

| Tool | Purpose | Destructive-guard? |
|------------------------|-------------------------------------------------------------------|--------------------------|
| `list_devices` | Enumerate physical devices + IR remotes | — |
| `get_device_status` | Live status for one device | — |
| `send_command` | Dispatch a built-in or customize command | yes (`confirm: true` required) |
| `list_scenes` | Enumerate saved manual scenes | — |
| `run_scene` | Execute a saved manual scene | — |
| `search_catalog` | Look up device type by name/alias | — |
| `describe_device` | Live status **plus** catalog-derived commands + suggested actions | — |
| `account_overview` | Single cold-start snapshot — devices, scenes, quota, cache, MQTT state. Call this first in a new agent session to avoid multiple round-trips. | — |
| `get_device_history` | Latest state + rolling history from disk — zero quota cost | — |

The MCP server refuses destructive commands (Smart Lock `unlock`, Garage Door `open`, etc.) unless the tool call includes `confirm: true`. The allowed list is the `destructive: true` commands in the catalog — `switchbot schema export | jq '[.data.types[].commands[] | select(.destructive)]'` shows every one.

### `get_device_history` — zero-cost state lookup

Reads `~/.switchbot/device-history/<deviceId>.json` written by `events mqtt-tail`. Requires no API call and costs zero quota.

```json
// Without deviceId — list all devices with stored history
{ "tool": "get_device_history" }
// → { "devices": [{ "deviceId": "ABC123", "latest": { "t": "...", "payload": {...} } }] }

// With deviceId — latest + rolling history (default 20, max 100 entries)
{ "tool": "get_device_history", "deviceId": "ABC123", "limit": 5 }
// → { "deviceId": "ABC123", "latest": {...}, "history": [{...}, ...] }
```

**Workflow**: run `switchbot events mqtt-tail` in the background (e.g. with pm2) to keep the history files fresh; then call `get_device_history` from any MCP session without consuming REST quota.

### MCP resource: `switchbot://events`

Read-only snapshot of recent MQTT shadow-update events from the ring buffer. Returns `{state, count, events[]}`.
Expand Down Expand Up @@ -238,7 +255,25 @@ The audit format is JSONL with this shape:
"dryRun": false, "result": "ok" }
```

Pair with `switchbot devices watch --interval=30s` for continuous state diffs (add `--include-unchanged` to emit every tick even when nothing changed), `switchbot events tail` to receive webhook pushes locally, or `switchbot events mqtt-tail` for real-time MQTT shadow updates (requires `SWITCHBOT_MQTT_HOST` env vars — see [Environment variables](../README.md#environment-variables)).
Pair with `switchbot devices watch --interval=30s` for continuous state diffs (add `--include-unchanged` to emit every tick even when nothing changed), `switchbot events tail` to receive webhook pushes locally, or `switchbot events mqtt-tail` for real-time MQTT shadow updates.

#### Routing MQTT events to an OpenClaw agent

Run `mqtt-tail` once with `--sink openclaw` to replace the SwitchBot channel plugin entirely — no separate plugin installation required:

```bash
switchbot events mqtt-tail \
--sink openclaw \
--openclaw-token <token> \
--openclaw-model my-home-agent

# Persist history at the same time:
switchbot events mqtt-tail \
--sink file --sink-file ~/.switchbot/events.jsonl \
--sink openclaw --openclaw-token <token> --openclaw-model home
```

OpenClaw exposes an OpenAI-compatible HTTP API at `http://localhost:18789/v1/chat/completions`. The sink formats each event as a short text message (e.g. `📱 Climate Panel: 27.5°C / 51%`) and POSTs it to the agent directly.

---

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@switchbot/openapi-cli",
"version": "2.1.0",
"version": "2.2.0",
"description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
"keywords": [
"switchbot",
Expand Down
24 changes: 17 additions & 7 deletions src/commands/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,28 @@ const MCP_TOOLS = [
'run_scene',
'search_catalog',
'account_overview',
'get_device_history',
];

export function registerCapabilitiesCommand(program: Command): void {
program
.command('capabilities')
.description('Print a machine-readable manifest of CLI capabilities (for agent bootstrap)')
.action(() => {
.option('--minimal', 'Omit per-subcommand flag details to reduce output size')
.action((opts: { minimal?: boolean }) => {
const catalog = getEffectiveCatalog();
const commands = program.commands
.filter((c) => c.name() !== 'capabilities')
.map((c) => ({
const allCommands = [
...program.commands,
// Commander adds 'help' implicitly; include it explicitly so it appears in the manifest
{ name: () => 'help', description: () => 'Display help for a command', commands: [], options: [], registeredArguments: [] } as unknown as Command,
];
const commands = allCommands.map((c) => {
const entry: Record<string, unknown> = {
name: c.name(),
description: c.description(),
subcommands: c.commands.map((s) => ({
};
if (!opts.minimal) {
entry.subcommands = c.commands.map((s) => ({
name: s.name(),
description: s.description(),
args: s.registeredArguments.map((a) => ({
Expand All @@ -54,8 +62,10 @@ export function registerCapabilitiesCommand(program: Command): void {
flags: o.flags,
description: o.description,
})),
})),
}));
}));
}
return entry;
});
const globalFlags = program.options.map((opt) => ({
flags: opt.flags,
description: opt.description,
Expand Down
Loading
Loading