@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.5 → 5.0.0-beta.7
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 +233 -55
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +1 -1
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/reconciler.js +2 -2
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/live-stream.js +41 -10
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +2 -1
- package/dist/media/recording.js.map +1 -1
- package/dist/platform.js +23 -4
- package/dist/platform.js.map +1 -1
- package/package.json +2 -2
package/dist/diagnostics.js
CHANGED
|
@@ -33,6 +33,8 @@ export function unconfirmedWriteCondition(property) {
|
|
|
33
33
|
? undefined
|
|
34
34
|
: { code: 'camera-control-operation-failed', ...named, active: true, reason: 'not-confirmed' };
|
|
35
35
|
}
|
|
36
|
+
/** Why a live session gave its source back: a stop from outside it, or the session ending itself. */
|
|
37
|
+
const LIVE_SESSION_RELEASES = ['requested', 'failed'];
|
|
36
38
|
/** The bounded reasons a stream request is refused before it reaches the source. */
|
|
37
39
|
const REFUSAL_REASONS = new Set(['disabled', 'at-capacity', 'cancelled', 'prepare-failed']);
|
|
38
40
|
/**
|
|
@@ -47,6 +49,28 @@ const HOMEKIT_LIVE_REQUEST_EVENTS = new Set([
|
|
|
47
49
|
'live-session-released',
|
|
48
50
|
'live-session-streaming',
|
|
49
51
|
]);
|
|
52
|
+
/**
|
|
53
|
+
* The error identities an SDK record may name, which is all a record carries of one.
|
|
54
|
+
*
|
|
55
|
+
* A message is never among them, so an identity absent here is recorded as `Error` and reads as any other
|
|
56
|
+
* failure. A station refusing a second camera is one of these identities, because a base serving another of its
|
|
57
|
+
* cameras is working correctly and must not read as a fault.
|
|
58
|
+
*/
|
|
59
|
+
const SDK_ERROR_TYPES = [
|
|
60
|
+
'Error',
|
|
61
|
+
'RangeError',
|
|
62
|
+
'SessionExpiredError',
|
|
63
|
+
'StationBusyError',
|
|
64
|
+
'StationKeyUnavailableError',
|
|
65
|
+
'StationUnreachableError',
|
|
66
|
+
'TypeError',
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* The retained fields that stand for a device rather than describing one, which is the privacy class a manifest
|
|
70
|
+
* declares for them. Each holds a per-case alias: stable within one archive, and resolvable to a device only by
|
|
71
|
+
* the owner whose console paired it with a name.
|
|
72
|
+
*/
|
|
73
|
+
const PSEUDONYMOUS_FIELDS = new Set(['accessory', 'accessoryAliases']);
|
|
50
74
|
const MAX_SDK_DETAILS = 16;
|
|
51
75
|
const MAX_LOG_RECORD_BYTES = 64 * 1024;
|
|
52
76
|
const MAX_CURRENT_LOG_BYTES = 50 * 1024 * 1024;
|
|
@@ -71,7 +95,12 @@ const DIAGNOSTICS_DIRECTORY = 'diagnostics';
|
|
|
71
95
|
const GUIDED_SESSION_FILE = 'session.json';
|
|
72
96
|
const REPRODUCTION_MARKERS_FILE = 'reproduction-markers.jsonl';
|
|
73
97
|
const UI_EVENTS_FILE = 'ui-events.jsonl';
|
|
74
|
-
const
|
|
98
|
+
const HOST_ENVIRONMENT_FILE = 'host.json';
|
|
99
|
+
const FLEET_FILE = 'fleet.json';
|
|
100
|
+
/** The longest device model or adapter key retained, which bounds values this plugin does not choose alone. */
|
|
101
|
+
const MAX_DEVICE_MODEL_LENGTH = 64;
|
|
102
|
+
/** How many HomeKit services one fleet entry names, which is above what any single device is represented by. */
|
|
103
|
+
const MAX_FLEET_SERVICES = 12;
|
|
75
104
|
/** How much of an FFmpeg path or version banner is kept, which is more than either needs. */
|
|
76
105
|
const MAX_FFMPEG_IDENTITY_LENGTH = 256;
|
|
77
106
|
const DEBUG_AUTHORIZATION_MS = 72 * 60 * 60 * 1_000;
|
|
@@ -153,6 +182,15 @@ const SDK_EVENT_KEYS = new Set([
|
|
|
153
182
|
'transport-error',
|
|
154
183
|
]);
|
|
155
184
|
const EN_MESSAGES = JSON.parse(readFileSync(new URL('../i18n/runtime/en.json', import.meta.url), 'utf8'));
|
|
185
|
+
/**
|
|
186
|
+
* The reporter's answer as a support archive carries it: every device, or how many they named.
|
|
187
|
+
*
|
|
188
|
+
* The serials themselves stay on the host. They identify one household's hardware, and no evidence class in an
|
|
189
|
+
* archive carries a device identity.
|
|
190
|
+
*/
|
|
191
|
+
function countedAffectedDevices(affected) {
|
|
192
|
+
return affected === 'all' ? 'all' : new Set(affected).size;
|
|
193
|
+
}
|
|
156
194
|
/** The bounded UI events a support session may record, and the only vocabulary the sink accepts. */
|
|
157
195
|
const DIAGNOSTICS_UI_EVENTS = [
|
|
158
196
|
'background-started',
|
|
@@ -302,54 +340,135 @@ function boundedText(value, maximumLength) {
|
|
|
302
340
|
.trim();
|
|
303
341
|
return printable === '' ? undefined : printable;
|
|
304
342
|
}
|
|
305
|
-
function
|
|
306
|
-
return join(storageRoot, DIAGNOSTICS_DIRECTORY,
|
|
343
|
+
function fleetPath(storageRoot) {
|
|
344
|
+
return join(storageRoot, DIAGNOSTICS_DIRECTORY, FLEET_FILE);
|
|
307
345
|
}
|
|
308
346
|
/**
|
|
309
|
-
* Records
|
|
347
|
+
* Records what the account's devices are and what each becomes, replacing the record whole.
|
|
310
348
|
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
* cannot be reported as
|
|
314
|
-
*
|
|
315
|
-
* would cost the user their cameras.
|
|
349
|
+
* Written by the process that holds both the inventory and the accessory aliases, because an archive is
|
|
350
|
+
* assembled by another one. Rewritten on every complete inventory, so a device withdrawn or newly recognised
|
|
351
|
+
* cannot be reported as still present. A write that fails is dropped: an archive then declares no fleet, which
|
|
352
|
+
* is honest, whereas failing over a diagnostic file would cost the user their cameras.
|
|
316
353
|
*/
|
|
317
|
-
export function
|
|
354
|
+
export function recordFleet(storageRoot, fleet) {
|
|
318
355
|
try {
|
|
319
|
-
const path =
|
|
356
|
+
const path = fleetPath(storageRoot);
|
|
320
357
|
mkdirSync(dirname(path), { mode: 0o700, recursive: true });
|
|
321
|
-
writeFileSync(path, `${JSON.stringify({ version: 1,
|
|
358
|
+
writeFileSync(path, `${JSON.stringify({ version: 1, devices: fleet })}\n`, { mode: 0o600 });
|
|
322
359
|
}
|
|
323
360
|
catch { }
|
|
324
361
|
}
|
|
362
|
+
/** Reads the recorded fleet, keeping each entry whose own fields narrow and dropping each that does not. */
|
|
363
|
+
function readFleet(storageRoot) {
|
|
364
|
+
try {
|
|
365
|
+
const candidate = JSON.parse(readFileSync(fleetPath(storageRoot), 'utf8'));
|
|
366
|
+
if (candidate.version !== 1 || !Array.isArray(candidate.devices)) {
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
369
|
+
const devices = candidate.devices.flatMap((entry) => {
|
|
370
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
371
|
+
return [];
|
|
372
|
+
const row = entry;
|
|
373
|
+
const accessory = opaqueAccessoryAlias(row.accessory);
|
|
374
|
+
if (accessory === undefined || typeof row.represented !== 'boolean' || typeof row.attached !== 'boolean') {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
const model = typeof row.model === 'string' ? boundedText(row.model, MAX_DEVICE_MODEL_LENGTH) : undefined;
|
|
378
|
+
const services = (Array.isArray(row.services) ? row.services : [])
|
|
379
|
+
.filter((service) => typeof service === 'string')
|
|
380
|
+
.slice(0, MAX_FLEET_SERVICES)
|
|
381
|
+
.flatMap((service) => {
|
|
382
|
+
const bounded = boundedText(service, MAX_DEVICE_MODEL_LENGTH);
|
|
383
|
+
return bounded === undefined ? [] : [bounded];
|
|
384
|
+
});
|
|
385
|
+
return [
|
|
386
|
+
{
|
|
387
|
+
accessory,
|
|
388
|
+
...(model === undefined ? {} : { model }),
|
|
389
|
+
represented: row.represented,
|
|
390
|
+
attached: row.attached,
|
|
391
|
+
services,
|
|
392
|
+
},
|
|
393
|
+
];
|
|
394
|
+
});
|
|
395
|
+
return devices.length === 0 ? undefined : devices.slice(0, MAX_ACCESSORY_ALIASES);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
function hostEnvironmentPath(storageRoot) {
|
|
402
|
+
return join(storageRoot, DIAGNOSTICS_DIRECTORY, HOST_ENVIRONMENT_FILE);
|
|
403
|
+
}
|
|
404
|
+
/** The longest host version string retained, which bounds a value this plugin does not choose. */
|
|
405
|
+
const MAX_HOST_VERSION_LENGTH = 64;
|
|
325
406
|
/**
|
|
326
|
-
*
|
|
407
|
+
* Records what this run is hosted by, so a support archive states the host a failure came from.
|
|
327
408
|
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
409
|
+
* It is persisted rather than held in memory because the process that knows these facts is not the one that
|
|
410
|
+
* assembles an archive, and it is rewritten on every start so a host or binary that changed between runs
|
|
411
|
+
* cannot be reported as the one in use. Each call replaces the record whole, so a caller states everything it
|
|
412
|
+
* knows rather than accumulating into a file two writers would race for.
|
|
413
|
+
*
|
|
414
|
+
* A write that fails is dropped: the archive then declares the environment without these facts, which is
|
|
415
|
+
* honest, whereas failing startup over a diagnostic file would cost the user their cameras.
|
|
331
416
|
*/
|
|
332
|
-
function
|
|
417
|
+
export function recordHostEnvironment(storageRoot, host) {
|
|
333
418
|
try {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
419
|
+
const path = hostEnvironmentPath(storageRoot);
|
|
420
|
+
mkdirSync(dirname(path), { mode: 0o700, recursive: true });
|
|
421
|
+
writeFileSync(path, `${JSON.stringify({ version: 2, ...host })}\n`, { mode: 0o600 });
|
|
422
|
+
}
|
|
423
|
+
catch { }
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Reads what this run is hosted by, keeping each fact that narrows and dropping each that does not.
|
|
427
|
+
*
|
|
428
|
+
* The facts are independent, so a malformed binary identity does not withhold a host version, and a record
|
|
429
|
+
* stating neither is no record at all.
|
|
430
|
+
*/
|
|
431
|
+
function readHostEnvironment(storageRoot) {
|
|
432
|
+
try {
|
|
433
|
+
const candidate = JSON.parse(readFileSync(hostEnvironmentPath(storageRoot), 'utf8'));
|
|
434
|
+
if (candidate.version !== 2) {
|
|
337
435
|
return undefined;
|
|
338
436
|
}
|
|
339
|
-
const
|
|
340
|
-
|
|
437
|
+
const homebridge = typeof candidate.homebridge === 'string' ? boundedText(candidate.homebridge, MAX_HOST_VERSION_LENGTH) : undefined;
|
|
438
|
+
const ffmpeg = readFfmpegIdentity(candidate.ffmpeg);
|
|
439
|
+
if (homebridge === undefined && ffmpeg === undefined) {
|
|
341
440
|
return undefined;
|
|
342
441
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
: { path, source: recorded.source, ...(version === undefined ? {} : { version }) };
|
|
442
|
+
return {
|
|
443
|
+
...(homebridge === undefined ? {} : { homebridge }),
|
|
444
|
+
...(ffmpeg === undefined ? {} : { ffmpeg }),
|
|
445
|
+
};
|
|
348
446
|
}
|
|
349
447
|
catch {
|
|
350
448
|
return undefined;
|
|
351
449
|
}
|
|
352
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* The recorded adaptation binary, or nothing where the record's own fields do not narrow.
|
|
453
|
+
*
|
|
454
|
+
* The path and the banner are kept as written rather than pattern-replaced, because naming the binary is the
|
|
455
|
+
* entire purpose of the record and a redacted one answers nothing. Neither is device or account material: the
|
|
456
|
+
* path is this plugin's own setting or the binary it ships, and the banner is that build's public identity.
|
|
457
|
+
*/
|
|
458
|
+
function readFfmpegIdentity(value) {
|
|
459
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
460
|
+
return undefined;
|
|
461
|
+
}
|
|
462
|
+
const recorded = value;
|
|
463
|
+
if (recorded.source !== 'bundled' && recorded.source !== 'configured') {
|
|
464
|
+
return undefined;
|
|
465
|
+
}
|
|
466
|
+
const path = typeof recorded.path === 'string' ? boundedText(recorded.path, MAX_FFMPEG_IDENTITY_LENGTH) : undefined;
|
|
467
|
+
const version = typeof recorded.version === 'string' ? boundedText(recorded.version, MAX_FFMPEG_IDENTITY_LENGTH) : undefined;
|
|
468
|
+
return path === undefined
|
|
469
|
+
? undefined
|
|
470
|
+
: { path, source: recorded.source, ...(version === undefined ? {} : { version }) };
|
|
471
|
+
}
|
|
353
472
|
/**
|
|
354
473
|
* The one log evidence class a retained record belongs to.
|
|
355
474
|
*
|
|
@@ -626,7 +745,8 @@ export class GuidedDiagnostics {
|
|
|
626
745
|
}
|
|
627
746
|
}
|
|
628
747
|
async collectSupportEvidence(session) {
|
|
629
|
-
const
|
|
748
|
+
const host = readHostEnvironment(this.storageRoot);
|
|
749
|
+
const fleet = readFleet(this.storageRoot);
|
|
630
750
|
const evidence = [
|
|
631
751
|
...(session.affectedDevices === undefined
|
|
632
752
|
? []
|
|
@@ -635,15 +755,34 @@ export class GuidedDiagnostics {
|
|
|
635
755
|
evidence: 'reporter-statement',
|
|
636
756
|
privacyClass: 'diagnostic',
|
|
637
757
|
contentType: 'application/json',
|
|
638
|
-
content: `${JSON.stringify({ version:
|
|
758
|
+
content: `${JSON.stringify({ version: 2, affectedDevices: countedAffectedDevices(session.affectedDevices) })}\n`,
|
|
639
759
|
/**
|
|
640
|
-
* What the reporter answered, not what the plugin observed. A serial
|
|
641
|
-
*
|
|
642
|
-
* the
|
|
760
|
+
* What the reporter answered, not what the plugin observed. A serial is a device identity and no
|
|
761
|
+
* evidence class carries one, so the answer travels as its scope: every device, or how many were
|
|
762
|
+
* named. Which ones is the reporter's to say in their own words.
|
|
643
763
|
*/
|
|
644
764
|
fields: [
|
|
645
765
|
{ field: 'version', privacyClass: 'operational' },
|
|
646
|
-
{ field: 'affectedDevices', privacyClass: '
|
|
766
|
+
{ field: 'affectedDevices', privacyClass: 'operational' },
|
|
767
|
+
],
|
|
768
|
+
},
|
|
769
|
+
]),
|
|
770
|
+
...(fleet === undefined
|
|
771
|
+
? []
|
|
772
|
+
: [
|
|
773
|
+
{
|
|
774
|
+
evidence: 'fleet',
|
|
775
|
+
privacyClass: 'operational',
|
|
776
|
+
contentType: 'application/json',
|
|
777
|
+
content: `${JSON.stringify({ version: 1, devices: fleet })}\n`,
|
|
778
|
+
/**
|
|
779
|
+
* What each device is and becomes, against the alias every other record about it carries. A model
|
|
780
|
+
* and an admission outcome are product facts rather than device identity; the alias is what makes
|
|
781
|
+
* an entry resolvable to a device, and only by the owner whose console paired it with a name.
|
|
782
|
+
*/
|
|
783
|
+
fields: [
|
|
784
|
+
{ field: 'version', privacyClass: 'operational' },
|
|
785
|
+
{ field: 'devices', privacyClass: 'pseudonymous' },
|
|
647
786
|
],
|
|
648
787
|
},
|
|
649
788
|
]),
|
|
@@ -651,18 +790,22 @@ export class GuidedDiagnostics {
|
|
|
651
790
|
evidence: 'environment',
|
|
652
791
|
privacyClass: 'operational',
|
|
653
792
|
contentType: 'application/json',
|
|
654
|
-
content: `${JSON.stringify({ version:
|
|
793
|
+
content: `${JSON.stringify({ version: 3, plugin: PLUGIN_VERSION, sdk: SDK_VERSION, node: process.version, ...(host?.homebridge ? { homebridge: host.homebridge } : {}), platform: process.platform, arch: process.arch, ...(host?.ffmpeg ? { ffmpeg: host.ffmpeg } : {}) })}\n`,
|
|
655
794
|
/**
|
|
656
795
|
* The record is operational and one field is classified above it: a resolved FFmpeg path is an
|
|
657
796
|
* environment fact, but it can carry the home directory of the account Homebridge runs as, so it is
|
|
658
797
|
* declared as diagnostic rather than presented alongside the host's own architecture.
|
|
798
|
+
*
|
|
799
|
+
* The Homebridge version is operational and load-bearing: this plugin requires Homebridge 2, and a
|
|
800
|
+
* host that predates it loads the plugin and then fails in ways a media fault cannot be told from.
|
|
659
801
|
*/
|
|
660
802
|
fields: [
|
|
661
803
|
...['version', 'plugin', 'sdk', 'node', 'platform', 'arch'].map((field) => ({
|
|
662
804
|
field,
|
|
663
805
|
privacyClass: 'operational',
|
|
664
806
|
})),
|
|
665
|
-
...(
|
|
807
|
+
...(host?.homebridge ? [{ field: 'homebridge', privacyClass: 'operational' }] : []),
|
|
808
|
+
...(host?.ffmpeg ? [{ field: 'ffmpeg', privacyClass: 'diagnostic' }] : []),
|
|
666
809
|
],
|
|
667
810
|
},
|
|
668
811
|
];
|
|
@@ -713,7 +856,11 @@ export class GuidedDiagnostics {
|
|
|
713
856
|
...(carried.retainedFrom === undefined ? {} : { retainedFrom: carried.retainedFrom }),
|
|
714
857
|
fields: [...new Set(carried.records.flatMap((record) => Object.keys(record)))].sort().map((field) => ({
|
|
715
858
|
field,
|
|
716
|
-
privacyClass: field
|
|
859
|
+
privacyClass: PSEUDONYMOUS_FIELDS.has(field)
|
|
860
|
+
? 'pseudonymous'
|
|
861
|
+
: field === 'timestamp'
|
|
862
|
+
? 'operational'
|
|
863
|
+
: 'diagnostic',
|
|
717
864
|
})),
|
|
718
865
|
});
|
|
719
866
|
}
|
|
@@ -1280,6 +1427,18 @@ function sanitizeSdkLiveStartTrace(value) {
|
|
|
1280
1427
|
function opaquePullHandle(value) {
|
|
1281
1428
|
return typeof value === 'string' && /^(?:pull|station)-\d{1,6}(?::\d{1,3})?$/.test(value) ? value : undefined;
|
|
1282
1429
|
}
|
|
1430
|
+
/**
|
|
1431
|
+
* One support-case accessory alias, or nothing where the value is not one.
|
|
1432
|
+
*
|
|
1433
|
+
* Matched against the shape an alias has rather than against what a serial looks like, on the same rule as
|
|
1434
|
+
* {@link opaquePullHandle}: a record carries an alias or it carries no accessory at all.
|
|
1435
|
+
*/
|
|
1436
|
+
function opaqueAccessoryAlias(value) {
|
|
1437
|
+
return typeof value === 'string' &&
|
|
1438
|
+
/^accessory-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)
|
|
1439
|
+
? value
|
|
1440
|
+
: undefined;
|
|
1441
|
+
}
|
|
1283
1442
|
/**
|
|
1284
1443
|
* Adapts SDK protocol detail to bounded debug output without preserving supplied values.
|
|
1285
1444
|
*
|
|
@@ -1321,9 +1480,7 @@ export function createSdkLogger(target) {
|
|
|
1321
1480
|
const subsystem = SDK_SUBSYSTEMS.has(requestedSubsystem ?? '') ? requestedSubsystem : (aliasedSubsystem ?? 'sdk');
|
|
1322
1481
|
const details = args.slice(0, MAX_SDK_DETAILS).map((value) => {
|
|
1323
1482
|
if (value instanceof Error) {
|
|
1324
|
-
const errorType =
|
|
1325
|
-
? value.name
|
|
1326
|
-
: 'Error';
|
|
1483
|
+
const errorType = SDK_ERROR_TYPES.includes(value.name) ? value.name : 'Error';
|
|
1327
1484
|
return { errorType };
|
|
1328
1485
|
}
|
|
1329
1486
|
if (typeof value === 'string') {
|
|
@@ -1742,12 +1899,7 @@ function sanitizeStructuredEvent(message) {
|
|
|
1742
1899
|
if (!detail || typeof detail !== 'object' || Array.isArray(detail))
|
|
1743
1900
|
continue;
|
|
1744
1901
|
const candidate = detail;
|
|
1745
|
-
const errorType = allowlistedLabel(candidate.errorType,
|
|
1746
|
-
'Error',
|
|
1747
|
-
'RangeError',
|
|
1748
|
-
'SessionExpiredError',
|
|
1749
|
-
'TypeError',
|
|
1750
|
-
]);
|
|
1902
|
+
const errorType = allowlistedLabel(candidate.errorType, SDK_ERROR_TYPES);
|
|
1751
1903
|
if (errorType) {
|
|
1752
1904
|
details.push({ errorType });
|
|
1753
1905
|
continue;
|
|
@@ -1813,6 +1965,7 @@ function sanitizeStructuredEvent(message) {
|
|
|
1813
1965
|
!HOMEKIT_OBSERVATIONS.has(value.observation)) {
|
|
1814
1966
|
return undefined;
|
|
1815
1967
|
}
|
|
1968
|
+
const alias = opaqueAccessoryAlias(value.accessory);
|
|
1816
1969
|
return {
|
|
1817
1970
|
scope: 'homekit',
|
|
1818
1971
|
level: 'debug',
|
|
@@ -1822,6 +1975,7 @@ function sanitizeStructuredEvent(message) {
|
|
|
1822
1975
|
...(typeof value.announcedBy === 'string' && HOMEKIT_ANNOUNCEMENTS.has(value.announcedBy)
|
|
1823
1976
|
? { announcedBy: value.announcedBy }
|
|
1824
1977
|
: {}),
|
|
1978
|
+
...(alias === undefined ? {} : { accessory: alias }),
|
|
1825
1979
|
};
|
|
1826
1980
|
}
|
|
1827
1981
|
if (value.scope === 'runtime') {
|
|
@@ -1938,17 +2092,25 @@ export function reportInvalidSnapshotCache(target) {
|
|
|
1938
2092
|
target[level](`[${code}] ${localize(target, messageKey)}`);
|
|
1939
2093
|
target.debug?.(JSON.stringify({ scope: 'media-notice', level, code, messageKey }));
|
|
1940
2094
|
}
|
|
1941
|
-
/**
|
|
1942
|
-
|
|
2095
|
+
/**
|
|
2096
|
+
* Emits one allowlisted HomeKit event trace only when host debug output is available.
|
|
2097
|
+
*
|
|
2098
|
+
* `aliasFor` resolves the trace's device identity to the support-case alias the conditions about that accessory
|
|
2099
|
+
* already carry. The identity itself never reaches the record: an unresolved one is recorded as no accessory.
|
|
2100
|
+
*/
|
|
2101
|
+
export function reportHomeKitEvent(target, trace, aliasFor) {
|
|
2102
|
+
const alias = typeof trace.serial === 'string' ? aliasFor?.(trace.serial) : undefined;
|
|
2103
|
+
const offered = { ...trace, accessory: alias };
|
|
2104
|
+
const accessory = alias === undefined ? {} : { accessory: alias };
|
|
1943
2105
|
if (trace.event === 'live-video-selected') {
|
|
1944
|
-
const selection = sanitizeLiveVideoSelection(
|
|
2106
|
+
const selection = sanitizeLiveVideoSelection(offered);
|
|
1945
2107
|
if (target.debug && selection) {
|
|
1946
2108
|
target.debug(JSON.stringify({ scope: 'homekit', level: 'debug', ...selection }));
|
|
1947
2109
|
}
|
|
1948
2110
|
return;
|
|
1949
2111
|
}
|
|
1950
2112
|
if (HOMEKIT_LIVE_REQUEST_EVENTS.has(trace.event)) {
|
|
1951
|
-
const lifecycle = sanitizeLiveSessionTrace(
|
|
2113
|
+
const lifecycle = sanitizeLiveSessionTrace(offered);
|
|
1952
2114
|
if (target.debug && lifecycle) {
|
|
1953
2115
|
target.debug(JSON.stringify({ scope: 'homekit', level: 'debug', ...lifecycle }));
|
|
1954
2116
|
}
|
|
@@ -1970,6 +2132,7 @@ export function reportHomeKitEvent(target, trace) {
|
|
|
1970
2132
|
...(typeof trace.announcedBy === 'string' && HOMEKIT_ANNOUNCEMENTS.has(trace.announcedBy)
|
|
1971
2133
|
? { announcedBy: trace.announcedBy }
|
|
1972
2134
|
: {}),
|
|
2135
|
+
...accessory,
|
|
1973
2136
|
}));
|
|
1974
2137
|
}
|
|
1975
2138
|
/**
|
|
@@ -2091,17 +2254,23 @@ function sanitizeLiveSessionTrace(value) {
|
|
|
2091
2254
|
if (value.adapter !== 'camera.streaming') {
|
|
2092
2255
|
return undefined;
|
|
2093
2256
|
}
|
|
2094
|
-
|
|
2095
|
-
|
|
2257
|
+
const alias = opaqueAccessoryAlias(value.accessory);
|
|
2258
|
+
const accessory = alias === undefined ? {} : { accessory: alias };
|
|
2259
|
+
if (value.event === 'live-session-released') {
|
|
2260
|
+
const release = allowlistedLabel(value.release, LIVE_SESSION_RELEASES);
|
|
2261
|
+
return release ? { adapter: value.adapter, event: value.event, release, ...accessory } : undefined;
|
|
2262
|
+
}
|
|
2263
|
+
if (value.event === 'live-session-streaming') {
|
|
2264
|
+
return { adapter: value.adapter, event: value.event, ...accessory };
|
|
2096
2265
|
}
|
|
2097
2266
|
if (value.event === 'live-request-refused') {
|
|
2098
2267
|
return typeof value.reason === 'string' && REFUSAL_REASONS.has(value.reason)
|
|
2099
|
-
? { adapter: value.adapter, event: value.event, reason: value.reason }
|
|
2268
|
+
? { adapter: value.adapter, event: value.event, reason: value.reason, ...accessory }
|
|
2100
2269
|
: undefined;
|
|
2101
2270
|
}
|
|
2102
2271
|
if (value.event === 'live-request-unaccounted') {
|
|
2103
2272
|
return typeof value.afterMs === 'number' && Number.isFinite(value.afterMs)
|
|
2104
|
-
? { adapter: value.adapter, event: value.event, afterMs: Math.round(value.afterMs) }
|
|
2273
|
+
? { adapter: value.adapter, event: value.event, afterMs: Math.round(value.afterMs), ...accessory }
|
|
2105
2274
|
: undefined;
|
|
2106
2275
|
}
|
|
2107
2276
|
if (value.event !== 'live-session-failed' ||
|
|
@@ -2118,6 +2287,7 @@ function sanitizeLiveSessionTrace(value) {
|
|
|
2118
2287
|
outcome: value.outcome,
|
|
2119
2288
|
reason: value.reason,
|
|
2120
2289
|
stage: value.stage,
|
|
2290
|
+
...accessory,
|
|
2121
2291
|
};
|
|
2122
2292
|
}
|
|
2123
2293
|
function sanitizeLiveVideoSelection(value) {
|
|
@@ -2143,6 +2313,7 @@ function sanitizeLiveVideoSelection(value) {
|
|
|
2143
2313
|
!HOMEKIT_LIVE_ADDRESS_VERSIONS.has(addressVersion)) {
|
|
2144
2314
|
return undefined;
|
|
2145
2315
|
}
|
|
2316
|
+
const alias = opaqueAccessoryAlias(value.accessory);
|
|
2146
2317
|
return {
|
|
2147
2318
|
adapter: 'camera.streaming',
|
|
2148
2319
|
event: 'live-video-selected',
|
|
@@ -2154,6 +2325,7 @@ function sanitizeLiveVideoSelection(value) {
|
|
|
2154
2325
|
fps,
|
|
2155
2326
|
mtu,
|
|
2156
2327
|
addressVersion,
|
|
2328
|
+
...(alias === undefined ? {} : { accessory: alias }),
|
|
2157
2329
|
};
|
|
2158
2330
|
}
|
|
2159
2331
|
/** Emits bounded normal-output condition transitions without stable device or account identity. */
|
|
@@ -2212,7 +2384,7 @@ export class DiagnosticConditions {
|
|
|
2212
2384
|
}
|
|
2213
2385
|
const uniqueDeviceIds = condition.active ? [...new Set(affectedDeviceIds)].sort() : [];
|
|
2214
2386
|
const accessoryAliases = uniqueDeviceIds
|
|
2215
|
-
.map((identity) => this.
|
|
2387
|
+
.map((identity) => this.aliasFor(identity))
|
|
2216
2388
|
.filter((alias) => alias !== undefined)
|
|
2217
2389
|
.sort();
|
|
2218
2390
|
this.write(condition.code, condition.active, condition.reason, definition[0], definition[1], condition.active ? 'warn' : 'info', {
|
|
@@ -2229,7 +2401,13 @@ export class DiagnosticConditions {
|
|
|
2229
2401
|
})
|
|
2230
2402
|
.filter((named) => named !== undefined));
|
|
2231
2403
|
}
|
|
2232
|
-
|
|
2404
|
+
/**
|
|
2405
|
+
* The support-case alias for one device identity, minted on first use.
|
|
2406
|
+
*
|
|
2407
|
+
* The one place an alias is decided, so every record about a device — a condition, a media trace — carries
|
|
2408
|
+
* the same one, and the owner's console line is what resolves it to a name.
|
|
2409
|
+
*/
|
|
2410
|
+
aliasFor(identity) {
|
|
2233
2411
|
let alias = this.aliases.get(identity);
|
|
2234
2412
|
if (!alias && this.aliases.size < MAX_ACCESSORY_ALIASES) {
|
|
2235
2413
|
alias = `accessory-${randomUUID()}`;
|