@homebridge-plugins/homebridge-eufy-security 4.6.0-beta.3 → 4.6.0-beta.30
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/.claude/CLAUDE.md +175 -0
- package/.claude/PRD.md +241 -0
- package/.claude/skills/architect/SKILL.md +76 -0
- package/.claude/skills/developer/SKILL.md +59 -0
- package/.claude/skills/new-device-support/SKILL.md +102 -51
- package/.claude/skills/new-device-support/check-device.mjs +363 -0
- package/.claude/skills/new-device-support/map-properties.mjs +144 -10
- package/.claude/skills/new-device-support/verify-device.mjs +272 -0
- package/.claude/skills/planner/SKILL.md +100 -0
- package/.claude/skills/qa/SKILL.md +79 -0
- package/.claude/skills/support/SKILL.md +175 -0
- package/dist/accessories/CameraAccessory.js +7 -0
- package/dist/accessories/CameraAccessory.js.map +1 -1
- package/dist/controller/LocalLivestreamManager.js +79 -8
- package/dist/controller/LocalLivestreamManager.js.map +1 -1
- package/dist/controller/recordingDelegate.js +19 -6
- package/dist/controller/recordingDelegate.js.map +1 -1
- package/dist/controller/streamingDelegate.js +6 -3
- package/dist/controller/streamingDelegate.js.map +1 -1
- package/dist/platform.js +3 -25
- package/dist/platform.js.map +1 -1
- package/dist/utils/configTypes.js +1 -0
- package/dist/utils/configTypes.js.map +1 -1
- package/dist/utils/ffmpeg.js +123 -15
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/utils.js +27 -2
- package/dist/utils/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/homebridge-ui/public/assets/devices/eufyCamS4_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
- package/homebridge-ui/public/assets/devices/nvr_s4_max_T8N00_large.png +0 -0
- package/homebridge-ui/public/utils/device-images.js +1 -0
- package/homebridge-ui/public/views/device-detail.js +10 -0
- package/homebridge-ui/server.js +1 -25
- package/package.json +7 -7
- package/scripts/decrypt-diagnostics.mjs +6 -7
|
@@ -6,13 +6,18 @@
|
|
|
6
6
|
* to the corresponding property constants in eufy-security-client.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
|
-
* node map-properties.mjs <raw-properties.json>
|
|
10
|
-
* cat raw.json | node map-properties.mjs
|
|
9
|
+
* node map-properties.mjs <raw-properties.json> [--closest]
|
|
10
|
+
* cat raw.json | node map-properties.mjs [--closest]
|
|
11
11
|
*
|
|
12
12
|
* The input JSON should be the rawProperties object from a device dump,
|
|
13
13
|
* e.g. { "1101": { "value": 100 }, "1013": { "value": 1 }, ... }
|
|
14
14
|
* or an array of { "param_type": 1101, "param_value": "..." } objects.
|
|
15
15
|
*
|
|
16
|
+
* Options:
|
|
17
|
+
* --closest Find the closest existing devices by Jaccard similarity
|
|
18
|
+
* on resolved PropertyName overlap. Useful to identify which
|
|
19
|
+
* existing device to base the DeviceProperties block on.
|
|
20
|
+
*
|
|
16
21
|
* Output: a table mapping each param_type to its CommandType/ParamType enum name
|
|
17
22
|
* and all property constants that use that key.
|
|
18
23
|
*/
|
|
@@ -122,14 +127,17 @@ function parsePropertyConstants(source) {
|
|
|
122
127
|
props.push({ constName, key, keyRaw, propertyName, spreadFrom });
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
// Resolve spreads:
|
|
130
|
+
// Resolve spreads: inherit missing fields from parent property
|
|
126
131
|
const byName = new Map(props.map((p) => [p.constName, p]));
|
|
127
132
|
for (const prop of props) {
|
|
128
|
-
if (prop.
|
|
133
|
+
if (prop.spreadFrom) {
|
|
129
134
|
const parent = byName.get(prop.spreadFrom);
|
|
130
135
|
if (parent) {
|
|
131
|
-
prop.key
|
|
132
|
-
|
|
136
|
+
if (prop.key === null) {
|
|
137
|
+
prop.key = parent.key;
|
|
138
|
+
if (!prop.keyRaw) prop.keyRaw = `(spread from ${prop.spreadFrom}) ${parent.keyRaw}`;
|
|
139
|
+
}
|
|
140
|
+
// Always inherit propertyName from parent when missing, even if key is overridden
|
|
133
141
|
if (!prop.propertyName) prop.propertyName = parent.propertyName;
|
|
134
142
|
}
|
|
135
143
|
}
|
|
@@ -182,7 +190,7 @@ const propUsage = parseDevicePropertiesUsage(httpSource);
|
|
|
182
190
|
// ── Step 4: Read and parse input JSON ──────────────────────────────────────
|
|
183
191
|
|
|
184
192
|
let inputData;
|
|
185
|
-
const inputFile = process.argv
|
|
193
|
+
const inputFile = process.argv.slice(2).find((a) => !a.startsWith("-"));
|
|
186
194
|
if (inputFile) {
|
|
187
195
|
inputData = readFileSync(inputFile, "utf-8");
|
|
188
196
|
} else {
|
|
@@ -298,19 +306,145 @@ if (unmatched.length > 0) {
|
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
|
|
309
|
+
// ── Step 6: Companion custom properties ─────────────────────────────────────
|
|
310
|
+
// Some param_type-based properties require a companion custom (runtime) property
|
|
311
|
+
// that never appears in raw device data. Define those pairings here so the
|
|
312
|
+
// suggested block always includes both.
|
|
313
|
+
|
|
314
|
+
const COMPANION_PROPERTIES = new Map([
|
|
315
|
+
// DeviceRTSPStream (CMD_NAS_SWITCH) → DeviceRTSPStreamUrl (custom_rtspStreamUrl)
|
|
316
|
+
["PropertyName.DeviceRTSPStream", {
|
|
317
|
+
propertyName: "PropertyName.DeviceRTSPStreamUrl",
|
|
318
|
+
constName: "DeviceRTSPStreamUrlProperty",
|
|
319
|
+
reason: "RTSP URL is set at runtime by the station — must accompany DeviceRTSPStream",
|
|
320
|
+
}],
|
|
321
|
+
// DeviceWifiRSSI → DeviceWifiSignalLevel (custom_wifiSignalLevel)
|
|
322
|
+
["PropertyName.DeviceWifiRSSI", {
|
|
323
|
+
propertyName: "PropertyName.DeviceWifiSignalLevel",
|
|
324
|
+
constName: "DeviceWifiSignalLevelProperty",
|
|
325
|
+
reason: "WiFi signal level is derived at runtime from RSSI",
|
|
326
|
+
}],
|
|
327
|
+
// DeviceCellularRSSI → DeviceCellularSignalLevel (custom_cellularSignalLevel)
|
|
328
|
+
["PropertyName.DeviceCellularRSSI", {
|
|
329
|
+
propertyName: "PropertyName.DeviceCellularSignalLevel",
|
|
330
|
+
constName: "DeviceCellularSignalLevelProperty",
|
|
331
|
+
reason: "Cellular signal level is derived at runtime from RSSI",
|
|
332
|
+
}],
|
|
333
|
+
]);
|
|
334
|
+
|
|
301
335
|
// Output a suggested PropertyName list for use in DeviceProperties block
|
|
302
336
|
console.log();
|
|
303
337
|
console.log("=".repeat(80));
|
|
304
338
|
console.log("SUGGESTED DeviceProperties block entries:");
|
|
305
339
|
console.log("=".repeat(80));
|
|
306
340
|
const seen = new Set();
|
|
341
|
+
const companionsAdded = [];
|
|
307
342
|
for (const m of matched) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
// Use original props lookup to get correct constName → propertyName mapping
|
|
344
|
+
// (the deduplicated m.propertyNames array loses the index association with m.constNames)
|
|
345
|
+
const propsForKey = keyToProps.get(m.paramType) || [];
|
|
346
|
+
for (const prop of propsForKey) {
|
|
347
|
+
const pn = prop.propertyName;
|
|
348
|
+
const cn = prop.constName;
|
|
311
349
|
if (pn && !seen.has(pn)) {
|
|
312
350
|
seen.add(pn);
|
|
313
351
|
console.log(` [${pn}]: ${cn},`);
|
|
352
|
+
|
|
353
|
+
// Check if this property has a required companion
|
|
354
|
+
const companion = COMPANION_PROPERTIES.get(pn);
|
|
355
|
+
if (companion && !seen.has(companion.propertyName)) {
|
|
356
|
+
seen.add(companion.propertyName);
|
|
357
|
+
console.log(` [${companion.propertyName}]: ${companion.constName}, // ⚠ companion (custom/runtime)`);
|
|
358
|
+
companionsAdded.push(companion);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (companionsAdded.length > 0) {
|
|
365
|
+
console.log();
|
|
366
|
+
console.log("=".repeat(80));
|
|
367
|
+
console.log("⚠ COMPANION PROPERTIES (custom/runtime — not in raw device data):");
|
|
368
|
+
console.log("=".repeat(80));
|
|
369
|
+
for (const c of companionsAdded) {
|
|
370
|
+
console.log(` ${c.constName}: ${c.reason}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Step 7: Closest device detection (--closest flag) ───────────────────────
|
|
375
|
+
|
|
376
|
+
if (process.argv.includes("--closest")) {
|
|
377
|
+
// Collect the set of PropertyName values from the matched raw properties
|
|
378
|
+
const targetPropNames = new Set();
|
|
379
|
+
for (const m of matched) {
|
|
380
|
+
for (const pn of m.propertyNames) {
|
|
381
|
+
if (pn) targetPropNames.add(pn);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (targetPropNames.size === 0) {
|
|
386
|
+
console.log("\nCannot compute closest device — no PropertyName matches found.");
|
|
387
|
+
} else {
|
|
388
|
+
// Parse DeviceProperties blocks to get property sets per device type
|
|
389
|
+
const dpStart = httpSource.indexOf("export const DeviceProperties");
|
|
390
|
+
const dpEnd = httpSource.indexOf("export const StationProperties");
|
|
391
|
+
if (dpStart !== -1) {
|
|
392
|
+
const dpSection = httpSource.slice(dpStart, dpEnd === -1 ? undefined : dpEnd);
|
|
393
|
+
|
|
394
|
+
const devicePropSets = new Map(); // DeviceType name → Set of PropertyName references
|
|
395
|
+
const blockRegex = /\[DeviceType\.(\w+)\]\s*:\s*\{([\s\S]*?)\}/g;
|
|
396
|
+
let bm;
|
|
397
|
+
while ((bm = blockRegex.exec(dpSection)) !== null) {
|
|
398
|
+
const dt = bm[1];
|
|
399
|
+
const body = bm[2];
|
|
400
|
+
const props = new Set();
|
|
401
|
+
// Extract property constants used, then resolve to PropertyName
|
|
402
|
+
const refRegex = /:\s*(\w+Property)/g;
|
|
403
|
+
let rm;
|
|
404
|
+
while ((rm = refRegex.exec(body)) !== null) {
|
|
405
|
+
const constName = rm[1];
|
|
406
|
+
const prop = allProps.find((p) => p.constName === constName);
|
|
407
|
+
if (prop && prop.propertyName) props.add(prop.propertyName);
|
|
408
|
+
}
|
|
409
|
+
devicePropSets.set(dt, props);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Jaccard similarity
|
|
413
|
+
const similarities = [];
|
|
414
|
+
for (const [dt, props] of devicePropSets) {
|
|
415
|
+
const intersection = new Set([...targetPropNames].filter((p) => props.has(p)));
|
|
416
|
+
const union = new Set([...targetPropNames, ...props]);
|
|
417
|
+
const jaccard = union.size > 0 ? intersection.size / union.size : 0;
|
|
418
|
+
if (jaccard > 0.2) {
|
|
419
|
+
similarities.push({
|
|
420
|
+
device: dt,
|
|
421
|
+
jaccard: Math.round(jaccard * 100),
|
|
422
|
+
shared: intersection.size,
|
|
423
|
+
targetOnly: [...targetPropNames].filter((p) => !props.has(p)).length,
|
|
424
|
+
otherOnly: [...props].filter((p) => !targetPropNames.has(p)).length,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
similarities.sort((a, b) => b.jaccard - a.jaccard);
|
|
430
|
+
const top = similarities.slice(0, 8);
|
|
431
|
+
|
|
432
|
+
console.log();
|
|
433
|
+
console.log("=".repeat(80));
|
|
434
|
+
console.log("CLOSEST EXISTING DEVICES (by property overlap with raw data)");
|
|
435
|
+
console.log("=".repeat(80));
|
|
436
|
+
if (top.length === 0) {
|
|
437
|
+
console.log(" No devices with >20% property overlap found.");
|
|
438
|
+
} else {
|
|
439
|
+
console.log(" " + "Device".padEnd(38) + "Similarity Shared Target-only Other-only");
|
|
440
|
+
for (const d of top) {
|
|
441
|
+
console.log(
|
|
442
|
+
` ${d.device.padEnd(38)} ${String(d.jaccard + "%").padEnd(11)} ${String(d.shared).padEnd(7)} ${String(d.targetOnly).padEnd(12)} ${d.otherOnly}`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
console.log();
|
|
446
|
+
console.log(` Recommended base: ${top[0].device} (${top[0].jaccard}% overlap)`);
|
|
447
|
+
}
|
|
314
448
|
}
|
|
315
449
|
}
|
|
316
450
|
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-device.mjs
|
|
4
|
+
*
|
|
5
|
+
* Post-implementation verification script. Checks that a device type is fully
|
|
6
|
+
* wired into all required registration points after implementation.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node verify-device.mjs <ENUM_NAME>
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* node verify-device.mjs CAMERA_4G_S330
|
|
13
|
+
* node verify-device.mjs INDOOR_PT_CAMERA_E30
|
|
14
|
+
*
|
|
15
|
+
* Output: PASS/FAIL per check, with a final summary.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, existsSync } from "fs";
|
|
19
|
+
import { join, dirname } from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
|
|
24
|
+
const CLIENT_ROOT = join(__dirname, "..", "..", "..", "..", "eufy-security-client");
|
|
25
|
+
const HB_ROOT = join(__dirname, "..", "..", "..");
|
|
26
|
+
const TYPES_HTTP = join(CLIENT_ROOT, "src", "http", "types.ts");
|
|
27
|
+
const DEVICE_TS = join(CLIENT_ROOT, "src", "http", "device.ts");
|
|
28
|
+
const DEVICE_IMAGES = join(HB_ROOT, "homebridge-ui", "public", "utils", "device-images.js");
|
|
29
|
+
const SUPPORTED_DEVICES = join(CLIENT_ROOT, "docs", "supported_devices.md");
|
|
30
|
+
|
|
31
|
+
const enumName = process.argv[2];
|
|
32
|
+
if (!enumName || enumName.startsWith("-")) {
|
|
33
|
+
console.error("Usage: verify-device.mjs <ENUM_NAME>");
|
|
34
|
+
console.error("Example: verify-device.mjs CAMERA_4G_S330");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const httpSource = readFileSync(TYPES_HTTP, "utf-8");
|
|
39
|
+
const deviceSource = readFileSync(DEVICE_TS, "utf-8");
|
|
40
|
+
|
|
41
|
+
// ── Extract type number from enum ───────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function getTypeNumber(source, enumN) {
|
|
44
|
+
const re = new RegExp(`^\\s*${enumN}\\s*=\\s*(\\d+)`, "m");
|
|
45
|
+
const match = source.match(re);
|
|
46
|
+
return match ? parseInt(match[1]) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const typeNumber = getTypeNumber(httpSource, enumName);
|
|
50
|
+
|
|
51
|
+
// ── Check functions ─────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function checkEnumExists(source, enumN) {
|
|
54
|
+
const re = new RegExp(`^\\s*${enumN}\\s*=\\s*\\d+`, "m");
|
|
55
|
+
return re.test(source);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkGenericTypePropertyStates(source, typeNum) {
|
|
59
|
+
if (!typeNum) return { pass: false, detail: "no type number" };
|
|
60
|
+
const gtpMatch = source.match(/export\s+const\s+GenericTypeProperty[\s\S]*?states:\s*\{([\s\S]*?)\}/);
|
|
61
|
+
if (!gtpMatch) return { pass: false, detail: "GenericTypeProperty not found" };
|
|
62
|
+
const re = new RegExp(`${typeNum}:\\s*"`);
|
|
63
|
+
const found = re.test(gtpMatch[1]);
|
|
64
|
+
return { pass: found, detail: found ? "label present" : "missing label entry" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function checkLookupMap(source, mapName, enumN) {
|
|
68
|
+
const dpStart = source.indexOf(`export const ${mapName}`);
|
|
69
|
+
if (dpStart === -1) return { pass: false, detail: `${mapName} not found` };
|
|
70
|
+
const nextExport = source.indexOf("export const ", dpStart + 20);
|
|
71
|
+
const section = source.slice(dpStart, nextExport === -1 ? undefined : nextExport);
|
|
72
|
+
const found = section.includes(`[DeviceType.${enumN}]`);
|
|
73
|
+
return { pass: found, detail: found ? "entry present" : "missing entry" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function checkTypeGuard(source, enumN) {
|
|
77
|
+
// Look for a static method that returns DeviceType.ENUM == type
|
|
78
|
+
const re = new RegExp(`static\\s+(is\\w+)\\s*\\(\\s*type:\\s*number\\s*\\)[^}]*DeviceType\\.${enumN}`, "g");
|
|
79
|
+
const methods = [];
|
|
80
|
+
let m;
|
|
81
|
+
while ((m = re.exec(source)) !== null) {
|
|
82
|
+
methods.push(m[1]);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
pass: methods.length > 0,
|
|
86
|
+
detail: methods.length > 0 ? `found: ${methods.join(", ")}()` : "no type guard method references this enum",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function checkInstanceMethod(source, enumN) {
|
|
91
|
+
// Find methods from the type guard check, then look for matching instance methods
|
|
92
|
+
const staticRe = new RegExp(`static\\s+(is\\w+)\\s*\\(\\s*type:\\s*number\\s*\\)[^}]*DeviceType\\.${enumN}`, "g");
|
|
93
|
+
const staticMethods = [];
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = staticRe.exec(source)) !== null) {
|
|
96
|
+
staticMethods.push(m[1]);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const instanceMethods = [];
|
|
100
|
+
for (const methodName of staticMethods) {
|
|
101
|
+
const instanceRe = new RegExp(`public\\s+${methodName}\\s*\\(\\s*\\)\\s*:\\s*boolean`);
|
|
102
|
+
if (instanceRe.test(source)) {
|
|
103
|
+
instanceMethods.push(methodName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
pass: instanceMethods.length > 0,
|
|
109
|
+
detail: instanceMethods.length > 0
|
|
110
|
+
? `found: ${instanceMethods.join(", ")}()`
|
|
111
|
+
: staticMethods.length > 0
|
|
112
|
+
? `static ${staticMethods.join(", ")}() found but missing instance method(s)`
|
|
113
|
+
: "no type guard found",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function checkClassificationMethods(source, enumN) {
|
|
118
|
+
// Check broad classification methods (isCamera, isLock, isSensor, etc.)
|
|
119
|
+
const broadMethods = [
|
|
120
|
+
"isCamera", "isLock", "isSensor", "isKeyPad", "isSmartDrop",
|
|
121
|
+
"isSmartSafe", "isSmartTrack", "hasBattery", "isFloodLight",
|
|
122
|
+
"isIndoorCamera", "isDoorbell", "isPanAndTiltCamera",
|
|
123
|
+
"isSoloCameras", "isLockWifi", "isLockBle", "isEntrySensor",
|
|
124
|
+
"isMotionSensor",
|
|
125
|
+
];
|
|
126
|
+
const included = [];
|
|
127
|
+
for (const method of broadMethods) {
|
|
128
|
+
const re = new RegExp(`static\\s+${method}\\s*\\(\\s*type:\\s*number\\s*\\)[\\s\\S]*?\\n\\s*\\}`, "g");
|
|
129
|
+
const match = source.match(re);
|
|
130
|
+
if (match && match[0].includes(`DeviceType.${enumN}`)) {
|
|
131
|
+
included.push(method);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
pass: included.length > 0,
|
|
136
|
+
detail: included.length > 0
|
|
137
|
+
? `in ${included.length} method(s): ${included.join(", ")}`
|
|
138
|
+
: "not in any broad classification method",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function checkDeviceImages(typeNum) {
|
|
143
|
+
if (!typeNum) return { pass: false, detail: "no type number" };
|
|
144
|
+
if (!existsSync(DEVICE_IMAGES)) return { pass: false, detail: "device-images.js not found" };
|
|
145
|
+
const source = readFileSync(DEVICE_IMAGES, "utf-8");
|
|
146
|
+
const re = new RegExp(`case\\s+${typeNum}:`);
|
|
147
|
+
const found = re.test(source);
|
|
148
|
+
return { pass: found, detail: found ? "case present" : "missing case" };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function checkSupportedDevicesDocs(enumN) {
|
|
152
|
+
if (!existsSync(SUPPORTED_DEVICES)) return { pass: false, detail: "supported_devices.md not found" };
|
|
153
|
+
const source = readFileSync(SUPPORTED_DEVICES, "utf-8");
|
|
154
|
+
// Check for the enum name or the display name pattern
|
|
155
|
+
const found = source.includes(enumN) || (typeNumber && new RegExp(`type\\s+${typeNumber}|${typeNumber}\\)`).test(source));
|
|
156
|
+
// Also check for model number pattern from enum comment (e.g. "CAMERA_4G_S330 = 111, //T86P2")
|
|
157
|
+
const enumLine = httpSource.match(new RegExp(`${enumN}\\s*=\\s*\\d+.*?//\\s*(\\S+)`));
|
|
158
|
+
const model = enumLine ? enumLine[1] : null;
|
|
159
|
+
const modelFound = model && source.includes(model);
|
|
160
|
+
if (modelFound) return { pass: true, detail: `model ${model} found in docs` };
|
|
161
|
+
if (found) return { pass: true, detail: "entry found in docs" };
|
|
162
|
+
return { pass: false, detail: model ? `model ${model} not found` : "not found in docs" };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function checkBySn(source, enumN) {
|
|
166
|
+
// Check if this device is referenced in any *BySn method
|
|
167
|
+
// First get the model prefix from the enum comment
|
|
168
|
+
const enumLine = httpSource.match(new RegExp(`${enumN}\\s*=\\s*\\d+.*?//\\s*(\\S+)`));
|
|
169
|
+
const model = enumLine ? enumLine[1] : null;
|
|
170
|
+
if (!model) return { pass: false, detail: "cannot determine model prefix from enum comment" };
|
|
171
|
+
|
|
172
|
+
const bySnMethods = ["isIntegratedDeviceBySn", "isSoloCameraBySn", "isSmartDropBySn", "isGarageCameraBySn", "isFloodlightBySn"];
|
|
173
|
+
const found = [];
|
|
174
|
+
for (const method of bySnMethods) {
|
|
175
|
+
const re = new RegExp(`static\\s+${method}\\s*\\([^)]*\\)[\\s\\S]*?\\n\\s*\\}`, "g");
|
|
176
|
+
const match = source.match(re);
|
|
177
|
+
if (match && match[0].includes(`"${model}"`)) {
|
|
178
|
+
found.push(method);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
pass: found.length > 0,
|
|
183
|
+
detail: found.length > 0
|
|
184
|
+
? `in ${found.join(", ")}()`
|
|
185
|
+
: `model ${model} not in any *BySn method (may not be needed)`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Run all checks ──────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
const W = 80;
|
|
192
|
+
console.log("=".repeat(W));
|
|
193
|
+
console.log(`Post-Implementation Verification: ${enumName}${typeNumber ? ` (type ${typeNumber})` : ""}`);
|
|
194
|
+
console.log("=".repeat(W));
|
|
195
|
+
console.log();
|
|
196
|
+
|
|
197
|
+
if (!typeNumber) {
|
|
198
|
+
console.log(`FATAL: ${enumName} not found in DeviceType enum. Cannot proceed.`);
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const results = [];
|
|
203
|
+
|
|
204
|
+
function run(label, check, required = true) {
|
|
205
|
+
const icon = check.pass ? "+" : required ? "-" : "~";
|
|
206
|
+
const status = check.pass ? "PASS" : required ? "FAIL" : "WARN";
|
|
207
|
+
results.push({ label, ...check, required, status });
|
|
208
|
+
console.log(` [${icon}] ${label.padEnd(35)} ${status.padEnd(6)} ${check.detail}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log("EUFY-SECURITY-CLIENT (src/http/types.ts)");
|
|
212
|
+
console.log("-".repeat(W));
|
|
213
|
+
run("DeviceType enum", { pass: checkEnumExists(httpSource, enumName), detail: `${enumName} = ${typeNumber}` });
|
|
214
|
+
run("GenericTypeProperty states", checkGenericTypePropertyStates(httpSource, typeNumber));
|
|
215
|
+
run("DeviceProperties map", checkLookupMap(httpSource, "DeviceProperties", enumName));
|
|
216
|
+
run("DeviceCommands map", checkLookupMap(httpSource, "DeviceCommands", enumName));
|
|
217
|
+
run("StationProperties map", checkLookupMap(httpSource, "StationProperties", enumName), false);
|
|
218
|
+
run("StationCommands map", checkLookupMap(httpSource, "StationCommands", enumName), false);
|
|
219
|
+
console.log();
|
|
220
|
+
|
|
221
|
+
console.log("EUFY-SECURITY-CLIENT (src/http/device.ts)");
|
|
222
|
+
console.log("-".repeat(W));
|
|
223
|
+
run("Dedicated type guard (static)", checkTypeGuard(deviceSource, enumName));
|
|
224
|
+
run("Instance method", checkInstanceMethod(deviceSource, enumName));
|
|
225
|
+
run("Classification methods", checkClassificationMethods(deviceSource, enumName));
|
|
226
|
+
console.log();
|
|
227
|
+
|
|
228
|
+
console.log("EUFY-SECURITY-CLIENT (docs)");
|
|
229
|
+
console.log("-".repeat(W));
|
|
230
|
+
run("docs/supported_devices.md", checkSupportedDevicesDocs(enumName), false);
|
|
231
|
+
console.log();
|
|
232
|
+
|
|
233
|
+
console.log("EUFY-SECURITY-CLIENT (serial number methods)");
|
|
234
|
+
console.log("-".repeat(W));
|
|
235
|
+
run("*BySn serial number methods", checkBySn(deviceSource, enumName), false);
|
|
236
|
+
console.log();
|
|
237
|
+
|
|
238
|
+
console.log("HOMEBRIDGE-EUFY-SECURITY");
|
|
239
|
+
console.log("-".repeat(W));
|
|
240
|
+
run("device-images.js", checkDeviceImages(typeNumber));
|
|
241
|
+
console.log();
|
|
242
|
+
|
|
243
|
+
// ── Summary ─────────────────────────────────────────────────────────────────
|
|
244
|
+
|
|
245
|
+
const failed = results.filter((r) => r.status === "FAIL");
|
|
246
|
+
const warned = results.filter((r) => r.status === "WARN");
|
|
247
|
+
const passed = results.filter((r) => r.status === "PASS");
|
|
248
|
+
|
|
249
|
+
console.log("=".repeat(W));
|
|
250
|
+
if (failed.length === 0) {
|
|
251
|
+
console.log(`ALL REQUIRED CHECKS PASSED (${passed.length} pass, ${warned.length} warn)`);
|
|
252
|
+
if (warned.length > 0) {
|
|
253
|
+
console.log("Warnings (optional):");
|
|
254
|
+
for (const w of warned) {
|
|
255
|
+
console.log(` - ${w.label}: ${w.detail}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
console.log(`${failed.length} REQUIRED CHECK(S) FAILED:`);
|
|
260
|
+
for (const f of failed) {
|
|
261
|
+
console.log(` - ${f.label}: ${f.detail}`);
|
|
262
|
+
}
|
|
263
|
+
if (warned.length > 0) {
|
|
264
|
+
console.log(`\n${warned.length} warning(s):`);
|
|
265
|
+
for (const w of warned) {
|
|
266
|
+
console.log(` - ${w.label}: ${w.detail}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
console.log("=".repeat(W));
|
|
271
|
+
|
|
272
|
+
process.exit(failed.length > 0 ? 1 : 0);
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
description: Build a precise, step-by-step action plan before making any code changes. Use this skill whenever the user describes a feature, bug fix, refactor, or any multi-file change. Also use when the user says "plan", "think through", "what would it take", or describes a problem without jumping to code. TRIGGER BEFORE writing any code for non-trivial changes. Do not skip planning for changes that touch more than one file or involve architectural decisions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Planner
|
|
7
|
+
|
|
8
|
+
You are a planning agent for homebridge-eufy-security. Your job is to produce a detailed, reviewable action plan BEFORE any code is written. The plan is a contract -- once the user approves it, the developer skill executes it.
|
|
9
|
+
|
|
10
|
+
Follow all project conventions from CLAUDE.md (architecture, ESM imports, lint, build, git workflow). Use the Architecture section to trace impact across files.
|
|
11
|
+
|
|
12
|
+
## When to plan
|
|
13
|
+
|
|
14
|
+
Always plan when:
|
|
15
|
+
- The change touches more than one file
|
|
16
|
+
- A new device type, accessory, or service is being added
|
|
17
|
+
- The change involves the streaming pipeline or recording delegates
|
|
18
|
+
- Configuration schema changes are needed
|
|
19
|
+
- The change crosses the plugin/eufy-security-client boundary
|
|
20
|
+
|
|
21
|
+
Skip planning (just do it) when:
|
|
22
|
+
- Single-line typo or constant fix
|
|
23
|
+
- The user explicitly says "just do it" or "quick fix"
|
|
24
|
+
|
|
25
|
+
## Planning process
|
|
26
|
+
|
|
27
|
+
### Step 1 -- Understand the goal
|
|
28
|
+
|
|
29
|
+
Read the relevant source files before planning. Never plan based on assumptions about code you haven't read. Identify:
|
|
30
|
+
|
|
31
|
+
- What is the user trying to achieve?
|
|
32
|
+
- Is this a bug fix, feature, refactor, or chore?
|
|
33
|
+
- Does this touch homebridge-eufy-security only, or also eufy-security-client?
|
|
34
|
+
|
|
35
|
+
### Step 2 -- Identify affected files
|
|
36
|
+
|
|
37
|
+
List every file that needs to change. For each file, note:
|
|
38
|
+
- What section/function changes
|
|
39
|
+
- Why it changes
|
|
40
|
+
- Dependencies on other changes in the plan
|
|
41
|
+
|
|
42
|
+
### Step 3 -- Define guardrails
|
|
43
|
+
|
|
44
|
+
For every plan, explicitly state:
|
|
45
|
+
- **Lint**: Will this pass `npm run lint` (zero warnings)?
|
|
46
|
+
- **Build**: Will `npm run build` succeed?
|
|
47
|
+
- **ESM**: Do new imports use `.js` extensions?
|
|
48
|
+
- **Breaking changes**: Does this change config schema, public behavior, or require user action?
|
|
49
|
+
- **Boundary**: Is any part of this change in the wrong layer? (plugin vs eufy-security-client)
|
|
50
|
+
|
|
51
|
+
### Step 4 -- Sequence the work
|
|
52
|
+
|
|
53
|
+
Order the changes so each step is independently buildable where possible. Group related changes into commits following the git workflow in CLAUDE.md.
|
|
54
|
+
|
|
55
|
+
### Step 5 -- Present the plan
|
|
56
|
+
|
|
57
|
+
Output the plan in this format:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
## Plan: <title>
|
|
61
|
+
|
|
62
|
+
### Goal
|
|
63
|
+
<one sentence>
|
|
64
|
+
|
|
65
|
+
### Changes
|
|
66
|
+
|
|
67
|
+
1. **<file path>** -- <what and why>
|
|
68
|
+
- <specific function/section>
|
|
69
|
+
- <detail>
|
|
70
|
+
|
|
71
|
+
2. **<file path>** -- <what and why>
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
### Guardrails
|
|
75
|
+
- [ ] Lint passes
|
|
76
|
+
- [ ] Build passes
|
|
77
|
+
- [ ] ESM imports correct
|
|
78
|
+
- [ ] No breaking config changes (or: breaking change documented)
|
|
79
|
+
- [ ] Correct architectural layer
|
|
80
|
+
|
|
81
|
+
### Commits
|
|
82
|
+
1. `feat: <message>` -- files: <list>
|
|
83
|
+
2. `fix: <message>` -- files: <list>
|
|
84
|
+
|
|
85
|
+
### Risks / Open questions
|
|
86
|
+
- <anything uncertain that needs user input>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Wait for the user to approve, modify, or reject before proceeding.
|
|
90
|
+
|
|
91
|
+
## Chaining
|
|
92
|
+
|
|
93
|
+
When the user approves the plan (says "go", "looks good", "approved", "do it", etc.), immediately invoke the **developer** skill to execute it. Pass the approved plan as context -- do not ask the user to repeat it.
|
|
94
|
+
|
|
95
|
+
## What NOT to do
|
|
96
|
+
|
|
97
|
+
- Never start editing files during planning
|
|
98
|
+
- Never assume device properties without reading raw data or existing code
|
|
99
|
+
- Never plan changes to `src/version.ts` (auto-generated)
|
|
100
|
+
- Never plan changes that mix plugin and eufy-security-client in one commit
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: qa
|
|
3
|
+
description: Verify that code changes are correct, safe, and ready to ship. Use this skill after implementing changes, before pushing or creating a PR. Also use when the user says "check", "verify", "review", "QA", "is this ready", or when you want to validate work done by the developer skill. Runs build, lint, and structural checks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# QA / Verification
|
|
7
|
+
|
|
8
|
+
You are a quality assurance agent for homebridge-eufy-security. Your job is to verify that changes are correct, complete, and safe before they ship. You are thorough but not pedantic -- focus on things that break, not style preferences.
|
|
9
|
+
|
|
10
|
+
Refer to CLAUDE.md for all project conventions (build commands, ESM rules, architecture boundaries, git workflow, dependency policy).
|
|
11
|
+
|
|
12
|
+
## Verification checklist
|
|
13
|
+
|
|
14
|
+
Run through these checks in order. Stop at the first failure and report it.
|
|
15
|
+
|
|
16
|
+
### 1. Build verification
|
|
17
|
+
|
|
18
|
+
Run `npm run lint` and `npm run build`. Both must pass with zero errors and zero warnings.
|
|
19
|
+
|
|
20
|
+
### 2. Import verification
|
|
21
|
+
|
|
22
|
+
Check all new or modified imports for `.js` extensions (NodeNext), resolution to real files, and circular imports.
|
|
23
|
+
|
|
24
|
+
### 3. Architectural boundary check
|
|
25
|
+
|
|
26
|
+
For each changed file, verify:
|
|
27
|
+
- Plugin code uses the eufy-security-client public API (events, commands, property accessors), not internal methods
|
|
28
|
+
- `homebridge-ui/server.js` and `src/utils/accessoriesStore.ts` are in sync if device/station record shapes changed
|
|
29
|
+
- No HomeKit service logic leaked into base classes that shouldn't have it
|
|
30
|
+
|
|
31
|
+
### 4. Configuration safety
|
|
32
|
+
|
|
33
|
+
If config schema changed (`src/utils/configTypes.ts`): defaults set for new options, existing configs still work, `config.schema.json` updated if applicable.
|
|
34
|
+
|
|
35
|
+
### 5. Streaming pipeline check
|
|
36
|
+
|
|
37
|
+
If streaming code changed (`src/controller/`): valid FFmpeg arguments, correct SRTP handling, clean stream lifecycle, concurrent stream limits respected.
|
|
38
|
+
|
|
39
|
+
### 6. Git hygiene
|
|
40
|
+
|
|
41
|
+
Per CLAUDE.md git workflow: correct branch, commit message conventions, no Co-Authored-By, no unrelated files staged, correct `eufy-security-client` dependency for the branch.
|
|
42
|
+
|
|
43
|
+
### 7. Diff review
|
|
44
|
+
|
|
45
|
+
Read the full diff and check for: accidental debug logging, hardcoded values that should be in `src/settings.ts`, missing `override` keyword, new eslint-disable comments.
|
|
46
|
+
|
|
47
|
+
## Output format
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
## QA Report
|
|
51
|
+
|
|
52
|
+
### Status: PASS / FAIL
|
|
53
|
+
|
|
54
|
+
### Checks
|
|
55
|
+
- [x] Build passes
|
|
56
|
+
- [x] Lint passes (0 warnings)
|
|
57
|
+
- [x] ESM imports correct
|
|
58
|
+
- [x] Architecture boundaries respected
|
|
59
|
+
- [ ] Config schema -- ISSUE: <description>
|
|
60
|
+
- [x] Git hygiene
|
|
61
|
+
|
|
62
|
+
### Issues found
|
|
63
|
+
1. **<severity>**: <description> -- <file>:<line>
|
|
64
|
+
|
|
65
|
+
### Ready to push: YES / NO
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Chaining
|
|
69
|
+
|
|
70
|
+
- **If QA passes**: Ask the user if they want to push and/or create a PR.
|
|
71
|
+
- **If QA fails**: Fix issues that are safe to fix silently (typos, missing `.js` extensions). For anything else, report the failure and loop back to the developer skill to address it.
|
|
72
|
+
|
|
73
|
+
## When to flag vs fix
|
|
74
|
+
|
|
75
|
+
- **Typos in your own changes**: Fix silently
|
|
76
|
+
- **Missing `.js` extension**: Fix silently
|
|
77
|
+
- **Architectural issue**: Flag to user, don't fix without approval
|
|
78
|
+
- **Potential breaking change**: Flag to user with impact assessment
|
|
79
|
+
- **Pre-existing issues unrelated to current changes**: Note but don't fix (avoid scope creep)
|