@switchbot/openapi-cli 3.4.0 → 3.5.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 +110 -602
- package/dist/index.js +436 -77
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -7221,7 +7221,7 @@ function emitJsonError(errorPayload) {
|
|
|
7221
7221
|
function emitStreamHeader(opts) {
|
|
7222
7222
|
console.log(
|
|
7223
7223
|
JSON.stringify({
|
|
7224
|
-
schemaVersion: SCHEMA_VERSION,
|
|
7224
|
+
schemaVersion: opts.schemaVersion ?? SCHEMA_VERSION,
|
|
7225
7225
|
stream: true,
|
|
7226
7226
|
eventKind: opts.eventKind,
|
|
7227
7227
|
cadence: opts.cadence
|
|
@@ -7531,7 +7531,7 @@ var init_output = __esm({
|
|
|
7531
7531
|
init_source();
|
|
7532
7532
|
init_client();
|
|
7533
7533
|
init_flags();
|
|
7534
|
-
SCHEMA_VERSION = "1.
|
|
7534
|
+
SCHEMA_VERSION = "1.2";
|
|
7535
7535
|
ASCII_BORDER_CHARS = {
|
|
7536
7536
|
top: "-",
|
|
7537
7537
|
"top-mid": "+",
|
|
@@ -8380,9 +8380,8 @@ function updateCacheFromDeviceList(body) {
|
|
|
8380
8380
|
for (const d of body.deviceList) {
|
|
8381
8381
|
if (!d.deviceId) continue;
|
|
8382
8382
|
devices[d.deviceId] = {
|
|
8383
|
-
|
|
8384
|
-
|
|
8385
|
-
type: d.deviceType ?? "",
|
|
8383
|
+
type: d.deviceType || d.controlType || "Unknown Device",
|
|
8384
|
+
typeSource: d.deviceType ? "deviceType" : d.controlType ? "controlType" : "deviceType",
|
|
8386
8385
|
name: d.deviceName,
|
|
8387
8386
|
category: "physical",
|
|
8388
8387
|
hubDeviceId: d.hubDeviceId,
|
|
@@ -8397,6 +8396,7 @@ function updateCacheFromDeviceList(body) {
|
|
|
8397
8396
|
if (!d.deviceId) continue;
|
|
8398
8397
|
devices[d.deviceId] = {
|
|
8399
8398
|
type: d.remoteType,
|
|
8399
|
+
typeSource: "remoteType",
|
|
8400
8400
|
name: d.deviceName,
|
|
8401
8401
|
category: "ir",
|
|
8402
8402
|
hubDeviceId: d.hubDeviceId,
|
|
@@ -8793,10 +8793,11 @@ async function fetchDeviceList(client, options = {}) {
|
|
|
8793
8793
|
const infraredRemoteList = [];
|
|
8794
8794
|
for (const [deviceId, entry] of Object.entries(cached2.devices)) {
|
|
8795
8795
|
if (entry.category === "physical") {
|
|
8796
|
+
const cachedDeviceType = entry.typeSource === "deviceType" ? entry.type : entry.typeSource === void 0 && entry.type !== entry.controlType ? entry.type : void 0;
|
|
8796
8797
|
deviceList.push({
|
|
8797
8798
|
deviceId,
|
|
8798
8799
|
deviceName: entry.name,
|
|
8799
|
-
...
|
|
8800
|
+
...cachedDeviceType && cachedDeviceType !== "Unknown Device" ? { deviceType: cachedDeviceType } : {},
|
|
8800
8801
|
enableCloudService: entry.enableCloudService ?? true,
|
|
8801
8802
|
hubDeviceId: entry.hubDeviceId ?? "",
|
|
8802
8803
|
roomID: entry.roomID,
|
|
@@ -8867,10 +8868,12 @@ async function executeCommand(deviceId, cmd, parameter, commandType, client, opt
|
|
|
8867
8868
|
if (err instanceof Error && err.name === "DryRunSignal") {
|
|
8868
8869
|
writeAudit({ ...baseAudit, result: "dry-run" });
|
|
8869
8870
|
} else {
|
|
8871
|
+
const statusCode = err instanceof ApiError ? err.code : void 0;
|
|
8870
8872
|
writeAudit({
|
|
8871
8873
|
...baseAudit,
|
|
8872
8874
|
result: "error",
|
|
8873
|
-
error: err instanceof Error ? err.message : String(err)
|
|
8875
|
+
error: err instanceof Error ? err.message : String(err),
|
|
8876
|
+
...statusCode !== void 0 ? { statusCode } : {}
|
|
8874
8877
|
});
|
|
8875
8878
|
}
|
|
8876
8879
|
throw err;
|
|
@@ -8991,7 +8994,23 @@ async function describeDevice(deviceId, options = {}, client) {
|
|
|
8991
8994
|
ir = infraredRemoteList.find((d) => d.deviceId === deviceId);
|
|
8992
8995
|
}
|
|
8993
8996
|
if (!physical && !ir) throw new DeviceNotFoundError(deviceId);
|
|
8994
|
-
|
|
8997
|
+
let typeName;
|
|
8998
|
+
let typeSource;
|
|
8999
|
+
if (physical) {
|
|
9000
|
+
if (physical.deviceType) {
|
|
9001
|
+
typeName = physical.deviceType;
|
|
9002
|
+
typeSource = "deviceType";
|
|
9003
|
+
} else if (physical.controlType) {
|
|
9004
|
+
typeName = physical.controlType;
|
|
9005
|
+
typeSource = "controlType";
|
|
9006
|
+
} else {
|
|
9007
|
+
typeName = "Unknown Device";
|
|
9008
|
+
typeSource = "deviceType";
|
|
9009
|
+
}
|
|
9010
|
+
} else {
|
|
9011
|
+
typeName = ir.remoteType;
|
|
9012
|
+
typeSource = "remoteType";
|
|
9013
|
+
}
|
|
8995
9014
|
const match = typeName ? findCatalogEntry(typeName) : null;
|
|
8996
9015
|
const catalogEntry = !match || Array.isArray(match) ? null : match;
|
|
8997
9016
|
let liveStatus;
|
|
@@ -9027,6 +9046,7 @@ async function describeDevice(deviceId, options = {}, client) {
|
|
|
9027
9046
|
device: selectedDevice,
|
|
9028
9047
|
isPhysical: Boolean(physical),
|
|
9029
9048
|
typeName,
|
|
9049
|
+
typeSource,
|
|
9030
9050
|
controlType: physical?.controlType ?? ir?.controlType ?? null,
|
|
9031
9051
|
catalog: catalogEntry,
|
|
9032
9052
|
capabilities,
|
|
@@ -28296,8 +28316,21 @@ function commandToJson(cmd, opts = {}) {
|
|
|
28296
28316
|
}
|
|
28297
28317
|
function resolveTargetCommand(root, argv) {
|
|
28298
28318
|
let cmd = root;
|
|
28319
|
+
const rootOptions = root.options;
|
|
28320
|
+
let consumeNext = false;
|
|
28299
28321
|
for (const token of argv) {
|
|
28300
|
-
if (
|
|
28322
|
+
if (consumeNext) {
|
|
28323
|
+
consumeNext = false;
|
|
28324
|
+
continue;
|
|
28325
|
+
}
|
|
28326
|
+
if (token.startsWith("-")) {
|
|
28327
|
+
if (!token.includes("=")) {
|
|
28328
|
+
const localOpts = cmd.options;
|
|
28329
|
+
const opt = localOpts.find((o) => o.short === token || o.long === token) || rootOptions.find((o) => o.short === token || o.long === token);
|
|
28330
|
+
if (opt && (opt.required || opt.optional)) consumeNext = true;
|
|
28331
|
+
}
|
|
28332
|
+
continue;
|
|
28333
|
+
}
|
|
28301
28334
|
const sub = cmd.commands.find(
|
|
28302
28335
|
(c) => c.name() === token || c.aliases().includes(token)
|
|
28303
28336
|
);
|
|
@@ -31697,6 +31730,161 @@ function applyFilter(clauses, deviceList, infraredRemoteList, hubLocation) {
|
|
|
31697
31730
|
// src/devices/param-validator.ts
|
|
31698
31731
|
init_cjs_shim();
|
|
31699
31732
|
init_output();
|
|
31733
|
+
|
|
31734
|
+
// src/devices/css-colors.ts
|
|
31735
|
+
init_cjs_shim();
|
|
31736
|
+
var CSS_COLORS = {
|
|
31737
|
+
aliceblue: [240, 248, 255],
|
|
31738
|
+
antiquewhite: [250, 235, 215],
|
|
31739
|
+
aqua: [0, 255, 255],
|
|
31740
|
+
aquamarine: [127, 255, 212],
|
|
31741
|
+
azure: [240, 255, 255],
|
|
31742
|
+
beige: [245, 245, 220],
|
|
31743
|
+
bisque: [255, 228, 196],
|
|
31744
|
+
black: [0, 0, 0],
|
|
31745
|
+
blanchedalmond: [255, 235, 205],
|
|
31746
|
+
blue: [0, 0, 255],
|
|
31747
|
+
blueviolet: [138, 43, 226],
|
|
31748
|
+
brown: [165, 42, 42],
|
|
31749
|
+
burlywood: [222, 184, 135],
|
|
31750
|
+
cadetblue: [95, 158, 160],
|
|
31751
|
+
chartreuse: [127, 255, 0],
|
|
31752
|
+
chocolate: [210, 105, 30],
|
|
31753
|
+
coral: [255, 127, 80],
|
|
31754
|
+
cornflowerblue: [100, 149, 237],
|
|
31755
|
+
cornsilk: [255, 248, 220],
|
|
31756
|
+
crimson: [220, 20, 60],
|
|
31757
|
+
cyan: [0, 255, 255],
|
|
31758
|
+
darkblue: [0, 0, 139],
|
|
31759
|
+
darkcyan: [0, 139, 139],
|
|
31760
|
+
darkgoldenrod: [184, 134, 11],
|
|
31761
|
+
darkgray: [169, 169, 169],
|
|
31762
|
+
darkgreen: [0, 100, 0],
|
|
31763
|
+
darkgrey: [169, 169, 169],
|
|
31764
|
+
darkkhaki: [189, 183, 107],
|
|
31765
|
+
darkmagenta: [139, 0, 139],
|
|
31766
|
+
darkolivegreen: [85, 107, 47],
|
|
31767
|
+
darkorange: [255, 140, 0],
|
|
31768
|
+
darkorchid: [153, 50, 204],
|
|
31769
|
+
darkred: [139, 0, 0],
|
|
31770
|
+
darksalmon: [233, 150, 122],
|
|
31771
|
+
darkseagreen: [143, 188, 143],
|
|
31772
|
+
darkslateblue: [72, 61, 139],
|
|
31773
|
+
darkslategray: [47, 79, 79],
|
|
31774
|
+
darkslategrey: [47, 79, 79],
|
|
31775
|
+
darkturquoise: [0, 206, 209],
|
|
31776
|
+
darkviolet: [148, 0, 211],
|
|
31777
|
+
deeppink: [255, 20, 147],
|
|
31778
|
+
deepskyblue: [0, 191, 255],
|
|
31779
|
+
dimgray: [105, 105, 105],
|
|
31780
|
+
dimgrey: [105, 105, 105],
|
|
31781
|
+
dodgerblue: [30, 144, 255],
|
|
31782
|
+
firebrick: [178, 34, 34],
|
|
31783
|
+
floralwhite: [255, 250, 240],
|
|
31784
|
+
forestgreen: [34, 139, 34],
|
|
31785
|
+
fuchsia: [255, 0, 255],
|
|
31786
|
+
gainsboro: [220, 220, 220],
|
|
31787
|
+
ghostwhite: [248, 248, 255],
|
|
31788
|
+
gold: [255, 215, 0],
|
|
31789
|
+
goldenrod: [218, 165, 32],
|
|
31790
|
+
gray: [128, 128, 128],
|
|
31791
|
+
green: [0, 128, 0],
|
|
31792
|
+
greenyellow: [173, 255, 47],
|
|
31793
|
+
grey: [128, 128, 128],
|
|
31794
|
+
honeydew: [240, 255, 240],
|
|
31795
|
+
hotpink: [255, 105, 180],
|
|
31796
|
+
indianred: [205, 92, 92],
|
|
31797
|
+
indigo: [75, 0, 130],
|
|
31798
|
+
ivory: [255, 255, 240],
|
|
31799
|
+
khaki: [240, 230, 140],
|
|
31800
|
+
lavender: [230, 230, 250],
|
|
31801
|
+
lavenderblush: [255, 240, 245],
|
|
31802
|
+
lawngreen: [124, 252, 0],
|
|
31803
|
+
lemonchiffon: [255, 250, 205],
|
|
31804
|
+
lightblue: [173, 216, 230],
|
|
31805
|
+
lightcoral: [240, 128, 128],
|
|
31806
|
+
lightcyan: [224, 255, 255],
|
|
31807
|
+
lightgoldenrodyellow: [250, 250, 210],
|
|
31808
|
+
lightgray: [211, 211, 211],
|
|
31809
|
+
lightgreen: [144, 238, 144],
|
|
31810
|
+
lightgrey: [211, 211, 211],
|
|
31811
|
+
lightpink: [255, 182, 193],
|
|
31812
|
+
lightsalmon: [255, 160, 122],
|
|
31813
|
+
lightseagreen: [32, 178, 170],
|
|
31814
|
+
lightskyblue: [135, 206, 250],
|
|
31815
|
+
lightslategray: [119, 136, 153],
|
|
31816
|
+
lightslategrey: [119, 136, 153],
|
|
31817
|
+
lightsteelblue: [176, 196, 222],
|
|
31818
|
+
lightyellow: [255, 255, 224],
|
|
31819
|
+
lime: [0, 255, 0],
|
|
31820
|
+
limegreen: [50, 205, 50],
|
|
31821
|
+
linen: [250, 240, 230],
|
|
31822
|
+
magenta: [255, 0, 255],
|
|
31823
|
+
maroon: [128, 0, 0],
|
|
31824
|
+
mediumaquamarine: [102, 205, 170],
|
|
31825
|
+
mediumblue: [0, 0, 205],
|
|
31826
|
+
mediumorchid: [186, 85, 211],
|
|
31827
|
+
mediumpurple: [147, 111, 219],
|
|
31828
|
+
mediumseagreen: [60, 179, 113],
|
|
31829
|
+
mediumslateblue: [123, 104, 238],
|
|
31830
|
+
mediumspringgreen: [0, 250, 154],
|
|
31831
|
+
mediumturquoise: [72, 209, 204],
|
|
31832
|
+
mediumvioletred: [199, 21, 133],
|
|
31833
|
+
midnightblue: [25, 25, 112],
|
|
31834
|
+
mintcream: [245, 255, 250],
|
|
31835
|
+
mistyrose: [255, 228, 225],
|
|
31836
|
+
moccasin: [255, 228, 181],
|
|
31837
|
+
navajowhite: [255, 222, 173],
|
|
31838
|
+
navy: [0, 0, 128],
|
|
31839
|
+
oldlace: [253, 245, 230],
|
|
31840
|
+
olive: [128, 128, 0],
|
|
31841
|
+
olivedrab: [107, 142, 35],
|
|
31842
|
+
orange: [255, 165, 0],
|
|
31843
|
+
orangered: [255, 69, 0],
|
|
31844
|
+
orchid: [218, 112, 214],
|
|
31845
|
+
palegoldenrod: [238, 232, 170],
|
|
31846
|
+
palegreen: [152, 251, 152],
|
|
31847
|
+
paleturquoise: [175, 238, 238],
|
|
31848
|
+
palevioletred: [219, 112, 147],
|
|
31849
|
+
papayawhip: [255, 239, 213],
|
|
31850
|
+
peachpuff: [255, 218, 185],
|
|
31851
|
+
peru: [205, 133, 63],
|
|
31852
|
+
pink: [255, 192, 203],
|
|
31853
|
+
plum: [221, 160, 221],
|
|
31854
|
+
powderblue: [176, 224, 230],
|
|
31855
|
+
purple: [128, 0, 128],
|
|
31856
|
+
rebeccapurple: [102, 51, 153],
|
|
31857
|
+
red: [255, 0, 0],
|
|
31858
|
+
rosybrown: [188, 143, 143],
|
|
31859
|
+
royalblue: [65, 105, 225],
|
|
31860
|
+
saddlebrown: [139, 69, 19],
|
|
31861
|
+
salmon: [250, 128, 114],
|
|
31862
|
+
sandybrown: [244, 164, 96],
|
|
31863
|
+
seagreen: [46, 139, 87],
|
|
31864
|
+
seashell: [255, 245, 238],
|
|
31865
|
+
sienna: [160, 82, 45],
|
|
31866
|
+
silver: [192, 192, 192],
|
|
31867
|
+
skyblue: [135, 206, 235],
|
|
31868
|
+
slateblue: [106, 90, 205],
|
|
31869
|
+
slategray: [112, 128, 144],
|
|
31870
|
+
slategrey: [112, 128, 144],
|
|
31871
|
+
snow: [255, 250, 250],
|
|
31872
|
+
springgreen: [0, 255, 127],
|
|
31873
|
+
steelblue: [70, 130, 180],
|
|
31874
|
+
tan: [210, 180, 140],
|
|
31875
|
+
teal: [0, 128, 128],
|
|
31876
|
+
thistle: [216, 191, 216],
|
|
31877
|
+
tomato: [255, 99, 71],
|
|
31878
|
+
turquoise: [64, 224, 208],
|
|
31879
|
+
violet: [238, 130, 238],
|
|
31880
|
+
wheat: [245, 222, 179],
|
|
31881
|
+
white: [255, 255, 255],
|
|
31882
|
+
whitesmoke: [245, 245, 245],
|
|
31883
|
+
yellow: [255, 255, 0],
|
|
31884
|
+
yellowgreen: [154, 205, 50]
|
|
31885
|
+
};
|
|
31886
|
+
|
|
31887
|
+
// src/devices/param-validator.ts
|
|
31700
31888
|
var AC_MODE_MAP = { auto: 1, cool: 2, dry: 3, fan: 4, heat: 5 };
|
|
31701
31889
|
var AC_FAN_MAP = { auto: 1, low: 2, mid: 3, high: 4 };
|
|
31702
31890
|
var CURTAIN_MODE_MAP = { default: "ff", performance: "0", silent: "1" };
|
|
@@ -31763,6 +31951,26 @@ function buildRelaySetMode(opts) {
|
|
|
31763
31951
|
}
|
|
31764
31952
|
return `${ch};${modeInt}`;
|
|
31765
31953
|
}
|
|
31954
|
+
function buildBrightnessSet(opts) {
|
|
31955
|
+
if (!opts.brightness) throw new UsageError("--brightness is required (1-100)");
|
|
31956
|
+
const b2 = parseInt(opts.brightness, 10);
|
|
31957
|
+
if (!Number.isFinite(b2) || b2 < 1 || b2 > 100) {
|
|
31958
|
+
throw new UsageError(`--brightness must be an integer between 1 and 100 (got "${opts.brightness}")`);
|
|
31959
|
+
}
|
|
31960
|
+
return String(b2);
|
|
31961
|
+
}
|
|
31962
|
+
function buildColorSet(opts) {
|
|
31963
|
+
if (!opts.color) throw new UsageError('--color is required (e.g. "255:0:0", "#FF0000", "red")');
|
|
31964
|
+
const result = validateSetColor(opts.color);
|
|
31965
|
+
if (!result.ok) throw new UsageError(result.error);
|
|
31966
|
+
return result.normalized ?? opts.color;
|
|
31967
|
+
}
|
|
31968
|
+
function buildColorTemperatureSet(opts) {
|
|
31969
|
+
if (!opts.colorTemp) throw new UsageError("--color-temp is required (2700-6500)");
|
|
31970
|
+
const result = validateSetColorTemperature(opts.colorTemp);
|
|
31971
|
+
if (!result.ok) throw new UsageError(result.error);
|
|
31972
|
+
return result.normalized ?? opts.colorTemp;
|
|
31973
|
+
}
|
|
31766
31974
|
function validateParameter(deviceType, command, raw) {
|
|
31767
31975
|
if (!deviceType) return { ok: true };
|
|
31768
31976
|
if (deviceType === "Air Conditioner" && command === "setAll") {
|
|
@@ -31783,7 +31991,7 @@ function validateParameter(deviceType, command, raw) {
|
|
|
31783
31991
|
if (command === "setColor" && isColorDevice(deviceType)) {
|
|
31784
31992
|
return validateSetColor(raw);
|
|
31785
31993
|
}
|
|
31786
|
-
if (command === "setColorTemperature" &&
|
|
31994
|
+
if (command === "setColorTemperature" && isBrightnessDevice(deviceType)) {
|
|
31787
31995
|
return validateSetColorTemperature(raw);
|
|
31788
31996
|
}
|
|
31789
31997
|
return { ok: true };
|
|
@@ -31792,7 +32000,12 @@ function isBrightnessDevice(deviceType) {
|
|
|
31792
32000
|
return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Ceiling Light" || deviceType === "Ceiling Light Pro" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Dimmer" || deviceType === "Fill Light";
|
|
31793
32001
|
}
|
|
31794
32002
|
function isColorDevice(deviceType) {
|
|
31795
|
-
return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "
|
|
32003
|
+
return deviceType === "Color Bulb" || deviceType === "Strip Light" || deviceType === "Strip Light 3" || deviceType === "Floor Lamp" || deviceType === "Light Strip" || deviceType === "Fill Light";
|
|
32004
|
+
}
|
|
32005
|
+
function isLightingCommandSupported(deviceType, command) {
|
|
32006
|
+
if (command === "setBrightness" || command === "setColorTemperature") return isBrightnessDevice(deviceType);
|
|
32007
|
+
if (command === "setColor") return isColorDevice(deviceType);
|
|
32008
|
+
return false;
|
|
31796
32009
|
}
|
|
31797
32010
|
function validateSetBrightness(raw) {
|
|
31798
32011
|
if (raw === void 0 || raw === "" || raw === "default") {
|
|
@@ -31820,29 +32033,18 @@ function validateSetBrightness(raw) {
|
|
|
31820
32033
|
function hintBrightnessRetry() {
|
|
31821
32034
|
return `Ask the user whether they meant a percentage (1-100). Example: "50".`;
|
|
31822
32035
|
}
|
|
31823
|
-
var
|
|
31824
|
-
red: [255, 0, 0],
|
|
31825
|
-
green: [0, 128, 0],
|
|
31826
|
-
lime: [0, 255, 0],
|
|
31827
|
-
blue: [0, 0, 255],
|
|
31828
|
-
yellow: [255, 255, 0],
|
|
31829
|
-
cyan: [0, 255, 255],
|
|
31830
|
-
magenta: [255, 0, 255],
|
|
31831
|
-
white: [255, 255, 255],
|
|
31832
|
-
black: [0, 0, 0],
|
|
31833
|
-
orange: [255, 165, 0],
|
|
31834
|
-
purple: [128, 0, 128],
|
|
31835
|
-
pink: [255, 192, 203],
|
|
31836
|
-
brown: [165, 42, 42],
|
|
31837
|
-
grey: [128, 128, 128],
|
|
31838
|
-
gray: [128, 128, 128],
|
|
32036
|
+
var CUSTOM_COLORS = {
|
|
31839
32037
|
warm: [255, 180, 100]
|
|
31840
32038
|
};
|
|
32039
|
+
var NAMED_COLORS = {
|
|
32040
|
+
...CSS_COLORS,
|
|
32041
|
+
...CUSTOM_COLORS
|
|
32042
|
+
};
|
|
31841
32043
|
function validateSetColor(raw) {
|
|
31842
32044
|
if (raw === void 0 || raw === "" || raw === "default") {
|
|
31843
32045
|
return {
|
|
31844
32046
|
ok: false,
|
|
31845
|
-
error: `setColor requires a color.
|
|
32047
|
+
error: `setColor requires a color. Use a CSS color name (e.g. coral, teal, salmon), hex (#RRGGBB / #RGB), or R:G:B format.`
|
|
31846
32048
|
};
|
|
31847
32049
|
}
|
|
31848
32050
|
const trimmed = raw.trim();
|
|
@@ -32357,8 +32559,11 @@ Examples:
|
|
|
32357
32559
|
}
|
|
32358
32560
|
return;
|
|
32359
32561
|
}
|
|
32562
|
+
const totalDevices = resolved.ids.length;
|
|
32563
|
+
const deviceIndices = new Map(resolved.ids.map((id, i) => [id, i + 1]));
|
|
32360
32564
|
const startedAt = Date.now();
|
|
32361
32565
|
const outcomes = await runPool(resolved.ids, concurrency, staggerMs, async (id) => {
|
|
32566
|
+
const stepIdx = deviceIndices.get(id);
|
|
32362
32567
|
const stepStart = Date.now();
|
|
32363
32568
|
const startedIso = new Date(stepStart).toISOString();
|
|
32364
32569
|
try {
|
|
@@ -32370,7 +32575,7 @@ Examples:
|
|
|
32370
32575
|
const durationMs = Date.now() - stepStart;
|
|
32371
32576
|
const replayed = typeof result2 === "object" && result2 !== null && result2.replayed === true;
|
|
32372
32577
|
if (!isJsonMode()) {
|
|
32373
|
-
console.log(
|
|
32578
|
+
console.log(`[${stepIdx}/${totalDevices}] \u2713 ${id}: ${cmd}${replayed ? " (replayed)" : ""}`);
|
|
32374
32579
|
}
|
|
32375
32580
|
return {
|
|
32376
32581
|
ok: true,
|
|
@@ -32393,7 +32598,7 @@ Examples:
|
|
|
32393
32598
|
}
|
|
32394
32599
|
const errorPayload = buildErrorPayload(err);
|
|
32395
32600
|
if (!isJsonMode()) {
|
|
32396
|
-
console.error(
|
|
32601
|
+
console.error(`[${stepIdx}/${totalDevices}] \u2717 ${id}: ${errorPayload.message}`);
|
|
32397
32602
|
}
|
|
32398
32603
|
return {
|
|
32399
32604
|
ok: false,
|
|
@@ -32643,7 +32848,7 @@ function registerWatchCommand(devices) {
|
|
|
32643
32848
|
`Polling interval: "30s", "1m", "500ms", ... (default 30s, min ${MIN_INTERVAL_MS / 1e3}s)`,
|
|
32644
32849
|
durationArg("--interval"),
|
|
32645
32850
|
"30s"
|
|
32646
|
-
).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").option("--initial <mode>", "How to handle the first poll: snapshot | emit | skip (default: snapshot)", enumArg("--initial", INITIAL_MODES), "snapshot").addHelpText(
|
|
32851
|
+
).option("--max <n>", "Stop after N ticks (default: run until Ctrl-C)", intArg("--max", { min: 1 })).option("--once", "Stop after one tick (shorthand for --max 1)").option("--for <dur>", 'Stop after elapsed time (e.g. "5m", "30s"). Combines with --max: first limit wins.', durationArg("--for")).option("--include-unchanged", "Emit a tick even when no field changed").option("--initial <mode>", "How to handle the first poll: snapshot | emit | skip (default: snapshot)", enumArg("--initial", INITIAL_MODES), "snapshot").addHelpText(
|
|
32647
32852
|
"after",
|
|
32648
32853
|
`
|
|
32649
32854
|
Default output is a human-readable table of field changes per tick; add --json
|
|
@@ -32672,6 +32877,12 @@ Examples:
|
|
|
32672
32877
|
).action(
|
|
32673
32878
|
async (deviceIds, options) => {
|
|
32674
32879
|
try {
|
|
32880
|
+
if (options.once && options.max !== void 0) {
|
|
32881
|
+
throw new UsageError("--once and --max are mutually exclusive.");
|
|
32882
|
+
}
|
|
32883
|
+
if (options.once) {
|
|
32884
|
+
options.max = "1";
|
|
32885
|
+
}
|
|
32675
32886
|
const allIds = [...deviceIds];
|
|
32676
32887
|
if (options.name) {
|
|
32677
32888
|
const resolved = resolveDeviceId(void 0, options.name);
|
|
@@ -32923,10 +33134,11 @@ init_arg_parsers();
|
|
|
32923
33134
|
init_output();
|
|
32924
33135
|
init_cache();
|
|
32925
33136
|
init_devices();
|
|
33137
|
+
init_catalog();
|
|
32926
33138
|
init_flags();
|
|
32927
33139
|
init_client();
|
|
32928
33140
|
function registerExpandCommand(devices) {
|
|
32929
|
-
devices.command("expand").description("Send a command with semantic flags instead of raw positional parameters").argument("[deviceId]", 'Target device ID from "devices list" (or use --name)').argument("[command]", "Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2)").option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: require-unique)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Curtain", "Air Conditioner")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--temp <celsius>", "AC setAll: temperature in Celsius (16-30)", intArg("--temp", { min: 16, max: 30 })).option("--mode <mode>", "AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary", stringArg("--mode")).option("--fan <speed>", "AC setAll: fan speed auto|low|mid|high", stringArg("--fan")).option("--power <state>", "AC setAll: on|off", stringArg("--power")).option("--position <percent>", "Curtain setPosition: 0-100 (0=open, 100=closed)", intArg("--position", { min: 0, max: 100 })).option("--direction <dir>", "Blind Tilt setPosition: up|down", stringArg("--direction")).option("--angle <percent>", "Blind Tilt setPosition: 0-100 (0=closed, 100=open)", intArg("--angle", { min: 0, max: 100 })).option("--channel <n>", "Relay Switch 2 setMode: channel 1 or 2", intArg("--channel", { min: 1, max: 2 })).option("--yes", "Confirm destructive commands").addHelpText("after", `
|
|
33141
|
+
devices.command("expand").description("Send a command with semantic flags instead of raw positional parameters").argument("[deviceId]", 'Target device ID from "devices list" (or use --name)').argument("[command]", "Command name: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch 2), setBrightness/setColor/setColorTemperature (lighting)").option("--name <query>", "Resolve device by fuzzy name instead of deviceId", stringArg("--name")).option("--name-strategy <s>", `Name match strategy: ${ALL_STRATEGIES.join("|")} (default: require-unique)`, stringArg("--name-strategy")).option("--name-type <type>", 'Narrow --name by device type (e.g. "Curtain", "Air Conditioner")', stringArg("--name-type")).option("--name-category <cat>", "Narrow --name by category: physical|ir", enumArg("--name-category", ["physical", "ir"])).option("--name-room <room>", "Narrow --name by room name (substring match)", stringArg("--name-room")).option("--temp <celsius>", "AC setAll: temperature in Celsius (16-30)", intArg("--temp", { min: 16, max: 30 })).option("--mode <mode>", "AC: auto|cool|dry|fan|heat Curtain: default|performance|silent Relay: toggle|edge|detached|momentary", stringArg("--mode")).option("--fan <speed>", "AC setAll: fan speed auto|low|mid|high", stringArg("--fan")).option("--power <state>", "AC setAll: on|off", stringArg("--power")).option("--position <percent>", "Curtain setPosition: 0-100 (0=open, 100=closed)", intArg("--position", { min: 0, max: 100 })).option("--direction <dir>", "Blind Tilt setPosition: up|down", stringArg("--direction")).option("--angle <percent>", "Blind Tilt setPosition: 0-100 (0=closed, 100=open)", intArg("--angle", { min: 0, max: 100 })).option("--channel <n>", "Relay Switch 2 setMode: channel 1 or 2", intArg("--channel", { min: 1, max: 2 })).option("--brightness <percent>", "setBrightness: 1-100 percent", intArg("--brightness", { min: 1, max: 100 })).option("--color <value>", "setColor: R:G:B, #RRGGBB, or named color (red, blue, etc.)", stringArg("--color")).option("--color-temp <kelvin>", "setColorTemperature: 2700-6500 Kelvin", intArg("--color-temp", { min: 2700, max: 6500 })).option("--yes", "Confirm destructive commands").addHelpText("after", `
|
|
32930
33142
|
Translates semantic flags into the wire parameter format, then sends the command.
|
|
32931
33143
|
|
|
32932
33144
|
Supported expansions:
|
|
@@ -32947,12 +33159,25 @@ Supported expansions:
|
|
|
32947
33159
|
--channel 1 --mode edge \u2192 "1;1"
|
|
32948
33160
|
--mode values: toggle (0) | edge (1) | detached (2) | momentary (3)
|
|
32949
33161
|
|
|
33162
|
+
Color Bulb / Strip Light / Ceiling Light \u2014 setBrightness
|
|
33163
|
+
--brightness 80 \u2192 "80"
|
|
33164
|
+
|
|
33165
|
+
Color Bulb / Strip Light / Floor Lamp \u2014 setColor
|
|
33166
|
+
--color "255:0:0" \u2192 "255:0:0"
|
|
33167
|
+
--color "#FF0000" \u2192 "255:0:0"
|
|
33168
|
+
--color red \u2192 "255:0:0"
|
|
33169
|
+
|
|
33170
|
+
Color Bulb / Strip Light / Ceiling Light \u2014 setColorTemperature
|
|
33171
|
+
--color-temp 4000 \u2192 "4000"
|
|
33172
|
+
|
|
32950
33173
|
Examples:
|
|
32951
|
-
$ switchbot devices expand <acId> setAll
|
|
32952
|
-
$ switchbot devices expand <curtainId> setPosition
|
|
32953
|
-
$ switchbot devices expand <blindId> setPosition
|
|
32954
|
-
$ switchbot devices expand <relayId> setMode
|
|
32955
|
-
$ switchbot devices expand <
|
|
33174
|
+
$ switchbot devices expand <acId> setAll --temp 26 --mode cool --fan low --power on
|
|
33175
|
+
$ switchbot devices expand <curtainId> setPosition --position 50 --mode silent
|
|
33176
|
+
$ switchbot devices expand <blindId> setPosition --direction up --angle 50
|
|
33177
|
+
$ switchbot devices expand <relayId> setMode --channel 1 --mode edge
|
|
33178
|
+
$ switchbot devices expand <stripId> setBrightness --brightness 80
|
|
33179
|
+
$ switchbot devices expand <bulbId> setColor --color "#FF0000"
|
|
33180
|
+
$ switchbot devices expand <bulbId> setColorTemperature --color-temp 4000
|
|
32956
33181
|
$ switchbot devices expand --name "Living Room AC" setAll --temp 26 --mode cool --fan low --power on
|
|
32957
33182
|
`).action(async (deviceIdArg, commandArg, options) => {
|
|
32958
33183
|
let deviceId = "";
|
|
@@ -32970,12 +33195,22 @@ Examples:
|
|
|
32970
33195
|
category: options.nameCategory,
|
|
32971
33196
|
room: options.nameRoom
|
|
32972
33197
|
});
|
|
32973
|
-
if (!effectiveCommand) throw new UsageError("A command argument is required (setAll, setPosition, setMode).");
|
|
33198
|
+
if (!effectiveCommand) throw new UsageError("A command argument is required (setAll, setPosition, setMode, setBrightness, setColor, setColorTemperature).");
|
|
32974
33199
|
command = effectiveCommand;
|
|
32975
33200
|
const cached2 = getCachedDevice(deviceId);
|
|
32976
33201
|
const deviceType = cached2?.type ?? "";
|
|
32977
33202
|
let parameter;
|
|
32978
33203
|
if (command === "setAll") {
|
|
33204
|
+
if (!cached2) {
|
|
33205
|
+
throw new UsageError(
|
|
33206
|
+
`Device ${deviceId} is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' can verify this is an Air Conditioner.`
|
|
33207
|
+
);
|
|
33208
|
+
}
|
|
33209
|
+
if (deviceType !== "Air Conditioner") {
|
|
33210
|
+
throw new UsageError(
|
|
33211
|
+
`"setAll" is only supported on Air Conditioner devices, but "${cached2.type}" was found.`
|
|
33212
|
+
);
|
|
33213
|
+
}
|
|
32979
33214
|
parameter = buildAcSetAll(options);
|
|
32980
33215
|
} else if (command === "setPosition") {
|
|
32981
33216
|
if (!cached2) {
|
|
@@ -32983,13 +33218,56 @@ Examples:
|
|
|
32983
33218
|
`Device ${deviceId} is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' knows whether this is a Curtain or a Blind Tilt.`
|
|
32984
33219
|
);
|
|
32985
33220
|
}
|
|
33221
|
+
const positionTypes = ["Curtain", "Curtain 3", "Roller Shade", "Blind Tilt"];
|
|
33222
|
+
if (!positionTypes.some((t) => deviceType.startsWith(t))) {
|
|
33223
|
+
throw new UsageError(
|
|
33224
|
+
`"setPosition" is only supported on Curtain, Roller Shade, and Blind Tilt devices, but "${cached2.type}" was found.`
|
|
33225
|
+
);
|
|
33226
|
+
}
|
|
32986
33227
|
const isBlind = deviceType.startsWith("Blind Tilt");
|
|
32987
|
-
|
|
33228
|
+
const isRollerShade = deviceType.startsWith("Roller Shade");
|
|
33229
|
+
if (isBlind) {
|
|
33230
|
+
parameter = buildBlindTiltSetPosition(options);
|
|
33231
|
+
} else if (isRollerShade) {
|
|
33232
|
+
if (!options.position) throw new UsageError("--position is required (0-100)");
|
|
33233
|
+
parameter = options.position;
|
|
33234
|
+
} else {
|
|
33235
|
+
parameter = buildCurtainSetPosition(options);
|
|
33236
|
+
}
|
|
32988
33237
|
} else if (command === "setMode" && deviceType.startsWith("Relay Switch")) {
|
|
32989
33238
|
parameter = buildRelaySetMode(options);
|
|
33239
|
+
} else if (command === "setBrightness" || command === "setColor" || command === "setColorTemperature") {
|
|
33240
|
+
if (!cached2) {
|
|
33241
|
+
throw new UsageError(
|
|
33242
|
+
`Device "${deviceId}" is not in the local cache \u2014 run 'switchbot devices list' first so 'expand' can verify this device supports ${command}.`
|
|
33243
|
+
);
|
|
33244
|
+
}
|
|
33245
|
+
const catalogResult = findCatalogEntry(cached2.type);
|
|
33246
|
+
const catalogEntry = Array.isArray(catalogResult) ? catalogResult[0] : catalogResult;
|
|
33247
|
+
const supportedHint = command === "setColor" ? "Color Bulb, Strip Light, Floor Lamp, and similar RGB lighting devices" : "Color Bulb, Strip Light, Ceiling Light, Floor Lamp, and similar lighting devices";
|
|
33248
|
+
if (catalogEntry !== null) {
|
|
33249
|
+
if (!catalogEntry.commands.some((c) => c.command === command)) {
|
|
33250
|
+
throw new UsageError(
|
|
33251
|
+
`Device type "${cached2.type}" does not support ${command}. Supported on: ${supportedHint}.`
|
|
33252
|
+
);
|
|
33253
|
+
}
|
|
33254
|
+
} else {
|
|
33255
|
+
if (!isLightingCommandSupported(cached2.type, command)) {
|
|
33256
|
+
throw new UsageError(
|
|
33257
|
+
`Device type "${cached2.type}" does not support ${command}. Supported on: ${supportedHint}.`
|
|
33258
|
+
);
|
|
33259
|
+
}
|
|
33260
|
+
}
|
|
33261
|
+
if (command === "setBrightness") {
|
|
33262
|
+
parameter = buildBrightnessSet(options);
|
|
33263
|
+
} else if (command === "setColor") {
|
|
33264
|
+
parameter = buildColorSet(options);
|
|
33265
|
+
} else {
|
|
33266
|
+
parameter = buildColorTemperatureSet(options);
|
|
33267
|
+
}
|
|
32990
33268
|
} else {
|
|
32991
33269
|
throw new UsageError(
|
|
32992
|
-
`'expand' does not support "${command}" for device type "${deviceType || "unknown"}". Use 'switchbot devices command' to send raw parameters instead.`
|
|
33270
|
+
`'expand' does not support "${command}" for device type "${deviceType || "unknown"}". Supported: setAll (AC), setPosition (Curtain/Blind Tilt), setMode (Relay Switch), setBrightness/setColor/setColorTemperature (lighting). Use 'switchbot devices command' to send raw parameters instead.`
|
|
32993
33271
|
);
|
|
32994
33272
|
}
|
|
32995
33273
|
if (!options.yes && !isDryRun() && isDestructiveCommand(deviceType, command, "command")) {
|
|
@@ -33153,13 +33431,16 @@ var EXPAND_HINTS = {
|
|
|
33153
33431
|
"Relay Switch 2PM": { command: "setMode", flags: "--channel 1 --mode edge" }
|
|
33154
33432
|
};
|
|
33155
33433
|
function annotateStatusPayload(deviceId, body) {
|
|
33156
|
-
const
|
|
33434
|
+
const cached2 = getCachedDevice(deviceId);
|
|
33435
|
+
const deviceType = cached2?.type ?? "";
|
|
33436
|
+
const annotated = { deviceId, deviceType, ...body };
|
|
33437
|
+
annotated.deviceId = deviceId;
|
|
33438
|
+
annotated.deviceType = deviceType;
|
|
33157
33439
|
if (Object.keys(body).length === 0) {
|
|
33158
33440
|
annotated.supported = false;
|
|
33159
33441
|
annotated.note = "this device does not expose cloud status";
|
|
33160
33442
|
return annotated;
|
|
33161
33443
|
}
|
|
33162
|
-
const cached2 = getCachedDevice(deviceId);
|
|
33163
33444
|
const looksLikeMeter = cached2?.type?.toLowerCase().includes("meter") ?? false;
|
|
33164
33445
|
const staleZeroReading = looksLikeMeter && !Object.prototype.hasOwnProperty.call(body, "onlineStatus") && body.battery === 0 && body.temperature === 0 && body.humidity === 0;
|
|
33165
33446
|
if (staleZeroReading) {
|
|
@@ -33316,7 +33597,7 @@ Examples:
|
|
|
33316
33597
|
rows.push([
|
|
33317
33598
|
d.deviceId,
|
|
33318
33599
|
d.deviceName,
|
|
33319
|
-
d.deviceType || "
|
|
33600
|
+
d.deviceType || d.controlType || "Unknown Device",
|
|
33320
33601
|
"physical",
|
|
33321
33602
|
d.controlType || "\u2014",
|
|
33322
33603
|
d.familyName || "\u2014",
|
|
@@ -33402,7 +33683,7 @@ Examples:
|
|
|
33402
33683
|
const results = await Promise.allSettled(ids.map((id) => fetchDeviceStatus(id)));
|
|
33403
33684
|
const fetchedAt2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
33404
33685
|
const batch = results.map(
|
|
33405
|
-
(r, i) => r.status === "fulfilled" ? { deviceId: ids[i], ok: true,
|
|
33686
|
+
(r, i) => r.status === "fulfilled" ? { deviceId: ids[i], ok: true, fetchedAt: fetchedAt2, ...annotateStatusPayload(ids[i], r.value) } : { deviceId: ids[i], ok: false, error: r.reason?.message ?? String(r.reason) }
|
|
33406
33687
|
);
|
|
33407
33688
|
const batchFmt = resolveFormat();
|
|
33408
33689
|
if (isJsonMode() || batchFmt === "json") {
|
|
@@ -33414,7 +33695,7 @@ Examples:
|
|
|
33414
33695
|
} else {
|
|
33415
33696
|
const rawFields = resolveFields();
|
|
33416
33697
|
for (const entry of batch) {
|
|
33417
|
-
const { deviceId: deviceId2, ok, error: error48,
|
|
33698
|
+
const { deviceId: deviceId2, ok, error: error48, fetchedAt: ts, ...status } = entry;
|
|
33418
33699
|
console.log(`
|
|
33419
33700
|
\u2500\u2500\u2500 ${String(deviceId2)} \u2500\u2500\u2500`);
|
|
33420
33701
|
if (!ok) {
|
|
@@ -33440,11 +33721,11 @@ Examples:
|
|
|
33440
33721
|
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
33441
33722
|
const fmt = resolveFormat();
|
|
33442
33723
|
if (fmt === "json" && process.argv.includes("--json")) {
|
|
33443
|
-
printJson({ ...body,
|
|
33724
|
+
printJson({ ...body, fetchedAt });
|
|
33444
33725
|
return;
|
|
33445
33726
|
}
|
|
33446
33727
|
if (fmt !== "table") {
|
|
33447
|
-
const statusWithTs = { ...body,
|
|
33728
|
+
const statusWithTs = { ...body, fetchedAt };
|
|
33448
33729
|
const allHeaders = Object.keys(statusWithTs);
|
|
33449
33730
|
const allRows = [Object.values(statusWithTs)];
|
|
33450
33731
|
const rawFields = resolveFields();
|
|
@@ -33773,7 +34054,7 @@ Examples:
|
|
|
33773
34054
|
const joinedMatch = findCatalogEntry(joined);
|
|
33774
34055
|
if (joinedMatch && !Array.isArray(joinedMatch)) {
|
|
33775
34056
|
if (isJsonMode()) {
|
|
33776
|
-
printJson(normalizeCatalogForJson(joinedMatch));
|
|
34057
|
+
printJson([normalizeCatalogForJson(joinedMatch)]);
|
|
33777
34058
|
} else {
|
|
33778
34059
|
renderCatalogEntry(joinedMatch);
|
|
33779
34060
|
}
|
|
@@ -33865,12 +34146,19 @@ Examples:
|
|
|
33865
34146
|
});
|
|
33866
34147
|
return;
|
|
33867
34148
|
}
|
|
34149
|
+
if (result.typeSource === "controlType") {
|
|
34150
|
+
const deviceName2 = device.deviceName ?? deviceId;
|
|
34151
|
+
console.error(`warning: ${deviceName2} (${deviceId}): deviceType not reported by API, using controlType "${result.controlType}". Capabilities may be limited.`);
|
|
34152
|
+
} else if (typeName === "Unknown Device") {
|
|
34153
|
+
const deviceName2 = device.deviceName ?? deviceId;
|
|
34154
|
+
console.error(`warning: ${deviceName2} (${deviceId}): neither deviceType nor controlType reported by API. Capabilities may be limited.`);
|
|
34155
|
+
}
|
|
33868
34156
|
if (isPhysical) {
|
|
33869
34157
|
const physical = device;
|
|
33870
34158
|
printKeyValue({
|
|
33871
34159
|
deviceId: physical.deviceId,
|
|
33872
34160
|
deviceName: physical.deviceName,
|
|
33873
|
-
deviceType: physical.deviceType || "
|
|
34161
|
+
deviceType: physical.deviceType || physical.controlType || "Unknown Device",
|
|
33874
34162
|
controlType: physical.controlType || "\u2014",
|
|
33875
34163
|
family: physical.familyName || "\u2014",
|
|
33876
34164
|
roomID: physical.roomID || "\u2014",
|
|
@@ -34049,7 +34337,7 @@ Examples:
|
|
|
34049
34337
|
handleError(error48);
|
|
34050
34338
|
}
|
|
34051
34339
|
});
|
|
34052
|
-
scenes.command("execute").description("Execute a manual scene by its ID").argument("<sceneId>", 'Scene ID from "scenes list"').addHelpText("after", `
|
|
34340
|
+
scenes.command("execute").alias("run").description("Execute a manual scene by its ID").argument("<sceneId>", 'Scene ID from "scenes list"').addHelpText("after", `
|
|
34053
34341
|
Example:
|
|
34054
34342
|
$ switchbot scenes execute T12345678
|
|
34055
34343
|
`).action(async (sceneId) => {
|
|
@@ -48672,9 +48960,9 @@ var logLevel = process.env.LOG_LEVEL || "warn";
|
|
|
48672
48960
|
var logFormat = process.env.LOG_FORMAT || "json";
|
|
48673
48961
|
var pinoConfig = {
|
|
48674
48962
|
level: logLevel,
|
|
48675
|
-
transport: logFormat === "pretty" ? { target: "pino-pretty" } : void 0
|
|
48963
|
+
transport: logFormat === "pretty" ? { target: "pino-pretty", options: { destination: 2 } } : void 0
|
|
48676
48964
|
};
|
|
48677
|
-
var log = pino(pinoConfig);
|
|
48965
|
+
var log = logFormat === "pretty" ? pino(pinoConfig) : pino(pinoConfig, pino.destination(2));
|
|
48678
48966
|
|
|
48679
48967
|
// src/mcp/device-history.ts
|
|
48680
48968
|
init_cjs_shim();
|
|
@@ -49782,15 +50070,27 @@ against the live API without executing any mutations.
|
|
|
49782
50070
|
handleError(err);
|
|
49783
50071
|
}
|
|
49784
50072
|
});
|
|
49785
|
-
plan.command("run").description("Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default").argument("[file]", 'Path to plan.json, or "-" / omit to read stdin').option("--yes", "Authorize destructive commands (e.g. Smart Lock unlock, Garage open)").option("--require-approval", "Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)").option("--continue-on-error", "Keep running after a failed step (default: stop at first error)").action(
|
|
50073
|
+
plan.command("run").description("Validate + preview/execute a plan. Respects --dry-run; destructive steps require the reviewed plan flow by default").argument("[file]", 'Path to plan.json, or "-" / omit to read stdin').option("--yes", "Authorize destructive commands (e.g. Smart Lock unlock, Garage open)").option("--require-approval", "Prompt for confirmation before each destructive step (TTY only; mutually exclusive with --json)").option("--continue-on-error", "Keep running after a failed step (default: stop at first error)").option("--plan <json>", "Inline plan JSON (alternative to file argument or stdin)").action(
|
|
49786
50074
|
async (file2, options) => {
|
|
49787
50075
|
if (options.requireApproval && isJsonMode()) {
|
|
49788
50076
|
console.error("error: --require-approval cannot be used with --json (no TTY available for prompts)");
|
|
49789
50077
|
process.exit(1);
|
|
49790
50078
|
}
|
|
50079
|
+
if (options.plan !== void 0 && file2 !== void 0) {
|
|
50080
|
+
console.error("error: --plan and a file argument are mutually exclusive.");
|
|
50081
|
+
process.exit(2);
|
|
50082
|
+
}
|
|
49791
50083
|
let raw;
|
|
49792
50084
|
try {
|
|
49793
|
-
|
|
50085
|
+
if (options.plan !== void 0) {
|
|
50086
|
+
try {
|
|
50087
|
+
raw = JSON.parse(options.plan);
|
|
50088
|
+
} catch (err) {
|
|
50089
|
+
throw new UsageError(`--plan is not valid JSON: ${err.message}`);
|
|
50090
|
+
}
|
|
50091
|
+
} else {
|
|
50092
|
+
raw = await readPlanSource(file2);
|
|
50093
|
+
}
|
|
49794
50094
|
} catch (err) {
|
|
49795
50095
|
handleError(err);
|
|
49796
50096
|
}
|
|
@@ -52513,12 +52813,27 @@ function listRegisteredTools(server) {
|
|
|
52513
52813
|
if (!internal._registeredTools) return [];
|
|
52514
52814
|
return Object.keys(internal._registeredTools).sort();
|
|
52515
52815
|
}
|
|
52816
|
+
function listRegisteredToolsWithMeta(server) {
|
|
52817
|
+
const internal = server;
|
|
52818
|
+
if (!internal._registeredTools) return [];
|
|
52819
|
+
return Object.entries(internal._registeredTools).sort(([a], [b2]) => a.localeCompare(b2)).map(([name, reg]) => {
|
|
52820
|
+
const entry = { name };
|
|
52821
|
+
if (reg.description) entry.description = reg.description;
|
|
52822
|
+
if (reg.inputSchema) {
|
|
52823
|
+
try {
|
|
52824
|
+
entry.inputSchema = external_exports.toJSONSchema(reg.inputSchema);
|
|
52825
|
+
} catch {
|
|
52826
|
+
}
|
|
52827
|
+
}
|
|
52828
|
+
return entry;
|
|
52829
|
+
});
|
|
52830
|
+
}
|
|
52516
52831
|
function listRegisteredResources() {
|
|
52517
52832
|
return ["switchbot://events"];
|
|
52518
52833
|
}
|
|
52519
52834
|
function printMcpToolDirectory() {
|
|
52520
52835
|
const server = createSwitchBotMcpServer();
|
|
52521
|
-
const tools =
|
|
52836
|
+
const tools = listRegisteredToolsWithMeta(server);
|
|
52522
52837
|
const resources = listRegisteredResources().map((uri) => ({ uri }));
|
|
52523
52838
|
if (isJsonMode()) {
|
|
52524
52839
|
printJson({ tools, resources });
|
|
@@ -52526,7 +52841,8 @@ function printMcpToolDirectory() {
|
|
|
52526
52841
|
}
|
|
52527
52842
|
console.log("Tools:");
|
|
52528
52843
|
for (const tool of tools) {
|
|
52529
|
-
|
|
52844
|
+
const desc = tool.description ? ` \u2014 ${tool.description.slice(0, 80)}` : "";
|
|
52845
|
+
console.log(` ${tool.name}${desc}`);
|
|
52530
52846
|
}
|
|
52531
52847
|
console.log("");
|
|
52532
52848
|
console.log("Resources:");
|
|
@@ -52538,7 +52854,7 @@ Total: ${tools.length} tool(s), ${resources.length} resource(s)`);
|
|
|
52538
52854
|
}
|
|
52539
52855
|
function registerMcpCommand(program3) {
|
|
52540
52856
|
const mcp = program3.command("mcp").description("Run as a Model Context Protocol server so AI agents can call SwitchBot tools").addHelpText("after", `
|
|
52541
|
-
The MCP server exposes twenty-
|
|
52857
|
+
The MCP server exposes twenty-four tools:
|
|
52542
52858
|
- list_devices fetch all physical + IR devices
|
|
52543
52859
|
- get_device_status live status for a physical device
|
|
52544
52860
|
- send_command control a device (destructive commands need confirm:true)
|
|
@@ -52561,6 +52877,8 @@ function registerMcpCommand(program3) {
|
|
|
52561
52877
|
- audit_stats aggregate audit counts by kind/result/device/rule
|
|
52562
52878
|
- rule_notifications query rule notify action delivery history
|
|
52563
52879
|
- rules_suggest draft an automation rule YAML from intent (heuristic, no LLM)
|
|
52880
|
+
- rules_explain show why a rule evaluation fired or was blocked
|
|
52881
|
+
- rules_simulate simulate a rule against historical events
|
|
52564
52882
|
- policy_add_rule append a rule into automation.rules[] in policy.yaml
|
|
52565
52883
|
|
|
52566
52884
|
Resource (read-only):
|
|
@@ -53003,7 +53321,7 @@ Examples:
|
|
|
53003
53321
|
throw new UsageError(`"${match.type}" exists in the effective catalog but not in source "${source}".`);
|
|
53004
53322
|
}
|
|
53005
53323
|
if (isJsonMode()) {
|
|
53006
|
-
printJson(picked);
|
|
53324
|
+
printJson([picked]);
|
|
53007
53325
|
return;
|
|
53008
53326
|
}
|
|
53009
53327
|
renderEntry(picked);
|
|
@@ -53745,7 +54063,7 @@ Examples:
|
|
|
53745
54063
|
let matchedCount = 0;
|
|
53746
54064
|
const ac = new AbortController();
|
|
53747
54065
|
const forTimer = forMs !== null && forMs > 0 ? setTimeout(() => ac.abort(), forMs) : null;
|
|
53748
|
-
if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push" });
|
|
54066
|
+
if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push", schemaVersion: EVENTS_SCHEMA_VERSION });
|
|
53749
54067
|
await new Promise((resolve2, reject) => {
|
|
53750
54068
|
let server = null;
|
|
53751
54069
|
try {
|
|
@@ -53897,7 +54215,7 @@ Examples:
|
|
|
53897
54215
|
if (!isJsonMode()) {
|
|
53898
54216
|
console.error("Fetching MQTT credentials from SwitchBot service\u2026");
|
|
53899
54217
|
}
|
|
53900
|
-
if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push" });
|
|
54218
|
+
if (isJsonMode()) emitStreamHeader({ eventKind: "event", cadence: "push", schemaVersion: EVENTS_SCHEMA_VERSION });
|
|
53901
54219
|
if (isJsonMode()) {
|
|
53902
54220
|
const sessionStartAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
53903
54221
|
emitJsonStreamRecord({
|
|
@@ -55467,7 +55785,7 @@ function runSchemaExport(options) {
|
|
|
55467
55785
|
payload.resources = RESOURCE_CATALOG;
|
|
55468
55786
|
payload.cliAddedFields = [
|
|
55469
55787
|
{
|
|
55470
|
-
field: "
|
|
55788
|
+
field: "fetchedAt",
|
|
55471
55789
|
appliesTo: ["devices status", "devices describe"],
|
|
55472
55790
|
type: "string (ISO-8601)",
|
|
55473
55791
|
description: "CLI-synthesized timestamp indicating when this status response was fetched or served from the cache. Not part of the upstream SwitchBot API."
|
|
@@ -55530,7 +55848,7 @@ Common top-level fields:
|
|
|
55530
55848
|
schemaVersion CLI schema version (stable for agent contracts)
|
|
55531
55849
|
data.version Catalog schema version
|
|
55532
55850
|
data.types Array of SchemaEntry (or CompactSchemaEntry with --compact)
|
|
55533
|
-
data.
|
|
55851
|
+
data.fetchedAt CLI-added; present on live-query responses ('devices status'),
|
|
55534
55852
|
not on this offline export.
|
|
55535
55853
|
|
|
55536
55854
|
Examples:
|
|
@@ -55566,7 +55884,7 @@ Examples:
|
|
|
55566
55884
|
$ switchbot history show --limit 10
|
|
55567
55885
|
$ switchbot history replay 3
|
|
55568
55886
|
`);
|
|
55569
|
-
history.command("show").description("Print recent audit entries").option("--file <path>", `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg("--file")).option("--limit <n>", "Show only the last N entries", intArg("--limit", { min: 1 })).action((options) => {
|
|
55887
|
+
history.command("show").alias("list").description("Print recent audit entries").option("--file <path>", `Path to the audit log (default ${DEFAULT_AUDIT})`, stringArg("--file")).option("--limit <n>", "Show only the last N entries", intArg("--limit", { min: 1 })).action((options) => {
|
|
55570
55888
|
const file2 = options.file ?? DEFAULT_AUDIT;
|
|
55571
55889
|
const entries = readAudit(file2);
|
|
55572
55890
|
const limited = options.limit !== void 0 ? entries.slice(-Math.max(1, Number(options.limit) || 1)) : entries;
|
|
@@ -56841,13 +57159,12 @@ function registerRun(rules) {
|
|
|
56841
57159
|
const loaded = loadAutomation(pathArg);
|
|
56842
57160
|
if (!loaded) return;
|
|
56843
57161
|
if (loaded.automation?.enabled !== true) {
|
|
56844
|
-
|
|
56845
|
-
|
|
56846
|
-
|
|
56847
|
-
|
|
56848
|
-
|
|
56849
|
-
}
|
|
56850
|
-
process.exit(0);
|
|
57162
|
+
exitWithError({
|
|
57163
|
+
code: 1,
|
|
57164
|
+
kind: "runtime",
|
|
57165
|
+
message: "automation.enabled is not true \u2014 set it to true in your policy file to start the daemon.",
|
|
57166
|
+
hint: "Set automation.enabled: true in your policy file, then re-run."
|
|
57167
|
+
});
|
|
56851
57168
|
}
|
|
56852
57169
|
const lint = lintRules(loaded.automation);
|
|
56853
57170
|
if (!lint.valid) {
|
|
@@ -59216,6 +59533,7 @@ import os25 from "node:os";
|
|
|
59216
59533
|
import path27 from "node:path";
|
|
59217
59534
|
var DEFAULT_AUDIT_PATH3 = path27.join(os25.homedir(), ".switchbot", "audit.log");
|
|
59218
59535
|
var AUDIT_ERROR_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
59536
|
+
var EXPECTED_ERROR_CODES = /* @__PURE__ */ new Set([161, 171, 190]);
|
|
59219
59537
|
function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
59220
59538
|
const now = /* @__PURE__ */ new Date();
|
|
59221
59539
|
const procHealth = {
|
|
@@ -59236,20 +59554,46 @@ function getHealthReport(auditPath = DEFAULT_AUDIT_PATH3) {
|
|
|
59236
59554
|
};
|
|
59237
59555
|
let auditHealth;
|
|
59238
59556
|
if (!fs31.existsSync(auditPath)) {
|
|
59239
|
-
auditHealth = {
|
|
59557
|
+
auditHealth = {
|
|
59558
|
+
present: false,
|
|
59559
|
+
recentErrors: 0,
|
|
59560
|
+
recentTotal: 0,
|
|
59561
|
+
errorRatePercent: 0,
|
|
59562
|
+
expectedErrors: 0,
|
|
59563
|
+
unexpectedErrors: 0,
|
|
59564
|
+
unexpectedRatePercent: 0,
|
|
59565
|
+
breakdown: {},
|
|
59566
|
+
status: "ok"
|
|
59567
|
+
};
|
|
59240
59568
|
} else {
|
|
59241
59569
|
const entries = readAudit(auditPath);
|
|
59242
59570
|
const windowStart = now.getTime() - AUDIT_ERROR_WINDOW_MS;
|
|
59243
59571
|
const recent = entries.filter((e) => new Date(e.t).getTime() >= windowStart);
|
|
59244
|
-
const
|
|
59572
|
+
const errorEntries = recent.filter((e) => e.result === "error");
|
|
59245
59573
|
const total = recent.length;
|
|
59574
|
+
const errors = errorEntries.length;
|
|
59246
59575
|
const errorRate = total > 0 ? Math.round(errors / total * 100) : 0;
|
|
59576
|
+
const breakdown = {};
|
|
59577
|
+
let expectedErrors = 0;
|
|
59578
|
+
for (const e of errorEntries) {
|
|
59579
|
+
const code = e.statusCode !== void 0 ? String(e.statusCode) : "unknown";
|
|
59580
|
+
breakdown[code] = (breakdown[code] ?? 0) + 1;
|
|
59581
|
+
if (e.statusCode !== void 0 && EXPECTED_ERROR_CODES.has(e.statusCode)) {
|
|
59582
|
+
expectedErrors++;
|
|
59583
|
+
}
|
|
59584
|
+
}
|
|
59585
|
+
const unexpectedErrors = errors - expectedErrors;
|
|
59586
|
+
const unexpectedRatePercent = total > 0 ? Math.round(unexpectedErrors / total * 100 * 10) / 10 : 0;
|
|
59247
59587
|
auditHealth = {
|
|
59248
59588
|
present: true,
|
|
59249
59589
|
recentErrors: errors,
|
|
59250
59590
|
recentTotal: total,
|
|
59251
59591
|
errorRatePercent: errorRate,
|
|
59252
|
-
|
|
59592
|
+
expectedErrors,
|
|
59593
|
+
unexpectedErrors,
|
|
59594
|
+
unexpectedRatePercent,
|
|
59595
|
+
breakdown,
|
|
59596
|
+
status: unexpectedRatePercent >= 30 ? "warn" : "ok"
|
|
59253
59597
|
};
|
|
59254
59598
|
}
|
|
59255
59599
|
const cbStats = apiCircuitBreaker.getStats();
|
|
@@ -59658,7 +60002,7 @@ The daemon reads the same policy file as \`switchbot rules run\`.
|
|
|
59658
60002
|
}
|
|
59659
60003
|
}
|
|
59660
60004
|
const thisFile = fileURLToPath3(import.meta.url);
|
|
59661
|
-
const cliEntry = path28.resolve(path28.dirname(thisFile), "..", "index.js");
|
|
60005
|
+
const cliEntry = path28.basename(thisFile) === "index.js" ? thisFile : path28.resolve(path28.dirname(thisFile), "..", "index.js");
|
|
59662
60006
|
const args = ["rules", "run"];
|
|
59663
60007
|
if (opts.policy) args.push(opts.policy);
|
|
59664
60008
|
fs32.mkdirSync(path28.dirname(DAEMON_PID_FILE), { recursive: true, mode: 448 });
|
|
@@ -60015,17 +60359,32 @@ try {
|
|
|
60015
60359
|
} catch (err) {
|
|
60016
60360
|
if (err instanceof CommanderError) {
|
|
60017
60361
|
if (err.code === "commander.helpDisplayed") {
|
|
60362
|
+
const helpRequested = process.argv.includes("--help") || process.argv.includes("-h") || process.argv.includes("help");
|
|
60363
|
+
if (helpRequested) {
|
|
60364
|
+
if (isJsonMode()) {
|
|
60365
|
+
const target = resolveTargetCommand(program2, process.argv.slice(2));
|
|
60366
|
+
printJson(commandToJson(target, { includeIdentity: target === program2 }));
|
|
60367
|
+
}
|
|
60368
|
+
process.exit(0);
|
|
60369
|
+
}
|
|
60018
60370
|
if (isJsonMode()) {
|
|
60019
60371
|
const target = resolveTargetCommand(program2, process.argv.slice(2));
|
|
60020
|
-
|
|
60372
|
+
const subNames = target.commands.map((c) => c.name()).join(", ");
|
|
60373
|
+
const usefulMessage = subNames ? `${target.name()}: a subcommand is required. Available: ${subNames}` : err.message;
|
|
60374
|
+
emitJsonError({ code: 2, kind: "usage", message: usefulMessage });
|
|
60021
60375
|
}
|
|
60022
|
-
process.exit(
|
|
60376
|
+
process.exit(2);
|
|
60023
60377
|
}
|
|
60024
60378
|
if (err.code === "commander.version") {
|
|
60025
60379
|
process.exit(0);
|
|
60026
60380
|
}
|
|
60027
60381
|
if (isJsonMode()) {
|
|
60028
|
-
|
|
60382
|
+
const errorMessage = err.code === "commander.help" ? (() => {
|
|
60383
|
+
const target = resolveTargetCommand(program2, process.argv.slice(2));
|
|
60384
|
+
const subNames = target.commands.map((c) => c.name()).join(", ");
|
|
60385
|
+
return subNames ? `${target.name()}: a subcommand is required. Available: ${subNames}` : err.message;
|
|
60386
|
+
})() : err.message;
|
|
60387
|
+
emitJsonError({ code: 2, kind: "usage", message: errorMessage });
|
|
60029
60388
|
}
|
|
60030
60389
|
process.exit(2);
|
|
60031
60390
|
}
|