@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.41 → 5.0.0-beta.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/diagnostics.js +126 -7
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapter.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +6 -2
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/reconciler.js +4 -1
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/live-stream.js +39 -7
- package/dist/media/live-stream.js.map +1 -1
- package/dist/platform.js +2 -2
- package/dist/platform.js.map +1 -1
- package/homebridge-ui/public/app.css +64 -19
- package/homebridge-ui/public/i18n/en.json +16 -1
- package/homebridge-ui/public/i18n/fr.json +16 -1
- package/homebridge-ui/public/index.html +22 -1
- package/homebridge-ui/public/js/app.js +180 -4
- package/package.json +2 -2
package/dist/diagnostics.js
CHANGED
|
@@ -264,6 +264,10 @@ const DIAGNOSTICS_UI_EVENTS = [
|
|
|
264
264
|
'authentication-commit-failed',
|
|
265
265
|
'authentication-timed-out',
|
|
266
266
|
'authentication-closed',
|
|
267
|
+
'live-picture-seen',
|
|
268
|
+
'live-picture-not-seen',
|
|
269
|
+
'live-preview-seen',
|
|
270
|
+
'live-preview-not-seen',
|
|
267
271
|
];
|
|
268
272
|
const SUPPORT_ARCHIVE_KEY = {
|
|
269
273
|
keyId: 'support-2026-08-01',
|
|
@@ -349,6 +353,18 @@ const DIAGNOSTICS_PROFILES = {
|
|
|
349
353
|
'dashboard-ui': ['plugin-log', 'ui-log'],
|
|
350
354
|
other: ['plugin-log', 'sdk-log', 'homekit-log', 'ffmpeg-log'],
|
|
351
355
|
};
|
|
356
|
+
/**
|
|
357
|
+
* The evidence a session collects: its profile's, and the UI log too for a live-media capture that named cameras,
|
|
358
|
+
* whose guided encoder attempts record the reporter's picture answers.
|
|
359
|
+
*/
|
|
360
|
+
function selectedEvidence(session) {
|
|
361
|
+
const evidence = DIAGNOSTICS_PROFILES[session.profile];
|
|
362
|
+
return session.profile === 'live-media' &&
|
|
363
|
+
Array.isArray(session.affectedDevices) &&
|
|
364
|
+
session.affectedDevices.length > 0
|
|
365
|
+
? [...evidence, 'ui-log']
|
|
366
|
+
: evidence;
|
|
367
|
+
}
|
|
352
368
|
/** Narrows external profile input against the diagnostics-owned profile registry. */
|
|
353
369
|
export function isDiagnosticsProfile(value) {
|
|
354
370
|
return typeof value === 'string' && Object.hasOwn(DIAGNOSTICS_PROFILES, value);
|
|
@@ -397,6 +413,35 @@ function activeDiagnosticsSession(storageRoot, now) {
|
|
|
397
413
|
const session = readDiagnosticsSession(storageRoot);
|
|
398
414
|
return session && Date.parse(session.expiresAt) > now ? session : undefined;
|
|
399
415
|
}
|
|
416
|
+
/** How many live starts of each named camera a guided live-media capture codes with an encoder variant. */
|
|
417
|
+
const LIVE_ENCODE_ATTEMPTS = 6;
|
|
418
|
+
/**
|
|
419
|
+
* Counts each live start of a camera that a reproducing live-media capture named, and returns where it falls: the
|
|
420
|
+
* camera's position among the named ones, and the start's index while it is below {@link LIVE_ENCODE_ATTEMPTS}.
|
|
421
|
+
* Counting restarts with each reproduction. Outside one, and for a camera it did not name, it counts nothing.
|
|
422
|
+
*/
|
|
423
|
+
export function liveEncodeAttempts(storageRoot, now = Date.now) {
|
|
424
|
+
let reproduction;
|
|
425
|
+
const starts = new Map();
|
|
426
|
+
return (serial) => {
|
|
427
|
+
const session = activeDiagnosticsSession(storageRoot, now());
|
|
428
|
+
const named = session?.affectedDevices;
|
|
429
|
+
const camera = named === undefined || named === 'all' ? -1 : named.indexOf(serial);
|
|
430
|
+
if (session?.profile !== 'live-media' ||
|
|
431
|
+
!session.reproductionStartedAt ||
|
|
432
|
+
session.reproductionEndedAt ||
|
|
433
|
+
camera < 0) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
if (reproduction !== `${session.supportCaseId}@${session.reproductionStartedAt}`) {
|
|
437
|
+
reproduction = `${session.supportCaseId}@${session.reproductionStartedAt}`;
|
|
438
|
+
starts.clear();
|
|
439
|
+
}
|
|
440
|
+
const started = starts.get(serial) ?? 0;
|
|
441
|
+
starts.set(serial, started + 1);
|
|
442
|
+
return { camera, ...(started < LIVE_ENCODE_ATTEMPTS ? { variant: started } : {}) };
|
|
443
|
+
};
|
|
444
|
+
}
|
|
400
445
|
/**
|
|
401
446
|
* A session as it stands at `now`: a capture still running when its authorization ends is finished at that end.
|
|
402
447
|
*
|
|
@@ -742,7 +787,7 @@ export class GuidedDiagnostics {
|
|
|
742
787
|
this.pendingUiEvents += 1;
|
|
743
788
|
const write = this.uiEventWrites.then(async () => {
|
|
744
789
|
const session = this.requireAuthorized();
|
|
745
|
-
if (!
|
|
790
|
+
if (!selectedEvidence(session).includes('ui-log') ||
|
|
746
791
|
!session.reproductionStartedAt ||
|
|
747
792
|
session.reproductionEndedAt) {
|
|
748
793
|
throw new Error('An active dashboard or sign-in reproduction is required');
|
|
@@ -796,7 +841,7 @@ export class GuidedDiagnostics {
|
|
|
796
841
|
}
|
|
797
842
|
const collected = await this.collectSupportEvidence(session);
|
|
798
843
|
const reproductionStartedAt = Date.parse(session.reproductionStartedAt);
|
|
799
|
-
const selected =
|
|
844
|
+
const selected = selectedEvidence(session);
|
|
800
845
|
const createdAt = this.now();
|
|
801
846
|
const manifest = {
|
|
802
847
|
version: 2,
|
|
@@ -995,7 +1040,7 @@ export class GuidedDiagnostics {
|
|
|
995
1040
|
});
|
|
996
1041
|
}
|
|
997
1042
|
const log = await this.readReproductionLog(session.reproductionStartedAt, session.reproductionEndedAt);
|
|
998
|
-
for (const selected of
|
|
1043
|
+
for (const selected of selectedEvidence(session)) {
|
|
999
1044
|
const carried = log.get(selected);
|
|
1000
1045
|
if (!carried)
|
|
1001
1046
|
continue;
|
|
@@ -1183,11 +1228,11 @@ export class GuidedDiagnostics {
|
|
|
1183
1228
|
return { status: 'inactive', selectedEvidence: [], missingEvidence: [], partialExportAvailable: false };
|
|
1184
1229
|
}
|
|
1185
1230
|
const { session, exportable } = settledDiagnosticsSession(persisted, this.now());
|
|
1186
|
-
const
|
|
1231
|
+
const selected = selectedEvidence(session);
|
|
1187
1232
|
const collected = session.reproductionStartedAt && session.reproductionEndedAt
|
|
1188
1233
|
? await this.collectSupportEvidence(session)
|
|
1189
1234
|
: undefined;
|
|
1190
|
-
const missingEvidence = collected ? missingEvidenceClasses(
|
|
1235
|
+
const missingEvidence = collected ? missingEvidenceClasses(selected, collected) : [];
|
|
1191
1236
|
const expired = Date.parse(session.expiresAt) <= this.now() && !exportable;
|
|
1192
1237
|
const status = expired
|
|
1193
1238
|
? 'expired'
|
|
@@ -1201,7 +1246,7 @@ export class GuidedDiagnostics {
|
|
|
1201
1246
|
supportCaseId: session.supportCaseId,
|
|
1202
1247
|
profile: session.profile,
|
|
1203
1248
|
reproductionMode: session.reproductionMode,
|
|
1204
|
-
selectedEvidence,
|
|
1249
|
+
selectedEvidence: selected,
|
|
1205
1250
|
missingEvidence,
|
|
1206
1251
|
authorizedAt: session.authorizedAt,
|
|
1207
1252
|
expiresAt: session.expiresAt,
|
|
@@ -1210,8 +1255,76 @@ export class GuidedDiagnostics {
|
|
|
1210
1255
|
partialExportAvailable: exportable,
|
|
1211
1256
|
...(session.affectedDevices === undefined ? {} : { affectedDevices: session.affectedDevices }),
|
|
1212
1257
|
issueUrl: bugReportUrl(session, missingEvidence, readHostEnvironment(this.storageRoot)),
|
|
1258
|
+
...(status === 'reproducing' && session.profile === 'live-media' && Array.isArray(session.affectedDevices)
|
|
1259
|
+
? { liveEncode: await this.liveEncode(session) }
|
|
1260
|
+
: {}),
|
|
1213
1261
|
};
|
|
1214
1262
|
}
|
|
1263
|
+
/** What {@link liveEncodeAttempts} was followed through so far, for one reproduction of one capture. */
|
|
1264
|
+
liveEncodeScan;
|
|
1265
|
+
/**
|
|
1266
|
+
* Follows the live starts a reproducing live-media capture recorded, reading only what the current plugin log
|
|
1267
|
+
* gained since the last read. The starts are kept across a rotation; a process that restarts after one follows
|
|
1268
|
+
* only what the new file holds.
|
|
1269
|
+
*/
|
|
1270
|
+
async liveEncode(session) {
|
|
1271
|
+
const reproduction = `${session.supportCaseId}@${session.reproductionStartedAt}`;
|
|
1272
|
+
if (this.liveEncodeScan?.reproduction !== reproduction) {
|
|
1273
|
+
this.liveEncodeScan = { reproduction, offset: 0, starts: [], byAccessory: new Map() };
|
|
1274
|
+
}
|
|
1275
|
+
const scan = this.liveEncodeScan;
|
|
1276
|
+
const startedAt = Date.parse(session.reproductionStartedAt);
|
|
1277
|
+
try {
|
|
1278
|
+
const file = await open(join(this.storageRoot, LOG_DIRECTORY, LOG_FILE), 'r');
|
|
1279
|
+
try {
|
|
1280
|
+
const size = (await file.stat()).size;
|
|
1281
|
+
if (size < scan.offset)
|
|
1282
|
+
scan.offset = 0;
|
|
1283
|
+
const buffer = Buffer.alloc(size - scan.offset);
|
|
1284
|
+
await file.read(buffer, 0, buffer.length, scan.offset);
|
|
1285
|
+
const end = buffer.lastIndexOf(0x0a) + 1;
|
|
1286
|
+
scan.offset += end;
|
|
1287
|
+
for (const line of buffer.subarray(0, end).toString('utf8').split('\n')) {
|
|
1288
|
+
let record;
|
|
1289
|
+
try {
|
|
1290
|
+
record = JSON.parse(line);
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (record.adapter !== 'camera.streaming' || !(Date.parse(String(record.timestamp)) >= startedAt))
|
|
1296
|
+
continue;
|
|
1297
|
+
const known = scan.byAccessory.get(record.accessory);
|
|
1298
|
+
if (record.event === 'live-video-selected' && record.operation === 'start') {
|
|
1299
|
+
const start = {
|
|
1300
|
+
...(typeof record.camera === 'number' ? { camera: record.camera } : {}),
|
|
1301
|
+
...(typeof record.variant === 'number' ? { variant: record.variant } : {}),
|
|
1302
|
+
state: 'started',
|
|
1303
|
+
};
|
|
1304
|
+
scan.starts.push(start);
|
|
1305
|
+
scan.byAccessory.set(record.accessory, start);
|
|
1306
|
+
}
|
|
1307
|
+
else if (record.event === 'live-session-streaming' && known?.state === 'started') {
|
|
1308
|
+
known.state = 'streaming';
|
|
1309
|
+
}
|
|
1310
|
+
else if (record.event === 'live-session-released' && known) {
|
|
1311
|
+
known.state = 'ended';
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
finally {
|
|
1316
|
+
await file.close();
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
catch (error) {
|
|
1320
|
+
if (error.code !== 'ENOENT')
|
|
1321
|
+
throw error;
|
|
1322
|
+
}
|
|
1323
|
+
const events = (await this.readUiEvents(session)).match(/"event":"live-(?:picture|preview)-[a-z-]+"/g) ?? [];
|
|
1324
|
+
const answers = events.filter((event) => event.includes('picture')).length;
|
|
1325
|
+
const previews = events.filter((event) => event.includes('preview')).map((event) => !event.includes('not-seen'));
|
|
1326
|
+
return { attempts: LIVE_ENCODE_ATTEMPTS, starts: scan.starts, answers, previews };
|
|
1327
|
+
}
|
|
1215
1328
|
/**
|
|
1216
1329
|
* Replaces the persisted session, and drops the `diagnostics/evidence` tree with it.
|
|
1217
1330
|
*
|
|
@@ -1388,7 +1501,7 @@ export function createDiagnosticLogger(target, storageRoot, now = Date.now) {
|
|
|
1388
1501
|
return;
|
|
1389
1502
|
const session = activeDiagnosticsSession(storageRoot, now());
|
|
1390
1503
|
const verbose = VERBOSE_SCOPES.has(String(event.scope));
|
|
1391
|
-
if (verbose && (!session || !
|
|
1504
|
+
if (verbose && (!session || !selectedEvidence(session).includes(evidenceClassOf(event)))) {
|
|
1392
1505
|
return;
|
|
1393
1506
|
}
|
|
1394
1507
|
file.write(event);
|
|
@@ -2609,6 +2722,7 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2609
2722
|
const sourceHeight = nonNegativeInteger(value.sourceHeight);
|
|
2610
2723
|
const cause = allowlistedLabel(value.cause, ADAPTATION_CAUSES);
|
|
2611
2724
|
const encoder = allowlistedLabel(value.encoder, LIVE_VIDEO_ENCODERS);
|
|
2725
|
+
const variant = boundedInteger(value.variant, LIVE_ENCODE_ATTEMPTS - 1);
|
|
2612
2726
|
const source = allowlistedLabel(value.source, LIVE_VIDEO_SOURCES);
|
|
2613
2727
|
return {
|
|
2614
2728
|
scope: 'ffmpeg',
|
|
@@ -2617,6 +2731,7 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2617
2731
|
event: value.event,
|
|
2618
2732
|
...(cause === undefined ? {} : { cause }),
|
|
2619
2733
|
...(encoder === undefined ? {} : { encoder }),
|
|
2734
|
+
...(variant === undefined ? {} : { variant }),
|
|
2620
2735
|
...(source === undefined ? {} : { source }),
|
|
2621
2736
|
...(sourceWidth === undefined || sourceHeight === undefined ? {} : { sourceWidth, sourceHeight }),
|
|
2622
2737
|
...(code === undefined ? {} : { code }),
|
|
@@ -2742,6 +2857,8 @@ function sanitizeLiveVideoSelection(value) {
|
|
|
2742
2857
|
const mtu = boundedInteger(value.mtu, MAX_LIVE_VIDEO_MTU, MIN_LIVE_VIDEO_MTU);
|
|
2743
2858
|
const maxBitRate = boundedInteger(value.maxBitRate, MAX_LIVE_VIDEO_BIT_RATE, 0);
|
|
2744
2859
|
const addressVersion = allowlistedLabel(value.addressVersion, HOMEKIT_LIVE_ADDRESS_VERSIONS);
|
|
2860
|
+
const camera = boundedInteger(value.camera, MAX_ACCESSORY_ALIASES);
|
|
2861
|
+
const variant = boundedInteger(value.variant, LIVE_ENCODE_ATTEMPTS - 1);
|
|
2745
2862
|
if (!operation ||
|
|
2746
2863
|
!profile ||
|
|
2747
2864
|
!codecLevel ||
|
|
@@ -2765,6 +2882,8 @@ function sanitizeLiveVideoSelection(value) {
|
|
|
2765
2882
|
mtu,
|
|
2766
2883
|
...(maxBitRate === undefined ? {} : { maxBitRate }),
|
|
2767
2884
|
addressVersion,
|
|
2885
|
+
...(camera === undefined ? {} : { camera }),
|
|
2886
|
+
...(variant === undefined ? {} : { variant }),
|
|
2768
2887
|
...(alias === undefined ? {} : { accessory: alias }),
|
|
2769
2888
|
};
|
|
2770
2889
|
}
|