@switchbot/openapi-cli 2.6.0 → 2.6.1

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 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 "客厅空调" setAll --temp 26 --mode cool --fan low --power on
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
@@ -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
@@ -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
@@ -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 "客厅空调" setAll --temp 26 --mode cool --fan low --power on
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 = '';
@@ -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,24 @@ 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
+ }
289
308
  const wouldSend = {
290
309
  deviceId,
291
310
  command,
292
- parameter: parameter ?? 'default',
311
+ parameter: effectiveParameter ?? 'default',
293
312
  commandType: effectiveType,
294
313
  };
295
314
  const structured = { ok: true, dryRun: true, wouldSend };
@@ -332,16 +351,30 @@ API docs: https://github.com/OpenWonderLabs/SwitchBotAPI`,
332
351
  },
333
352
  });
334
353
  }
335
- // stringifiedParam is what validateCommand expects to decide
336
- // "no-parameter" conflicts mirror the CLI behavior.
337
- const stringifiedParam = parameter === undefined ? undefined : typeof parameter === 'string' ? parameter : JSON.stringify(parameter);
354
+ // validateCommand covers command existence + required/unexpected-parameter.
355
+ // stringifiedParam was computed once at the top of the handler so dry-run
356
+ // and live paths share the same shape.
338
357
  const validation = validateCommand(deviceId, command, stringifiedParam, effectiveType);
339
358
  if (!validation.ok) {
340
359
  return mcpError('usage', 2, validation.error.message, { hint: validation.error.hint, context: { validationKind: validation.error.kind } });
341
360
  }
361
+ // R-2: run B-1 client-side parameter validator (range/format checks).
362
+ // Customize commands (user-defined IR buttons) opt out — the catalog
363
+ // cannot know their expected shape.
364
+ if (effectiveType !== 'customize') {
365
+ const pv = validateParameter(typeName, command, stringifiedParam);
366
+ if (!pv.ok) {
367
+ return mcpError('usage', 2, pv.error, {
368
+ context: { deviceType: typeName, command, parameter: stringifiedParam, validationKind: 'param-out-of-range' },
369
+ });
370
+ }
371
+ if (pv.normalized !== undefined) {
372
+ effectiveParameter = pv.normalized;
373
+ }
374
+ }
342
375
  let result;
343
376
  try {
344
- result = await executeCommand(deviceId, command, parameter, effectiveType, undefined, {
377
+ result = await executeCommand(deviceId, command, effectiveParameter, effectiveType, undefined, {
345
378
  idempotencyKey,
346
379
  });
347
380
  }
@@ -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 "客厅空调" --interval 10s
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.0",
3
+ "version": "2.6.1",
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",