@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.40 → 5.0.0-beta.42
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 +141 -10
- 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/dist/ui/server.js +13 -3
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +64 -19
- package/homebridge-ui/public/i18n/en.json +18 -2
- package/homebridge-ui/public/i18n/fr.json +18 -2
- package/homebridge-ui/public/index.html +22 -2
- package/homebridge-ui/public/js/app.js +180 -6
- package/i18n/runtime/en.json +1 -1
- package/package.json +1 -1
package/dist/diagnostics.js
CHANGED
|
@@ -254,6 +254,20 @@ const DIAGNOSTICS_UI_EVENTS = [
|
|
|
254
254
|
'dashboard-opened',
|
|
255
255
|
'authentication-opened',
|
|
256
256
|
'request-failed',
|
|
257
|
+
'authentication-submitted',
|
|
258
|
+
'authentication-captcha',
|
|
259
|
+
'authentication-two-factor',
|
|
260
|
+
'authentication-restart-required',
|
|
261
|
+
'authentication-blocked',
|
|
262
|
+
'authentication-plugin-running',
|
|
263
|
+
'authentication-failed',
|
|
264
|
+
'authentication-commit-failed',
|
|
265
|
+
'authentication-timed-out',
|
|
266
|
+
'authentication-closed',
|
|
267
|
+
'live-picture-seen',
|
|
268
|
+
'live-picture-not-seen',
|
|
269
|
+
'live-preview-seen',
|
|
270
|
+
'live-preview-not-seen',
|
|
257
271
|
];
|
|
258
272
|
const SUPPORT_ARCHIVE_KEY = {
|
|
259
273
|
keyId: 'support-2026-08-01',
|
|
@@ -331,7 +345,7 @@ function bugReportUrl(session, missingEvidence, host) {
|
|
|
331
345
|
return url.toString();
|
|
332
346
|
}
|
|
333
347
|
const DIAGNOSTICS_PROFILES = {
|
|
334
|
-
'startup-authentication': ['plugin-log', 'sdk-log'],
|
|
348
|
+
'startup-authentication': ['plugin-log', 'sdk-log', 'ui-log'],
|
|
335
349
|
'device-representation': ['plugin-log', 'sdk-log', 'homekit-log'],
|
|
336
350
|
'control-state': ['plugin-log', 'sdk-log', 'homekit-log'],
|
|
337
351
|
'live-media': ['plugin-log', 'sdk-log', 'homekit-log', 'ffmpeg-log'],
|
|
@@ -339,6 +353,18 @@ const DIAGNOSTICS_PROFILES = {
|
|
|
339
353
|
'dashboard-ui': ['plugin-log', 'ui-log'],
|
|
340
354
|
other: ['plugin-log', 'sdk-log', 'homekit-log', 'ffmpeg-log'],
|
|
341
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
|
+
}
|
|
342
368
|
/** Narrows external profile input against the diagnostics-owned profile registry. */
|
|
343
369
|
export function isDiagnosticsProfile(value) {
|
|
344
370
|
return typeof value === 'string' && Object.hasOwn(DIAGNOSTICS_PROFILES, value);
|
|
@@ -387,6 +413,35 @@ function activeDiagnosticsSession(storageRoot, now) {
|
|
|
387
413
|
const session = readDiagnosticsSession(storageRoot);
|
|
388
414
|
return session && Date.parse(session.expiresAt) > now ? session : undefined;
|
|
389
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
|
+
}
|
|
390
445
|
/**
|
|
391
446
|
* A session as it stands at `now`: a capture still running when its authorization ends is finished at that end.
|
|
392
447
|
*
|
|
@@ -732,8 +787,10 @@ export class GuidedDiagnostics {
|
|
|
732
787
|
this.pendingUiEvents += 1;
|
|
733
788
|
const write = this.uiEventWrites.then(async () => {
|
|
734
789
|
const session = this.requireAuthorized();
|
|
735
|
-
if (session.
|
|
736
|
-
|
|
790
|
+
if (!selectedEvidence(session).includes('ui-log') ||
|
|
791
|
+
!session.reproductionStartedAt ||
|
|
792
|
+
session.reproductionEndedAt) {
|
|
793
|
+
throw new Error('An active dashboard or sign-in reproduction is required');
|
|
737
794
|
}
|
|
738
795
|
const directory = join(this.storageRoot, DIAGNOSTICS_DIRECTORY);
|
|
739
796
|
const path = join(directory, UI_EVENTS_FILE);
|
|
@@ -784,7 +841,7 @@ export class GuidedDiagnostics {
|
|
|
784
841
|
}
|
|
785
842
|
const collected = await this.collectSupportEvidence(session);
|
|
786
843
|
const reproductionStartedAt = Date.parse(session.reproductionStartedAt);
|
|
787
|
-
const selected =
|
|
844
|
+
const selected = selectedEvidence(session);
|
|
788
845
|
const createdAt = this.now();
|
|
789
846
|
const manifest = {
|
|
790
847
|
version: 2,
|
|
@@ -983,7 +1040,7 @@ export class GuidedDiagnostics {
|
|
|
983
1040
|
});
|
|
984
1041
|
}
|
|
985
1042
|
const log = await this.readReproductionLog(session.reproductionStartedAt, session.reproductionEndedAt);
|
|
986
|
-
for (const selected of
|
|
1043
|
+
for (const selected of selectedEvidence(session)) {
|
|
987
1044
|
const carried = log.get(selected);
|
|
988
1045
|
if (!carried)
|
|
989
1046
|
continue;
|
|
@@ -1171,11 +1228,11 @@ export class GuidedDiagnostics {
|
|
|
1171
1228
|
return { status: 'inactive', selectedEvidence: [], missingEvidence: [], partialExportAvailable: false };
|
|
1172
1229
|
}
|
|
1173
1230
|
const { session, exportable } = settledDiagnosticsSession(persisted, this.now());
|
|
1174
|
-
const
|
|
1231
|
+
const selected = selectedEvidence(session);
|
|
1175
1232
|
const collected = session.reproductionStartedAt && session.reproductionEndedAt
|
|
1176
1233
|
? await this.collectSupportEvidence(session)
|
|
1177
1234
|
: undefined;
|
|
1178
|
-
const missingEvidence = collected ? missingEvidenceClasses(
|
|
1235
|
+
const missingEvidence = collected ? missingEvidenceClasses(selected, collected) : [];
|
|
1179
1236
|
const expired = Date.parse(session.expiresAt) <= this.now() && !exportable;
|
|
1180
1237
|
const status = expired
|
|
1181
1238
|
? 'expired'
|
|
@@ -1189,7 +1246,7 @@ export class GuidedDiagnostics {
|
|
|
1189
1246
|
supportCaseId: session.supportCaseId,
|
|
1190
1247
|
profile: session.profile,
|
|
1191
1248
|
reproductionMode: session.reproductionMode,
|
|
1192
|
-
selectedEvidence,
|
|
1249
|
+
selectedEvidence: selected,
|
|
1193
1250
|
missingEvidence,
|
|
1194
1251
|
authorizedAt: session.authorizedAt,
|
|
1195
1252
|
expiresAt: session.expiresAt,
|
|
@@ -1198,8 +1255,76 @@ export class GuidedDiagnostics {
|
|
|
1198
1255
|
partialExportAvailable: exportable,
|
|
1199
1256
|
...(session.affectedDevices === undefined ? {} : { affectedDevices: session.affectedDevices }),
|
|
1200
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
|
+
: {}),
|
|
1201
1261
|
};
|
|
1202
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
|
+
}
|
|
1203
1328
|
/**
|
|
1204
1329
|
* Replaces the persisted session, and drops the `diagnostics/evidence` tree with it.
|
|
1205
1330
|
*
|
|
@@ -1376,7 +1501,7 @@ export function createDiagnosticLogger(target, storageRoot, now = Date.now) {
|
|
|
1376
1501
|
return;
|
|
1377
1502
|
const session = activeDiagnosticsSession(storageRoot, now());
|
|
1378
1503
|
const verbose = VERBOSE_SCOPES.has(String(event.scope));
|
|
1379
|
-
if (verbose && (!session || !
|
|
1504
|
+
if (verbose && (!session || !selectedEvidence(session).includes(evidenceClassOf(event)))) {
|
|
1380
1505
|
return;
|
|
1381
1506
|
}
|
|
1382
1507
|
file.write(event);
|
|
@@ -2184,7 +2309,7 @@ const HOMEKIT_REASON_ACTIONS = {
|
|
|
2184
2309
|
'camera-live-session-failed:station-unreachable': 'log.action.checkStation',
|
|
2185
2310
|
'camera-live-session-failed:source-warm-timeout': 'log.action.retryLiveViewNoPicture',
|
|
2186
2311
|
'camera-snapshot-unavailable:live-station-unreachable': 'log.action.checkStation',
|
|
2187
|
-
'camera-talkback-failed:no-controller-audio': 'log.action.
|
|
2312
|
+
'camera-talkback-failed:no-controller-audio': 'log.action.checkTalkbackMicrophone',
|
|
2188
2313
|
};
|
|
2189
2314
|
/** Projects a value onto an active condition, or nothing where the catalog does not write that code with those keys. */
|
|
2190
2315
|
export function knownCondition(value) {
|
|
@@ -2597,6 +2722,7 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2597
2722
|
const sourceHeight = nonNegativeInteger(value.sourceHeight);
|
|
2598
2723
|
const cause = allowlistedLabel(value.cause, ADAPTATION_CAUSES);
|
|
2599
2724
|
const encoder = allowlistedLabel(value.encoder, LIVE_VIDEO_ENCODERS);
|
|
2725
|
+
const variant = boundedInteger(value.variant, LIVE_ENCODE_ATTEMPTS - 1);
|
|
2600
2726
|
const source = allowlistedLabel(value.source, LIVE_VIDEO_SOURCES);
|
|
2601
2727
|
return {
|
|
2602
2728
|
scope: 'ffmpeg',
|
|
@@ -2605,6 +2731,7 @@ function sanitizeAdaptationNotice(value, level) {
|
|
|
2605
2731
|
event: value.event,
|
|
2606
2732
|
...(cause === undefined ? {} : { cause }),
|
|
2607
2733
|
...(encoder === undefined ? {} : { encoder }),
|
|
2734
|
+
...(variant === undefined ? {} : { variant }),
|
|
2608
2735
|
...(source === undefined ? {} : { source }),
|
|
2609
2736
|
...(sourceWidth === undefined || sourceHeight === undefined ? {} : { sourceWidth, sourceHeight }),
|
|
2610
2737
|
...(code === undefined ? {} : { code }),
|
|
@@ -2730,6 +2857,8 @@ function sanitizeLiveVideoSelection(value) {
|
|
|
2730
2857
|
const mtu = boundedInteger(value.mtu, MAX_LIVE_VIDEO_MTU, MIN_LIVE_VIDEO_MTU);
|
|
2731
2858
|
const maxBitRate = boundedInteger(value.maxBitRate, MAX_LIVE_VIDEO_BIT_RATE, 0);
|
|
2732
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);
|
|
2733
2862
|
if (!operation ||
|
|
2734
2863
|
!profile ||
|
|
2735
2864
|
!codecLevel ||
|
|
@@ -2753,6 +2882,8 @@ function sanitizeLiveVideoSelection(value) {
|
|
|
2753
2882
|
mtu,
|
|
2754
2883
|
...(maxBitRate === undefined ? {} : { maxBitRate }),
|
|
2755
2884
|
addressVersion,
|
|
2885
|
+
...(camera === undefined ? {} : { camera }),
|
|
2886
|
+
...(variant === undefined ? {} : { variant }),
|
|
2756
2887
|
...(alias === undefined ? {} : { accessory: alias }),
|
|
2757
2888
|
};
|
|
2758
2889
|
}
|