@switchbot/openapi-cli 1.1.0 → 1.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 +174 -18
- package/dist/api/client.d.ts +7 -1
- package/dist/api/client.js +44 -8
- package/dist/api/client.js.map +1 -1
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +252 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/cache.d.ts +2 -0
- package/dist/commands/cache.js +108 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/capabilities.d.ts +2 -0
- package/dist/commands/capabilities.js +91 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/catalog.d.ts +2 -0
- package/dist/commands/catalog.js +291 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/config.js +123 -10
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/device-meta.d.ts +2 -0
- package/dist/commands/device-meta.js +142 -0
- package/dist/commands/device-meta.js.map +1 -0
- package/dist/commands/devices.js +272 -152
- package/dist/commands/devices.js.map +1 -1
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.js +147 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.d.ts +15 -0
- package/dist/commands/events.js +188 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/expand.d.ts +2 -0
- package/dist/commands/expand.js +192 -0
- package/dist/commands/expand.js.map +1 -0
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +137 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +104 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/mcp.d.ts +4 -0
- package/dist/commands/mcp.js +386 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/plan.d.ts +38 -0
- package/dist/commands/plan.js +356 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/quota.d.ts +2 -0
- package/dist/commands/quota.js +77 -0
- package/dist/commands/quota.js.map +1 -0
- package/dist/commands/scenes.js +19 -13
- package/dist/commands/scenes.js.map +1 -1
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/schema.js +77 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +161 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/webhook.js +37 -22
- package/dist/commands/webhook.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.js +32 -6
- package/dist/config.js.map +1 -1
- package/dist/devices/cache.d.ts +50 -0
- package/dist/devices/cache.js +152 -1
- package/dist/devices/cache.js.map +1 -1
- package/dist/devices/catalog.d.ts +49 -0
- package/dist/devices/catalog.js +362 -92
- package/dist/devices/catalog.js.map +1 -1
- package/dist/devices/device-meta.d.ts +15 -0
- package/dist/devices/device-meta.js +52 -0
- package/dist/devices/device-meta.js.map +1 -0
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/devices.d.ts +144 -0
- package/dist/lib/devices.js +329 -0
- package/dist/lib/devices.js.map +1 -0
- package/dist/lib/scenes.d.ts +7 -0
- package/dist/lib/scenes.js +11 -0
- package/dist/lib/scenes.js.map +1 -0
- package/dist/utils/audit.d.ts +13 -0
- package/dist/utils/audit.js +43 -0
- package/dist/utils/audit.js.map +1 -0
- package/dist/utils/filter.d.ts +45 -0
- package/dist/utils/filter.js +96 -0
- package/dist/utils/filter.js.map +1 -0
- package/dist/utils/flags.d.ts +42 -0
- package/dist/utils/flags.js +108 -0
- package/dist/utils/flags.js.map +1 -1
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +109 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/name-resolver.d.ts +17 -0
- package/dist/utils/name-resolver.js +78 -0
- package/dist/utils/name-resolver.js.map +1 -0
- package/dist/utils/output.d.ts +18 -0
- package/dist/utils/output.js +66 -6
- package/dist/utils/output.js.map +1 -1
- package/dist/utils/quota.d.ts +48 -0
- package/dist/utils/quota.js +144 -0
- package/dist/utils/quota.js.map +1 -0
- package/dist/utils/retry.d.ts +23 -0
- package/dist/utils/retry.js +60 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/string.d.ts +2 -0
- package/dist/utils/string.js +23 -0
- package/dist/utils/string.js.map +1 -0
- package/package.json +4 -1
package/dist/commands/devices.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { getCachedDevice
|
|
1
|
+
import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError } from '../utils/output.js';
|
|
2
|
+
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
3
|
+
import { findCatalogEntry, getEffectiveCatalog } from '../devices/catalog.js';
|
|
4
|
+
import { getCachedDevice } from '../devices/cache.js';
|
|
5
|
+
import { loadDeviceMeta } from '../devices/device-meta.js';
|
|
6
|
+
import { resolveDeviceId } from '../utils/name-resolver.js';
|
|
7
|
+
import { fetchDeviceList, fetchDeviceStatus, executeCommand, describeDevice, validateCommand, isDestructiveCommand, getDestructiveReason, buildHubLocationMap, DeviceNotFoundError, } from '../lib/devices.js';
|
|
8
|
+
import { registerBatchCommand } from './batch.js';
|
|
9
|
+
import { registerWatchCommand } from './watch.js';
|
|
10
|
+
import { registerExplainCommand } from './explain.js';
|
|
11
|
+
import { registerExpandCommand } from './expand.js';
|
|
12
|
+
import { registerDevicesMetaCommand } from './device-meta.js';
|
|
13
|
+
import { isDryRun } from '../utils/flags.js';
|
|
5
14
|
export function registerDevicesCommand(program) {
|
|
6
15
|
const devices = program
|
|
7
16
|
.command('devices')
|
|
@@ -31,9 +40,12 @@ Run any subcommand with --help for its own flags and examples.
|
|
|
31
40
|
.command('list')
|
|
32
41
|
.description('List all physical devices and IR remote devices on the account')
|
|
33
42
|
.addHelpText('after', `
|
|
34
|
-
|
|
43
|
+
Default columns: deviceId, deviceName, type, category
|
|
44
|
+
Pass --wide for the full operator view: + controlType, family, roomID, room, hub, cloud
|
|
45
|
+
--fields accepts any subset of all column names (exit 2 on unknown names).
|
|
35
46
|
|
|
36
|
-
type - physical deviceType (e.g. "Bot", "Curtain") or
|
|
47
|
+
type - physical deviceType (e.g. "Bot", "Curtain") or IR remoteType (e.g. "TV")
|
|
48
|
+
category - "physical" or "ir"
|
|
37
49
|
controlType - functional classification from the API (e.g. "Bot", "Switch",
|
|
38
50
|
"TV") — may differ from 'type' and groups devices by behavior
|
|
39
51
|
family - home/family name (IR remotes inherit this from their bound Hub)
|
|
@@ -50,55 +62,74 @@ the table; --json returns the raw API body unchanged.)
|
|
|
50
62
|
|
|
51
63
|
Examples:
|
|
52
64
|
$ switchbot devices list
|
|
65
|
+
$ switchbot devices list --wide
|
|
66
|
+
$ switchbot devices list --format tsv --fields deviceId,deviceName,type,category
|
|
53
67
|
$ switchbot devices list --json | jq '.deviceList[] | select(.familyName == "家里")'
|
|
54
68
|
$ switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
|
|
55
69
|
`)
|
|
56
|
-
.
|
|
70
|
+
.option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
|
|
71
|
+
.option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
|
|
72
|
+
.action(async (options) => {
|
|
57
73
|
try {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
printJson(
|
|
74
|
+
const body = await fetchDeviceList();
|
|
75
|
+
const { deviceList, infraredRemoteList } = body;
|
|
76
|
+
const fmt = resolveFormat();
|
|
77
|
+
const deviceMeta = loadDeviceMeta();
|
|
78
|
+
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
79
|
+
printJson(body);
|
|
64
80
|
return;
|
|
65
81
|
}
|
|
66
82
|
const hubLocation = buildHubLocationMap(deviceList);
|
|
83
|
+
const narrowHeaders = ['deviceId', 'deviceName', 'type', 'category'];
|
|
84
|
+
const wideHeaders = ['deviceId', 'deviceName', 'type', 'category', 'controlType', 'family', 'roomID', 'room', 'hub', 'cloud', 'alias'];
|
|
85
|
+
const userFields = resolveFields();
|
|
86
|
+
const headers = userFields ? wideHeaders : (options.wide ? wideHeaders : narrowHeaders);
|
|
67
87
|
const rows = [];
|
|
68
88
|
for (const d of deviceList) {
|
|
89
|
+
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
90
|
+
continue;
|
|
69
91
|
rows.push([
|
|
70
92
|
d.deviceId,
|
|
71
93
|
d.deviceName,
|
|
72
94
|
d.deviceType || '—',
|
|
95
|
+
'physical',
|
|
73
96
|
d.controlType || '—',
|
|
74
97
|
d.familyName || '—',
|
|
75
98
|
d.roomID || '—',
|
|
76
99
|
d.roomName || '—',
|
|
77
100
|
!d.hubDeviceId || d.hubDeviceId === '000000000000' ? '—' : d.hubDeviceId,
|
|
78
101
|
d.enableCloudService,
|
|
102
|
+
deviceMeta.devices[d.deviceId]?.alias ?? '—',
|
|
79
103
|
]);
|
|
80
104
|
}
|
|
81
105
|
for (const d of infraredRemoteList) {
|
|
106
|
+
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
107
|
+
continue;
|
|
82
108
|
const inherited = hubLocation.get(d.hubDeviceId);
|
|
83
109
|
rows.push([
|
|
84
110
|
d.deviceId,
|
|
85
111
|
d.deviceName,
|
|
86
|
-
|
|
112
|
+
d.remoteType,
|
|
113
|
+
'ir',
|
|
87
114
|
d.controlType || '—',
|
|
88
115
|
inherited?.family || '—',
|
|
89
116
|
inherited?.roomID || '—',
|
|
90
117
|
inherited?.room || '—',
|
|
91
118
|
d.hubDeviceId,
|
|
92
119
|
null,
|
|
120
|
+
deviceMeta.devices[d.deviceId]?.alias ?? '—',
|
|
93
121
|
]);
|
|
94
122
|
}
|
|
95
|
-
if (rows.length === 0) {
|
|
123
|
+
if (rows.length === 0 && fmt === 'table') {
|
|
96
124
|
console.log('No devices found');
|
|
97
125
|
return;
|
|
98
126
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
const defaultFields = options.wide ? undefined : narrowHeaders;
|
|
128
|
+
renderRows(wideHeaders, rows, fmt, userFields ?? defaultFields);
|
|
129
|
+
if (fmt === 'table') {
|
|
130
|
+
console.log(`\nTotal: ${deviceList.length} physical device(s), ${infraredRemoteList.length} IR remote device(s)`);
|
|
131
|
+
console.log(`Tip: 'switchbot devices describe <deviceId>' shows a device's supported commands.`);
|
|
132
|
+
}
|
|
102
133
|
}
|
|
103
134
|
catch (error) {
|
|
104
135
|
handleError(error);
|
|
@@ -108,35 +139,41 @@ Examples:
|
|
|
108
139
|
devices
|
|
109
140
|
.command('status')
|
|
110
141
|
.description('Query the real-time status of a specific device')
|
|
111
|
-
.argument('
|
|
142
|
+
.argument('[deviceId]', 'Device ID from "devices list" (or use --name)')
|
|
143
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
112
144
|
.addHelpText('after', `
|
|
113
|
-
|
|
114
|
-
returns temperature/humidity/battery, Curtain returns slidePosition/moving,
|
|
115
|
-
Color Bulb returns brightness/color/colorTemperature, etc.
|
|
145
|
+
Status fields vary by device type. To discover them without a live call:
|
|
116
146
|
|
|
117
|
-
|
|
118
|
-
API, use the offline catalog:
|
|
147
|
+
switchbot devices commands <type> (prints the "Status fields" section)
|
|
119
148
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
IR remote devices cannot be queried — the SwitchBot API returns no status
|
|
123
|
-
channel for them. Use 'devices list' to confirm a deviceId is a physical
|
|
124
|
-
device (not in the 'infraredRemoteList').
|
|
149
|
+
For --fields: run the command once with --format yaml (no --fields) to see
|
|
150
|
+
all field names returned by your specific device, then narrow with --fields.
|
|
125
151
|
|
|
126
152
|
Examples:
|
|
127
153
|
$ switchbot devices status ABC123DEF456
|
|
154
|
+
$ switchbot devices status --name "客厅空调"
|
|
128
155
|
$ switchbot devices status ABC123DEF456 --json
|
|
156
|
+
$ switchbot devices status ABC123DEF456 --format yaml
|
|
157
|
+
$ switchbot devices status ABC123DEF456 --format tsv --fields power,battery
|
|
129
158
|
$ switchbot devices status ABC123DEF456 --json | jq '.battery'
|
|
130
159
|
`)
|
|
131
|
-
.action(async (
|
|
160
|
+
.action(async (deviceIdArg, options) => {
|
|
132
161
|
try {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
const deviceId = resolveDeviceId(deviceIdArg, options.name);
|
|
163
|
+
const body = await fetchDeviceStatus(deviceId);
|
|
164
|
+
const fmt = resolveFormat();
|
|
165
|
+
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
166
|
+
printJson(body);
|
|
137
167
|
return;
|
|
138
168
|
}
|
|
139
|
-
|
|
169
|
+
if (fmt !== 'table') {
|
|
170
|
+
const allHeaders = Object.keys(body);
|
|
171
|
+
const allRows = [Object.values(body)];
|
|
172
|
+
const fields = resolveFields();
|
|
173
|
+
renderRows(allHeaders, allRows, fmt, fields);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
printKeyValue(body);
|
|
140
177
|
}
|
|
141
178
|
catch (error) {
|
|
142
179
|
handleError(error);
|
|
@@ -146,10 +183,12 @@ Examples:
|
|
|
146
183
|
devices
|
|
147
184
|
.command('command')
|
|
148
185
|
.description('Send a control command to a device')
|
|
149
|
-
.argument('
|
|
186
|
+
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
150
187
|
.argument('<cmd>', 'Command name, e.g. turnOn, turnOff, setColor, setBrightness, setAll, startClean')
|
|
151
188
|
.argument('[parameter]', 'Command parameter. Omit for commands like turnOn/turnOff (defaults to "default"). Format depends on the command (see below).')
|
|
189
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
152
190
|
.option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', 'command')
|
|
191
|
+
.option('--yes', 'Confirm a destructive command (Smart Lock unlock, Garage open, …). --dry-run is always allowed without --yes.')
|
|
153
192
|
.addHelpText('after', `
|
|
154
193
|
────────────────────────────────────────────────────────────────────────
|
|
155
194
|
For the full list of commands a specific device supports — and their
|
|
@@ -182,17 +221,73 @@ Common errors:
|
|
|
182
221
|
161 device offline (BLE devices need a Hub bridge)
|
|
183
222
|
171 hub offline
|
|
184
223
|
|
|
224
|
+
Safety:
|
|
225
|
+
Destructive commands (Smart Lock unlock, Garage Door Opener turnOn/turnOff,
|
|
226
|
+
Keypad createKey/deleteKey, …) are blocked by default. Pass --yes to confirm,
|
|
227
|
+
or --dry-run to preview without sending.
|
|
228
|
+
|
|
185
229
|
Examples:
|
|
186
230
|
$ switchbot devices command ABC123 turnOn
|
|
187
231
|
$ switchbot devices command ABC123 setColor "255:0:0"
|
|
188
232
|
$ switchbot devices command ABC123 setAll "26,1,3,on"
|
|
189
233
|
$ switchbot devices command ABC123 startClean '{"action":"sweep","param":{"fanLevel":2,"times":1}}'
|
|
190
234
|
$ switchbot devices command ABC123 "MyButton" --type customize
|
|
235
|
+
$ switchbot devices command <lockId> unlock --yes
|
|
191
236
|
`)
|
|
192
|
-
.action(async (
|
|
193
|
-
|
|
237
|
+
.action(async (deviceIdArg, cmd, parameter, options) => {
|
|
238
|
+
const deviceId = resolveDeviceId(deviceIdArg, options.name);
|
|
239
|
+
const validation = validateCommand(deviceId, cmd, parameter, options.type);
|
|
240
|
+
if (!validation.ok) {
|
|
241
|
+
const err = validation.error;
|
|
242
|
+
if (isJsonMode()) {
|
|
243
|
+
const obj = { code: 2, kind: 'usage', message: err.message };
|
|
244
|
+
if (err.hint)
|
|
245
|
+
obj.hint = err.hint;
|
|
246
|
+
obj.context = { validationKind: err.kind };
|
|
247
|
+
console.error(JSON.stringify({ error: obj }));
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
console.error(`Error: ${err.message}`);
|
|
251
|
+
if (err.hint)
|
|
252
|
+
console.error(err.hint);
|
|
253
|
+
if (err.kind === 'unknown-command') {
|
|
254
|
+
const cached = getCachedDevice(deviceId);
|
|
255
|
+
if (cached) {
|
|
256
|
+
console.error(`Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.`);
|
|
257
|
+
console.error(`(If the catalog is out of date, run 'switchbot devices list' to refresh the local cache, or pass --type customize for custom IR buttons.)`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
process.exit(2);
|
|
262
|
+
}
|
|
263
|
+
const cachedForGuard = getCachedDevice(deviceId);
|
|
264
|
+
if (!options.yes &&
|
|
265
|
+
!isDryRun() &&
|
|
266
|
+
isDestructiveCommand(cachedForGuard?.type, cmd, options.type)) {
|
|
267
|
+
const typeLabel = cachedForGuard?.type ?? 'unknown';
|
|
268
|
+
const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
|
|
269
|
+
if (isJsonMode()) {
|
|
270
|
+
console.error(JSON.stringify({
|
|
271
|
+
error: {
|
|
272
|
+
code: 2,
|
|
273
|
+
kind: 'guard',
|
|
274
|
+
message: `"${cmd}" on ${typeLabel} is destructive and requires --yes.`,
|
|
275
|
+
hint: reason
|
|
276
|
+
? `Re-run with --yes to confirm. Reason: ${reason}`
|
|
277
|
+
: 'Re-run with --yes to confirm, or --dry-run to preview without sending.',
|
|
278
|
+
context: { command: cmd, deviceType: typeLabel, deviceId, ...(reason ? { destructiveReason: reason } : {}) },
|
|
279
|
+
},
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.error(`Refusing to run destructive command "${cmd}" on ${typeLabel} without --yes.`);
|
|
284
|
+
if (reason)
|
|
285
|
+
console.error(`Reason: ${reason}`);
|
|
286
|
+
console.error(`Re-run with --yes to confirm, or --dry-run to preview without sending.`);
|
|
287
|
+
}
|
|
288
|
+
process.exit(2);
|
|
289
|
+
}
|
|
194
290
|
try {
|
|
195
|
-
const client = createClient();
|
|
196
291
|
// parameter may be a JSON object string (e.g. S10 startClean) or a plain string
|
|
197
292
|
let parsedParam = parameter ?? 'default';
|
|
198
293
|
if (parameter) {
|
|
@@ -203,19 +298,23 @@ Examples:
|
|
|
203
298
|
// keep as string
|
|
204
299
|
}
|
|
205
300
|
}
|
|
206
|
-
const body =
|
|
207
|
-
|
|
208
|
-
parameter: parsedParam,
|
|
209
|
-
commandType: options.type,
|
|
210
|
-
};
|
|
211
|
-
const res = await client.post(`/v1.1/devices/${deviceId}/commands`, body);
|
|
301
|
+
const body = await executeCommand(deviceId, cmd, parsedParam, options.type);
|
|
302
|
+
const isIr = getCachedDevice(deviceId)?.category === 'ir';
|
|
212
303
|
if (isJsonMode()) {
|
|
213
|
-
|
|
304
|
+
const result = { ok: true, command: cmd, deviceId };
|
|
305
|
+
if (isIr)
|
|
306
|
+
result.subKind = 'ir-no-feedback';
|
|
307
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
308
|
+
Object.assign(result, body);
|
|
309
|
+
}
|
|
310
|
+
printJson(result);
|
|
214
311
|
return;
|
|
215
312
|
}
|
|
216
313
|
console.log(`✓ Command sent: ${cmd}`);
|
|
217
|
-
if (
|
|
218
|
-
|
|
314
|
+
if (isIr)
|
|
315
|
+
console.log(' Note: IR command sent — no device confirmation (fire-and-forget).');
|
|
316
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
317
|
+
printKeyValue(body);
|
|
219
318
|
}
|
|
220
319
|
}
|
|
221
320
|
catch (error) {
|
|
@@ -235,18 +334,28 @@ Examples:
|
|
|
235
334
|
$ switchbot devices types --json
|
|
236
335
|
`)
|
|
237
336
|
.action(() => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
337
|
+
try {
|
|
338
|
+
const catalog = getEffectiveCatalog();
|
|
339
|
+
const fmt = resolveFormat();
|
|
340
|
+
if (fmt === 'json') {
|
|
341
|
+
printJson(catalog);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const headers = ['type', 'category', 'commands', 'aliases'];
|
|
345
|
+
const rows = catalog.map((e) => [
|
|
346
|
+
e.type,
|
|
347
|
+
e.category,
|
|
348
|
+
String(e.commands.length),
|
|
349
|
+
(e.aliases ?? []).join(', ') || '—',
|
|
350
|
+
]);
|
|
351
|
+
renderRows(headers, rows, fmt, resolveFields());
|
|
352
|
+
if (fmt === 'table') {
|
|
353
|
+
console.log(`\nTotal: ${catalog.length} device type(s)`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
handleError(error);
|
|
241
358
|
}
|
|
242
|
-
const rows = DEVICE_CATALOG.map((e) => [
|
|
243
|
-
e.type,
|
|
244
|
-
e.category,
|
|
245
|
-
String(e.commands.length),
|
|
246
|
-
(e.aliases ?? []).join(', ') || '—',
|
|
247
|
-
]);
|
|
248
|
-
printTable(['type', 'category', 'commands', 'aliases'], rows);
|
|
249
|
-
console.log(`\nTotal: ${DEVICE_CATALOG.length} device type(s)`);
|
|
250
359
|
});
|
|
251
360
|
// switchbot devices commands <type>
|
|
252
361
|
devices
|
|
@@ -270,65 +379,78 @@ Examples:
|
|
|
270
379
|
`)
|
|
271
380
|
.action((typeParts) => {
|
|
272
381
|
const type = typeParts.join(' ');
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
382
|
+
try {
|
|
383
|
+
const match = findCatalogEntry(type);
|
|
384
|
+
if (!match) {
|
|
385
|
+
throw new UsageError(`No device type matches "${type}". Try 'switchbot devices types' to see the full list.`);
|
|
386
|
+
}
|
|
387
|
+
if (Array.isArray(match)) {
|
|
388
|
+
const types = match.map((m) => m.type).join(', ');
|
|
389
|
+
throw new UsageError(`"${type}" matches multiple types: ${types}. Be more specific.`);
|
|
390
|
+
}
|
|
391
|
+
if (isJsonMode()) {
|
|
392
|
+
printJson(match);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
renderCatalogEntry(match);
|
|
284
396
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
return;
|
|
397
|
+
catch (error) {
|
|
398
|
+
handleError(error);
|
|
288
399
|
}
|
|
289
|
-
renderCatalogEntry(match);
|
|
290
400
|
});
|
|
291
401
|
// switchbot devices describe <deviceId>
|
|
292
402
|
devices
|
|
293
403
|
.command('describe')
|
|
294
404
|
.description('Describe a device by ID: metadata + supported commands + status fields (1 API call)')
|
|
295
|
-
.argument('
|
|
405
|
+
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
406
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId')
|
|
407
|
+
.option('--live', 'Also fetch live status values and merge them into capabilities (costs 1 extra API call)')
|
|
296
408
|
.addHelpText('after', `
|
|
297
|
-
Makes a
|
|
298
|
-
|
|
299
|
-
|
|
409
|
+
Makes a GET /v1.1/devices call to look up the device's type, then prints its
|
|
410
|
+
metadata alongside the matching catalog entry (supported commands + parameter
|
|
411
|
+
formats + status field names). With --live, makes a second call to fetch the
|
|
412
|
+
current status values and merges them into the output.
|
|
300
413
|
|
|
301
|
-
|
|
414
|
+
JSON output shape (--json):
|
|
415
|
+
{
|
|
416
|
+
device: <raw API fields>,
|
|
417
|
+
controlType: <string|null>,
|
|
418
|
+
catalog: <catalog entry, or null>,
|
|
419
|
+
capabilities: {
|
|
420
|
+
role: <functional role>,
|
|
421
|
+
readOnly: <boolean>,
|
|
422
|
+
commands: [{command, parameter, description, idempotent?, destructive?, exampleParams?}],
|
|
423
|
+
statusFields: [<name>],
|
|
424
|
+
liveStatus: <status payload when --live was passed>
|
|
425
|
+
},
|
|
426
|
+
source: "catalog" | "live" | "catalog+live" | "none",
|
|
427
|
+
suggestedActions: [{command, parameter?, description}]
|
|
428
|
+
}
|
|
302
429
|
|
|
303
430
|
Examples:
|
|
304
431
|
$ switchbot devices describe ABC123DEF456
|
|
432
|
+
$ switchbot devices describe ABC123DEF456 --live
|
|
305
433
|
$ switchbot devices describe ABC123DEF456 --json
|
|
434
|
+
$ switchbot devices describe <lockId> --json | jq '.capabilities.commands[] | select(.destructive)'
|
|
306
435
|
`)
|
|
307
|
-
.action(async (
|
|
436
|
+
.action(async (deviceIdArg, options) => {
|
|
308
437
|
try {
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
const {
|
|
312
|
-
updateCacheFromDeviceList(res.data.body);
|
|
313
|
-
const physical = deviceList.find((d) => d.deviceId === deviceId);
|
|
314
|
-
const ir = infraredRemoteList.find((d) => d.deviceId === deviceId);
|
|
315
|
-
if (!physical && !ir) {
|
|
316
|
-
console.error(`No device with id "${deviceId}" found on this account.`);
|
|
317
|
-
console.error(`Try 'switchbot devices list' to see the full list.`);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
const typeName = physical ? (physical.deviceType ?? '') : ir.remoteType;
|
|
321
|
-
const match = typeName ? findCatalogEntry(typeName) : null;
|
|
322
|
-
const catalogEntry = !match || Array.isArray(match) ? null : match;
|
|
438
|
+
const deviceId = resolveDeviceId(deviceIdArg, options.name);
|
|
439
|
+
const result = await describeDevice(deviceId, options);
|
|
440
|
+
const { device, isPhysical, typeName, controlType, catalog, capabilities, source, suggestedActions: picks } = result;
|
|
323
441
|
if (isJsonMode()) {
|
|
324
442
|
printJson({
|
|
325
|
-
device
|
|
326
|
-
controlType
|
|
327
|
-
catalog
|
|
443
|
+
device,
|
|
444
|
+
controlType,
|
|
445
|
+
catalog,
|
|
446
|
+
capabilities,
|
|
447
|
+
source,
|
|
448
|
+
suggestedActions: picks,
|
|
328
449
|
});
|
|
329
450
|
return;
|
|
330
451
|
}
|
|
331
|
-
if (
|
|
452
|
+
if (isPhysical) {
|
|
453
|
+
const physical = device;
|
|
332
454
|
printKeyValue({
|
|
333
455
|
deviceId: physical.deviceId,
|
|
334
456
|
deviceName: physical.deviceName,
|
|
@@ -341,8 +463,9 @@ Examples:
|
|
|
341
463
|
cloudService: physical.enableCloudService,
|
|
342
464
|
});
|
|
343
465
|
}
|
|
344
|
-
else
|
|
345
|
-
const
|
|
466
|
+
else {
|
|
467
|
+
const ir = device;
|
|
468
|
+
const inherited = result.inheritedLocation;
|
|
346
469
|
printKeyValue({
|
|
347
470
|
deviceId: ir.deviceId,
|
|
348
471
|
deviceName: ir.deviceName,
|
|
@@ -354,66 +477,55 @@ Examples:
|
|
|
354
477
|
hub: ir.hubDeviceId || '—',
|
|
355
478
|
});
|
|
356
479
|
}
|
|
480
|
+
const liveStatus = capabilities && 'liveStatus' in capabilities ? capabilities.liveStatus : undefined;
|
|
357
481
|
console.log('');
|
|
358
|
-
if (!
|
|
482
|
+
if (!catalog) {
|
|
359
483
|
console.log(`(Type "${typeName}" is not in the built-in catalog — no command reference available.)`);
|
|
360
|
-
|
|
484
|
+
if (isPhysical) {
|
|
485
|
+
console.log(`Try 'switchbot devices status ${deviceId}' to see what this device reports.`);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
console.log(`Send custom IR buttons with: switchbot devices command ${deviceId} "<buttonName>" --type customize`);
|
|
489
|
+
}
|
|
490
|
+
if (liveStatus) {
|
|
491
|
+
console.log('\nLive status:');
|
|
492
|
+
printKeyValue(liveStatus);
|
|
493
|
+
}
|
|
361
494
|
return;
|
|
362
495
|
}
|
|
363
|
-
renderCatalogEntry(
|
|
496
|
+
renderCatalogEntry(catalog);
|
|
497
|
+
if (liveStatus) {
|
|
498
|
+
console.log('\nLive status:');
|
|
499
|
+
printKeyValue(liveStatus);
|
|
500
|
+
}
|
|
364
501
|
}
|
|
365
502
|
catch (error) {
|
|
503
|
+
if (error instanceof DeviceNotFoundError) {
|
|
504
|
+
console.error(error.message);
|
|
505
|
+
console.error(`Try 'switchbot devices list' to see the full list.`);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
366
508
|
handleError(error);
|
|
367
509
|
}
|
|
368
510
|
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
return map;
|
|
382
|
-
}
|
|
383
|
-
function validateCommandAgainstCache(deviceId, cmd, parameter, commandType) {
|
|
384
|
-
// Custom IR buttons have arbitrary names — skip validation.
|
|
385
|
-
if (commandType === 'customize')
|
|
386
|
-
return;
|
|
387
|
-
const cached = getCachedDevice(deviceId);
|
|
388
|
-
if (!cached)
|
|
389
|
-
return;
|
|
390
|
-
const match = findCatalogEntry(cached.type);
|
|
391
|
-
if (!match || Array.isArray(match))
|
|
392
|
-
return;
|
|
393
|
-
const entry = match;
|
|
394
|
-
const builtinCommands = entry.commands.filter((c) => c.commandType !== 'customize');
|
|
395
|
-
if (builtinCommands.length === 0)
|
|
396
|
-
return;
|
|
397
|
-
const spec = builtinCommands.find((c) => c.command === cmd);
|
|
398
|
-
if (!spec) {
|
|
399
|
-
const unique = [...new Set(builtinCommands.map((c) => c.command))];
|
|
400
|
-
console.error(`Error: "${cmd}" is not a supported command for ${cached.name} (${cached.type}).`);
|
|
401
|
-
console.error(`Supported commands: ${unique.join(', ')}`);
|
|
402
|
-
console.error(`Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.`);
|
|
403
|
-
console.error(`(If the catalog is out of date, run 'switchbot devices list' to refresh the local cache, or pass --type customize for custom IR buttons.)`);
|
|
404
|
-
process.exit(2);
|
|
405
|
-
}
|
|
406
|
-
const noParamExpected = spec.parameter === '—';
|
|
407
|
-
const userProvidedParam = parameter !== undefined && parameter !== 'default';
|
|
408
|
-
if (noParamExpected && userProvidedParam) {
|
|
409
|
-
console.error(`Error: "${cmd}" takes no parameter, but one was provided: "${parameter}".`);
|
|
410
|
-
console.error(`Try: switchbot devices command ${deviceId} ${cmd}`);
|
|
411
|
-
process.exit(2);
|
|
412
|
-
}
|
|
511
|
+
// switchbot devices batch <command> ...
|
|
512
|
+
registerBatchCommand(devices);
|
|
513
|
+
// switchbot devices watch <id...>
|
|
514
|
+
registerWatchCommand(devices);
|
|
515
|
+
// switchbot devices explain <id>
|
|
516
|
+
registerExplainCommand(devices);
|
|
517
|
+
// switchbot devices expand <id> <cmd> [semantic flags]
|
|
518
|
+
registerExpandCommand(devices);
|
|
519
|
+
// switchbot devices meta set/get/list/clear
|
|
520
|
+
registerDevicesMetaCommand(devices);
|
|
413
521
|
}
|
|
414
522
|
function renderCatalogEntry(entry) {
|
|
415
523
|
console.log(`Type: ${entry.type}`);
|
|
416
524
|
console.log(`Category: ${entry.category === 'ir' ? 'IR remote' : 'Physical device'}`);
|
|
525
|
+
if (entry.role)
|
|
526
|
+
console.log(`Role: ${entry.role}`);
|
|
527
|
+
if (entry.readOnly)
|
|
528
|
+
console.log(`ReadOnly: yes (status-only device, no control commands)`);
|
|
417
529
|
if (entry.aliases && entry.aliases.length > 0) {
|
|
418
530
|
console.log(`Aliases: ${entry.aliases.join(', ')}`);
|
|
419
531
|
}
|
|
@@ -422,12 +534,20 @@ function renderCatalogEntry(entry) {
|
|
|
422
534
|
}
|
|
423
535
|
else {
|
|
424
536
|
console.log('\nCommands:');
|
|
425
|
-
const rows = entry.commands.map((c) =>
|
|
426
|
-
|
|
427
|
-
c.
|
|
428
|
-
|
|
429
|
-
|
|
537
|
+
const rows = entry.commands.map((c) => {
|
|
538
|
+
const flags = [];
|
|
539
|
+
if (c.commandType === 'customize')
|
|
540
|
+
flags.push('customize');
|
|
541
|
+
if (c.destructive)
|
|
542
|
+
flags.push('!destructive');
|
|
543
|
+
const label = flags.length > 0 ? `${c.command} [${flags.join(', ')}]` : c.command;
|
|
544
|
+
return [label, c.parameter, c.description];
|
|
545
|
+
});
|
|
430
546
|
printTable(['command', 'parameter', 'description'], rows);
|
|
547
|
+
const hasDestructive = entry.commands.some((c) => c.destructive);
|
|
548
|
+
if (hasDestructive) {
|
|
549
|
+
console.log('\n[!destructive] commands have hard-to-reverse real-world effects — confirm before issuing.');
|
|
550
|
+
}
|
|
431
551
|
}
|
|
432
552
|
if (entry.statusFields && entry.statusFields.length > 0) {
|
|
433
553
|
console.log('\nStatus fields (from "devices status"):');
|