@switchbot/openapi-cli 3.2.0 → 3.2.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/dist/api/client.d.ts +31 -0
- package/dist/api/client.js +236 -0
- package/dist/api/client.js.map +1 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +21 -0
- package/dist/auth.js.map +1 -0
- package/dist/commands/agent-bootstrap.d.ts +10 -0
- package/dist/commands/agent-bootstrap.js +200 -0
- package/dist/commands/agent-bootstrap.js.map +1 -0
- package/dist/commands/auth.d.ts +18 -0
- package/dist/commands/auth.js +355 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +414 -0
- package/dist/commands/batch.js.map +1 -0
- package/dist/commands/cache.d.ts +2 -0
- package/dist/commands/cache.js +127 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/capabilities.d.ts +31 -0
- package/dist/commands/capabilities.js +383 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/catalog.d.ts +2 -0
- package/dist/commands/catalog.js +360 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/completion.d.ts +2 -0
- package/dist/commands/completion.js +386 -0
- package/dist/commands/completion.js.map +1 -0
- package/dist/commands/config.d.ts +21 -0
- package/dist/commands/config.js +377 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +411 -0
- package/dist/commands/daemon.js.map +1 -0
- package/dist/commands/device-meta.d.ts +2 -0
- package/dist/commands/device-meta.js +160 -0
- package/dist/commands/device-meta.js.map +1 -0
- package/dist/commands/devices.d.ts +2 -0
- package/dist/commands/devices.js +949 -0
- package/dist/commands/devices.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +1016 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/events.d.ts +31 -0
- package/dist/commands/events.js +564 -0
- package/dist/commands/events.js.map +1 -0
- package/dist/commands/expand.d.ts +2 -0
- package/dist/commands/expand.js +131 -0
- package/dist/commands/expand.js.map +1 -0
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +140 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/health.d.ts +8 -0
- package/dist/commands/health.js +114 -0
- package/dist/commands/health.js.map +1 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +321 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/identity.d.ts +45 -0
- package/dist/commands/identity.js +60 -0
- package/dist/commands/identity.js.map +1 -0
- package/dist/commands/install.d.ts +20 -0
- package/dist/commands/install.js +247 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/mcp.d.ts +14 -0
- package/dist/commands/mcp.js +2018 -0
- package/dist/commands/mcp.js.map +1 -0
- package/dist/commands/plan.d.ts +51 -0
- package/dist/commands/plan.js +654 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/policy.d.ts +24 -0
- package/dist/commands/policy.js +587 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/quota.d.ts +2 -0
- package/dist/commands/quota.js +79 -0
- package/dist/commands/quota.js.map +1 -0
- package/dist/commands/rules.d.ts +2 -0
- package/dist/commands/rules.js +876 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/commands/scenes.d.ts +2 -0
- package/dist/commands/scenes.js +265 -0
- package/dist/commands/scenes.js.map +1 -0
- package/dist/commands/schema.d.ts +2 -0
- package/dist/commands/schema.js +185 -0
- package/dist/commands/schema.js.map +1 -0
- package/dist/commands/status-sync.d.ts +2 -0
- package/dist/commands/status-sync.js +132 -0
- package/dist/commands/status-sync.js.map +1 -0
- package/dist/commands/uninstall.d.ts +20 -0
- package/dist/commands/uninstall.js +238 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts +2 -0
- package/dist/commands/upgrade-check.js +107 -0
- package/dist/commands/upgrade-check.js.map +1 -0
- package/dist/commands/watch.d.ts +2 -0
- package/dist/commands/watch.js +195 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/webhook.d.ts +2 -0
- package/dist/commands/webhook.js +183 -0
- package/dist/commands/webhook.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.js +259 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials/backends/file.d.ts +18 -0
- package/dist/credentials/backends/file.js +102 -0
- package/dist/credentials/backends/file.js.map +1 -0
- package/dist/credentials/backends/linux.d.ts +16 -0
- package/dist/credentials/backends/linux.js +130 -0
- package/dist/credentials/backends/linux.js.map +1 -0
- package/dist/credentials/backends/macos.d.ts +18 -0
- package/dist/credentials/backends/macos.js +130 -0
- package/dist/credentials/backends/macos.js.map +1 -0
- package/dist/credentials/backends/windows.d.ts +23 -0
- package/dist/credentials/backends/windows.js +216 -0
- package/dist/credentials/backends/windows.js.map +1 -0
- package/dist/credentials/keychain.d.ts +83 -0
- package/dist/credentials/keychain.js +89 -0
- package/dist/credentials/keychain.js.map +1 -0
- package/dist/credentials/prime.d.ts +32 -0
- package/dist/credentials/prime.js +53 -0
- package/dist/credentials/prime.js.map +1 -0
- package/dist/devices/cache.d.ts +79 -0
- package/dist/devices/cache.js +294 -0
- package/dist/devices/cache.js.map +1 -0
- package/dist/devices/catalog.d.ts +138 -0
- package/dist/devices/catalog.js +768 -0
- package/dist/devices/catalog.js.map +1 -0
- package/dist/devices/device-meta.d.ts +15 -0
- package/dist/devices/device-meta.js +57 -0
- package/dist/devices/device-meta.js.map +1 -0
- package/dist/devices/history-agg.d.ts +37 -0
- package/dist/devices/history-agg.js +139 -0
- package/dist/devices/history-agg.js.map +1 -0
- package/dist/devices/history-query.d.ts +45 -0
- package/dist/devices/history-query.js +182 -0
- package/dist/devices/history-query.js.map +1 -0
- package/dist/devices/param-validator.d.ts +40 -0
- package/dist/devices/param-validator.js +434 -0
- package/dist/devices/param-validator.js.map +1 -0
- package/dist/devices/resources.d.ts +74 -0
- package/dist/devices/resources.js +271 -0
- package/dist/devices/resources.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +170 -56946
- package/dist/index.js.map +1 -0
- package/dist/install/default-steps.d.ts +66 -0
- package/dist/install/default-steps.js +258 -0
- package/dist/install/default-steps.js.map +1 -0
- package/dist/install/preflight.d.ts +60 -0
- package/dist/install/preflight.js +213 -0
- package/dist/install/preflight.js.map +1 -0
- package/dist/install/steps.d.ts +61 -0
- package/dist/install/steps.js +68 -0
- package/dist/install/steps.js.map +1 -0
- package/dist/lib/command-keywords.d.ts +5 -0
- package/dist/lib/command-keywords.js +18 -0
- package/dist/lib/command-keywords.js.map +1 -0
- package/dist/lib/daemon-state.d.ts +24 -0
- package/dist/lib/daemon-state.js +47 -0
- package/dist/lib/daemon-state.js.map +1 -0
- package/dist/lib/destructive-mode.d.ts +2 -0
- package/dist/lib/destructive-mode.js +13 -0
- package/dist/lib/destructive-mode.js.map +1 -0
- package/dist/lib/devices.d.ts +151 -0
- package/dist/lib/devices.js +383 -0
- package/dist/lib/devices.js.map +1 -0
- package/dist/lib/idempotency.d.ts +46 -0
- package/dist/lib/idempotency.js +107 -0
- package/dist/lib/idempotency.js.map +1 -0
- package/dist/lib/plan-store.d.ts +19 -0
- package/dist/lib/plan-store.js +69 -0
- package/dist/lib/plan-store.js.map +1 -0
- package/dist/lib/request-context.d.ts +7 -0
- package/dist/lib/request-context.js +13 -0
- package/dist/lib/request-context.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/logger.d.ts +4 -0
- package/dist/logger.js +17 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp/device-history.d.ts +36 -0
- package/dist/mcp/device-history.js +146 -0
- package/dist/mcp/device-history.js.map +1 -0
- package/dist/mcp/events-subscription.d.ts +45 -0
- package/dist/mcp/events-subscription.js +214 -0
- package/dist/mcp/events-subscription.js.map +1 -0
- package/dist/mqtt/client.d.ts +25 -0
- package/dist/mqtt/client.js +181 -0
- package/dist/mqtt/client.js.map +1 -0
- package/dist/mqtt/credential.d.ts +16 -0
- package/dist/mqtt/credential.js +31 -0
- package/dist/mqtt/credential.js.map +1 -0
- package/dist/policy/add-rule.d.ts +21 -0
- package/dist/policy/add-rule.js +125 -0
- package/dist/policy/add-rule.js.map +1 -0
- package/dist/policy/diff.d.ts +21 -0
- package/dist/policy/diff.js +92 -0
- package/dist/policy/diff.js.map +1 -0
- package/dist/policy/format.d.ts +6 -0
- package/dist/policy/format.js +58 -0
- package/dist/policy/format.js.map +1 -0
- package/dist/policy/load.d.ts +32 -0
- package/dist/policy/load.js +62 -0
- package/dist/policy/load.js.map +1 -0
- package/dist/policy/migrate.d.ts +21 -0
- package/dist/policy/migrate.js +68 -0
- package/dist/policy/migrate.js.map +1 -0
- package/dist/policy/schema.d.ts +5 -0
- package/dist/policy/schema.js +19 -0
- package/dist/policy/schema.js.map +1 -0
- package/dist/policy/validate.d.ts +19 -0
- package/dist/policy/validate.js +263 -0
- package/dist/policy/validate.js.map +1 -0
- package/dist/rules/action.d.ts +65 -0
- package/dist/rules/action.js +217 -0
- package/dist/rules/action.js.map +1 -0
- package/dist/rules/audit-query.d.ts +51 -0
- package/dist/rules/audit-query.js +90 -0
- package/dist/rules/audit-query.js.map +1 -0
- package/dist/rules/conflict-analyzer.d.ts +57 -0
- package/dist/rules/conflict-analyzer.js +215 -0
- package/dist/rules/conflict-analyzer.js.map +1 -0
- package/dist/rules/cron-scheduler.d.ts +62 -0
- package/dist/rules/cron-scheduler.js +187 -0
- package/dist/rules/cron-scheduler.js.map +1 -0
- package/dist/rules/destructive.d.ts +20 -0
- package/dist/rules/destructive.js +53 -0
- package/dist/rules/destructive.js.map +1 -0
- package/dist/rules/engine.d.ts +193 -0
- package/dist/rules/engine.js +758 -0
- package/dist/rules/engine.js.map +1 -0
- package/dist/rules/matcher.d.ts +56 -0
- package/dist/rules/matcher.js +231 -0
- package/dist/rules/matcher.js.map +1 -0
- package/dist/rules/pid-file.d.ts +43 -0
- package/dist/rules/pid-file.js +96 -0
- package/dist/rules/pid-file.js.map +1 -0
- package/dist/rules/quiet-hours.d.ts +26 -0
- package/dist/rules/quiet-hours.js +46 -0
- package/dist/rules/quiet-hours.js.map +1 -0
- package/dist/rules/suggest.d.ts +20 -0
- package/dist/rules/suggest.js +96 -0
- package/dist/rules/suggest.js.map +1 -0
- package/dist/rules/throttle.d.ts +61 -0
- package/dist/rules/throttle.js +117 -0
- package/dist/rules/throttle.js.map +1 -0
- package/dist/rules/types.d.ts +117 -0
- package/dist/rules/types.js +35 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/webhook-listener.d.ts +63 -0
- package/dist/rules/webhook-listener.js +224 -0
- package/dist/rules/webhook-listener.js.map +1 -0
- package/dist/rules/webhook-token.d.ts +50 -0
- package/dist/rules/webhook-token.js +91 -0
- package/dist/rules/webhook-token.js.map +1 -0
- package/dist/schema/field-aliases.d.ts +34 -0
- package/dist/schema/field-aliases.js +132 -0
- package/dist/schema/field-aliases.js.map +1 -0
- package/dist/sinks/dispatcher.d.ts +7 -0
- package/dist/sinks/dispatcher.js +13 -0
- package/dist/sinks/dispatcher.js.map +1 -0
- package/dist/sinks/file.d.ts +6 -0
- package/dist/sinks/file.js +20 -0
- package/dist/sinks/file.js.map +1 -0
- package/dist/sinks/format.d.ts +20 -0
- package/dist/sinks/format.js +57 -0
- package/dist/sinks/format.js.map +1 -0
- package/dist/sinks/homeassistant.d.ts +18 -0
- package/dist/sinks/homeassistant.js +45 -0
- package/dist/sinks/homeassistant.js.map +1 -0
- package/dist/sinks/openclaw.d.ts +13 -0
- package/dist/sinks/openclaw.js +34 -0
- package/dist/sinks/openclaw.js.map +1 -0
- package/dist/sinks/stdout.d.ts +4 -0
- package/dist/sinks/stdout.js +6 -0
- package/dist/sinks/stdout.js.map +1 -0
- package/dist/sinks/telegram.d.ts +11 -0
- package/dist/sinks/telegram.js +29 -0
- package/dist/sinks/telegram.js.map +1 -0
- package/dist/sinks/types.d.ts +13 -0
- package/dist/sinks/types.js +2 -0
- package/dist/sinks/types.js.map +1 -0
- package/dist/sinks/webhook.d.ts +6 -0
- package/dist/sinks/webhook.js +23 -0
- package/dist/sinks/webhook.js.map +1 -0
- package/dist/status-sync/manager.d.ts +48 -0
- package/dist/status-sync/manager.js +269 -0
- package/dist/status-sync/manager.js.map +1 -0
- package/dist/utils/arg-parsers.d.ts +16 -0
- package/dist/utils/arg-parsers.js +67 -0
- package/dist/utils/arg-parsers.js.map +1 -0
- package/dist/utils/audit.d.ts +69 -0
- package/dist/utils/audit.js +122 -0
- package/dist/utils/audit.js.map +1 -0
- package/dist/utils/filter.d.ts +81 -0
- package/dist/utils/filter.js +190 -0
- package/dist/utils/filter.js.map +1 -0
- package/dist/utils/flags.d.ts +72 -0
- package/dist/utils/flags.js +187 -0
- package/dist/utils/flags.js.map +1 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +118 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/health.d.ts +48 -0
- package/dist/utils/health.js +102 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/help-json.d.ts +39 -0
- package/dist/utils/help-json.js +55 -0
- package/dist/utils/help-json.js.map +1 -0
- package/dist/utils/name-resolver.d.ts +26 -0
- package/dist/utils/name-resolver.js +138 -0
- package/dist/utils/name-resolver.js.map +1 -0
- package/dist/utils/output.d.ts +73 -0
- package/dist/utils/output.js +405 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/quota.d.ts +61 -0
- package/dist/utils/quota.js +228 -0
- package/dist/utils/quota.js.map +1 -0
- package/dist/utils/redact.d.ts +23 -0
- package/dist/utils/redact.js +69 -0
- package/dist/utils/redact.js.map +1 -0
- package/dist/utils/retry.d.ts +72 -0
- package/dist/utils/retry.js +141 -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/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import { enumArg, stringArg } from '../utils/arg-parsers.js';
|
|
2
|
+
import { printTable, printKeyValue, printJson, isJsonMode, handleError, UsageError, StructuredUsageError, exitWithError } from '../utils/output.js';
|
|
3
|
+
import { resolveFormat, resolveFields, renderRows } from '../utils/format.js';
|
|
4
|
+
import { findCatalogEntry, getEffectiveCatalog, deriveSafetyTier, getCommandSafetyReason, } from '../devices/catalog.js';
|
|
5
|
+
import { getCachedDevice, loadCache } from '../devices/cache.js';
|
|
6
|
+
import { loadDeviceMeta } from '../devices/device-meta.js';
|
|
7
|
+
import { resolveDeviceId, ALL_STRATEGIES } from '../utils/name-resolver.js';
|
|
8
|
+
import { fetchDeviceList, fetchDeviceStatus, executeCommand, describeDevice, validateCommand, isDestructiveCommand, getDestructiveReason, buildHubLocationMap, DeviceNotFoundError, } from '../lib/devices.js';
|
|
9
|
+
import { parseFilterExpr, matchClause, FilterSyntaxError } from '../utils/filter.js';
|
|
10
|
+
import { validateParameter } from '../devices/param-validator.js';
|
|
11
|
+
import { registerBatchCommand } from './batch.js';
|
|
12
|
+
import { registerWatchCommand } from './watch.js';
|
|
13
|
+
import { registerExplainCommand } from './explain.js';
|
|
14
|
+
import { registerExpandCommand } from './expand.js';
|
|
15
|
+
import { registerDevicesMetaCommand } from './device-meta.js';
|
|
16
|
+
import { isDryRun } from '../utils/flags.js';
|
|
17
|
+
import { DryRunSignal } from '../api/client.js';
|
|
18
|
+
import { resolveField, resolveFieldList, listSupportedFieldInputs } from '../schema/field-aliases.js';
|
|
19
|
+
import { allowsDirectDestructiveExecution, destructiveExecutionHint } from '../lib/destructive-mode.js';
|
|
20
|
+
const EXPAND_HINTS = {
|
|
21
|
+
'Air Conditioner': { command: 'setAll', flags: '--temp 26 --mode cool --fan low --power on' },
|
|
22
|
+
'Curtain': { command: 'setPosition', flags: '--position 50 --mode silent' },
|
|
23
|
+
'Curtain 3': { command: 'setPosition', flags: '--position 50' },
|
|
24
|
+
'Blind Tilt': { command: 'setPosition', flags: '--direction up --angle 50' },
|
|
25
|
+
'Relay Switch 2PM': { command: 'setMode', flags: '--channel 1 --mode edge' },
|
|
26
|
+
};
|
|
27
|
+
export function registerDevicesCommand(program) {
|
|
28
|
+
const COMMAND_TYPES = ['command', 'customize'];
|
|
29
|
+
const devices = program
|
|
30
|
+
.command('devices')
|
|
31
|
+
.description('Manage and control SwitchBot devices')
|
|
32
|
+
.addHelpText('after', `
|
|
33
|
+
Typical workflow:
|
|
34
|
+
1. Discover your devices → switchbot devices list
|
|
35
|
+
2. Describe a specific device → switchbot devices describe <id>
|
|
36
|
+
3. Or look up a type offline → switchbot devices types
|
|
37
|
+
switchbot devices commands <type>
|
|
38
|
+
4. Send a command → switchbot devices command <id> <cmd> [param]
|
|
39
|
+
|
|
40
|
+
Online subcommands (hit the SwitchBot API):
|
|
41
|
+
list List all physical + IR remote devices on your account
|
|
42
|
+
status Query a device's real-time status values
|
|
43
|
+
command Send a control command (turnOn, setColor, setAll, startClean, …)
|
|
44
|
+
describe Show one device's metadata + its supported commands + status fields
|
|
45
|
+
|
|
46
|
+
Offline subcommands (built-in catalog, no API call):
|
|
47
|
+
types List every device type this CLI knows about
|
|
48
|
+
commands Show commands + parameter formats + status fields for a type
|
|
49
|
+
|
|
50
|
+
Run any subcommand with --help for its own flags and examples.
|
|
51
|
+
`);
|
|
52
|
+
// switchbot devices list
|
|
53
|
+
devices
|
|
54
|
+
.command('list')
|
|
55
|
+
.alias('ls')
|
|
56
|
+
.description('List all physical devices and IR remote devices on the account')
|
|
57
|
+
.addHelpText('after', `
|
|
58
|
+
Default columns: deviceId, deviceName, type, category
|
|
59
|
+
Pass --wide for the full operator view: + controlType, family, roomID, room, hub, cloud
|
|
60
|
+
--fields accepts any subset of all column names (exit 2 on unknown names).
|
|
61
|
+
|
|
62
|
+
type - physical deviceType (e.g. "Bot", "Curtain") or IR remoteType (e.g. "TV")
|
|
63
|
+
category - "physical" or "ir"
|
|
64
|
+
controlType - functional classification from the API (e.g. "Bot", "Switch",
|
|
65
|
+
"TV") — may differ from 'type' and groups devices by behavior
|
|
66
|
+
family - home/family name (IR remotes inherit this from their bound Hub)
|
|
67
|
+
roomID - internal room identifier (IR remotes inherit from their
|
|
68
|
+
bound Hub; — when unassigned/unknown)
|
|
69
|
+
room - room name this device is assigned to (IR remotes inherit from
|
|
70
|
+
Hub; — when unassigned/unknown)
|
|
71
|
+
hub - "—" when the device is its own hub or hubDeviceId is empty
|
|
72
|
+
cloud - ✓/✗: whether cloud service is enabled (— for IR remotes)
|
|
73
|
+
|
|
74
|
+
controlType, family/room, and roomID require the 'src: OpenClaw' header, which
|
|
75
|
+
this CLI always sends. (IR family/room inheritance is computed client-side for
|
|
76
|
+
the table; --json returns the raw API body unchanged.)
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
$ switchbot devices list
|
|
80
|
+
$ switchbot devices list --wide
|
|
81
|
+
$ switchbot devices list --format tsv --fields deviceId,deviceName,type,category
|
|
82
|
+
$ switchbot devices list --json | jq '.deviceList[] | select(.familyName == "home")'
|
|
83
|
+
$ switchbot devices list --json | jq '[.deviceList[], .infraredRemoteList[]] | group_by(.familyName)'
|
|
84
|
+
$ switchbot devices list --filter type="Air Conditioner"
|
|
85
|
+
$ switchbot devices list --filter category=ir
|
|
86
|
+
$ switchbot devices list --filter name=living,category=physical
|
|
87
|
+
$ switchbot devices list --filter 'name~living' # explicit substring
|
|
88
|
+
$ switchbot devices list --filter 'type=/Air.*/' # regex (case-insensitive)
|
|
89
|
+
`)
|
|
90
|
+
.option('--wide', 'Show all columns (controlType, family, roomID, room, hub, cloud)')
|
|
91
|
+
.option('--show-hidden', 'Include devices hidden via "devices meta set --hide"')
|
|
92
|
+
.option('--filter <expr>', 'Filter devices: comma-separated clauses. Each clause is "key=value" (substring; exact for category), "key!=value" (negated substring), "key~value" (explicit substring), or "key=/regex/" (case-insensitive regex). Supported keys: deviceId/id, deviceName/name, deviceType/type, controlType, roomName/room, category, familyName/family, hubDeviceId/hub, roomID/roomid, enableCloudService/cloud, alias.', stringArg('--filter'))
|
|
93
|
+
.action(async (options) => {
|
|
94
|
+
try {
|
|
95
|
+
const body = await fetchDeviceList();
|
|
96
|
+
const { deviceList, infraredRemoteList } = body;
|
|
97
|
+
const fmt = resolveFormat();
|
|
98
|
+
const deviceMeta = loadDeviceMeta();
|
|
99
|
+
const hubLocation = buildHubLocationMap(deviceList);
|
|
100
|
+
// Parse --filter into a list of clauses. Shared grammar across
|
|
101
|
+
// `devices list`, `devices batch`, and `events tail` / `mqtt-tail`.
|
|
102
|
+
const LIST_KEYS = ['deviceId', 'type', 'name', 'category', 'room', 'controlType',
|
|
103
|
+
'family', 'hub', 'roomID', 'cloud', 'alias'];
|
|
104
|
+
const LIST_FILTER_CANONICAL = ['deviceId', 'deviceName', 'deviceType', 'controlType',
|
|
105
|
+
'roomName', 'category', 'familyName', 'hubDeviceId', 'roomID',
|
|
106
|
+
'enableCloudService', 'alias'];
|
|
107
|
+
const LIST_FILTER_TO_RUNTIME = {
|
|
108
|
+
deviceId: 'deviceId',
|
|
109
|
+
deviceName: 'name',
|
|
110
|
+
deviceType: 'type',
|
|
111
|
+
controlType: 'controlType',
|
|
112
|
+
roomName: 'room',
|
|
113
|
+
category: 'category',
|
|
114
|
+
familyName: 'family',
|
|
115
|
+
hubDeviceId: 'hub',
|
|
116
|
+
roomID: 'roomID',
|
|
117
|
+
enableCloudService: 'cloud',
|
|
118
|
+
alias: 'alias',
|
|
119
|
+
};
|
|
120
|
+
let listClauses = null;
|
|
121
|
+
if (options.filter) {
|
|
122
|
+
try {
|
|
123
|
+
listClauses = parseFilterExpr(options.filter, LIST_KEYS, {
|
|
124
|
+
resolveKey: (input) => {
|
|
125
|
+
const canonical = resolveField(input, LIST_FILTER_CANONICAL);
|
|
126
|
+
return LIST_FILTER_TO_RUNTIME[canonical];
|
|
127
|
+
},
|
|
128
|
+
supportedKeys: listSupportedFieldInputs(LIST_FILTER_CANONICAL),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
if (err instanceof FilterSyntaxError)
|
|
133
|
+
throw new UsageError(err.message);
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const matchesFilter = (entry) => {
|
|
138
|
+
if (!listClauses || listClauses.length === 0)
|
|
139
|
+
return true;
|
|
140
|
+
for (const c of listClauses) {
|
|
141
|
+
const fieldVal = entry[c.key] ?? '';
|
|
142
|
+
if (!matchClause(fieldVal, c))
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
};
|
|
147
|
+
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
148
|
+
if (listClauses) {
|
|
149
|
+
const filteredDeviceList = deviceList.filter((d) => matchesFilter({ deviceId: d.deviceId, type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '', controlType: d.controlType || '', family: d.familyName || '', hub: d.hubDeviceId || '', roomID: d.roomID || '', cloud: String(d.enableCloudService), alias: deviceMeta.devices[d.deviceId]?.alias || '' }));
|
|
150
|
+
const filteredIrList = infraredRemoteList.filter((d) => {
|
|
151
|
+
const inherited = hubLocation.get(d.hubDeviceId);
|
|
152
|
+
return matchesFilter({ deviceId: d.deviceId, type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '', controlType: d.controlType || '', family: inherited?.family || '', hub: d.hubDeviceId || '', roomID: inherited?.roomID || '', cloud: '', alias: deviceMeta.devices[d.deviceId]?.alias || '' });
|
|
153
|
+
});
|
|
154
|
+
printJson({ ok: true, deviceList: filteredDeviceList, infraredRemoteList: filteredIrList });
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
printJson({ ok: true, ...body });
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const narrowHeaders = ['deviceId', 'deviceName', 'type', 'category'];
|
|
162
|
+
const wideHeaders = ['deviceId', 'deviceName', 'type', 'category', 'controlType', 'family', 'roomID', 'room', 'hub', 'cloud', 'alias'];
|
|
163
|
+
const userFields = resolveFields();
|
|
164
|
+
const headers = userFields ? wideHeaders : (options.wide ? wideHeaders : narrowHeaders);
|
|
165
|
+
const rows = [];
|
|
166
|
+
for (const d of deviceList) {
|
|
167
|
+
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
168
|
+
continue;
|
|
169
|
+
if (!matchesFilter({ deviceId: d.deviceId, type: d.deviceType || '', name: d.deviceName, category: 'physical', room: d.roomName || '', controlType: d.controlType || '', family: d.familyName || '', hub: d.hubDeviceId || '', roomID: d.roomID || '', cloud: String(d.enableCloudService), alias: deviceMeta.devices[d.deviceId]?.alias || '' }))
|
|
170
|
+
continue;
|
|
171
|
+
rows.push([
|
|
172
|
+
d.deviceId,
|
|
173
|
+
d.deviceName,
|
|
174
|
+
d.deviceType || '—',
|
|
175
|
+
'physical',
|
|
176
|
+
d.controlType || '—',
|
|
177
|
+
d.familyName || '—',
|
|
178
|
+
d.roomID || '—',
|
|
179
|
+
d.roomName || '—',
|
|
180
|
+
!d.hubDeviceId || d.hubDeviceId === '000000000000' ? '—' : d.hubDeviceId,
|
|
181
|
+
d.enableCloudService,
|
|
182
|
+
deviceMeta.devices[d.deviceId]?.alias ?? '—',
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
for (const d of infraredRemoteList) {
|
|
186
|
+
if (!options.showHidden && deviceMeta.devices[d.deviceId]?.hidden)
|
|
187
|
+
continue;
|
|
188
|
+
const inherited = hubLocation.get(d.hubDeviceId);
|
|
189
|
+
if (!matchesFilter({ deviceId: d.deviceId, type: d.remoteType, name: d.deviceName, category: 'ir', room: inherited?.room || '', controlType: d.controlType || '', family: inherited?.family || '', hub: d.hubDeviceId || '', roomID: inherited?.roomID || '', cloud: '', alias: deviceMeta.devices[d.deviceId]?.alias || '' }))
|
|
190
|
+
continue;
|
|
191
|
+
rows.push([
|
|
192
|
+
d.deviceId,
|
|
193
|
+
d.deviceName,
|
|
194
|
+
d.remoteType,
|
|
195
|
+
'ir',
|
|
196
|
+
d.controlType || '—',
|
|
197
|
+
inherited?.family || '—',
|
|
198
|
+
inherited?.roomID || '—',
|
|
199
|
+
inherited?.room || '—',
|
|
200
|
+
d.hubDeviceId,
|
|
201
|
+
null,
|
|
202
|
+
deviceMeta.devices[d.deviceId]?.alias ?? '—',
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
if (rows.length === 0 && fmt === 'table') {
|
|
206
|
+
console.log(listClauses ? 'No devices matched the filter.' : 'No devices found');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const defaultFields = options.wide ? undefined : narrowHeaders;
|
|
210
|
+
// Accept API field names and short aliases alongside canonical column names
|
|
211
|
+
const DEVICE_LIST_ALIASES = {
|
|
212
|
+
id: 'deviceId',
|
|
213
|
+
name: 'deviceName',
|
|
214
|
+
deviceType: 'type',
|
|
215
|
+
type: 'type',
|
|
216
|
+
roomName: 'room',
|
|
217
|
+
familyName: 'family',
|
|
218
|
+
hubDeviceId: 'hub',
|
|
219
|
+
enableCloudService: 'cloud',
|
|
220
|
+
controlType: 'controlType',
|
|
221
|
+
deviceName: 'deviceName',
|
|
222
|
+
deviceId: 'deviceId',
|
|
223
|
+
category: 'category',
|
|
224
|
+
alias: 'alias',
|
|
225
|
+
};
|
|
226
|
+
renderRows(wideHeaders, rows, fmt, userFields ?? defaultFields, DEVICE_LIST_ALIASES);
|
|
227
|
+
if (fmt === 'table') {
|
|
228
|
+
const totalLabel = listClauses
|
|
229
|
+
? `${rows.length} match(es) (${deviceList.length} physical + ${infraredRemoteList.length} IR before filter)`
|
|
230
|
+
: `${deviceList.length} physical device(s), ${infraredRemoteList.length} IR remote device(s)`;
|
|
231
|
+
console.log(`\nTotal: ${totalLabel}`);
|
|
232
|
+
console.log(`Tip: 'switchbot devices describe <deviceId>' shows a device's supported commands.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
handleError(error);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// switchbot devices status <deviceId>
|
|
240
|
+
devices
|
|
241
|
+
.command('status')
|
|
242
|
+
.description('Query the real-time status of a specific device')
|
|
243
|
+
.argument('[deviceId]', 'Device ID from "devices list" (or use --name or --ids)')
|
|
244
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
245
|
+
.option('--name-strategy <s>', `Name match strategy: ${ALL_STRATEGIES.join('|')} (default: fuzzy)`, stringArg('--name-strategy'))
|
|
246
|
+
.option('--name-type <type>', 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg('--name-type'))
|
|
247
|
+
.option('--name-category <cat>', 'Narrow --name by category: physical|ir', enumArg('--name-category', ['physical', 'ir']))
|
|
248
|
+
.option('--name-room <room>', 'Narrow --name by room name (substring match)', stringArg('--name-room'))
|
|
249
|
+
.option('--ids <list>', 'Comma-separated device IDs for batch status (incompatible with --name)', stringArg('--ids'))
|
|
250
|
+
.addHelpText('after', `
|
|
251
|
+
Status fields vary by device type. To discover them without a live call:
|
|
252
|
+
|
|
253
|
+
switchbot devices commands <type> (prints the "Status fields" section)
|
|
254
|
+
|
|
255
|
+
For --fields: run the command once with --format yaml (no --fields) to see
|
|
256
|
+
all field names returned by your specific device, then narrow with --fields.
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
$ switchbot devices status ABC123DEF456
|
|
260
|
+
$ switchbot devices status --name "Living Room AC"
|
|
261
|
+
$ switchbot devices status ABC123DEF456 --json
|
|
262
|
+
$ switchbot devices status ABC123DEF456 --format yaml
|
|
263
|
+
$ switchbot devices status ABC123DEF456 --format tsv --fields power,battery
|
|
264
|
+
$ switchbot devices status ABC123DEF456 --json | jq '.data.battery'
|
|
265
|
+
$ switchbot devices status --ids ABC123,DEF456,GHI789
|
|
266
|
+
$ switchbot devices status --ids ABC123,DEF456 --fields power,battery
|
|
267
|
+
`)
|
|
268
|
+
.action(async (deviceIdArg, options) => {
|
|
269
|
+
try {
|
|
270
|
+
// Batch mode: --ids id1,id2,id3
|
|
271
|
+
if (options.ids) {
|
|
272
|
+
if (options.name)
|
|
273
|
+
throw new UsageError('--ids and --name cannot be used together.');
|
|
274
|
+
const ids = options.ids.split(',').map((s) => s.trim()).filter(Boolean);
|
|
275
|
+
if (ids.length === 0)
|
|
276
|
+
throw new UsageError('--ids requires at least one device ID.');
|
|
277
|
+
const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
|
|
278
|
+
const fetchedAt = new Date().toISOString();
|
|
279
|
+
const batch = results.map((r, i) => r.status === 'fulfilled'
|
|
280
|
+
? { deviceId: ids[i], ok: true, _fetchedAt: fetchedAt, ...r.value }
|
|
281
|
+
: { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) });
|
|
282
|
+
const batchFmt = resolveFormat();
|
|
283
|
+
if (isJsonMode() || batchFmt === 'json') {
|
|
284
|
+
printJson(batch);
|
|
285
|
+
}
|
|
286
|
+
else if (batchFmt === 'jsonl') {
|
|
287
|
+
for (const entry of batch) {
|
|
288
|
+
console.log(JSON.stringify(entry));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const rawFields = resolveFields();
|
|
293
|
+
for (const entry of batch) {
|
|
294
|
+
const { deviceId, ok, error, _fetchedAt: ts, ...status } = entry;
|
|
295
|
+
console.log(`\n─── ${String(deviceId)} ───`);
|
|
296
|
+
if (!ok) {
|
|
297
|
+
console.error(` error: ${String(error)}`);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const statusMap = status;
|
|
301
|
+
const fields = rawFields
|
|
302
|
+
? resolveFieldList(rawFields, Object.keys(statusMap))
|
|
303
|
+
: undefined;
|
|
304
|
+
const displayStatus = fields
|
|
305
|
+
? Object.fromEntries(fields.map((f) => [f, statusMap[f] ?? null]))
|
|
306
|
+
: statusMap;
|
|
307
|
+
printKeyValue(displayStatus);
|
|
308
|
+
console.error(` fetched at ${String(ts)}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const deviceId = resolveDeviceId(deviceIdArg, options.name, {
|
|
315
|
+
strategy: options.nameStrategy ?? 'fuzzy',
|
|
316
|
+
type: options.nameType,
|
|
317
|
+
category: options.nameCategory,
|
|
318
|
+
room: options.nameRoom,
|
|
319
|
+
});
|
|
320
|
+
const body = await fetchDeviceStatus(deviceId);
|
|
321
|
+
const fetchedAt = new Date().toISOString();
|
|
322
|
+
const fmt = resolveFormat();
|
|
323
|
+
if (fmt === 'json' && process.argv.includes('--json')) {
|
|
324
|
+
printJson({ ...body, _fetchedAt: fetchedAt });
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (fmt !== 'table') {
|
|
328
|
+
const statusWithTs = { ...body, _fetchedAt: fetchedAt };
|
|
329
|
+
const allHeaders = Object.keys(statusWithTs);
|
|
330
|
+
const allRows = [Object.values(statusWithTs)];
|
|
331
|
+
const rawFields = resolveFields();
|
|
332
|
+
const fields = rawFields
|
|
333
|
+
? resolveFieldList(rawFields, allHeaders)
|
|
334
|
+
: undefined;
|
|
335
|
+
renderRows(allHeaders, allRows, fmt, fields);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
printKeyValue(body);
|
|
339
|
+
console.error(`\nfetched at ${fetchedAt}`);
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
handleError(error);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
// switchbot devices command <deviceId> <command> [parameter]
|
|
346
|
+
devices
|
|
347
|
+
.command('command')
|
|
348
|
+
.description('Send a control command to a device')
|
|
349
|
+
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
350
|
+
.argument('[cmd]', 'Command name, e.g. turnOn, turnOff, setColor, setBrightness, setAll, startClean')
|
|
351
|
+
.argument('[parameter]', 'Command parameter. Omit for commands like turnOn/turnOff (defaults to "default"). Format depends on the command (see below). Negative numbers like -1 are accepted as-is (use `--` before them only if Commander mis-parses in your shell).')
|
|
352
|
+
.allowUnknownOption()
|
|
353
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
354
|
+
.option('--name-strategy <s>', `Name match strategy: ${ALL_STRATEGIES.join('|')} (default for command: require-unique)`, stringArg('--name-strategy'))
|
|
355
|
+
.option('--name-type <type>', 'Narrow --name by device type (e.g. "Bot", "Color Bulb")', stringArg('--name-type'))
|
|
356
|
+
.option('--name-category <cat>', 'Narrow --name by category: physical|ir', enumArg('--name-category', ['physical', 'ir']))
|
|
357
|
+
.option('--name-room <room>', 'Narrow --name by room name (substring match)', stringArg('--name-room'))
|
|
358
|
+
.option('--type <commandType>', 'Command type: "command" for built-in commands (default), "customize" for user-defined IR buttons', enumArg('--type', COMMAND_TYPES), 'command')
|
|
359
|
+
.option('--yes', 'Confirm a destructive command in an explicit dev profile. --dry-run is always allowed without --yes.')
|
|
360
|
+
.option('--explain', 'Print a human-readable summary of what this command would do (risk level, device type, idempotency) then exit without executing.')
|
|
361
|
+
.option('--allow-unknown-device', 'Allow targeting a deviceId that is not in the local cache. By default unknown IDs exit 2 so --dry-run is a reliable pre-flight gate; use this flag for scripted pass-through.')
|
|
362
|
+
.option('--skip-param-validation', 'Skip client-side parameter validation (escape hatch — prefer fixing the argument over using this).')
|
|
363
|
+
.option('--idempotency-key <key>', 'Client-supplied key to dedupe retries. process-local 60s window; cache is per Node process (MCP session, batch run, plan run). Independent CLI invocations do not share cache.', stringArg('--idempotency-key'))
|
|
364
|
+
.addHelpText('after', `
|
|
365
|
+
────────────────────────────────────────────────────────────────────────
|
|
366
|
+
For the full list of commands a specific device supports — and their
|
|
367
|
+
exact parameter formats — run:
|
|
368
|
+
|
|
369
|
+
switchbot devices commands <type> (e.g. Bot, Curtain, "Smart Lock")
|
|
370
|
+
|
|
371
|
+
The catalog is the authoritative per-device reference. This page only
|
|
372
|
+
covers the generic mechanics that apply to every device.
|
|
373
|
+
────────────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
Rules:
|
|
376
|
+
• Command names are CASE-SENSITIVE (e.g. SetChannel, FastForward, volumeAdd).
|
|
377
|
+
• Quote any parameter containing ':' ',' ';' or '{ }' to protect it from the shell.
|
|
378
|
+
• The parameter is parsed as JSON when possible; otherwise passed through as a string.
|
|
379
|
+
• Omit the parameter for no-arg commands — it auto-defaults to "default".
|
|
380
|
+
• Use --type customize to trigger a user-defined IR button by name.
|
|
381
|
+
|
|
382
|
+
Generic parameter shapes (see 'devices commands <type>' for which one applies):
|
|
383
|
+
|
|
384
|
+
(none) turnOn, turnOff, toggle, press, play, pause, …
|
|
385
|
+
<integer> setBrightness 75, setColorTemperature 4000, SetChannel 15
|
|
386
|
+
<R:G:B> setColor "255:0:0"
|
|
387
|
+
<direction;angle> setPosition "up;60" (Blind Tilt)
|
|
388
|
+
<a,b,c,…> setAll "26,1,3,on" (IR AC)
|
|
389
|
+
<json object> startClean '{"action":"sweep","param":{"fanLevel":2,"times":1}}'
|
|
390
|
+
|
|
391
|
+
Common errors:
|
|
392
|
+
160 command not supported by this device
|
|
393
|
+
161 device offline (BLE devices need a Hub bridge)
|
|
394
|
+
171 hub offline
|
|
395
|
+
|
|
396
|
+
Safety:
|
|
397
|
+
Destructive commands (Smart Lock unlock, Garage Door Opener turnOn/turnOff,
|
|
398
|
+
Keypad createKey/deleteKey, …) are blocked by default. Use the reviewed plan
|
|
399
|
+
flow instead, or --dry-run to preview without sending.
|
|
400
|
+
|
|
401
|
+
Examples:
|
|
402
|
+
$ switchbot devices command ABC123 turnOn
|
|
403
|
+
$ switchbot devices command ABC123 setColor "255:0:0"
|
|
404
|
+
$ switchbot devices command ABC123 setAll "26,1,3,on"
|
|
405
|
+
$ switchbot devices command ABC123 startClean '{"action":"sweep","param":{"fanLevel":2,"times":1}}'
|
|
406
|
+
$ switchbot devices command ABC123 "MyButton" --type customize
|
|
407
|
+
$ switchbot devices command <lockId> unlock --dry-run
|
|
408
|
+
`)
|
|
409
|
+
.action(async (deviceIdArg, cmdArg, parameter, options) => {
|
|
410
|
+
// Declared outside try so the DryRunSignal catch branch can reference them.
|
|
411
|
+
let _deviceId;
|
|
412
|
+
let _cmd;
|
|
413
|
+
let _parsedParam;
|
|
414
|
+
try {
|
|
415
|
+
// BUG-FIX: When --name is provided, Commander fills positionals left-to-right
|
|
416
|
+
// starting at [deviceId]. Shift them back to their semantic slots.
|
|
417
|
+
let cmd;
|
|
418
|
+
let effectiveDeviceIdArg;
|
|
419
|
+
if (options.name) {
|
|
420
|
+
// `--name "x" <cmd> [parameter]` → Commander binds deviceIdArg=<cmd>, cmdArg=[parameter].
|
|
421
|
+
if (!deviceIdArg) {
|
|
422
|
+
throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
|
|
423
|
+
}
|
|
424
|
+
cmd = deviceIdArg;
|
|
425
|
+
if (cmdArg !== undefined) {
|
|
426
|
+
if (parameter !== undefined) {
|
|
427
|
+
throw new UsageError('Too many positional arguments after --name. Expected: --name <query> <cmd> [parameter].');
|
|
428
|
+
}
|
|
429
|
+
parameter = cmdArg;
|
|
430
|
+
}
|
|
431
|
+
effectiveDeviceIdArg = undefined;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
if (!cmdArg) {
|
|
435
|
+
throw new UsageError('Command name is required (e.g. turnOn, turnOff, setAll).');
|
|
436
|
+
}
|
|
437
|
+
cmd = cmdArg;
|
|
438
|
+
effectiveDeviceIdArg = deviceIdArg;
|
|
439
|
+
}
|
|
440
|
+
const deviceId = resolveDeviceId(effectiveDeviceIdArg, options.name, {
|
|
441
|
+
// Mutating command → default require-unique (never silently pick between ambiguous matches).
|
|
442
|
+
strategy: options.nameStrategy ?? 'require-unique',
|
|
443
|
+
type: options.nameType,
|
|
444
|
+
category: options.nameCategory,
|
|
445
|
+
room: options.nameRoom,
|
|
446
|
+
});
|
|
447
|
+
_deviceId = deviceId;
|
|
448
|
+
if (!getCachedDevice(deviceId)) {
|
|
449
|
+
if (options.allowUnknownDevice) {
|
|
450
|
+
console.error(`Note: device ${deviceId} is not in the local cache — run 'switchbot devices list' first to enable command validation. (--allow-unknown-device is set, continuing.)`);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const cache = loadCache();
|
|
454
|
+
const allIds = cache ? Object.keys(cache.devices) : [];
|
|
455
|
+
const candidates = allIds
|
|
456
|
+
.filter((id) => id.toLowerCase().includes(deviceId.toLowerCase()) || id.startsWith(deviceId.slice(0, 4)))
|
|
457
|
+
.slice(0, 5)
|
|
458
|
+
.map((id) => {
|
|
459
|
+
const dev = cache.devices[id];
|
|
460
|
+
return { deviceId: id, name: dev.name, type: dev.type };
|
|
461
|
+
});
|
|
462
|
+
throw new StructuredUsageError(`Unknown deviceId "${deviceId}" — not in local cache. Run 'switchbot devices list' first, or pass --allow-unknown-device to bypass this check.`, {
|
|
463
|
+
error: 'unknown_device_id',
|
|
464
|
+
deviceId,
|
|
465
|
+
candidates,
|
|
466
|
+
hint: `Pass --allow-unknown-device to skip this check (and rely on the API for validation).`,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const validation = validateCommand(deviceId, cmd, parameter, options.type);
|
|
471
|
+
if (!validation.ok) {
|
|
472
|
+
const err = validation.error;
|
|
473
|
+
let hint = err.hint;
|
|
474
|
+
if (err.kind === 'unknown-command') {
|
|
475
|
+
const cached = getCachedDevice(deviceId);
|
|
476
|
+
if (cached) {
|
|
477
|
+
const extra = `Run 'switchbot devices commands ${JSON.stringify(cached.type)}' for parameter formats and descriptions.\n` +
|
|
478
|
+
`(If the catalog is out of date, run 'switchbot devices list' to refresh the local cache, or pass --type customize for custom IR buttons.)`;
|
|
479
|
+
hint = hint ? `${hint}\n${extra}` : extra;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
exitWithError({
|
|
483
|
+
code: 2,
|
|
484
|
+
kind: 'usage',
|
|
485
|
+
message: err.message,
|
|
486
|
+
hint,
|
|
487
|
+
context: { validationKind: err.kind },
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
// Case-only mismatch: emit a warning and continue with the canonical name.
|
|
491
|
+
if (validation.caseNormalizedFrom && validation.normalized) {
|
|
492
|
+
console.error(`Note: '${validation.caseNormalizedFrom}' normalized to '${validation.normalized}' (case mismatch). Use exact casing to silence this warning.`);
|
|
493
|
+
cmd = validation.normalized;
|
|
494
|
+
}
|
|
495
|
+
else if (validation.normalized) {
|
|
496
|
+
cmd = validation.normalized;
|
|
497
|
+
}
|
|
498
|
+
// Raw-parameter validation (runs for known (deviceType, command) pairs only).
|
|
499
|
+
const cachedForParam = getCachedDevice(deviceId);
|
|
500
|
+
if (cachedForParam && options.type === 'command' && !options.skipParamValidation) {
|
|
501
|
+
const paramCheck = validateParameter(cachedForParam.type, cmd, parameter);
|
|
502
|
+
if (!paramCheck.ok) {
|
|
503
|
+
exitWithError({
|
|
504
|
+
message: `Error: ${paramCheck.error}`,
|
|
505
|
+
context: { command: cmd, deviceType: cachedForParam.type, deviceId, humanHint: paramCheck.error },
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
if (paramCheck.normalized !== undefined)
|
|
509
|
+
parameter = paramCheck.normalized;
|
|
510
|
+
}
|
|
511
|
+
const cachedForGuard = getCachedDevice(deviceId);
|
|
512
|
+
// --explain: print intent + risk metadata without executing
|
|
513
|
+
if (options.explain) {
|
|
514
|
+
const isDestructive = isDestructiveCommand(cachedForGuard?.type, cmd, options.type);
|
|
515
|
+
const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
|
|
516
|
+
const riskLevel = isDestructive ? 'high' : options.type === 'command' ? 'medium' : 'low';
|
|
517
|
+
const recommendedMode = isDestructive ? 'review-before-execute' : 'direct';
|
|
518
|
+
if (isJsonMode()) {
|
|
519
|
+
printJson({
|
|
520
|
+
intent: `Send command "${cmd}" to device ${deviceId}`,
|
|
521
|
+
deviceType: cachedForGuard?.type ?? 'unknown',
|
|
522
|
+
deviceName: cachedForGuard?.name ?? null,
|
|
523
|
+
command: cmd,
|
|
524
|
+
parameter: parameter ?? null,
|
|
525
|
+
commandType: options.type,
|
|
526
|
+
riskLevel,
|
|
527
|
+
requiresConfirmation: isDestructive,
|
|
528
|
+
safetyReason: reason ?? null,
|
|
529
|
+
recommendedMode,
|
|
530
|
+
note: 'This is a dry explanation only — command was NOT executed.',
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(`Command: ${cmd} on device ${deviceId}`);
|
|
535
|
+
console.log(`Device type: ${cachedForGuard?.type ?? 'unknown'}${cachedForGuard?.name ? ` (${cachedForGuard.name})` : ''}`);
|
|
536
|
+
console.log(`Parameter: ${parameter ?? '(none)'}`);
|
|
537
|
+
console.log(`Risk level: ${riskLevel}`);
|
|
538
|
+
if (reason)
|
|
539
|
+
console.log(`Safety reason: ${reason}`);
|
|
540
|
+
if (isDestructive)
|
|
541
|
+
console.log(`Requires plan approval by default. ${destructiveExecutionHint()}`);
|
|
542
|
+
console.log('(not executed — remove --explain to run)');
|
|
543
|
+
}
|
|
544
|
+
process.exit(0);
|
|
545
|
+
}
|
|
546
|
+
const destructive = isDestructiveCommand(cachedForGuard?.type, cmd, options.type);
|
|
547
|
+
if (!isDryRun() && destructive && !options.yes && !allowsDirectDestructiveExecution()) {
|
|
548
|
+
const typeLabel = cachedForGuard?.type ?? 'unknown';
|
|
549
|
+
const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
|
|
550
|
+
exitWithError({
|
|
551
|
+
kind: 'guard',
|
|
552
|
+
message: `Direct destructive execution disabled — destructive command "${cmd}" on ${typeLabel}.`,
|
|
553
|
+
hint: reason ? `${destructiveExecutionHint()} Reason: ${reason}` : destructiveExecutionHint(),
|
|
554
|
+
context: {
|
|
555
|
+
command: cmd,
|
|
556
|
+
deviceType: typeLabel,
|
|
557
|
+
deviceId,
|
|
558
|
+
directExecutionAllowed: false,
|
|
559
|
+
requiredWorkflow: 'plan-approval',
|
|
560
|
+
...(reason ? { safetyReason: reason, destructiveReason: reason } : {}),
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
if (!options.yes && !isDryRun() && destructive) {
|
|
565
|
+
const typeLabel = cachedForGuard?.type ?? 'unknown';
|
|
566
|
+
const reason = getDestructiveReason(cachedForGuard?.type, cmd, options.type);
|
|
567
|
+
exitWithError({
|
|
568
|
+
kind: 'guard',
|
|
569
|
+
message: `Refusing to run destructive command "${cmd}" on ${typeLabel} without --yes.`,
|
|
570
|
+
hint: reason
|
|
571
|
+
? `Re-run with --yes only from an explicit dev profile, or use the reviewed plan flow. Reason: ${reason}`
|
|
572
|
+
: `Re-run with --yes only from an explicit dev profile, use the reviewed plan flow, or --dry-run to preview without sending.`,
|
|
573
|
+
context: { command: cmd, deviceType: typeLabel, deviceId, ...(reason ? { safetyReason: reason, destructiveReason: reason } : {}) },
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
// Warn when --yes is given but the command is not destructive (no-op flag)
|
|
577
|
+
if (options.yes && !destructive && !isDryRun()) {
|
|
578
|
+
console.error(`Note: --yes has no effect; "${cmd}" is not a destructive command.`);
|
|
579
|
+
}
|
|
580
|
+
// parameter may be a JSON object string (e.g. S10 startClean) or a plain string
|
|
581
|
+
let parsedParam = parameter ?? 'default';
|
|
582
|
+
if (parameter) {
|
|
583
|
+
try {
|
|
584
|
+
parsedParam = JSON.parse(parameter);
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
// keep as string
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Capture for DryRunSignal catch branch (which runs after executeCommand throws).
|
|
591
|
+
_cmd = cmd;
|
|
592
|
+
_parsedParam = parsedParam;
|
|
593
|
+
const body = await executeCommand(deviceId, cmd, parsedParam, options.type, undefined, { idempotencyKey: options.idempotencyKey });
|
|
594
|
+
const isIr = getCachedDevice(deviceId)?.category === 'ir';
|
|
595
|
+
const verification = isIr
|
|
596
|
+
? {
|
|
597
|
+
verifiable: false,
|
|
598
|
+
reason: 'IR transmission is unidirectional; no receipt acknowledgment is possible.',
|
|
599
|
+
suggestedFollowup: 'Confirm visible change manually or via a paired state sensor.',
|
|
600
|
+
}
|
|
601
|
+
: null;
|
|
602
|
+
if (isJsonMode()) {
|
|
603
|
+
const result = { ok: true, command: cmd, deviceId };
|
|
604
|
+
if (isIr) {
|
|
605
|
+
result.subKind = 'ir-no-feedback';
|
|
606
|
+
result.verification = verification;
|
|
607
|
+
}
|
|
608
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
609
|
+
Object.assign(result, body);
|
|
610
|
+
}
|
|
611
|
+
printJson(result);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (isIr) {
|
|
615
|
+
console.log(`→ IR signal sent: ${cmd} (no feedback — fire-and-forget)`);
|
|
616
|
+
console.error('⚠ IR (unverifiable) — no receipt acknowledgment. Confirm state manually.');
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
console.log(`✓ Command sent: ${cmd}`);
|
|
620
|
+
if (body && typeof body === 'object' && Object.keys(body).length > 0) {
|
|
621
|
+
printKeyValue(body);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
// Re-throw mock process.exit signals (Vitest intercepts process.exit as thrown
|
|
627
|
+
// Error('__exit__')) so they aren't double-handled and the exit code is preserved.
|
|
628
|
+
if (error instanceof Error && error.message === '__exit__')
|
|
629
|
+
throw error;
|
|
630
|
+
if (error instanceof DryRunSignal) {
|
|
631
|
+
const commandType = (options.type ?? 'command');
|
|
632
|
+
const wouldSend = { deviceId: _deviceId, command: _cmd, parameter: _parsedParam, commandType };
|
|
633
|
+
if (isJsonMode()) {
|
|
634
|
+
printJson({ dryRun: true, wouldSend });
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
console.log(`[dry-run] Would POST devices/${_deviceId}/commands with ${JSON.stringify({ command: _cmd, parameter: _parsedParam, commandType })}`);
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
handleError(error);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
// switchbot devices types
|
|
645
|
+
devices
|
|
646
|
+
.command('types')
|
|
647
|
+
.description('List all device types known to this CLI (offline reference, no API call)')
|
|
648
|
+
.addHelpText('after', `
|
|
649
|
+
Output columns: type, category (physical | ir), commands, aliases
|
|
650
|
+
Use 'switchbot devices commands <type>' to see what a given type supports.
|
|
651
|
+
|
|
652
|
+
Examples:
|
|
653
|
+
$ switchbot devices types
|
|
654
|
+
$ switchbot devices types --json
|
|
655
|
+
`)
|
|
656
|
+
.action(() => {
|
|
657
|
+
try {
|
|
658
|
+
const catalog = getEffectiveCatalog();
|
|
659
|
+
const fmt = resolveFormat();
|
|
660
|
+
if (fmt === 'json') {
|
|
661
|
+
printJson(catalog);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const headers = ['type', 'role', 'category', 'commands', 'aliases'];
|
|
665
|
+
const rows = catalog.map((e) => [
|
|
666
|
+
e.type,
|
|
667
|
+
e.role ?? '—',
|
|
668
|
+
e.category,
|
|
669
|
+
String(e.commands.length),
|
|
670
|
+
(e.aliases ?? []).join(', ') || '—',
|
|
671
|
+
]);
|
|
672
|
+
renderRows(headers, rows, fmt, resolveFields());
|
|
673
|
+
if (fmt === 'table') {
|
|
674
|
+
console.log(`\nTotal: ${catalog.length} device type(s)`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
handleError(error);
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
// switchbot devices commands <type>
|
|
682
|
+
devices
|
|
683
|
+
.command('commands')
|
|
684
|
+
.description('Show supported commands, parameter formats, and status fields for a device type')
|
|
685
|
+
.argument('<type...>', 'Device type name or alias (case-insensitive, partial matches supported; multi-word types do not need quoting)')
|
|
686
|
+
.addHelpText('after', `
|
|
687
|
+
This is the authoritative per-device reference — every command the CLI
|
|
688
|
+
can send to a given type, its parameter format, and the status fields
|
|
689
|
+
'devices status' will return. Runs fully offline (no API call).
|
|
690
|
+
|
|
691
|
+
Multi-word types can be passed either quoted or unquoted — both work:
|
|
692
|
+
$ switchbot devices commands "Air Conditioner"
|
|
693
|
+
$ switchbot devices commands Air Conditioner
|
|
694
|
+
$ switchbot devices commands "Smart Lock"
|
|
695
|
+
|
|
696
|
+
Examples:
|
|
697
|
+
$ switchbot devices commands Bot
|
|
698
|
+
$ switchbot devices commands curtain
|
|
699
|
+
$ switchbot devices commands Robot --json
|
|
700
|
+
`)
|
|
701
|
+
.action((typeParts) => {
|
|
702
|
+
try {
|
|
703
|
+
// First try the joined form so legacy multi-word unquoted input still
|
|
704
|
+
// works (`devices commands Air Conditioner` → "Air Conditioner"). If
|
|
705
|
+
// that doesn't match and every individual token resolves on its own,
|
|
706
|
+
// treat it as variadic and emit a section per type.
|
|
707
|
+
const joined = typeParts.join(' ');
|
|
708
|
+
const joinedMatch = findCatalogEntry(joined);
|
|
709
|
+
if (joinedMatch && !Array.isArray(joinedMatch)) {
|
|
710
|
+
if (isJsonMode()) {
|
|
711
|
+
printJson(normalizeCatalogForJson(joinedMatch));
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
renderCatalogEntry(joinedMatch);
|
|
715
|
+
}
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
if (typeParts.length > 1) {
|
|
719
|
+
const individualMatches = [];
|
|
720
|
+
for (const t of typeParts) {
|
|
721
|
+
const m = findCatalogEntry(t);
|
|
722
|
+
if (!m || Array.isArray(m)) {
|
|
723
|
+
individualMatches.length = 0;
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
individualMatches.push(m);
|
|
727
|
+
}
|
|
728
|
+
if (individualMatches.length === typeParts.length) {
|
|
729
|
+
if (isJsonMode()) {
|
|
730
|
+
printJson(individualMatches.map(normalizeCatalogForJson));
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
individualMatches.forEach((entry, i) => {
|
|
734
|
+
if (i > 0)
|
|
735
|
+
console.log('');
|
|
736
|
+
renderCatalogEntry(entry);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (!joinedMatch) {
|
|
743
|
+
throw new UsageError(`No device type matches "${joined}". Try 'switchbot devices types' to see the full list.`);
|
|
744
|
+
}
|
|
745
|
+
// joinedMatch is an ambiguous-match array here
|
|
746
|
+
const types = joinedMatch.map((m) => m.type).join(', ');
|
|
747
|
+
throw new UsageError(`"${joined}" matches multiple types: ${types}. Be more specific.`);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
handleError(error);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
// switchbot devices describe <deviceId>
|
|
754
|
+
devices
|
|
755
|
+
.command('describe')
|
|
756
|
+
.description('Describe a device by ID: metadata + supported commands + status fields (1 API call)')
|
|
757
|
+
.argument('[deviceId]', 'Target device ID (or use --name)')
|
|
758
|
+
.option('--name <query>', 'Resolve device by fuzzy name instead of deviceId', stringArg('--name'))
|
|
759
|
+
.option('--name-strategy <s>', `Name match strategy: ${ALL_STRATEGIES.join('|')} (default: fuzzy)`, stringArg('--name-strategy'))
|
|
760
|
+
.option('--name-type <type>', 'Narrow --name by device type', stringArg('--name-type'))
|
|
761
|
+
.option('--name-category <cat>', 'Narrow --name by category: physical|ir', enumArg('--name-category', ['physical', 'ir']))
|
|
762
|
+
.option('--name-room <room>', 'Narrow --name by room name (substring match)', stringArg('--name-room'))
|
|
763
|
+
.option('--live', 'Also fetch live status values and merge them into capabilities (costs 1 extra API call)')
|
|
764
|
+
.addHelpText('after', `
|
|
765
|
+
Makes a GET /v1.1/devices call to look up the device's type, then prints its
|
|
766
|
+
metadata alongside the matching catalog entry (supported commands + parameter
|
|
767
|
+
formats + status field names). With --live, makes a second call to fetch the
|
|
768
|
+
current status values and merges them into the output.
|
|
769
|
+
|
|
770
|
+
JSON output shape (--json):
|
|
771
|
+
{
|
|
772
|
+
device: <raw API fields>,
|
|
773
|
+
controlType: <string|null>,
|
|
774
|
+
catalog: <catalog entry, or null>,
|
|
775
|
+
capabilities: {
|
|
776
|
+
role: <functional role>,
|
|
777
|
+
readOnly: <boolean>,
|
|
778
|
+
commands: [{command, parameter, description, idempotent?, destructive?, exampleParams?}],
|
|
779
|
+
statusFields: [<name>],
|
|
780
|
+
liveStatus: <status payload when --live was passed>
|
|
781
|
+
},
|
|
782
|
+
source: "catalog" | "live" | "catalog+live" | "none",
|
|
783
|
+
suggestedActions: [{command, parameter?, description}],
|
|
784
|
+
expandHint?: {command, flags, example} // present when the type supports 'devices expand'
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
Examples:
|
|
788
|
+
$ switchbot devices describe ABC123DEF456
|
|
789
|
+
$ switchbot devices describe ABC123DEF456 --live
|
|
790
|
+
$ switchbot devices describe ABC123DEF456 --json
|
|
791
|
+
$ switchbot devices describe <lockId> --json | jq '.capabilities.commands[] | select(.destructive)'
|
|
792
|
+
`)
|
|
793
|
+
.action(async (deviceIdArg, options) => {
|
|
794
|
+
try {
|
|
795
|
+
const deviceId = resolveDeviceId(deviceIdArg, options.name, {
|
|
796
|
+
strategy: options.nameStrategy ?? 'fuzzy',
|
|
797
|
+
type: options.nameType,
|
|
798
|
+
category: options.nameCategory,
|
|
799
|
+
room: options.nameRoom,
|
|
800
|
+
});
|
|
801
|
+
const result = await describeDevice(deviceId, options);
|
|
802
|
+
const { device, isPhysical, typeName, controlType, catalog, capabilities, source, suggestedActions: picks } = result;
|
|
803
|
+
if (isJsonMode()) {
|
|
804
|
+
const expandHint = catalog ? EXPAND_HINTS[catalog.type] : undefined;
|
|
805
|
+
printJson({
|
|
806
|
+
device,
|
|
807
|
+
controlType,
|
|
808
|
+
catalog,
|
|
809
|
+
capabilities,
|
|
810
|
+
source,
|
|
811
|
+
suggestedActions: picks,
|
|
812
|
+
...(expandHint ? { expandHint: { command: expandHint.command, flags: expandHint.flags, example: `switchbot devices expand ${deviceId} ${expandHint.command} ${expandHint.flags}` } } : {}),
|
|
813
|
+
});
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
if (isPhysical) {
|
|
817
|
+
const physical = device;
|
|
818
|
+
printKeyValue({
|
|
819
|
+
deviceId: physical.deviceId,
|
|
820
|
+
deviceName: physical.deviceName,
|
|
821
|
+
deviceType: physical.deviceType || '—',
|
|
822
|
+
controlType: physical.controlType || '—',
|
|
823
|
+
family: physical.familyName || '—',
|
|
824
|
+
roomID: physical.roomID || '—',
|
|
825
|
+
room: physical.roomName || '—',
|
|
826
|
+
hub: !physical.hubDeviceId || physical.hubDeviceId === '000000000000' ? '—' : physical.hubDeviceId,
|
|
827
|
+
cloudService: physical.enableCloudService,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
else {
|
|
831
|
+
const ir = device;
|
|
832
|
+
const inherited = result.inheritedLocation;
|
|
833
|
+
printKeyValue({
|
|
834
|
+
deviceId: ir.deviceId,
|
|
835
|
+
deviceName: ir.deviceName,
|
|
836
|
+
remoteType: ir.remoteType,
|
|
837
|
+
controlType: ir.controlType || '—',
|
|
838
|
+
family: inherited?.family || '—',
|
|
839
|
+
roomID: inherited?.roomID || '—',
|
|
840
|
+
room: inherited?.room || '—',
|
|
841
|
+
hub: ir.hubDeviceId || '—',
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
const liveStatus = capabilities && 'liveStatus' in capabilities ? capabilities.liveStatus : undefined;
|
|
845
|
+
console.log('');
|
|
846
|
+
if (!catalog) {
|
|
847
|
+
console.log(`(Type "${typeName}" is not in the built-in catalog — no command reference available.)`);
|
|
848
|
+
if (isPhysical) {
|
|
849
|
+
console.log(`Try 'switchbot devices status ${deviceId}' to see what this device reports.`);
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
console.log(`Send custom IR buttons with: switchbot devices command ${deviceId} "<buttonName>" --type customize`);
|
|
853
|
+
}
|
|
854
|
+
if (liveStatus) {
|
|
855
|
+
console.log('\nLive status:');
|
|
856
|
+
printKeyValue(liveStatus);
|
|
857
|
+
}
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
renderCatalogEntry(catalog);
|
|
861
|
+
if (liveStatus) {
|
|
862
|
+
console.log('\nLive status:');
|
|
863
|
+
printKeyValue(liveStatus);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
if (error instanceof DeviceNotFoundError) {
|
|
868
|
+
const message = `${error.message} Try 'switchbot devices list' to see the full list.`;
|
|
869
|
+
exitWithError({
|
|
870
|
+
code: 1,
|
|
871
|
+
kind: 'runtime',
|
|
872
|
+
message,
|
|
873
|
+
extra: { errorClass: 'runtime', transient: false },
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
handleError(error);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
// switchbot devices batch <command> ...
|
|
880
|
+
registerBatchCommand(devices);
|
|
881
|
+
// switchbot devices watch <id...>
|
|
882
|
+
registerWatchCommand(devices);
|
|
883
|
+
// switchbot devices explain <id>
|
|
884
|
+
registerExplainCommand(devices);
|
|
885
|
+
// switchbot devices expand <id> <cmd> [semantic flags]
|
|
886
|
+
registerExpandCommand(devices);
|
|
887
|
+
// switchbot devices meta set/get/list/clear
|
|
888
|
+
registerDevicesMetaCommand(devices);
|
|
889
|
+
}
|
|
890
|
+
function normalizeCatalogForJson(entry) {
|
|
891
|
+
return {
|
|
892
|
+
...entry,
|
|
893
|
+
commands: entry.commands.map((c) => {
|
|
894
|
+
const tier = deriveSafetyTier(c, entry);
|
|
895
|
+
const reason = getCommandSafetyReason(c);
|
|
896
|
+
return {
|
|
897
|
+
...c,
|
|
898
|
+
safetyTier: tier,
|
|
899
|
+
...(reason ? { safetyReason: reason } : {}),
|
|
900
|
+
};
|
|
901
|
+
}),
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function renderCatalogEntry(entry) {
|
|
905
|
+
console.log(`Type: ${entry.type}`);
|
|
906
|
+
console.log(`Category: ${entry.category === 'ir' ? 'IR remote' : 'Physical device'}`);
|
|
907
|
+
if (entry.role)
|
|
908
|
+
console.log(`Role: ${entry.role}`);
|
|
909
|
+
if (entry.readOnly)
|
|
910
|
+
console.log(`ReadOnly: yes (status-only device, no control commands)`);
|
|
911
|
+
if (entry.aliases && entry.aliases.length > 0) {
|
|
912
|
+
console.log(`Aliases: ${entry.aliases.join(', ')}`);
|
|
913
|
+
}
|
|
914
|
+
if (entry.commands.length === 0) {
|
|
915
|
+
console.log('\nCommands: (none — status-only device)');
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
console.log('\nCommands:');
|
|
919
|
+
const hasExamples = entry.commands.some((c) => c.exampleParams && c.exampleParams.length > 0);
|
|
920
|
+
const rows = entry.commands.map((c) => {
|
|
921
|
+
const tier = deriveSafetyTier(c, entry);
|
|
922
|
+
const flags = [];
|
|
923
|
+
if (c.commandType === 'customize')
|
|
924
|
+
flags.push('customize');
|
|
925
|
+
if (tier === 'destructive')
|
|
926
|
+
flags.push('!destructive');
|
|
927
|
+
const label = flags.length > 0 ? `${c.command} [${flags.join(', ')}]` : c.command;
|
|
928
|
+
const base = [label, c.parameter, c.description];
|
|
929
|
+
return hasExamples ? [...base, (c.exampleParams ?? []).join(' | ') || ''] : base;
|
|
930
|
+
});
|
|
931
|
+
const tableHeaders = hasExamples
|
|
932
|
+
? ['command', 'parameter', 'description', 'example']
|
|
933
|
+
: ['command', 'parameter', 'description'];
|
|
934
|
+
printTable(tableHeaders, rows);
|
|
935
|
+
const hasDestructive = entry.commands.some((c) => deriveSafetyTier(c, entry) === 'destructive');
|
|
936
|
+
if (hasDestructive) {
|
|
937
|
+
console.log('\n[!destructive] commands have hard-to-reverse real-world effects — confirm before issuing.');
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (entry.statusFields && entry.statusFields.length > 0) {
|
|
941
|
+
console.log('\nStatus fields (from "devices status"):');
|
|
942
|
+
console.log(' ' + entry.statusFields.join(', '));
|
|
943
|
+
}
|
|
944
|
+
const expandHint = EXPAND_HINTS[entry.type];
|
|
945
|
+
if (expandHint) {
|
|
946
|
+
console.log(`\nTip: Use 'devices expand <id> ${expandHint.command} ${expandHint.flags}' for semantic flags instead of raw parameters.`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
//# sourceMappingURL=devices.js.map
|