@switchbot/openapi-cli 2.6.0 → 2.6.2
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 -2
- package/dist/commands/batch.js +1 -1
- package/dist/commands/devices.js +2 -2
- package/dist/commands/expand.js +1 -1
- package/dist/commands/mcp.js +61 -6
- package/dist/commands/scenes.js +11 -0
- package/dist/commands/watch.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -238,7 +238,7 @@ switchbot devices status <deviceId>
|
|
|
238
238
|
switchbot devices status <deviceId> --json
|
|
239
239
|
|
|
240
240
|
# Resolve device by fuzzy name instead of ID (status, command, describe, expand, watch)
|
|
241
|
-
switchbot devices status --name "
|
|
241
|
+
switchbot devices status --name "Living Room AC"
|
|
242
242
|
switchbot devices command --name "Office Light" turnOn
|
|
243
243
|
switchbot devices describe --name "Kitchen Bot"
|
|
244
244
|
|
|
@@ -276,6 +276,9 @@ but each exposes its own key set:
|
|
|
276
276
|
Clauses are comma-separated and AND-ed. No OR across clauses — use regex
|
|
277
277
|
alternation (`=/A|B/`) for that. `category` is the one key that stays exact
|
|
278
278
|
under `=` / `!=` to preserve `category=physical` / `category!=ir` semantics.
|
|
279
|
+
A clause with an empty value (e.g. `name~`, `type=`) is rejected with exit 2 —
|
|
280
|
+
the parser refuses to guess whether an empty value means "no constraint" or
|
|
281
|
+
"match empty string". Drop the clause outright to remove the constraint.
|
|
279
282
|
|
|
280
283
|
#### Parameter formats
|
|
281
284
|
|
|
@@ -316,7 +319,7 @@ Some commands require a packed string like `"26,2,2,on"`. `devices expand` build
|
|
|
316
319
|
# Air Conditioner — setAll
|
|
317
320
|
switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
|
|
318
321
|
# Resolve by name
|
|
319
|
-
switchbot devices expand --name "
|
|
322
|
+
switchbot devices expand --name "Living Room AC" setAll --temp 26 --mode cool --fan low --power on
|
|
320
323
|
|
|
321
324
|
# Curtain / Roller Shade — setPosition
|
|
322
325
|
switchbot devices expand <curtainId> setPosition --position 50 --mode silent
|
package/dist/commands/batch.js
CHANGED
|
@@ -137,7 +137,7 @@ Safety:
|
|
|
137
137
|
hitting the API.
|
|
138
138
|
|
|
139
139
|
Examples:
|
|
140
|
-
$ switchbot devices batch turnOff --filter 'type~=Light,family
|
|
140
|
+
$ switchbot devices batch turnOff --filter 'type~=Light,family=home'
|
|
141
141
|
$ switchbot devices batch turnOn --ids ID1,ID2,ID3
|
|
142
142
|
$ switchbot devices list --format=id --filter 'type=Bot' | switchbot devices batch toggle -
|
|
143
143
|
$ switchbot devices batch unlock --filter 'type=Smart Lock' --yes
|
package/dist/commands/devices.js
CHANGED
|
@@ -70,7 +70,7 @@ Examples:
|
|
|
70
70
|
$ switchbot devices list
|
|
71
71
|
$ switchbot devices list --wide
|
|
72
72
|
$ switchbot devices list --format tsv --fields deviceId,deviceName,type,category
|
|
73
|
-
$ switchbot devices list --json | jq '.deviceList[] | select(.familyName == "
|
|
73
|
+
$ switchbot devices list --json | jq '.deviceList[] | select(.familyName == "home")'
|
|
74
74
|
$ switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
|
|
75
75
|
$ switchbot devices list --filter type="Air Conditioner"
|
|
76
76
|
$ switchbot devices list --filter category=ir
|
|
@@ -215,7 +215,7 @@ all field names returned by your specific device, then narrow with --fields.
|
|
|
215
215
|
|
|
216
216
|
Examples:
|
|
217
217
|
$ switchbot devices status ABC123DEF456
|
|
218
|
-
$ switchbot devices status --name "
|
|
218
|
+
$ switchbot devices status --name "Living Room AC"
|
|
219
219
|
$ switchbot devices status ABC123DEF456 --json
|
|
220
220
|
$ switchbot devices status ABC123DEF456 --format yaml
|
|
221
221
|
$ switchbot devices status ABC123DEF456 --format tsv --fields power,battery
|
package/dist/commands/expand.js
CHANGED
|
@@ -50,7 +50,7 @@ Examples:
|
|
|
50
50
|
$ switchbot devices expand <blindId> setPosition --direction up --angle 50
|
|
51
51
|
$ switchbot devices expand <relayId> setMode --channel 1 --mode edge
|
|
52
52
|
$ switchbot devices expand <acId> setAll --temp 22 --mode heat --fan auto --power on --dry-run
|
|
53
|
-
$ switchbot devices expand --name "
|
|
53
|
+
$ switchbot devices expand --name "Living Room AC" setAll --temp 26 --mode cool --fan low --power on
|
|
54
54
|
`)
|
|
55
55
|
.action(async (deviceIdArg, commandArg, options) => {
|
|
56
56
|
let deviceId = '';
|
package/dist/commands/mcp.js
CHANGED
|
@@ -9,6 +9,7 @@ import { fetchDeviceList, fetchDeviceStatus, executeCommand, describeDevice, val
|
|
|
9
9
|
import { fetchScenes, executeScene } from '../lib/scenes.js';
|
|
10
10
|
import { findCatalogEntry } from '../devices/catalog.js';
|
|
11
11
|
import { getCachedDevice } from '../devices/cache.js';
|
|
12
|
+
import { validateParameter } from '../devices/param-validator.js';
|
|
12
13
|
import { EventSubscriptionManager } from '../mcp/events-subscription.js';
|
|
13
14
|
import { deviceHistoryStore } from '../mcp/device-history.js';
|
|
14
15
|
import { queryDeviceHistory } from '../devices/history-query.js';
|
|
@@ -273,6 +274,10 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
|
|
|
273
274
|
},
|
|
274
275
|
}, async ({ deviceId, command, parameter, commandType, confirm, idempotencyKey, dryRun }) => {
|
|
275
276
|
const effectiveType = commandType ?? 'command';
|
|
277
|
+
let effectiveParameter = parameter;
|
|
278
|
+
// stringifiedParam mirrors the CLI form that validateCommand /
|
|
279
|
+
// validateParameter expect — B-1 runs on the string representation.
|
|
280
|
+
const stringifiedParam = parameter === undefined ? undefined : typeof parameter === 'string' ? parameter : JSON.stringify(parameter);
|
|
276
281
|
// dryRun early-return — no API call. We still preflight the deviceId
|
|
277
282
|
// against the local cache so fabricated IDs don't silently pass
|
|
278
283
|
// validation (bug #SYS-3). Dry-run is meant to catch bad inputs; a
|
|
@@ -286,10 +291,31 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
|
|
|
286
291
|
context: { deviceId },
|
|
287
292
|
});
|
|
288
293
|
}
|
|
294
|
+
// R-2: run B-1 param validation in dry-run too, so dry-run doesn't
|
|
295
|
+
// falsely accept inputs the live API would reject.
|
|
296
|
+
if (effectiveType !== 'customize') {
|
|
297
|
+
const pv = validateParameter(cached.type, command, stringifiedParam);
|
|
298
|
+
if (!pv.ok) {
|
|
299
|
+
return mcpError('usage', 2, pv.error, {
|
|
300
|
+
hint: 'Dry-run rejected the parameter client-side; the API would reject it too.',
|
|
301
|
+
context: { deviceType: cached.type, command, parameter: stringifiedParam },
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (pv.normalized !== undefined) {
|
|
305
|
+
effectiveParameter = pv.normalized;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const cmdVal = validateCommand(deviceId, command, stringifiedParam, effectiveType);
|
|
309
|
+
if (!cmdVal.ok) {
|
|
310
|
+
return mcpError('usage', 2, cmdVal.error.message, {
|
|
311
|
+
hint: cmdVal.error.hint,
|
|
312
|
+
context: { validationKind: cmdVal.error.kind },
|
|
313
|
+
});
|
|
314
|
+
}
|
|
289
315
|
const wouldSend = {
|
|
290
316
|
deviceId,
|
|
291
317
|
command,
|
|
292
|
-
parameter:
|
|
318
|
+
parameter: effectiveParameter ?? 'default',
|
|
293
319
|
commandType: effectiveType,
|
|
294
320
|
};
|
|
295
321
|
const structured = { ok: true, dryRun: true, wouldSend };
|
|
@@ -332,16 +358,30 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
|
|
|
332
358
|
},
|
|
333
359
|
});
|
|
334
360
|
}
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
361
|
+
// validateCommand covers command existence + required/unexpected-parameter.
|
|
362
|
+
// stringifiedParam was computed once at the top of the handler so dry-run
|
|
363
|
+
// and live paths share the same shape.
|
|
338
364
|
const validation = validateCommand(deviceId, command, stringifiedParam, effectiveType);
|
|
339
365
|
if (!validation.ok) {
|
|
340
366
|
return mcpError('usage', 2, validation.error.message, { hint: validation.error.hint, context: { validationKind: validation.error.kind } });
|
|
341
367
|
}
|
|
368
|
+
// R-2: run B-1 client-side parameter validator (range/format checks).
|
|
369
|
+
// Customize commands (user-defined IR buttons) opt out — the catalog
|
|
370
|
+
// cannot know their expected shape.
|
|
371
|
+
if (effectiveType !== 'customize') {
|
|
372
|
+
const pv = validateParameter(typeName, command, stringifiedParam);
|
|
373
|
+
if (!pv.ok) {
|
|
374
|
+
return mcpError('usage', 2, pv.error, {
|
|
375
|
+
context: { deviceType: typeName, command, parameter: stringifiedParam, validationKind: 'param-out-of-range' },
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (pv.normalized !== undefined) {
|
|
379
|
+
effectiveParameter = pv.normalized;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
342
382
|
let result;
|
|
343
383
|
try {
|
|
344
|
-
result = await executeCommand(deviceId, command,
|
|
384
|
+
result = await executeCommand(deviceId, command, effectiveParameter, effectiveType, undefined, {
|
|
345
385
|
idempotencyKey,
|
|
346
386
|
});
|
|
347
387
|
}
|
|
@@ -393,7 +433,22 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
|
|
|
393
433
|
},
|
|
394
434
|
}, async ({ sceneId, dryRun }) => {
|
|
395
435
|
if (dryRun) {
|
|
396
|
-
|
|
436
|
+
let scenes = [];
|
|
437
|
+
try {
|
|
438
|
+
scenes = await fetchScenes();
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
// network failure — degrade gracefully, skip validation
|
|
442
|
+
}
|
|
443
|
+
const found = scenes.find((s) => s.sceneId === sceneId);
|
|
444
|
+
if (scenes.length > 0 && !found) {
|
|
445
|
+
return mcpError('usage', 2, `Scene not found: ${sceneId}`, {
|
|
446
|
+
subKind: 'scene-not-found',
|
|
447
|
+
hint: "Check the sceneId with 'list_scenes' (IDs are case-sensitive).",
|
|
448
|
+
context: { sceneId, candidates: scenes.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })).slice(0, 5) },
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
const wouldSend = { sceneId, sceneName: found?.sceneName ?? null };
|
|
397
452
|
const structured = { ok: true, dryRun: true, wouldSend };
|
|
398
453
|
return {
|
|
399
454
|
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
package/dist/commands/scenes.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { printJson, isJsonMode, handleError, StructuredUsageError } from '../utils/output.js';
|
|
2
2
|
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
3
|
import { fetchScenes, executeScene } from '../lib/scenes.js';
|
|
4
|
+
import { isDryRun } from '../utils/flags.js';
|
|
4
5
|
export function registerScenesCommand(program) {
|
|
5
6
|
const scenes = program
|
|
6
7
|
.command('scenes')
|
|
@@ -56,6 +57,16 @@ Example:
|
|
|
56
57
|
candidates: sceneList.map((s) => ({ sceneId: s.sceneId, sceneName: s.sceneName })),
|
|
57
58
|
});
|
|
58
59
|
}
|
|
60
|
+
if (isDryRun()) {
|
|
61
|
+
const wouldSend = { method: 'POST', url: `/v1.1/scenes/${sceneId}/execute`, sceneId, sceneName: found.sceneName };
|
|
62
|
+
if (isJsonMode()) {
|
|
63
|
+
printJson({ dryRun: true, wouldSend });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(`[dry-run] Would POST /v1.1/scenes/${sceneId}/execute (${found.sceneName})`);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
59
70
|
await executeScene(sceneId);
|
|
60
71
|
if (isJsonMode()) {
|
|
61
72
|
printJson({ ok: true, sceneId });
|
package/dist/commands/watch.js
CHANGED
|
@@ -75,7 +75,7 @@ Examples:
|
|
|
75
75
|
$ switchbot devices watch ABC123 --fields battery,power --interval 1m
|
|
76
76
|
$ switchbot devices watch ABC123 DEF456 --interval 30s --max 10
|
|
77
77
|
$ switchbot devices watch ABC123 --json | jq 'select(.changed.power)'
|
|
78
|
-
$ switchbot devices watch --name "
|
|
78
|
+
$ switchbot devices watch --name "Living Room AC" --interval 10s
|
|
79
79
|
`)
|
|
80
80
|
.action(async (deviceIds, options) => {
|
|
81
81
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@switchbot/openapi-cli",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"description": "SwitchBot smart home CLI — control devices, run scenes, stream real-time events, and integrate AI agents via MCP. Full API v1.1 coverage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"switchbot",
|