@switchbot/openapi-cli 2.6.4 → 2.7.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 +2 -2
- package/dist/api/client.js +13 -12
- package/dist/commands/agent-bootstrap.js +21 -15
- package/dist/commands/batch.js +26 -21
- package/dist/commands/capabilities.js +29 -21
- package/dist/commands/catalog.js +4 -3
- package/dist/commands/config.js +27 -37
- package/dist/commands/devices.js +64 -37
- package/dist/commands/doctor.js +355 -19
- package/dist/commands/events.js +112 -23
- package/dist/commands/expand.js +7 -15
- package/dist/commands/explain.js +10 -6
- package/dist/commands/history.js +12 -18
- package/dist/commands/identity.js +59 -0
- package/dist/commands/mcp.js +100 -13
- package/dist/commands/plan.js +1 -1
- package/dist/commands/schema.js +22 -12
- package/dist/commands/watch.js +15 -2
- package/dist/devices/catalog.js +124 -11
- package/dist/devices/resources.js +270 -0
- package/dist/index.js +16 -3
- package/dist/lib/devices.js +16 -5
- package/dist/schema/field-aliases.js +95 -0
- package/dist/utils/help-json.js +54 -0
- package/dist/utils/output.js +17 -0
- package/package.json +1 -1
package/dist/commands/watch.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
1
|
+
import { printJson, isJsonMode, handleError, UsageError, emitStreamHeader } from '../utils/output.js';
|
|
2
2
|
import { fetchDeviceStatus } from '../lib/devices.js';
|
|
3
3
|
import { getCachedDevice } from '../devices/cache.js';
|
|
4
4
|
import { parseDurationToMs, getFields } from '../utils/flags.js';
|
|
5
5
|
import { intArg, durationArg, stringArg } from '../utils/arg-parsers.js';
|
|
6
6
|
import { createClient } from '../api/client.js';
|
|
7
7
|
import { resolveDeviceId } from '../utils/name-resolver.js';
|
|
8
|
+
import { resolveFieldList, listAllCanonical } from '../schema/field-aliases.js';
|
|
8
9
|
const DEFAULT_INTERVAL_MS = 30_000;
|
|
9
10
|
const MIN_INTERVAL_MS = 1_000;
|
|
10
11
|
function diff(prev, next, fields) {
|
|
@@ -101,7 +102,15 @@ Examples:
|
|
|
101
102
|
maxTicks = Math.floor(n);
|
|
102
103
|
}
|
|
103
104
|
const forMs = options.for ? parseDurationToMs(options.for) : null;
|
|
104
|
-
const
|
|
105
|
+
const rawFields = getFields() ?? null;
|
|
106
|
+
// Resolve aliases upfront against the static canonical registry.
|
|
107
|
+
// Validating here lets UsageError exit the command before any
|
|
108
|
+
// polling starts, and keeps mid-loop error handling free of
|
|
109
|
+
// "misuse" concerns. Unknown fields that are not registered as
|
|
110
|
+
// aliases but happen to match an API key pass through unchanged.
|
|
111
|
+
const fields = rawFields
|
|
112
|
+
? resolveFieldList(rawFields, listAllCanonical())
|
|
113
|
+
: null;
|
|
105
114
|
const ac = new AbortController();
|
|
106
115
|
const onSig = () => ac.abort();
|
|
107
116
|
process.on('SIGINT', onSig);
|
|
@@ -109,6 +118,10 @@ Examples:
|
|
|
109
118
|
const forTimer = forMs !== null && forMs > 0
|
|
110
119
|
? setTimeout(() => ac.abort(), forMs)
|
|
111
120
|
: null;
|
|
121
|
+
// P7: streaming JSON contract — first line under --json is the
|
|
122
|
+
// stream header so consumers can route by eventKind/cadence.
|
|
123
|
+
if (isJsonMode())
|
|
124
|
+
emitStreamHeader({ eventKind: 'tick', cadence: 'poll' });
|
|
112
125
|
try {
|
|
113
126
|
const prev = new Map();
|
|
114
127
|
const client = createClient();
|
package/dist/devices/catalog.js
CHANGED
|
@@ -7,14 +7,99 @@
|
|
|
7
7
|
* - CommandSpec.idempotent: repeat-safe — calling it N times ends in the
|
|
8
8
|
* same state as calling it once (turnOn, setBrightness 50). Agents can
|
|
9
9
|
* retry these freely. Counter-examples: toggle, press, volumeAdd.
|
|
10
|
-
* - CommandSpec.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - CommandSpec.safetyTier: explicit action safety classification. See
|
|
11
|
+
* SafetyTier for the 5-tier enum. Built-in entries set this on the
|
|
12
|
+
* destructive tier; other tiers are derived (see deriveSafetyTier).
|
|
13
|
+
* - CommandSpec.destructive (deprecated, v3.0 removal): legacy boolean
|
|
14
|
+
* that maps to safetyTier === 'destructive'. Still accepted in
|
|
15
|
+
* ~/.switchbot/catalog.json overlays and derived into safetyTier.
|
|
13
16
|
* - DeviceCatalogEntry.role: functional grouping for filter/search
|
|
14
17
|
* ("all lighting", "all security"). Does not affect API behavior.
|
|
15
18
|
* - DeviceCatalogEntry.readOnly: the device has no control commands; it
|
|
16
19
|
* can only be queried via 'devices status'.
|
|
17
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* Catalog shape version. Bump when any of the exported interfaces
|
|
23
|
+
* (CommandSpec / DeviceCatalogEntry / SafetyTier) gain/lose/rename a
|
|
24
|
+
* load-bearing field. The agent-bootstrap payload's schemaVersion must
|
|
25
|
+
* stay pinned to this value; `doctor` fails the `catalog-schema` check
|
|
26
|
+
* when they drift.
|
|
27
|
+
*/
|
|
28
|
+
export const CATALOG_SCHEMA_VERSION = '1.0';
|
|
29
|
+
/**
|
|
30
|
+
* Human-readable descriptions for common status fields. Populated from
|
|
31
|
+
* the SwitchBot API v1.1 docs. Used by deriveStatusQueries() so every
|
|
32
|
+
* query has a meaningful description even when the entry itself only
|
|
33
|
+
* declares the field name.
|
|
34
|
+
*/
|
|
35
|
+
const STATUS_FIELD_DESCRIPTIONS = {
|
|
36
|
+
power: 'Power state (on/off)',
|
|
37
|
+
battery: 'Battery percentage (0-100)',
|
|
38
|
+
version: 'Firmware version string',
|
|
39
|
+
temperature: 'Ambient temperature (°C)',
|
|
40
|
+
humidity: 'Ambient humidity (% RH)',
|
|
41
|
+
CO2: 'CO2 concentration (ppm)',
|
|
42
|
+
brightness: 'Current brightness (0-100)',
|
|
43
|
+
color: 'Current RGB color (r:g:b)',
|
|
44
|
+
colorTemperature: 'Color temperature in Kelvin',
|
|
45
|
+
mode: 'Operating mode',
|
|
46
|
+
deviceMode: 'Hardware mode (Bot-specific)',
|
|
47
|
+
lockState: 'Lock state (locked/unlocked)',
|
|
48
|
+
doorState: 'Door contact state (open/closed)',
|
|
49
|
+
calibrate: 'Calibration status',
|
|
50
|
+
moving: 'Motion in progress (boolean)',
|
|
51
|
+
slidePosition: 'Slide position (0-100)',
|
|
52
|
+
group: 'Multi-device group membership',
|
|
53
|
+
direction: 'Tilt direction',
|
|
54
|
+
voltage: 'Line voltage',
|
|
55
|
+
electricCurrent: 'Instantaneous current draw',
|
|
56
|
+
electricityOfDay: 'kWh consumed today',
|
|
57
|
+
usedElectricity: 'Cumulative kWh',
|
|
58
|
+
useTime: 'Total runtime (seconds)',
|
|
59
|
+
weight: 'Load / weight reading',
|
|
60
|
+
switchStatus: 'Relay state (integer encoded)',
|
|
61
|
+
switch1Status: 'Channel 1 relay state',
|
|
62
|
+
switch2Status: 'Channel 2 relay state',
|
|
63
|
+
workingStatus: 'Device working status (vacuum/purifier)',
|
|
64
|
+
onlineStatus: 'Online / offline (string)',
|
|
65
|
+
online: 'Online / offline (boolean or int)',
|
|
66
|
+
taskType: 'Current task identifier',
|
|
67
|
+
nightStatus: 'Night-mode status',
|
|
68
|
+
oscillation: 'Horizontal oscillation on/off',
|
|
69
|
+
verticalOscillation: 'Vertical oscillation on/off',
|
|
70
|
+
chargingStatus: 'Charging (boolean)',
|
|
71
|
+
fanSpeed: 'Current fan speed level',
|
|
72
|
+
nebulizationEfficiency: 'Humidifier mist level',
|
|
73
|
+
childLock: 'Child-lock engaged',
|
|
74
|
+
sound: 'Beep / audio feedback enabled',
|
|
75
|
+
lackWater: 'Water tank low (boolean)',
|
|
76
|
+
filterElement: 'Filter life remaining',
|
|
77
|
+
auto: 'Auto mode enabled',
|
|
78
|
+
targetTemperature: 'Thermostat target temperature',
|
|
79
|
+
moveDetected: 'Motion detected (boolean)',
|
|
80
|
+
openState: 'Contact sensor open/closed',
|
|
81
|
+
status: 'Device-specific status word',
|
|
82
|
+
lightLevel: 'Ambient light level',
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* P11: derive the read-only query list for an entry. If the entry has
|
|
86
|
+
* explicit `statusQueries`, return them as-is; otherwise synthesize one
|
|
87
|
+
* ReadOnlyQuerySpec per `statusFields` entry, all keyed to the `status`
|
|
88
|
+
* endpoint. IR-category entries have no status channel so return [].
|
|
89
|
+
*/
|
|
90
|
+
export function deriveStatusQueries(entry) {
|
|
91
|
+
if (entry.statusQueries && entry.statusQueries.length > 0)
|
|
92
|
+
return entry.statusQueries;
|
|
93
|
+
if (entry.category === 'ir')
|
|
94
|
+
return [];
|
|
95
|
+
const fields = entry.statusFields ?? [];
|
|
96
|
+
return fields.map((f) => ({
|
|
97
|
+
field: f,
|
|
98
|
+
description: STATUS_FIELD_DESCRIPTIONS[f] ?? `${f} (see API docs)`,
|
|
99
|
+
endpoint: 'status',
|
|
100
|
+
safetyTier: 'read',
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
18
103
|
// ---- Command fragments (reused across entries) -------------------------
|
|
19
104
|
const onOff = [
|
|
20
105
|
{ command: 'turnOn', parameter: '—', description: 'Power on', idempotent: true },
|
|
@@ -64,7 +149,7 @@ export const DEVICE_CATALOG = [
|
|
|
64
149
|
aliases: ['Smart Lock Pro'],
|
|
65
150
|
commands: [
|
|
66
151
|
{ command: 'lock', parameter: '—', description: 'Lock the door', idempotent: true },
|
|
67
|
-
{ command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true,
|
|
152
|
+
{ command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true, safetyTier: 'destructive', safetyReason: 'Physically unlocks the door — anyone nearby can open it.' },
|
|
68
153
|
{ command: 'deadbolt', parameter: '—', description: 'Pro only: engage deadbolt', idempotent: true },
|
|
69
154
|
],
|
|
70
155
|
statusFields: ['battery', 'version', 'lockState', 'doorState', 'calibrate'],
|
|
@@ -76,7 +161,7 @@ export const DEVICE_CATALOG = [
|
|
|
76
161
|
role: 'security',
|
|
77
162
|
commands: [
|
|
78
163
|
{ command: 'lock', parameter: '—', description: 'Lock the door', idempotent: true },
|
|
79
|
-
{ command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true,
|
|
164
|
+
{ command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true, safetyTier: 'destructive', safetyReason: 'Physically unlocks the door — anyone nearby can open it.' },
|
|
80
165
|
],
|
|
81
166
|
statusFields: ['battery', 'version', 'lockState', 'doorState', 'calibrate'],
|
|
82
167
|
},
|
|
@@ -87,7 +172,7 @@ export const DEVICE_CATALOG = [
|
|
|
87
172
|
role: 'security',
|
|
88
173
|
commands: [
|
|
89
174
|
{ command: 'lock', parameter: '—', description: 'Lock the door', idempotent: true },
|
|
90
|
-
{ command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true,
|
|
175
|
+
{ command: 'unlock', parameter: '—', description: 'Unlock the door', idempotent: true, safetyTier: 'destructive', safetyReason: 'Physically unlocks the door — anyone nearby can open it.' },
|
|
91
176
|
{ command: 'deadbolt', parameter: '—', description: 'Engage deadbolt', idempotent: true },
|
|
92
177
|
],
|
|
93
178
|
statusFields: ['battery', 'version', 'lockState', 'doorState', 'calibrate'],
|
|
@@ -306,8 +391,8 @@ export const DEVICE_CATALOG = [
|
|
|
306
391
|
description: 'Cloud-connected garage door controller; turnOn opens and turnOff closes the door.',
|
|
307
392
|
role: 'security',
|
|
308
393
|
commands: [
|
|
309
|
-
{ command: 'turnOn', parameter: '—', description: 'Open the garage door', idempotent: true,
|
|
310
|
-
{ command: 'turnOff', parameter: '—', description: 'Close the garage door', idempotent: true,
|
|
394
|
+
{ command: 'turnOn', parameter: '—', description: 'Open the garage door', idempotent: true, safetyTier: 'destructive', safetyReason: 'Opens the garage door — anyone nearby can enter the space.' },
|
|
395
|
+
{ command: 'turnOff', parameter: '—', description: 'Close the garage door', idempotent: true, safetyTier: 'destructive', safetyReason: 'Closes the garage door — verify no person or obstacle is in the way.' },
|
|
311
396
|
],
|
|
312
397
|
statusFields: ['switchStatus', 'version', 'online'],
|
|
313
398
|
},
|
|
@@ -329,8 +414,8 @@ export const DEVICE_CATALOG = [
|
|
|
329
414
|
role: 'security',
|
|
330
415
|
aliases: ['Keypad Touch'],
|
|
331
416
|
commands: [
|
|
332
|
-
{ command: 'createKey', parameter: '\'{"name":"...","type":"permanent|timeLimit|disposable|urgent","password":"6-12 digits","startTime":<s>,"endTime":<s>}\'', description: 'Create a passcode (async; result via webhook)', idempotent: false,
|
|
333
|
-
{ command: 'deleteKey', parameter: '\'{"id":<passcode_id>}\'', description: 'Delete a passcode (async; result via webhook)', idempotent: true,
|
|
417
|
+
{ command: 'createKey', parameter: '\'{"name":"...","type":"permanent|timeLimit|disposable|urgent","password":"6-12 digits","startTime":<s>,"endTime":<s>}\'', description: 'Create a passcode (async; result via webhook)', idempotent: false, safetyTier: 'destructive', safetyReason: 'Provisions a new access credential — anyone with this passcode can unlock the door.' },
|
|
418
|
+
{ command: 'deleteKey', parameter: '\'{"id":<passcode_id>}\'', description: 'Delete a passcode (async; result via webhook)', idempotent: true, safetyTier: 'destructive', safetyReason: 'Permanently removes a passcode — the holder immediately loses door access.' },
|
|
334
419
|
],
|
|
335
420
|
statusFields: ['version'],
|
|
336
421
|
},
|
|
@@ -538,13 +623,41 @@ export function findCatalogEntry(query) {
|
|
|
538
623
|
return matches[0];
|
|
539
624
|
return matches;
|
|
540
625
|
}
|
|
626
|
+
/**
|
|
627
|
+
* Derive the safety tier for a catalog command, honouring an explicit
|
|
628
|
+
* `safetyTier` when present and falling back to heuristic inference.
|
|
629
|
+
*
|
|
630
|
+
* The inference order is:
|
|
631
|
+
* 1. Explicit `spec.safetyTier`.
|
|
632
|
+
* 2. Legacy `spec.destructive: true` → `'destructive'` (overlay compat).
|
|
633
|
+
* 3. IR context (customize command OR entry.category === 'ir')
|
|
634
|
+
* → `'ir-fire-forget'`.
|
|
635
|
+
* 4. Default → `'mutation'`.
|
|
636
|
+
*/
|
|
637
|
+
export function deriveSafetyTier(spec, entry) {
|
|
638
|
+
if (spec.safetyTier)
|
|
639
|
+
return spec.safetyTier;
|
|
640
|
+
if (spec.destructive)
|
|
641
|
+
return 'destructive';
|
|
642
|
+
if (spec.commandType === 'customize')
|
|
643
|
+
return 'ir-fire-forget';
|
|
644
|
+
if (entry?.category === 'ir')
|
|
645
|
+
return 'ir-fire-forget';
|
|
646
|
+
return 'mutation';
|
|
647
|
+
}
|
|
648
|
+
/** Read the safety reason for a command, with fallback to the legacy field. */
|
|
649
|
+
export function getCommandSafetyReason(spec) {
|
|
650
|
+
return spec.safetyReason ?? spec.destructiveReason ?? null;
|
|
651
|
+
}
|
|
541
652
|
/**
|
|
542
653
|
* Pick up to 3 non-destructive, idempotent commands an agent can safely invoke
|
|
543
654
|
* to explore or exercise a device. Used by `devices describe --json` to hint
|
|
544
655
|
* at concrete next steps.
|
|
545
656
|
*/
|
|
546
657
|
export function suggestedActions(entry) {
|
|
547
|
-
const safe = entry.commands.filter((c) => c.idempotent === true &&
|
|
658
|
+
const safe = entry.commands.filter((c) => c.idempotent === true &&
|
|
659
|
+
deriveSafetyTier(c, entry) !== 'destructive' &&
|
|
660
|
+
c.commandType !== 'customize');
|
|
548
661
|
const picks = [];
|
|
549
662
|
const seen = new Set();
|
|
550
663
|
for (const c of safe) {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative metadata for non-device resources exposed by the SwitchBot API:
|
|
3
|
+
* scenes, webhooks, and keypad credentials ("keys").
|
|
4
|
+
*
|
|
5
|
+
* Consumed by `capabilities --json` and `schema export` so AI agents can
|
|
6
|
+
* discover these surfaces the same way they discover device commands.
|
|
7
|
+
*
|
|
8
|
+
* Scope:
|
|
9
|
+
* - Descriptive metadata only (no runtime execution — CLI/MCP handlers stay
|
|
10
|
+
* source-of-truth for behavior).
|
|
11
|
+
* - Webhook event list is derived from the device catalog and is advisory —
|
|
12
|
+
* not every SwitchBot device actually pushes every listed event; refer to
|
|
13
|
+
* the SwitchBot webhook docs for authoritative shapes.
|
|
14
|
+
*/
|
|
15
|
+
const COMMON_WEBHOOK_FIELDS = [
|
|
16
|
+
{ name: 'deviceType', type: 'string', description: 'SwitchBot device type string', example: 'WoMeter' },
|
|
17
|
+
{ name: 'deviceMac', type: 'string', description: 'Bluetooth MAC address (uppercase, colon-separated)', example: 'AA:BB:CC:11:22:33' },
|
|
18
|
+
{ name: 'timeOfSample', type: 'timestamp', description: 'Millisecond Unix timestamp when the sample was taken', example: 1700000000000 },
|
|
19
|
+
];
|
|
20
|
+
export const RESOURCE_CATALOG = {
|
|
21
|
+
scenes: {
|
|
22
|
+
description: 'Manual scenes (IFTTT-style rules) authored in the SwitchBot app. Execution is fire-and-forget from the cloud — side-effects happen on the user\'s devices.',
|
|
23
|
+
operations: [
|
|
24
|
+
{
|
|
25
|
+
verb: 'list',
|
|
26
|
+
method: 'GET',
|
|
27
|
+
endpoint: '/v1.1/scenes',
|
|
28
|
+
params: [],
|
|
29
|
+
safetyTier: 'read',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
verb: 'execute',
|
|
33
|
+
method: 'POST',
|
|
34
|
+
endpoint: '/v1.1/scenes/{sceneId}/execute',
|
|
35
|
+
params: [{ name: 'sceneId', required: true, type: 'string' }],
|
|
36
|
+
safetyTier: 'mutation',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
verb: 'describe',
|
|
40
|
+
method: 'GET',
|
|
41
|
+
endpoint: '/v1.1/scenes/{sceneId}',
|
|
42
|
+
params: [{ name: 'sceneId', required: true, type: 'string' }],
|
|
43
|
+
safetyTier: 'read',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
webhooks: {
|
|
48
|
+
endpoints: [
|
|
49
|
+
{
|
|
50
|
+
verb: 'setup',
|
|
51
|
+
method: 'POST',
|
|
52
|
+
path: '/v1.1/webhook/setupWebhook',
|
|
53
|
+
safetyTier: 'mutation',
|
|
54
|
+
requiredParams: ['url'],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
verb: 'query',
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: '/v1.1/webhook/queryWebhook',
|
|
60
|
+
safetyTier: 'read',
|
|
61
|
+
requiredParams: ['action'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
verb: 'update',
|
|
65
|
+
method: 'POST',
|
|
66
|
+
path: '/v1.1/webhook/updateWebhook',
|
|
67
|
+
safetyTier: 'mutation',
|
|
68
|
+
requiredParams: ['url', 'enable'],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
verb: 'delete',
|
|
72
|
+
method: 'POST',
|
|
73
|
+
path: '/v1.1/webhook/deleteWebhook',
|
|
74
|
+
safetyTier: 'destructive',
|
|
75
|
+
requiredParams: ['url'],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
events: [
|
|
79
|
+
{
|
|
80
|
+
eventType: 'WoMeter',
|
|
81
|
+
devicePattern: 'Meter / Meter Plus / Indoor-Outdoor Meter',
|
|
82
|
+
fields: [
|
|
83
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
84
|
+
{ name: 'temperature', type: 'number', description: 'Ambient temperature in Celsius', example: 22.5 },
|
|
85
|
+
{ name: 'humidity', type: 'number', description: 'Relative humidity (%)', example: 45 },
|
|
86
|
+
{ name: 'battery', type: 'number', description: 'Battery remaining (%)', example: 88 },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
eventType: 'WoCO2Sensor',
|
|
91
|
+
devicePattern: 'CO2 Monitor',
|
|
92
|
+
fields: [
|
|
93
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
94
|
+
{ name: 'CO2', type: 'number', description: 'CO2 concentration in ppm', example: 520 },
|
|
95
|
+
{ name: 'temperature', type: 'number', description: 'Ambient temperature in Celsius' },
|
|
96
|
+
{ name: 'humidity', type: 'number', description: 'Relative humidity (%)' },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
eventType: 'WoPresence',
|
|
101
|
+
devicePattern: 'Motion Sensor / Video Doorbell motion',
|
|
102
|
+
fields: [
|
|
103
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
104
|
+
{ name: 'detectionState', type: 'string', description: 'Detection result word', example: 'DETECTED' },
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
eventType: 'WoContact',
|
|
109
|
+
devicePattern: 'Contact Sensor',
|
|
110
|
+
fields: [
|
|
111
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
112
|
+
{ name: 'openState', type: 'string', description: 'Door/window state', example: 'open' },
|
|
113
|
+
{ name: 'moveDetected', type: 'boolean', description: 'Motion detected during this sample' },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
eventType: 'WoLock',
|
|
118
|
+
devicePattern: 'Smart Lock / Smart Lock Lite / Smart Lock Pro',
|
|
119
|
+
fields: [
|
|
120
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
121
|
+
{ name: 'lockState', type: 'string', description: 'Lock state: locked, unlocked, jammed', example: 'locked' },
|
|
122
|
+
{ name: 'battery', type: 'number', description: 'Battery remaining (%)' },
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
eventType: 'WoPlug',
|
|
127
|
+
devicePattern: 'Plug Mini / Plug / Relay Switch',
|
|
128
|
+
fields: [
|
|
129
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
130
|
+
{ name: 'power', type: 'string', description: 'Power state (on/off)', example: 'on' },
|
|
131
|
+
{ name: 'voltage', type: 'number', description: 'Instantaneous voltage (V)' },
|
|
132
|
+
{ name: 'electricCurrent', type: 'number', description: 'Instantaneous current (A)' },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
eventType: 'WoBot',
|
|
137
|
+
devicePattern: 'Bot',
|
|
138
|
+
fields: [
|
|
139
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
140
|
+
{ name: 'power', type: 'string', description: 'Power state (on/off)' },
|
|
141
|
+
{ name: 'battery', type: 'number', description: 'Battery remaining (%)' },
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
eventType: 'WoCurtain',
|
|
146
|
+
devicePattern: 'Curtain / Blind Tilt / Roller Shade',
|
|
147
|
+
fields: [
|
|
148
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
149
|
+
{ name: 'slidePosition', type: 'number', description: 'Current slide position (0–100)' },
|
|
150
|
+
{ name: 'calibrate', type: 'boolean', description: 'True if device is calibrated' },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
eventType: 'WoDoorbell',
|
|
155
|
+
devicePattern: 'Video Doorbell button press',
|
|
156
|
+
fields: [
|
|
157
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
158
|
+
{ name: 'buttonName', type: 'string', description: 'Identifier of the pressed button' },
|
|
159
|
+
{ name: 'pressedAt', type: 'timestamp', description: 'Press timestamp in milliseconds' },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
eventType: 'WoKeypad',
|
|
164
|
+
devicePattern: 'Keypad scan / createKey result / deleteKey result',
|
|
165
|
+
fields: [
|
|
166
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
167
|
+
{ name: 'eventType', type: 'string', description: 'Sub-event (createKey / deleteKey / invalidCode)' },
|
|
168
|
+
{ name: 'commandId', type: 'string', description: 'Correlation id returned by the original command' },
|
|
169
|
+
{ name: 'result', type: 'string', description: 'Outcome (success / failed / timeout)' },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
eventType: 'WoColorBulb',
|
|
174
|
+
devicePattern: 'Color Bulb',
|
|
175
|
+
fields: [
|
|
176
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
177
|
+
{ name: 'power', type: 'string', description: 'Power state (on/off)' },
|
|
178
|
+
{ name: 'brightness', type: 'number', description: 'Brightness (0–100)' },
|
|
179
|
+
{ name: 'color', type: 'string', description: 'RGB triplet "r:g:b"' },
|
|
180
|
+
{ name: 'colorTemperature', type: 'number', description: 'Color temperature in Kelvin' },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
eventType: 'WoStrip',
|
|
185
|
+
devicePattern: 'Strip Light',
|
|
186
|
+
fields: [
|
|
187
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
188
|
+
{ name: 'power', type: 'string', description: 'Power state (on/off)' },
|
|
189
|
+
{ name: 'brightness', type: 'number', description: 'Brightness (0–100)' },
|
|
190
|
+
{ name: 'color', type: 'string', description: 'RGB triplet "r:g:b"' },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
eventType: 'WoSweeper',
|
|
195
|
+
devicePattern: 'Robot Vacuum',
|
|
196
|
+
fields: [
|
|
197
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
198
|
+
{ name: 'workingStatus', type: 'string', description: 'Cleaning state' },
|
|
199
|
+
{ name: 'battery', type: 'number', description: 'Battery remaining (%)' },
|
|
200
|
+
{ name: 'taskType', type: 'string', description: 'Current task (standby / clean / charge)' },
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
eventType: 'WoWaterLeakDetect',
|
|
205
|
+
devicePattern: 'Water Leak Detector',
|
|
206
|
+
fields: [
|
|
207
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
208
|
+
{ name: 'waterLeakDetect', type: 'number', description: 'Leak flag (0 = dry, 1 = leak detected)' },
|
|
209
|
+
{ name: 'battery', type: 'number', description: 'Battery remaining (%)' },
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
eventType: 'WoHub',
|
|
214
|
+
devicePattern: 'Hub 2 / Hub 3 (ambient sensors)',
|
|
215
|
+
fields: [
|
|
216
|
+
...COMMON_WEBHOOK_FIELDS,
|
|
217
|
+
{ name: 'temperature', type: 'number', description: 'Ambient temperature in Celsius' },
|
|
218
|
+
{ name: 'humidity', type: 'number', description: 'Relative humidity (%)' },
|
|
219
|
+
{ name: 'lightLevel', type: 'number', description: 'Illuminance level' },
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
constraints: {
|
|
224
|
+
maxUrlLength: 2048,
|
|
225
|
+
maxWebhooksPerAccount: 1,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
keys: [
|
|
229
|
+
{
|
|
230
|
+
keyType: 'permanent',
|
|
231
|
+
description: 'Passcode that never expires — valid until manually deleted.',
|
|
232
|
+
requiredParams: ['name', 'password'],
|
|
233
|
+
optionalParams: [],
|
|
234
|
+
supportedDevices: ['Keypad', 'Keypad Touch'],
|
|
235
|
+
safetyTier: 'destructive',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
keyType: 'timeLimit',
|
|
239
|
+
description: 'Passcode valid only between startTime and endTime (Unix seconds).',
|
|
240
|
+
requiredParams: ['name', 'password', 'startTime', 'endTime'],
|
|
241
|
+
optionalParams: [],
|
|
242
|
+
supportedDevices: ['Keypad', 'Keypad Touch'],
|
|
243
|
+
safetyTier: 'destructive',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
keyType: 'disposable',
|
|
247
|
+
description: 'Passcode that can be used once and then auto-expires.',
|
|
248
|
+
requiredParams: ['name', 'password'],
|
|
249
|
+
optionalParams: ['startTime', 'endTime'],
|
|
250
|
+
supportedDevices: ['Keypad', 'Keypad Touch'],
|
|
251
|
+
safetyTier: 'destructive',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
keyType: 'urgent',
|
|
255
|
+
description: 'Emergency passcode (typically tied to panic / audit workflow).',
|
|
256
|
+
requiredParams: ['name', 'password'],
|
|
257
|
+
optionalParams: [],
|
|
258
|
+
supportedDevices: ['Keypad', 'Keypad Touch'],
|
|
259
|
+
safetyTier: 'destructive',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
};
|
|
263
|
+
/** Convenience: return the list of known webhook event types. */
|
|
264
|
+
export function listWebhookEventTypes() {
|
|
265
|
+
return RESOURCE_CATALOG.webhooks.events.map((e) => e.eventType);
|
|
266
|
+
}
|
|
267
|
+
/** Convenience: return the list of supported keypad key types. */
|
|
268
|
+
export function listKeyTypes() {
|
|
269
|
+
return RESOURCE_CATALOG.keys.map((k) => k.keyType);
|
|
270
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,9 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { intArg, stringArg, enumArg } from './utils/arg-parsers.js';
|
|
6
6
|
import { parseDurationToMs } from './utils/flags.js';
|
|
7
|
-
import { emitJsonError, isJsonMode } from './utils/output.js';
|
|
7
|
+
import { emitJsonError, isJsonMode, printJson } from './utils/output.js';
|
|
8
|
+
import { commandToJson, resolveTargetCommand } from './utils/help-json.js';
|
|
9
|
+
import { PRODUCT_TAGLINE } from './commands/identity.js';
|
|
8
10
|
import { registerConfigCommand } from './commands/config.js';
|
|
9
11
|
import { registerDevicesCommand } from './commands/devices.js';
|
|
10
12
|
import { registerScenesCommand } from './commands/scenes.js';
|
|
@@ -54,7 +56,7 @@ const cacheModeArg = (value) => {
|
|
|
54
56
|
};
|
|
55
57
|
program
|
|
56
58
|
.name('switchbot')
|
|
57
|
-
.description(
|
|
59
|
+
.description(PRODUCT_TAGLINE)
|
|
58
60
|
.version(pkgVersion)
|
|
59
61
|
.option('--no-color', 'Disable ANSI colors in output')
|
|
60
62
|
.option('--json', 'Output raw JSON response (disables tables; useful for pipes/scripts)')
|
|
@@ -150,6 +152,10 @@ function enableSuggestions(cmd) {
|
|
|
150
152
|
cmd.commands.forEach(enableSuggestions);
|
|
151
153
|
}
|
|
152
154
|
enableSuggestions(program);
|
|
155
|
+
// In JSON mode suppress the plain-text help output so we can emit structured JSON instead.
|
|
156
|
+
if (isJsonMode()) {
|
|
157
|
+
program.configureOutput({ writeOut: () => { } });
|
|
158
|
+
}
|
|
153
159
|
try {
|
|
154
160
|
await program.parseAsync();
|
|
155
161
|
}
|
|
@@ -158,7 +164,14 @@ catch (err) {
|
|
|
158
164
|
// argParser on a subcommand option) don't always hit the root exitOverride.
|
|
159
165
|
// Mirror the root mapping so all usage errors surface as exit 2.
|
|
160
166
|
if (err instanceof CommanderError) {
|
|
161
|
-
if (err.code === 'commander.helpDisplayed'
|
|
167
|
+
if (err.code === 'commander.helpDisplayed') {
|
|
168
|
+
if (isJsonMode()) {
|
|
169
|
+
const target = resolveTargetCommand(program, process.argv.slice(2));
|
|
170
|
+
printJson(commandToJson(target, { includeIdentity: target === program }));
|
|
171
|
+
}
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
if (err.code === 'commander.version') {
|
|
162
175
|
process.exit(0);
|
|
163
176
|
}
|
|
164
177
|
if (isJsonMode()) {
|
package/dist/lib/devices.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createClient } from '../api/client.js';
|
|
2
2
|
import { idempotencyCache } from './idempotency.js';
|
|
3
|
-
import { findCatalogEntry, suggestedActions, getEffectiveCatalog, } from '../devices/catalog.js';
|
|
3
|
+
import { findCatalogEntry, suggestedActions, getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
|
|
4
4
|
import { getCachedDevice, updateCacheFromDeviceList, loadCache, isListCacheFresh, getCachedStatus, setCachedStatus, } from '../devices/cache.js';
|
|
5
5
|
import { getCacheMode } from '../utils/flags.js';
|
|
6
6
|
import { writeAudit } from '../utils/audit.js';
|
|
@@ -216,9 +216,11 @@ export function isDestructiveCommand(deviceType, cmd, commandType) {
|
|
|
216
216
|
if (!match || Array.isArray(match))
|
|
217
217
|
return false;
|
|
218
218
|
const spec = match.commands.find((c) => c.command === cmd);
|
|
219
|
-
|
|
219
|
+
if (!spec)
|
|
220
|
+
return false;
|
|
221
|
+
return deriveSafetyTier(spec, match) === 'destructive';
|
|
220
222
|
}
|
|
221
|
-
/** Return the
|
|
223
|
+
/** Return the safetyReason for a command, or null if not destructive / not found. */
|
|
222
224
|
export function getDestructiveReason(deviceType, cmd, commandType) {
|
|
223
225
|
if (commandType === 'customize')
|
|
224
226
|
return null;
|
|
@@ -228,7 +230,7 @@ export function getDestructiveReason(deviceType, cmd, commandType) {
|
|
|
228
230
|
if (!match || Array.isArray(match))
|
|
229
231
|
return null;
|
|
230
232
|
const spec = match.commands.find((c) => c.command === cmd);
|
|
231
|
-
return spec
|
|
233
|
+
return spec ? getCommandSafetyReason(spec) : null;
|
|
232
234
|
}
|
|
233
235
|
/**
|
|
234
236
|
* Describe a device by id: metadata + catalog entry (if known) +
|
|
@@ -264,7 +266,16 @@ export async function describeDevice(deviceId, options = {}, client) {
|
|
|
264
266
|
? {
|
|
265
267
|
role: catalogEntry.role ?? null,
|
|
266
268
|
readOnly: catalogEntry.readOnly ?? false,
|
|
267
|
-
commands: catalogEntry.commands
|
|
269
|
+
commands: catalogEntry.commands.map((c) => {
|
|
270
|
+
const tier = deriveSafetyTier(c, catalogEntry);
|
|
271
|
+
const reason = getCommandSafetyReason(c);
|
|
272
|
+
return {
|
|
273
|
+
...c,
|
|
274
|
+
safetyTier: tier,
|
|
275
|
+
destructive: tier === 'destructive',
|
|
276
|
+
...(reason ? { safetyReason: reason } : {}),
|
|
277
|
+
};
|
|
278
|
+
}),
|
|
268
279
|
statusFields: catalogEntry.statusFields ?? [],
|
|
269
280
|
...(liveStatus !== undefined ? { liveStatus } : {}),
|
|
270
281
|
}
|