@switchbot/openapi-cli 2.2.0 → 2.3.0
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/README.md +5 -0
- package/dist/commands/batch.js +7 -5
- package/dist/commands/cache.js +3 -1
- package/dist/commands/catalog.js +3 -1
- package/dist/commands/completion.js +139 -12
- package/dist/commands/config.js +4 -3
- package/dist/commands/device-meta.js +3 -2
- package/dist/commands/devices.js +56 -15
- package/dist/commands/events.js +30 -17
- package/dist/commands/expand.js +11 -86
- package/dist/commands/history.js +4 -3
- package/dist/commands/mcp.js +6 -5
- package/dist/commands/schema.js +6 -3
- package/dist/commands/watch.js +4 -3
- package/dist/commands/webhook.js +2 -1
- package/dist/config.js +7 -2
- package/dist/devices/param-validator.js +263 -0
- package/dist/index.js +48 -19
- package/dist/lib/devices.js +26 -14
- package/dist/utils/arg-parsers.js +65 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -177,6 +177,8 @@ switchbot --help
|
|
|
177
177
|
switchbot devices command --help
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
+
> **Tip — required-value flags and subcommands.** Flags like `--profile`, `--timeout`, `--max`, and `--interval` take a value. If you omit it, Commander will happily consume the next token — including a subcommand name. Since v2.2.1 the CLI rejects that eagerly (exit 2 with a clear error), but if you ever hit `unknown command 'list'` after something like `switchbot --profile list`, use the `--flag=value` form: `switchbot --profile=home devices list`.
|
|
181
|
+
|
|
180
182
|
### `--dry-run`
|
|
181
183
|
|
|
182
184
|
Intercepts every non-GET request: the CLI prints the URL/body it would have
|
|
@@ -206,6 +208,7 @@ switchbot config list-profiles # List saved profiles
|
|
|
206
208
|
# Default columns (4): deviceId, deviceName, type, category
|
|
207
209
|
# Pass --wide for the full 10-column operator view
|
|
208
210
|
switchbot devices list
|
|
211
|
+
switchbot devices ls # short alias for 'list'
|
|
209
212
|
switchbot devices list --wide
|
|
210
213
|
switchbot devices list --json | jq '.deviceList[].deviceId'
|
|
211
214
|
|
|
@@ -275,6 +278,8 @@ Generic parameter shapes (which one applies is decided by the device — see the
|
|
|
275
278
|
| `<json object>` | `'{"action":"sweep","param":{"fanLevel":2,"times":1}}'` |
|
|
276
279
|
| Custom IR button | `devices command <id> MyButton --type customize` |
|
|
277
280
|
|
|
281
|
+
Parameters for `setAll` (Air Conditioner), `setPosition` (Curtain / Blind Tilt), and `setMode` (Relay Switch) are validated client-side before the request — malformed shapes, out-of-range values, and JSON for CSV fields all fail fast with exit 2. Command names are also case-normalized against the catalog (e.g. `turnon` is auto-corrected to `turnOn` with a stderr warning); unknown names still exit 2 with the supported-commands list.
|
|
282
|
+
|
|
278
283
|
For the complete per-device command reference, see the [SwitchBot API docs](https://github.com/OpenWonderLabs/SwitchBotAPI#send-device-control-commands).
|
|
279
284
|
|
|
280
285
|
#### `devices expand` — named flags for packed parameters
|
package/dist/commands/batch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { intArg, enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printJson, isJsonMode, handleError, buildErrorPayload } from '../utils/output.js';
|
|
2
3
|
import { fetchDeviceList, executeCommand, isDestructiveCommand, buildHubLocationMap, } from '../lib/devices.js';
|
|
3
4
|
import { createClient } from '../api/client.js';
|
|
@@ -6,6 +7,7 @@ import { isDryRun } from '../utils/flags.js';
|
|
|
6
7
|
import { DryRunSignal } from '../api/client.js';
|
|
7
8
|
import { getCachedTypeMap } from '../devices/cache.js';
|
|
8
9
|
const DEFAULT_CONCURRENCY = 5;
|
|
10
|
+
const COMMAND_TYPES = ['command', 'customize'];
|
|
9
11
|
/** Run `task(x)` for every element with at most `concurrency` running at once. */
|
|
10
12
|
async function runPool(items, concurrency, task) {
|
|
11
13
|
const results = new Array(items.length);
|
|
@@ -84,13 +86,13 @@ export function registerBatchCommand(devices) {
|
|
|
84
86
|
.description('Send the same command to many devices in one run (filter- or stdin-driven)')
|
|
85
87
|
.argument('<command>', 'Command name, e.g. turnOn, turnOff, setBrightness')
|
|
86
88
|
.argument('[parameter]', 'Command parameter (same rules as `devices command`; omit for no-arg)')
|
|
87
|
-
.option('--filter <expr>', 'Target devices matching a filter, e.g. type=Bot,family=Home')
|
|
88
|
-
.option('--ids <csv>', 'Explicit comma-separated list of deviceIds')
|
|
89
|
-
.option('--concurrency <n>', 'Max parallel in-flight requests (default 5)', '5')
|
|
89
|
+
.option('--filter <expr>', 'Target devices matching a filter, e.g. type=Bot,family=Home', stringArg('--filter'))
|
|
90
|
+
.option('--ids <csv>', 'Explicit comma-separated list of deviceIds', stringArg('--ids'))
|
|
91
|
+
.option('--concurrency <n>', 'Max parallel in-flight requests (default 5)', intArg('--concurrency', { min: 1 }), '5')
|
|
90
92
|
.option('--yes', 'Allow destructive commands (Smart Lock unlock, garage open, ...)')
|
|
91
|
-
.option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', 'command')
|
|
93
|
+
.option('--type <commandType>', '"command" (default) or "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
|
|
92
94
|
.option('--stdin', 'Read deviceIds from stdin, one per line (same as trailing "-")')
|
|
93
|
-
.option('--idempotency-key-prefix <prefix>', 'Prefix for idempotency keys (key per device: <prefix>-<deviceId>)')
|
|
95
|
+
.option('--idempotency-key-prefix <prefix>', 'Prefix for idempotency keys (key per device: <prefix>-<deviceId>)', stringArg('--idempotency-key-prefix'))
|
|
94
96
|
.addHelpText('after', `
|
|
95
97
|
Targets are resolved in this priority order:
|
|
96
98
|
1. --ids when present (explicit deviceIds)
|
package/dist/commands/cache.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enumArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
3
|
import { clearCache, clearStatusCache, describeCache, loadStatusCache, } from '../devices/cache.js';
|
|
3
4
|
function formatAge(ms) {
|
|
@@ -15,6 +16,7 @@ function formatAge(ms) {
|
|
|
15
16
|
return `${h}h ${m % 60}m`;
|
|
16
17
|
}
|
|
17
18
|
export function registerCacheCommand(program) {
|
|
19
|
+
const CACHE_KEYS = ['list', 'status', 'all'];
|
|
18
20
|
const cache = program
|
|
19
21
|
.command('cache')
|
|
20
22
|
.description('Inspect and manage the local SwitchBot CLI caches')
|
|
@@ -78,7 +80,7 @@ Examples:
|
|
|
78
80
|
cache
|
|
79
81
|
.command('clear')
|
|
80
82
|
.description('Delete cache files')
|
|
81
|
-
.option('--key <which>', 'Which cache to clear: "list" | "status" | "all" (default)', 'all')
|
|
83
|
+
.option('--key <which>', 'Which cache to clear: "list" | "status" | "all" (default)', enumArg('--key', CACHE_KEYS), 'all')
|
|
82
84
|
.action((options) => {
|
|
83
85
|
try {
|
|
84
86
|
const key = options.key;
|
package/dist/commands/catalog.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { enumArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printTable, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
3
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
4
|
import { DEVICE_CATALOG, findCatalogEntry, getCatalogOverlayPath, getEffectiveCatalog, loadCatalogOverlay, resetCatalogOverlayCache, } from '../devices/catalog.js';
|
|
4
5
|
export function registerCatalogCommand(program) {
|
|
6
|
+
const SOURCES = ['built-in', 'overlay', 'effective'];
|
|
5
7
|
const catalog = program
|
|
6
8
|
.command('catalog')
|
|
7
9
|
.description('Inspect the built-in device catalog and any local overlay')
|
|
@@ -66,7 +68,7 @@ Examples:
|
|
|
66
68
|
.command('show')
|
|
67
69
|
.description("Show the effective catalog (or one entry). Defaults to 'effective' source.")
|
|
68
70
|
.argument('[type...]', 'Optional device type/alias (case-insensitive, partial match)')
|
|
69
|
-
.option('--source <source>', 'Which catalog to show: built-in | overlay | effective (default)', 'effective')
|
|
71
|
+
.option('--source <source>', 'Which catalog to show: built-in | overlay | effective (default)', enumArg('--source', SOURCES), 'effective')
|
|
70
72
|
.action((typeParts, options) => {
|
|
71
73
|
try {
|
|
72
74
|
const source = options.source;
|
|
@@ -12,13 +12,19 @@ _switchbot_completion() {
|
|
|
12
12
|
cword="\${COMP_CWORD}"
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
local top_cmds="config devices scenes webhook completion help"
|
|
16
|
-
local config_sub="set-token show"
|
|
17
|
-
local devices_sub="list status command types commands"
|
|
15
|
+
local top_cmds="config devices scenes webhook completion mcp quota catalog cache events doctor schema history plan capabilities help"
|
|
16
|
+
local config_sub="set-token show list-profiles"
|
|
17
|
+
local devices_sub="list ls status command types commands describe batch watch explain expand meta"
|
|
18
18
|
local scenes_sub="list execute"
|
|
19
19
|
local webhook_sub="setup query update delete"
|
|
20
|
+
local events_sub="tail mqtt-tail"
|
|
21
|
+
local quota_sub="status reset"
|
|
22
|
+
local catalog_sub="path show diff refresh"
|
|
23
|
+
local cache_sub="show clear"
|
|
24
|
+
local history_sub="show replay"
|
|
25
|
+
local plan_sub="schema validate run"
|
|
20
26
|
local completion_shells="bash zsh fish powershell"
|
|
21
|
-
local global_opts="--json --verbose -v --dry-run --timeout --config --help -h --version -V"
|
|
27
|
+
local global_opts="--json --format --fields --verbose -v --dry-run --timeout --retry-on-429 --backoff --no-retry --no-quota --cache --no-cache --config --profile --audit-log --audit-log-path --help -h --version -V"
|
|
22
28
|
|
|
23
29
|
if [[ \${cword} -eq 1 ]]; then
|
|
24
30
|
COMPREPLY=( $(compgen -W "\${top_cmds} \${global_opts}" -- "\${cur}") )
|
|
@@ -43,6 +49,36 @@ _switchbot_completion() {
|
|
|
43
49
|
COMPREPLY=( $(compgen -W "\${scenes_sub}" -- "\${cur}") )
|
|
44
50
|
fi
|
|
45
51
|
;;
|
|
52
|
+
events)
|
|
53
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
54
|
+
COMPREPLY=( $(compgen -W "\${events_sub}" -- "\${cur}") )
|
|
55
|
+
fi
|
|
56
|
+
;;
|
|
57
|
+
quota)
|
|
58
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
59
|
+
COMPREPLY=( $(compgen -W "\${quota_sub}" -- "\${cur}") )
|
|
60
|
+
fi
|
|
61
|
+
;;
|
|
62
|
+
catalog)
|
|
63
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
64
|
+
COMPREPLY=( $(compgen -W "\${catalog_sub}" -- "\${cur}") )
|
|
65
|
+
fi
|
|
66
|
+
;;
|
|
67
|
+
cache)
|
|
68
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
69
|
+
COMPREPLY=( $(compgen -W "\${cache_sub}" -- "\${cur}") )
|
|
70
|
+
fi
|
|
71
|
+
;;
|
|
72
|
+
history)
|
|
73
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
74
|
+
COMPREPLY=( $(compgen -W "\${history_sub}" -- "\${cur}") )
|
|
75
|
+
fi
|
|
76
|
+
;;
|
|
77
|
+
plan)
|
|
78
|
+
if [[ \${cword} -eq 2 ]]; then
|
|
79
|
+
COMPREPLY=( $(compgen -W "\${plan_sub}" -- "\${cur}") )
|
|
80
|
+
fi
|
|
81
|
+
;;
|
|
46
82
|
webhook)
|
|
47
83
|
if [[ \${cword} -eq 2 ]]; then
|
|
48
84
|
COMPREPLY=( $(compgen -W "\${webhook_sub}" -- "\${cur}") )
|
|
@@ -69,22 +105,39 @@ const ZSH_SCRIPT = `# switchbot zsh completion
|
|
|
69
105
|
# source <(switchbot completion zsh)
|
|
70
106
|
|
|
71
107
|
_switchbot() {
|
|
72
|
-
local -a top_cmds config_sub devices_sub scenes_sub webhook_sub completion_shells
|
|
108
|
+
local -a top_cmds config_sub devices_sub scenes_sub webhook_sub events_sub quota_sub catalog_sub cache_sub history_sub plan_sub completion_shells
|
|
73
109
|
top_cmds=(
|
|
74
110
|
'config:Manage API credentials'
|
|
75
111
|
'devices:List and control devices'
|
|
76
112
|
'scenes:List and execute scenes'
|
|
77
113
|
'webhook:Manage webhook configuration'
|
|
78
114
|
'completion:Print a shell completion script'
|
|
115
|
+
'mcp:Run the MCP server'
|
|
116
|
+
'quota:Inspect local request quota'
|
|
117
|
+
'catalog:Inspect the built-in device catalog'
|
|
118
|
+
'cache:Inspect local caches'
|
|
119
|
+
'events:Receive webhook or MQTT events'
|
|
120
|
+
'doctor:Run self-checks'
|
|
121
|
+
'schema:Export the device catalog as JSON'
|
|
122
|
+
'history:View and replay audited commands'
|
|
123
|
+
'plan:Validate and run batch plans'
|
|
124
|
+
'capabilities:Print a machine-readable manifest'
|
|
79
125
|
'help:Show help for a command'
|
|
80
126
|
)
|
|
81
|
-
config_sub=('set-token:Save token + secret' 'show:Show current credential source')
|
|
127
|
+
config_sub=('set-token:Save token + secret' 'show:Show current credential source' 'list-profiles:List named credential profiles')
|
|
82
128
|
devices_sub=(
|
|
83
129
|
'list:List all devices'
|
|
130
|
+
'ls:Alias for list'
|
|
84
131
|
'status:Query device status'
|
|
85
132
|
'command:Send a control command'
|
|
86
133
|
'types:List known device types (offline)'
|
|
87
134
|
'commands:Show commands for a device type (offline)'
|
|
135
|
+
'describe:Show metadata + supported commands for one device'
|
|
136
|
+
'batch:Send one command to many devices'
|
|
137
|
+
'watch:Poll device status and emit changes'
|
|
138
|
+
'explain:One-shot device summary'
|
|
139
|
+
'expand:Build wire-format params from semantic flags'
|
|
140
|
+
'meta:Manage local device metadata'
|
|
88
141
|
)
|
|
89
142
|
scenes_sub=('list:List manual scenes' 'execute:Run a scene')
|
|
90
143
|
webhook_sub=(
|
|
@@ -93,15 +146,32 @@ _switchbot() {
|
|
|
93
146
|
'update:Enable/disable a webhook'
|
|
94
147
|
'delete:Delete a webhook'
|
|
95
148
|
)
|
|
149
|
+
events_sub=('tail:Run a local webhook receiver' 'mqtt-tail:Stream MQTT shadow events')
|
|
150
|
+
quota_sub=('status:Show today and recent quota usage' 'reset:Delete the local quota counter')
|
|
151
|
+
catalog_sub=('path:Show overlay path' 'show:Show built-in/overlay/effective catalog' 'diff:Show overlay changes' 'refresh:Clear overlay cache')
|
|
152
|
+
cache_sub=('show:Summarize cache files' 'clear:Delete cache files')
|
|
153
|
+
history_sub=('show:Print recent audit entries' 'replay:Re-run one audited command')
|
|
154
|
+
plan_sub=('schema:Print the plan schema' 'validate:Validate a plan file' 'run:Validate and execute a plan')
|
|
96
155
|
completion_shells=('bash' 'zsh' 'fish' 'powershell')
|
|
97
156
|
|
|
98
157
|
local global_opts
|
|
99
158
|
global_opts=(
|
|
100
159
|
'--json[Raw JSON output]'
|
|
160
|
+
'--format[Output format]:type:(table json jsonl tsv yaml id)'
|
|
161
|
+
'--fields[Comma-separated output columns]:csv:'
|
|
101
162
|
'(-v --verbose)'{-v,--verbose}'[Log HTTP details to stderr]'
|
|
102
163
|
'--dry-run[Print mutating requests without sending]'
|
|
103
164
|
'--timeout[HTTP timeout in ms]:ms:'
|
|
165
|
+
'--retry-on-429[Max 429 retries]:n:'
|
|
166
|
+
'--backoff[Retry backoff strategy]:strategy:(linear exponential)'
|
|
167
|
+
'--no-retry[Disable 429 retries]'
|
|
168
|
+
'--no-quota[Disable the local quota counter]'
|
|
169
|
+
'--cache[Cache mode]:mode:'
|
|
170
|
+
'--no-cache[Disable cache reads]'
|
|
104
171
|
'--config[Override credential file path]:path:_files'
|
|
172
|
+
'--profile[Use a named credential profile]:name:'
|
|
173
|
+
'--audit-log[Append mutating commands to ~/.switchbot/audit.log]'
|
|
174
|
+
'--audit-log-path[Custom audit log file path]:path:_files'
|
|
105
175
|
'(-h --help)'{-h,--help}'[Show help]'
|
|
106
176
|
'(-V --version)'{-V,--version}'[Show version]'
|
|
107
177
|
)
|
|
@@ -122,6 +192,12 @@ _switchbot() {
|
|
|
122
192
|
devices) _describe 'devices' devices_sub ;;
|
|
123
193
|
scenes) _describe 'scenes' scenes_sub ;;
|
|
124
194
|
webhook) _describe 'webhook' webhook_sub ;;
|
|
195
|
+
events) _describe 'events' events_sub ;;
|
|
196
|
+
quota) _describe 'quota' quota_sub ;;
|
|
197
|
+
catalog) _describe 'catalog' catalog_sub ;;
|
|
198
|
+
cache) _describe 'cache' cache_sub ;;
|
|
199
|
+
history) _describe 'history' history_sub ;;
|
|
200
|
+
plan) _describe 'plan' plan_sub ;;
|
|
125
201
|
completion) _values 'shell' $completion_shells ;;
|
|
126
202
|
esac
|
|
127
203
|
;;
|
|
@@ -143,10 +219,21 @@ complete -c switchbot -f
|
|
|
143
219
|
|
|
144
220
|
# Global options
|
|
145
221
|
complete -c switchbot -l json -d 'Raw JSON output'
|
|
222
|
+
complete -c switchbot -l format -r -d 'Output format'
|
|
223
|
+
complete -c switchbot -l fields -r -d 'Comma-separated output columns'
|
|
146
224
|
complete -c switchbot -s v -l verbose -d 'Log HTTP details to stderr'
|
|
147
225
|
complete -c switchbot -l dry-run -d 'Print mutating requests without sending'
|
|
148
226
|
complete -c switchbot -l timeout -r -d 'HTTP timeout in ms'
|
|
227
|
+
complete -c switchbot -l retry-on-429 -r -d 'Max 429 retries'
|
|
228
|
+
complete -c switchbot -l backoff -r -d 'Retry backoff strategy'
|
|
229
|
+
complete -c switchbot -l no-retry -d 'Disable 429 retries'
|
|
230
|
+
complete -c switchbot -l no-quota -d 'Disable the local quota counter'
|
|
231
|
+
complete -c switchbot -l cache -r -d 'Cache mode'
|
|
232
|
+
complete -c switchbot -l no-cache -d 'Disable cache reads'
|
|
149
233
|
complete -c switchbot -l config -r -d 'Credential file path'
|
|
234
|
+
complete -c switchbot -l profile -r -d 'Named credential profile'
|
|
235
|
+
complete -c switchbot -l audit-log -d 'Append mutating commands to audit log'
|
|
236
|
+
complete -c switchbot -l audit-log-path -r -d 'Custom audit log file path'
|
|
150
237
|
complete -c switchbot -s h -l help -d 'Show help'
|
|
151
238
|
complete -c switchbot -s V -l version -d 'Show version'
|
|
152
239
|
|
|
@@ -156,13 +243,23 @@ complete -c switchbot -n '__fish_use_subcommand' -a 'devices' -d 'List and co
|
|
|
156
243
|
complete -c switchbot -n '__fish_use_subcommand' -a 'scenes' -d 'List and execute scenes'
|
|
157
244
|
complete -c switchbot -n '__fish_use_subcommand' -a 'webhook' -d 'Manage webhook configuration'
|
|
158
245
|
complete -c switchbot -n '__fish_use_subcommand' -a 'completion' -d 'Print a shell completion script'
|
|
246
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'mcp' -d 'Run the MCP server'
|
|
247
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'quota' -d 'Inspect local request quota'
|
|
248
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'catalog' -d 'Inspect the built-in device catalog'
|
|
249
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'cache' -d 'Inspect local caches'
|
|
250
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'events' -d 'Receive webhook or MQTT events'
|
|
251
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'doctor' -d 'Run self-checks'
|
|
252
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'schema' -d 'Export the device catalog as JSON'
|
|
253
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'history' -d 'View and replay audited commands'
|
|
254
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'plan' -d 'Validate and run batch plans'
|
|
255
|
+
complete -c switchbot -n '__fish_use_subcommand' -a 'capabilities' -d 'Print a machine-readable manifest'
|
|
159
256
|
complete -c switchbot -n '__fish_use_subcommand' -a 'help' -d 'Show help'
|
|
160
257
|
|
|
161
258
|
# config
|
|
162
|
-
complete -c switchbot -n '__fish_seen_subcommand_from config' -a 'set-token show'
|
|
259
|
+
complete -c switchbot -n '__fish_seen_subcommand_from config' -a 'set-token show list-profiles'
|
|
163
260
|
|
|
164
261
|
# devices
|
|
165
|
-
complete -c switchbot -n '__fish_seen_subcommand_from devices' -a 'list status command types commands'
|
|
262
|
+
complete -c switchbot -n '__fish_seen_subcommand_from devices' -a 'list ls status command types commands describe batch watch explain expand meta'
|
|
166
263
|
|
|
167
264
|
# scenes
|
|
168
265
|
complete -c switchbot -n '__fish_seen_subcommand_from scenes' -a 'list execute'
|
|
@@ -172,6 +269,24 @@ complete -c switchbot -n '__fish_seen_subcommand_from webhook' -a 'setup query u
|
|
|
172
269
|
complete -c switchbot -n '__fish_seen_subcommand_from webhook; and __fish_seen_subcommand_from update' -l enable -d 'Enable the webhook'
|
|
173
270
|
complete -c switchbot -n '__fish_seen_subcommand_from webhook; and __fish_seen_subcommand_from update' -l disable -d 'Disable the webhook'
|
|
174
271
|
|
|
272
|
+
# events
|
|
273
|
+
complete -c switchbot -n '__fish_seen_subcommand_from events' -a 'tail mqtt-tail'
|
|
274
|
+
|
|
275
|
+
# quota
|
|
276
|
+
complete -c switchbot -n '__fish_seen_subcommand_from quota' -a 'status reset'
|
|
277
|
+
|
|
278
|
+
# catalog
|
|
279
|
+
complete -c switchbot -n '__fish_seen_subcommand_from catalog' -a 'path show diff refresh'
|
|
280
|
+
|
|
281
|
+
# cache
|
|
282
|
+
complete -c switchbot -n '__fish_seen_subcommand_from cache' -a 'show clear'
|
|
283
|
+
|
|
284
|
+
# history
|
|
285
|
+
complete -c switchbot -n '__fish_seen_subcommand_from history' -a 'show replay'
|
|
286
|
+
|
|
287
|
+
# plan
|
|
288
|
+
complete -c switchbot -n '__fish_seen_subcommand_from plan' -a 'schema validate run'
|
|
289
|
+
|
|
175
290
|
# completion
|
|
176
291
|
complete -c switchbot -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish powershell'
|
|
177
292
|
`;
|
|
@@ -186,13 +301,19 @@ Register-ArgumentCompleter -Native -CommandName switchbot -ScriptBlock {
|
|
|
186
301
|
$tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
|
|
187
302
|
$count = $tokens.Count
|
|
188
303
|
|
|
189
|
-
$top = 'config','devices','scenes','webhook','completion','help'
|
|
190
|
-
$configSub = 'set-token','show'
|
|
191
|
-
$devicesSub = 'list','status','command','types','commands'
|
|
304
|
+
$top = 'config','devices','scenes','webhook','completion','mcp','quota','catalog','cache','events','doctor','schema','history','plan','capabilities','help'
|
|
305
|
+
$configSub = 'set-token','show','list-profiles'
|
|
306
|
+
$devicesSub = 'list','ls','status','command','types','commands','describe','batch','watch','explain','expand','meta'
|
|
192
307
|
$scenesSub = 'list','execute'
|
|
193
308
|
$webhookSub = 'setup','query','update','delete'
|
|
309
|
+
$eventsSub = 'tail','mqtt-tail'
|
|
310
|
+
$quotaSub = 'status','reset'
|
|
311
|
+
$catalogSub = 'path','show','diff','refresh'
|
|
312
|
+
$cacheSub = 'show','clear'
|
|
313
|
+
$historySub = 'show','replay'
|
|
314
|
+
$planSub = 'schema','validate','run'
|
|
194
315
|
$shells = 'bash','zsh','fish','powershell'
|
|
195
|
-
$globalOpts = '--json','--verbose','-v','--dry-run','--timeout','--config','--help','-h','--version','-V'
|
|
316
|
+
$globalOpts = '--json','--format','--fields','--verbose','-v','--dry-run','--timeout','--retry-on-429','--backoff','--no-retry','--no-quota','--cache','--no-cache','--config','--profile','--audit-log','--audit-log-path','--help','-h','--version','-V'
|
|
196
317
|
|
|
197
318
|
function _emit($values) {
|
|
198
319
|
$values |
|
|
@@ -206,6 +327,12 @@ Register-ArgumentCompleter -Native -CommandName switchbot -ScriptBlock {
|
|
|
206
327
|
'config' { if ($count -eq 3) { return _emit $configSub } }
|
|
207
328
|
'devices' { if ($count -eq 3) { return _emit $devicesSub } }
|
|
208
329
|
'scenes' { if ($count -eq 3) { return _emit $scenesSub } }
|
|
330
|
+
'events' { if ($count -eq 3) { return _emit $eventsSub } }
|
|
331
|
+
'quota' { if ($count -eq 3) { return _emit $quotaSub } }
|
|
332
|
+
'catalog' { if ($count -eq 3) { return _emit $catalogSub } }
|
|
333
|
+
'cache' { if ($count -eq 3) { return _emit $cacheSub } }
|
|
334
|
+
'history' { if ($count -eq 3) { return _emit $historySub } }
|
|
335
|
+
'plan' { if ($count -eq 3) { return _emit $planSub } }
|
|
209
336
|
'webhook' {
|
|
210
337
|
if ($count -eq 3) { return _emit $webhookSub }
|
|
211
338
|
if ($tokens[2] -eq 'update') { return _emit ('--enable','--disable' + $globalOpts) }
|
package/dist/commands/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { stringArg } from '../utils/arg-parsers.js';
|
|
3
4
|
import { saveConfig, showConfig, listProfiles } from '../config.js';
|
|
4
5
|
import { isJsonMode, printJson } from '../utils/output.js';
|
|
5
6
|
import chalk from 'chalk';
|
|
@@ -49,9 +50,9 @@ Obtain your token/secret from the SwitchBot mobile app:
|
|
|
49
50
|
.description('Save token and secret (mode 0600). Use --profile to target a named profile.')
|
|
50
51
|
.argument('[token]', 'API token; omit when using --from-env-file / --from-op')
|
|
51
52
|
.argument('[secret]', 'API client secret; omit when using --from-env-file / --from-op')
|
|
52
|
-
.option('--from-env-file <path>', 'Read SWITCHBOT_TOKEN and SWITCHBOT_SECRET from a dotenv file')
|
|
53
|
-
.option('--from-op <tokenRef>', 'Read token via 1Password CLI (op read). Pair with --op-secret <ref>')
|
|
54
|
-
.option('--op-secret <secretRef>', '1Password reference for the secret, used with --from-op')
|
|
53
|
+
.option('--from-env-file <path>', 'Read SWITCHBOT_TOKEN and SWITCHBOT_SECRET from a dotenv file', stringArg('--from-env-file'))
|
|
54
|
+
.option('--from-op <tokenRef>', 'Read token via 1Password CLI (op read). Pair with --op-secret <ref>', stringArg('--from-op'))
|
|
55
|
+
.option('--op-secret <secretRef>', '1Password reference for the secret, used with --from-op', stringArg('--op-secret'))
|
|
55
56
|
.addHelpText('after', `
|
|
56
57
|
Examples:
|
|
57
58
|
$ switchbot config set-token <token> <secret>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { handleError, isJsonMode, printJson, printTable, UsageError } from '../utils/output.js';
|
|
2
3
|
import { loadDeviceMeta, setDeviceMeta, clearDeviceMeta, getDeviceMeta, getMetaFilePath, } from '../devices/device-meta.js';
|
|
3
4
|
export function registerDevicesMetaCommand(devices) {
|
|
@@ -9,10 +10,10 @@ export function registerDevicesMetaCommand(devices) {
|
|
|
9
10
|
.command('set')
|
|
10
11
|
.description('Set local metadata for a device (alias, hide/show, notes)')
|
|
11
12
|
.argument('<deviceId>', 'Target device ID')
|
|
12
|
-
.option('--alias <name>', 'Local alias for the device (used with --name flag)')
|
|
13
|
+
.option('--alias <name>', 'Local alias for the device (used with --name flag)', stringArg('--alias'))
|
|
13
14
|
.option('--hide', 'Hide this device from "devices list"')
|
|
14
15
|
.option('--show', 'Un-hide this device')
|
|
15
|
-
.option('--notes <text>', 'Freeform notes shown in "devices describe"')
|
|
16
|
+
.option('--notes <text>', 'Freeform notes shown in "devices describe"', stringArg('--notes'))
|
|
16
17
|
.action((deviceId, options) => {
|
|
17
18
|
try {
|
|
18
19
|
if (options.hide && options.show) {
|
package/dist/commands/devices.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
1
2
|
import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
3
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
4
|
import { findCatalogEntry, getEffectiveCatalog } from '../devices/catalog.js';
|
|
@@ -5,6 +6,7 @@ import { getCachedDevice } from '../devices/cache.js';
|
|
|
5
6
|
import { loadDeviceMeta } from '../devices/device-meta.js';
|
|
6
7
|
import { resolveDeviceId } from '../utils/name-resolver.js';
|
|
7
8
|
import { fetchDeviceList, fetchDeviceStatus, executeCommand, describeDevice, validateCommand, isDestructiveCommand, getDestructiveReason, buildHubLocationMap, DeviceNotFoundError, } from '../lib/devices.js';
|
|
9
|
+
import { validateParameter } from '../devices/param-validator.js';
|
|
8
10
|
import { registerBatchCommand } from './batch.js';
|
|
9
11
|
import { registerWatchCommand } from './watch.js';
|
|
10
12
|
import { registerExplainCommand } from './explain.js';
|
|
@@ -12,6 +14,7 @@ import { registerExpandCommand } from './expand.js';
|
|
|
12
14
|
import { registerDevicesMetaCommand } from './device-meta.js';
|
|
13
15
|
import { isDryRun } from '../utils/flags.js';
|
|
14
16
|
export function registerDevicesCommand(program) {
|
|
17
|
+
const COMMAND_TYPES = ['command', 'customize'];
|
|
15
18
|
const devices = program
|
|
16
19
|
.command('devices')
|
|
17
20
|
.description('Manage and control SwitchBot devices')
|
|
@@ -38,6 +41,7 @@ Run any subcommand with --help for its own flags and examples.
|
|
|
38
41
|
// switchbot devices list
|
|
39
42
|
devices
|
|
40
43
|
.command('list')
|
|
44
|
+
.alias('ls')
|
|
41
45
|
.description('List all physical devices and IR remote devices on the account')
|
|
42
46
|
.addHelpText('after', `
|
|
43
47
|
Default columns: deviceId, deviceName, type, category
|
|
@@ -72,7 +76,7 @@ Examples:
|
|
|
72
76
|
`)
|
|
73
77
|
.option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
|
|
74
78
|
.option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
|
|
75
|
-
.option('--filter <expr>', 'Filter devices: "type=X", "name=X", "category=physical|ir", "room=X" (comma-separated key=value pairs)')
|
|
79
|
+
.option('--filter <expr>', 'Filter devices: "type=X", "name=X", "category=physical|ir", "room=X" (comma-separated key=value pairs)', stringArg('--filter'))
|
|
76
80
|
.action(async (options) => {
|
|
77
81
|
try {
|
|
78
82
|
const body = await fetchDeviceList();
|
|
@@ -195,8 +199,8 @@ Examples:
|
|
|
195
199
|
.command('status')
|
|
196
200
|
.description('Query the real-time status of a specific device')
|
|
197
201
|
.argument('[deviceId]', 'Device ID from "devices list" (or use --name or --ids)')
|
|
198
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
199
|
-
.option('--ids <list>', 'Comma-separated device IDs for batch status (incompatible with --name)')
|
|
202
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
203
|
+
.option('--ids <list>', 'Comma-separated device IDs for batch status (incompatible with --name)', stringArg('--ids'))
|
|
200
204
|
.addHelpText('after', `
|
|
201
205
|
Status fields vary by device type. To discover them without a live call:
|
|
202
206
|
|
|
@@ -287,10 +291,10 @@ Examples:
|
|
|
287
291
|
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
288
292
|
.argument('[cmd]', 'Command name, e.g. turnOn, turnOff, setColor, setBrightness, setAll, startClean')
|
|
289
293
|
.argument('[parameter]', 'Command parameter. Omit for commands like turnOn/turnOff (defaults to "default"). Format depends on the command (see below).')
|
|
290
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
291
|
-
.option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', 'command')
|
|
294
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
295
|
+
.option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
|
|
292
296
|
.option('--yes', 'Confirm a destructive command (Smart Lock unlock, Garage open, …). --dry-run is always allowed without --yes.')
|
|
293
|
-
.option('--idempotency-key <key>', 'Idempotency key for deduplication (60s window; same key replays cached result)')
|
|
297
|
+
.option('--idempotency-key <key>', 'Idempotency key for deduplication (60s window; same key replays cached result)', stringArg('--idempotency-key'))
|
|
294
298
|
.addHelpText('after', `
|
|
295
299
|
────────────────────────────────────────────────────────────────────────
|
|
296
300
|
For the full list of commands a specific device supports — and their
|
|
@@ -338,19 +342,22 @@ Examples:
|
|
|
338
342
|
`)
|
|
339
343
|
.action(async (deviceIdArg, cmdArg, parameter, options) => {
|
|
340
344
|
try {
|
|
341
|
-
// BUG-FIX: When --name is provided, Commander
|
|
342
|
-
//
|
|
345
|
+
// BUG-FIX: When --name is provided, Commander fills positionals left-to-right
|
|
346
|
+
// starting at [deviceId]. Shift them back to their semantic slots.
|
|
343
347
|
let cmd;
|
|
344
348
|
let effectiveDeviceIdArg;
|
|
345
349
|
if (options.name) {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
if (!deviceIdArg && !cmdArg) {
|
|
350
|
+
// `--name "x" <cmd> [parameter]` → Commander binds deviceIdArg=<cmd>, cmdArg=[parameter].
|
|
351
|
+
if (!deviceIdArg) {
|
|
350
352
|
throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
|
|
351
353
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
+
cmd = deviceIdArg;
|
|
355
|
+
if (cmdArg !== undefined) {
|
|
356
|
+
if (parameter !== undefined) {
|
|
357
|
+
throw new UsageError('Too many positional arguments after --name. Expected: --name <query> <cmd> [parameter].');
|
|
358
|
+
}
|
|
359
|
+
parameter = cmdArg;
|
|
360
|
+
}
|
|
354
361
|
effectiveDeviceIdArg = undefined;
|
|
355
362
|
}
|
|
356
363
|
else {
|
|
@@ -361,6 +368,9 @@ Examples:
|
|
|
361
368
|
effectiveDeviceIdArg = deviceIdArg;
|
|
362
369
|
}
|
|
363
370
|
const deviceId = resolveDeviceId(effectiveDeviceIdArg, options.name);
|
|
371
|
+
if (!getCachedDevice(deviceId)) {
|
|
372
|
+
console.error(`Note: device ${deviceId} is not in the local cache — run 'switchbot devices list' first to enable command validation.`);
|
|
373
|
+
}
|
|
364
374
|
const validation = validateCommand(deviceId, cmd, parameter, options.type);
|
|
365
375
|
if (!validation.ok) {
|
|
366
376
|
const err = validation.error;
|
|
@@ -385,6 +395,37 @@ Examples:
|
|
|
385
395
|
}
|
|
386
396
|
process.exit(2);
|
|
387
397
|
}
|
|
398
|
+
// Case-only mismatch: emit a warning and continue with the canonical name.
|
|
399
|
+
if (validation.caseNormalizedFrom && validation.normalized) {
|
|
400
|
+
console.error(`Note: '${validation.caseNormalizedFrom}' normalized to '${validation.normalized}' (case mismatch). Use exact casing to silence this warning.`);
|
|
401
|
+
cmd = validation.normalized;
|
|
402
|
+
}
|
|
403
|
+
else if (validation.normalized) {
|
|
404
|
+
cmd = validation.normalized;
|
|
405
|
+
}
|
|
406
|
+
// Raw-parameter validation (runs for known (deviceType, command) pairs only).
|
|
407
|
+
const cachedForParam = getCachedDevice(deviceId);
|
|
408
|
+
if (cachedForParam && options.type === 'command') {
|
|
409
|
+
const paramCheck = validateParameter(cachedForParam.type, cmd, parameter);
|
|
410
|
+
if (!paramCheck.ok) {
|
|
411
|
+
if (isJsonMode()) {
|
|
412
|
+
console.error(JSON.stringify({
|
|
413
|
+
error: {
|
|
414
|
+
code: 2,
|
|
415
|
+
kind: 'usage',
|
|
416
|
+
message: paramCheck.error,
|
|
417
|
+
context: { command: cmd, deviceType: cachedForParam.type, deviceId },
|
|
418
|
+
},
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.error(`Error: ${paramCheck.error}`);
|
|
423
|
+
}
|
|
424
|
+
process.exit(2);
|
|
425
|
+
}
|
|
426
|
+
if (paramCheck.normalized !== undefined)
|
|
427
|
+
parameter = paramCheck.normalized;
|
|
428
|
+
}
|
|
388
429
|
const cachedForGuard = getCachedDevice(deviceId);
|
|
389
430
|
if (!options.yes &&
|
|
390
431
|
!isDryRun() &&
|
|
@@ -539,7 +580,7 @@ Examples:
|
|
|
539
580
|
.command('describe')
|
|
540
581
|
.description('Describe a device by ID: metadata + supported commands + status fields (1 API call)')
|
|
541
582
|
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
542
|
-
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
583
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
543
584
|
.option('--live', 'Also fetch live status values and merge them into capabilities (costs 1 extra API call)')
|
|
544
585
|
.addHelpText('after', `
|
|
545
586
|
Makes a GET /v1.1/devices call to look up the device's type, then prints its
|
package/dist/commands/events.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
3
|
+
import { intArg, stringArg } from '../utils/arg-parsers.js';
|
|
3
4
|
import { SwitchBotMqttClient } from '../mqtt/client.js';
|
|
4
5
|
import { fetchMqttCredential } from '../mqtt/credential.js';
|
|
5
6
|
import { tryLoadConfig } from '../config.js';
|
|
@@ -118,10 +119,10 @@ export function registerEventsCommand(program) {
|
|
|
118
119
|
events
|
|
119
120
|
.command('tail')
|
|
120
121
|
.description('Run a local HTTP receiver and print incoming webhook events as JSONL')
|
|
121
|
-
.option('--port <n>', `Local port to listen on (default ${DEFAULT_PORT})`, String(DEFAULT_PORT))
|
|
122
|
-
.option('--path <p>', `HTTP path to match (default "${DEFAULT_PATH}"; use "*" for all paths)`, DEFAULT_PATH)
|
|
123
|
-
.option('--filter <expr>', 'Filter events, e.g. "deviceId=ABC123" or "type=Bot" (comma-separated)')
|
|
124
|
-
.option('--max <n>', 'Stop after N matching events (default: run until Ctrl-C)')
|
|
122
|
+
.option('--port <n>', `Local port to listen on (default ${DEFAULT_PORT})`, intArg('--port', { min: 1, max: 65535 }), String(DEFAULT_PORT))
|
|
123
|
+
.option('--path <p>', `HTTP path to match (default "${DEFAULT_PATH}"; use "*" for all paths)`, stringArg('--path'), DEFAULT_PATH)
|
|
124
|
+
.option('--filter <expr>', 'Filter events, e.g. "deviceId=ABC123" or "type=Bot" (comma-separated)', stringArg('--filter'))
|
|
125
|
+
.option('--max <n>', 'Stop after N matching events (default: run until Ctrl-C)', intArg('--max', { min: 1 }))
|
|
125
126
|
.addHelpText('after', `
|
|
126
127
|
SwitchBot posts events to a single webhook URL configured via:
|
|
127
128
|
$ switchbot webhook setup https://<your-public-host>/<path>
|
|
@@ -199,20 +200,20 @@ Examples:
|
|
|
199
200
|
events
|
|
200
201
|
.command('mqtt-tail')
|
|
201
202
|
.description('Subscribe to SwitchBot MQTT shadow events and stream them as JSONL')
|
|
202
|
-
.option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)')
|
|
203
|
-
.option('--max <n>', 'Stop after N events (default: run until Ctrl-C)')
|
|
203
|
+
.option('--topic <pattern>', 'MQTT topic filter (default: SwitchBot shadow topic from credential)', stringArg('--topic'))
|
|
204
|
+
.option('--max <n>', 'Stop after N events (default: run until Ctrl-C)', intArg('--max', { min: 1 }))
|
|
204
205
|
.option('--sink <type>', 'Output sink: stdout (default), file, webhook, openclaw, telegram, homeassistant (repeatable)', (val, prev) => [...prev, val], [])
|
|
205
|
-
.option('--sink-file <path>', 'File path for file sink')
|
|
206
|
-
.option('--webhook-url <url>', 'Webhook URL for webhook sink')
|
|
207
|
-
.option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)')
|
|
208
|
-
.option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)')
|
|
209
|
-
.option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to')
|
|
210
|
-
.option('--telegram-token <token>', 'Telegram bot token (or env TELEGRAM_TOKEN)')
|
|
211
|
-
.option('--telegram-chat <id>', 'Telegram chat/channel ID to send messages to')
|
|
212
|
-
.option('--ha-url <url>', 'Home Assistant base URL (e.g. http://homeassistant.local:8123)')
|
|
213
|
-
.option('--ha-token <token>', 'HA long-lived access token (for REST event API)')
|
|
214
|
-
.option('--ha-webhook-id <id>', 'HA webhook ID (no auth; takes priority over --ha-token)')
|
|
215
|
-
.option('--ha-event-type <type>', 'HA event type for REST API (default: switchbot_event)')
|
|
206
|
+
.option('--sink-file <path>', 'File path for file sink', stringArg('--sink-file'))
|
|
207
|
+
.option('--webhook-url <url>', 'Webhook URL for webhook sink', stringArg('--webhook-url'))
|
|
208
|
+
.option('--openclaw-url <url>', 'OpenClaw gateway URL (default: http://localhost:18789)', stringArg('--openclaw-url'))
|
|
209
|
+
.option('--openclaw-token <token>', 'Bearer token for OpenClaw (or env OPENCLAW_TOKEN)', stringArg('--openclaw-token'))
|
|
210
|
+
.option('--openclaw-model <id>', 'OpenClaw agent model ID to route events to', stringArg('--openclaw-model'))
|
|
211
|
+
.option('--telegram-token <token>', 'Telegram bot token (or env TELEGRAM_TOKEN)', stringArg('--telegram-token'))
|
|
212
|
+
.option('--telegram-chat <id>', 'Telegram chat/channel ID to send messages to', stringArg('--telegram-chat'))
|
|
213
|
+
.option('--ha-url <url>', 'Home Assistant base URL (e.g. http://homeassistant.local:8123)', stringArg('--ha-url'))
|
|
214
|
+
.option('--ha-token <token>', 'HA long-lived access token (for REST event API)', stringArg('--ha-token'))
|
|
215
|
+
.option('--ha-webhook-id <id>', 'HA webhook ID (no auth; takes priority over --ha-token)', stringArg('--ha-webhook-id'))
|
|
216
|
+
.option('--ha-event-type <type>', 'HA event type for REST API (default: switchbot_event)', stringArg('--ha-event-type'))
|
|
216
217
|
.addHelpText('after', `
|
|
217
218
|
Connects to the SwitchBot MQTT service using your existing credentials
|
|
218
219
|
(SWITCHBOT_TOKEN + SWITCHBOT_SECRET or ~/.switchbot/config.json).
|
|
@@ -343,10 +344,18 @@ Examples:
|
|
|
343
344
|
ac.abort();
|
|
344
345
|
}
|
|
345
346
|
});
|
|
347
|
+
let mqttFailed = false;
|
|
346
348
|
const unsubState = client.onStateChange((state) => {
|
|
347
349
|
if (!isJsonMode()) {
|
|
348
350
|
console.error(`[${new Date().toLocaleTimeString()}] MQTT state: ${state}`);
|
|
349
351
|
}
|
|
352
|
+
if (state === 'failed') {
|
|
353
|
+
mqttFailed = true;
|
|
354
|
+
if (!isJsonMode()) {
|
|
355
|
+
console.error('MQTT connection failed permanently (credential expired or reconnect exhausted) — exiting.');
|
|
356
|
+
}
|
|
357
|
+
ac.abort();
|
|
358
|
+
}
|
|
350
359
|
});
|
|
351
360
|
await client.connect();
|
|
352
361
|
client.subscribe(topic);
|
|
@@ -366,6 +375,10 @@ Examples:
|
|
|
366
375
|
process.once('SIGTERM', cleanup);
|
|
367
376
|
ac.signal.addEventListener('abort', cleanup, { once: true });
|
|
368
377
|
});
|
|
378
|
+
if (mqttFailed) {
|
|
379
|
+
// Surface as a runtime error so supervisors (pm2, systemd) can restart.
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
369
382
|
}
|
|
370
383
|
catch (error) {
|
|
371
384
|
handleError(error);
|