@homebridge-plugins/homebridge-eufy-security 4.6.0-beta.4 → 4.6.0-beta.41
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/docs/hksv-recording-fix.md +160 -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 +20 -20
- package/dist/accessories/CameraAccessory.js.map +1 -1
- package/dist/controller/LocalLivestreamManager.js +197 -10
- package/dist/controller/LocalLivestreamManager.js.map +1 -1
- package/dist/controller/recordingDelegate.js +25 -9
- package/dist/controller/recordingDelegate.js.map +1 -1
- package/dist/controller/snapshotDelegate.js +5 -1
- package/dist/controller/snapshotDelegate.js.map +1 -1
- package/dist/controller/streamingDelegate.js +13 -7
- package/dist/controller/streamingDelegate.js.map +1 -1
- package/dist/platform.js +0 -23
- package/dist/platform.js.map +1 -1
- package/dist/settings.js +7 -0
- package/dist/settings.js.map +1 -1
- package/dist/utils/Talkback.js +127 -55
- package/dist/utils/Talkback.js.map +1 -1
- package/dist/utils/configTypes.js +1 -0
- package/dist/utils/configTypes.js.map +1 -1
- package/dist/utils/ffmpeg.js +137 -23
- 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/batterydoorbell2k_large.png +0 -0
- 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/assets/devices/poe_bullet_ptz_cam_s4_T8E00_large.png +0 -0
- package/homebridge-ui/public/utils/device-images.js +2 -0
- package/homebridge-ui/public/views/device-detail.js +10 -0
- package/homebridge-ui/server.js +23 -31
- package/package.json +12 -12
- package/scripts/decrypt-diagnostics.mjs +6 -7
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-device.mjs
|
|
4
|
+
*
|
|
5
|
+
* Pre-implementation audit for a new device type. Checks what already exists
|
|
6
|
+
* in the codebase and what's missing before starting work.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node check-device.mjs <device-type-number> [--pr-search <model>]
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* node check-device.mjs 105
|
|
13
|
+
* node check-device.mjs 203 --pr-search T85V0
|
|
14
|
+
*
|
|
15
|
+
* Output: status report showing what exists, what's missing, closest device,
|
|
16
|
+
* and upstream PR search results.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, existsSync } from "fs";
|
|
20
|
+
import { join, dirname } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { execSync } from "child_process";
|
|
23
|
+
|
|
24
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
const CLIENT_ROOT = join(__dirname, "..", "..", "..", "..", "eufy-security-client");
|
|
27
|
+
const HB_ROOT = join(__dirname, "..", "..", "..");
|
|
28
|
+
const TYPES_HTTP = join(CLIENT_ROOT, "src", "http", "types.ts");
|
|
29
|
+
const DEVICE_TS = join(CLIENT_ROOT, "src", "http", "device.ts");
|
|
30
|
+
const STATION_TS = join(CLIENT_ROOT, "src", "http", "station.ts");
|
|
31
|
+
const PUSH_SERVICE = join(CLIENT_ROOT, "src", "push", "service.ts");
|
|
32
|
+
const DEVICE_IMAGES = join(HB_ROOT, "homebridge-ui", "public", "utils", "device-images.js");
|
|
33
|
+
|
|
34
|
+
const typeNumber = parseInt(process.argv[2]);
|
|
35
|
+
if (isNaN(typeNumber)) {
|
|
36
|
+
console.error("Usage: check-device.mjs <device-type-number> [--pr-search <model>]");
|
|
37
|
+
console.error("Example: check-device.mjs 105");
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let prSearchTerm = null;
|
|
42
|
+
const prIdx = process.argv.indexOf("--pr-search");
|
|
43
|
+
if (prIdx !== -1 && process.argv[prIdx + 1]) {
|
|
44
|
+
prSearchTerm = process.argv[prIdx + 1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const httpSource = readFileSync(TYPES_HTTP, "utf-8");
|
|
48
|
+
const deviceSource = readFileSync(DEVICE_TS, "utf-8");
|
|
49
|
+
|
|
50
|
+
// ── Check 1: DeviceType enum ───────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
function checkDeviceTypeEnum(source, typeNum) {
|
|
53
|
+
const re = new RegExp(`^\\s*(\\w+)\\s*=\\s*${typeNum}\\s*[,/]`, "m");
|
|
54
|
+
const match = source.match(re);
|
|
55
|
+
return match ? match[1] : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const enumName = checkDeviceTypeEnum(httpSource, typeNumber);
|
|
59
|
+
|
|
60
|
+
// ── Check 2: GenericTypeProperty states ─────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function checkGenericTypePropertyStates(source, typeNum) {
|
|
63
|
+
const gtpMatch = source.match(/export\s+const\s+GenericTypeProperty[\s\S]*?states:\s*\{([\s\S]*?)\}/);
|
|
64
|
+
if (!gtpMatch) return null;
|
|
65
|
+
const statesBody = gtpMatch[1];
|
|
66
|
+
const entryRe = new RegExp(`${typeNum}:\\s*"([^"]+)"`);
|
|
67
|
+
const match = statesBody.match(entryRe);
|
|
68
|
+
return match ? match[1] : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const gtpLabel = checkGenericTypePropertyStates(httpSource, typeNumber);
|
|
72
|
+
|
|
73
|
+
// ── Check 3: DeviceProperties ──────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function checkLookupMap(source, mapName, enumN) {
|
|
76
|
+
if (!enumN) return false;
|
|
77
|
+
const dpStart = source.indexOf(`export const ${mapName}`);
|
|
78
|
+
if (dpStart === -1) return false;
|
|
79
|
+
// Find next export const to bound the search
|
|
80
|
+
const nextExport = source.indexOf("export const ", dpStart + 20);
|
|
81
|
+
const section = source.slice(dpStart, nextExport === -1 ? undefined : nextExport);
|
|
82
|
+
return section.includes(`[DeviceType.${enumN}]`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inDeviceProperties = checkLookupMap(httpSource, "DeviceProperties", enumName);
|
|
86
|
+
const inStationProperties = checkLookupMap(httpSource, "StationProperties", enumName);
|
|
87
|
+
const inDeviceCommands = checkLookupMap(httpSource, "DeviceCommands", enumName);
|
|
88
|
+
const inStationCommands = checkLookupMap(httpSource, "StationCommands", enumName);
|
|
89
|
+
|
|
90
|
+
// ── Check 4: Classification methods in device.ts ───────────────────────────
|
|
91
|
+
|
|
92
|
+
function findClassificationMethods(source, enumN) {
|
|
93
|
+
if (!enumN) return { inMethods: [], hasTypeGuard: false, hasInstanceMethod: false };
|
|
94
|
+
|
|
95
|
+
const inMethods = [];
|
|
96
|
+
// Find all static isXxx() methods and check if they reference this enum
|
|
97
|
+
const methodRegex = /static\s+(is\w+)\s*\(\s*type:\s*number\s*\)[\s\S]*?\n\s*\}/g;
|
|
98
|
+
let m;
|
|
99
|
+
while ((m = methodRegex.exec(source)) !== null) {
|
|
100
|
+
const methodName = m[1];
|
|
101
|
+
const methodBody = m[0];
|
|
102
|
+
if (methodBody.includes(`DeviceType.${enumN}`)) {
|
|
103
|
+
inMethods.push(methodName);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for dedicated type guard (single-enum return)
|
|
108
|
+
const hasTypeGuard = inMethods.some((name) => {
|
|
109
|
+
const singleCheck = new RegExp(`static\\s+${name}\\s*\\(\\s*type:\\s*number\\s*\\)[^}]*return\\s+DeviceType\\.${enumN}\\s*==\\s*type`, "s");
|
|
110
|
+
return singleCheck.test(source);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// If type guard exists, instance method likely does too
|
|
114
|
+
const hasInstanceMethod = hasTypeGuard;
|
|
115
|
+
|
|
116
|
+
return { inMethods, hasTypeGuard, hasInstanceMethod };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const classInfo = findClassificationMethods(deviceSource, enumName);
|
|
120
|
+
|
|
121
|
+
// ── Check 5: device-images.js ──────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function checkDeviceImages(typeNum) {
|
|
124
|
+
if (!existsSync(DEVICE_IMAGES)) return null;
|
|
125
|
+
const source = readFileSync(DEVICE_IMAGES, "utf-8");
|
|
126
|
+
const re = new RegExp(`case\\s+${typeNum}:\\s*return\\s+'([^']+)'`);
|
|
127
|
+
const match = source.match(re);
|
|
128
|
+
return match ? match[1] : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const imageName = checkDeviceImages(typeNumber);
|
|
132
|
+
|
|
133
|
+
// ── Check 6: station.ts integration ─────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function checkStationIntegration(enumN) {
|
|
136
|
+
if (!enumN || !existsSync(STATION_TS)) return { referenced: false, methods: [] };
|
|
137
|
+
const source = readFileSync(STATION_TS, "utf-8");
|
|
138
|
+
const referenced = source.includes(`DeviceType.${enumN}`);
|
|
139
|
+
// Also check via classification methods
|
|
140
|
+
const methods = [];
|
|
141
|
+
const methodRefs = source.match(/Device\.(is\w+)\(/g);
|
|
142
|
+
if (methodRefs) {
|
|
143
|
+
const uniqueMethods = [...new Set(methodRefs.map((m) => m.match(/Device\.(is\w+)/)[1]))];
|
|
144
|
+
for (const method of uniqueMethods) {
|
|
145
|
+
const re = new RegExp(`static\\s+${method}\\s*\\(\\s*type:\\s*number\\s*\\)[\\s\\S]*?\\n\\s*\\}`, "g");
|
|
146
|
+
const match = deviceSource.match(re);
|
|
147
|
+
if (match && match[0].includes(`DeviceType.${enumN}`)) {
|
|
148
|
+
methods.push(method);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { referenced, methods };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const stationInfo = checkStationIntegration(enumName);
|
|
156
|
+
|
|
157
|
+
// ── Check 6b: push/service.ts normalization ─────────────────────────────────
|
|
158
|
+
|
|
159
|
+
function checkPushService(enumN) {
|
|
160
|
+
if (!enumN || !existsSync(PUSH_SERVICE)) return false;
|
|
161
|
+
const source = readFileSync(PUSH_SERVICE, "utf-8");
|
|
162
|
+
return source.includes(`DeviceType.${enumN}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const inPushService = checkPushService(enumName);
|
|
166
|
+
|
|
167
|
+
// ── Check 7: Find closest device by property overlap ───────────────────────
|
|
168
|
+
|
|
169
|
+
function findClosestDevice(source, enumN) {
|
|
170
|
+
if (!enumN || !inDeviceProperties) return null;
|
|
171
|
+
|
|
172
|
+
// Parse DeviceProperties section
|
|
173
|
+
const dpStart = source.indexOf("export const DeviceProperties");
|
|
174
|
+
const dpEnd = source.indexOf("export const StationProperties");
|
|
175
|
+
if (dpStart === -1) return null;
|
|
176
|
+
const dpSection = source.slice(dpStart, dpEnd === -1 ? undefined : dpEnd);
|
|
177
|
+
|
|
178
|
+
// Extract property sets for each device
|
|
179
|
+
const deviceProps = new Map();
|
|
180
|
+
const blockRegex = /\[DeviceType\.(\w+)\]\s*:\s*\{([\s\S]*?)\}/g;
|
|
181
|
+
let m;
|
|
182
|
+
while ((m = blockRegex.exec(dpSection)) !== null) {
|
|
183
|
+
const dt = m[1];
|
|
184
|
+
const body = m[2];
|
|
185
|
+
const props = new Set();
|
|
186
|
+
const propRef = /\[PropertyName\.(\w+)\]/g;
|
|
187
|
+
let pm;
|
|
188
|
+
while ((pm = propRef.exec(body)) !== null) {
|
|
189
|
+
props.add(pm[1]);
|
|
190
|
+
}
|
|
191
|
+
deviceProps.set(dt, props);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const targetProps = deviceProps.get(enumN);
|
|
195
|
+
if (!targetProps || targetProps.size === 0) return null;
|
|
196
|
+
|
|
197
|
+
// Jaccard similarity
|
|
198
|
+
const similarities = [];
|
|
199
|
+
for (const [dt, props] of deviceProps) {
|
|
200
|
+
if (dt === enumN) continue;
|
|
201
|
+
const intersection = new Set([...targetProps].filter((p) => props.has(p)));
|
|
202
|
+
const union = new Set([...targetProps, ...props]);
|
|
203
|
+
const jaccard = union.size > 0 ? intersection.size / union.size : 0;
|
|
204
|
+
if (jaccard > 0.3) {
|
|
205
|
+
similarities.push({
|
|
206
|
+
device: dt,
|
|
207
|
+
jaccard: Math.round(jaccard * 100),
|
|
208
|
+
shared: intersection.size,
|
|
209
|
+
total: union.size,
|
|
210
|
+
targetOnly: [...targetProps].filter((p) => !props.has(p)).length,
|
|
211
|
+
otherOnly: [...props].filter((p) => !targetProps.has(p)).length,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
similarities.sort((a, b) => b.jaccard - a.jaccard);
|
|
217
|
+
return similarities.slice(0, 5);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const closestDevices = findClosestDevice(httpSource, enumName);
|
|
221
|
+
|
|
222
|
+
// ── Check 7: Upstream PR search ────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
// Sanitize shell arguments to prevent command injection
|
|
225
|
+
function shellEscape(str) {
|
|
226
|
+
return "'" + String(str).replace(/'/g, "'\\''") + "'";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function searchUpstreamPRs(searchTerm) {
|
|
230
|
+
if (!searchTerm) return null;
|
|
231
|
+
try {
|
|
232
|
+
const result = execSync(
|
|
233
|
+
`gh pr list --repo bropat/eufy-security-client --state all --search ${shellEscape(searchTerm)} --limit 5 --json number,title,state,mergedAt 2>/dev/null`,
|
|
234
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
235
|
+
);
|
|
236
|
+
return JSON.parse(result);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Also search by type number (typeNum is validated as integer, but escape for safety)
|
|
243
|
+
function searchByTypeNumber(typeNum) {
|
|
244
|
+
try {
|
|
245
|
+
const result = execSync(
|
|
246
|
+
`gh pr list --repo bropat/eufy-security-client --state all --search ${shellEscape(String(typeNum))} --limit 5 --json number,title,state,mergedAt 2>/dev/null`,
|
|
247
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
248
|
+
);
|
|
249
|
+
return JSON.parse(result);
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const prsByModel = prSearchTerm ? searchUpstreamPRs(prSearchTerm) : null;
|
|
256
|
+
const prsByType = searchByTypeNumber(typeNumber);
|
|
257
|
+
|
|
258
|
+
// ── Output ─────────────────────────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
const W = 80;
|
|
261
|
+
const PASS = "FOUND";
|
|
262
|
+
const FAIL = "MISSING";
|
|
263
|
+
const NA = "N/A";
|
|
264
|
+
|
|
265
|
+
console.log("=".repeat(W));
|
|
266
|
+
console.log(`Device Type Audit: ${typeNumber}${enumName ? ` (${enumName})` : ""}`);
|
|
267
|
+
console.log("=".repeat(W));
|
|
268
|
+
console.log();
|
|
269
|
+
|
|
270
|
+
// Registration status
|
|
271
|
+
console.log("REGISTRATION STATUS");
|
|
272
|
+
console.log("-".repeat(W));
|
|
273
|
+
const checks = [
|
|
274
|
+
["DeviceType enum", enumName ? `${PASS} — ${enumName}` : FAIL],
|
|
275
|
+
["GenericTypeProperty states", gtpLabel ? `${PASS} — "${gtpLabel}"` : FAIL],
|
|
276
|
+
["DeviceProperties map", inDeviceProperties ? PASS : FAIL],
|
|
277
|
+
["StationProperties map", inStationProperties ? PASS : `${FAIL} (may not be needed)`],
|
|
278
|
+
["DeviceCommands map", inDeviceCommands ? PASS : FAIL],
|
|
279
|
+
["StationCommands map", inStationCommands ? PASS : `${FAIL} (may not be needed)`],
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
for (const [label, status] of checks) {
|
|
283
|
+
const icon = status.startsWith(PASS) ? "+" : status === NA ? " " : "-";
|
|
284
|
+
console.log(` [${icon}] ${label.padEnd(30)} ${status}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log();
|
|
288
|
+
|
|
289
|
+
// Classification methods
|
|
290
|
+
console.log("CLASSIFICATION METHODS");
|
|
291
|
+
console.log("-".repeat(W));
|
|
292
|
+
if (classInfo.inMethods.length > 0) {
|
|
293
|
+
console.log(` Included in ${classInfo.inMethods.length} method(s):`);
|
|
294
|
+
for (const m of classInfo.inMethods) {
|
|
295
|
+
console.log(` - ${m}()`);
|
|
296
|
+
}
|
|
297
|
+
console.log(` Dedicated type guard: ${classInfo.hasTypeGuard ? "YES" : "NO"}`);
|
|
298
|
+
} else if (enumName) {
|
|
299
|
+
console.log(" Not included in any classification methods");
|
|
300
|
+
} else {
|
|
301
|
+
console.log(" Cannot check — device type not in enum");
|
|
302
|
+
}
|
|
303
|
+
console.log();
|
|
304
|
+
|
|
305
|
+
// Device images
|
|
306
|
+
console.log("PLUGIN UI");
|
|
307
|
+
console.log("-".repeat(W));
|
|
308
|
+
console.log(` device-images.js: ${imageName ? `${PASS} — ${imageName}` : FAIL}`);
|
|
309
|
+
console.log();
|
|
310
|
+
|
|
311
|
+
// Station & push integration
|
|
312
|
+
console.log("INTEGRATION POINTS");
|
|
313
|
+
console.log("-".repeat(W));
|
|
314
|
+
if (stationInfo.referenced) {
|
|
315
|
+
console.log(` station.ts: directly referenced`);
|
|
316
|
+
} else if (stationInfo.methods.length > 0) {
|
|
317
|
+
console.log(` station.ts: indirectly via ${stationInfo.methods.join(", ")}()`);
|
|
318
|
+
} else {
|
|
319
|
+
console.log(` station.ts: not referenced (may need integration for standalone/integrated devices)`);
|
|
320
|
+
}
|
|
321
|
+
console.log(` push/service.ts: ${inPushService ? "referenced (has push normalization)" : "not referenced (normal for most devices)"}`);
|
|
322
|
+
console.log();
|
|
323
|
+
|
|
324
|
+
// Closest device
|
|
325
|
+
if (closestDevices && closestDevices.length > 0) {
|
|
326
|
+
console.log("CLOSEST EXISTING DEVICES (by property overlap)");
|
|
327
|
+
console.log("-".repeat(W));
|
|
328
|
+
console.log(" Device".padEnd(40) + "Similarity Shared Target-only Other-only");
|
|
329
|
+
for (const d of closestDevices) {
|
|
330
|
+
console.log(
|
|
331
|
+
` ${d.device.padEnd(38)} ${String(d.jaccard + "%").padEnd(11)} ${String(d.shared).padEnd(7)} ${String(d.targetOnly).padEnd(12)} ${d.otherOnly}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
console.log();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Upstream PRs
|
|
338
|
+
const allPRs = new Map();
|
|
339
|
+
if (prsByModel) for (const pr of prsByModel) allPRs.set(pr.number, pr);
|
|
340
|
+
if (prsByType) for (const pr of prsByType) allPRs.set(pr.number, pr);
|
|
341
|
+
|
|
342
|
+
if (allPRs.size > 0) {
|
|
343
|
+
console.log("UPSTREAM PRs (bropat/eufy-security-client)");
|
|
344
|
+
console.log("-".repeat(W));
|
|
345
|
+
for (const pr of allPRs.values()) {
|
|
346
|
+
const state = pr.mergedAt ? "MERGED" : pr.state;
|
|
347
|
+
console.log(` #${pr.number} [${state}] ${pr.title}`);
|
|
348
|
+
}
|
|
349
|
+
console.log();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Summary
|
|
353
|
+
console.log("=".repeat(W));
|
|
354
|
+
const missing = checks.filter(([, s]) => s.startsWith(FAIL) && !s.includes("may not")).length;
|
|
355
|
+
const total = 4; // Required: enum, GTP, DeviceProperties, DeviceCommands
|
|
356
|
+
if (missing === 0) {
|
|
357
|
+
console.log("STATUS: All required registrations present. Device may only need homebridge-side work.");
|
|
358
|
+
} else if (missing === total) {
|
|
359
|
+
console.log("STATUS: Device is completely new. Full implementation needed across both repos.");
|
|
360
|
+
} else {
|
|
361
|
+
console.log(`STATUS: Partially implemented. ${missing} required registration(s) missing.`);
|
|
362
|
+
}
|
|
363
|
+
console.log("=".repeat(W));
|
|
@@ -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
|
}
|