@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.0 → 5.0.0-beta.10
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/README.md +4 -6
- package/config.schema.json +20 -0
- package/dist/configuration.js +26 -2
- package/dist/configuration.js.map +1 -1
- package/dist/device/member-evidence.js +8 -1
- package/dist/device/member-evidence.js.map +1 -1
- package/dist/diagnostics.js +428 -60
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +54 -26
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/adapters/security-system.js +85 -44
- package/dist/homekit/adapters/security-system.js.map +1 -1
- package/dist/homekit/reconciler.js +5 -2
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/live-stream.js +98 -54
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +7 -5
- package/dist/media/recording.js.map +1 -1
- package/dist/media/snapshot.js +4 -4
- package/dist/media/station-live-sessions.js +47 -43
- package/dist/media/station-live-sessions.js.map +1 -1
- package/dist/platform.js +24 -5
- package/dist/platform.js.map +1 -1
- package/dist/ui/dashboard.js +5 -1
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/server.js +26 -4
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +356 -3
- package/homebridge-ui/public/i18n/en.json +25 -14
- package/homebridge-ui/public/i18n/fr.json +25 -14
- package/homebridge-ui/public/index.html +24 -32
- package/homebridge-ui/public/js/app.js +187 -65
- package/homebridge-ui/public/js/dashboard.js +80 -1
- package/homebridge-ui/public/js/profile-wizard.js +40 -28
- package/package.json +2 -2
|
@@ -188,44 +188,34 @@
|
|
|
188
188
|
<div class="diagnostics-stage">
|
|
189
189
|
<div class="diagnostics-wizard diagnostics-surface" data-diagnostics-wizard aria-live="polite">
|
|
190
190
|
<div class="diagnostics-question" data-diagnostics-question>
|
|
191
|
-
<strong
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
></button>
|
|
199
|
-
<button type="button" data-diagnostics-
|
|
191
|
+
<strong
|
|
192
|
+
id="diagnostics-question-heading"
|
|
193
|
+
tabindex="-1"
|
|
194
|
+
data-diagnostics-question-text
|
|
195
|
+
data-i18n="diagnosticsTilesHeading"
|
|
196
|
+
></strong>
|
|
197
|
+
<div class="diagnostics-tiles" role="group" aria-labelledby="diagnostics-question-heading">
|
|
198
|
+
<button type="button" data-diagnostics-tile="startup-authentication" data-i18n="diagnosticsProfileStartup"></button>
|
|
199
|
+
<button type="button" data-diagnostics-tile="device-representation" data-i18n="diagnosticsProfileDevices"></button>
|
|
200
|
+
<button type="button" data-diagnostics-tile="control-state" data-i18n="diagnosticsProfileControl"></button>
|
|
201
|
+
<button type="button" data-diagnostics-tile="live-media" data-i18n="diagnosticsProfileLiveMedia"></button>
|
|
202
|
+
<button type="button" data-diagnostics-tile="hksv-recording" data-i18n="diagnosticsProfileRecording"></button>
|
|
203
|
+
<button type="button" data-diagnostics-tile="dashboard-ui" data-i18n="diagnosticsProfileDashboard"></button>
|
|
204
|
+
<button type="button" data-diagnostics-tile="other" data-i18n="diagnosticsProfileOther"></button>
|
|
200
205
|
</div>
|
|
201
|
-
<button
|
|
202
|
-
type="button"
|
|
203
|
-
class="diagnostics-quiet-action"
|
|
204
|
-
data-diagnostics-direct
|
|
205
|
-
data-i18n="diagnosticsChooseDirectly"
|
|
206
|
-
></button>
|
|
207
206
|
</div>
|
|
208
|
-
<div class="diagnostics-
|
|
209
|
-
<
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
<option value="startup-authentication" data-i18n="diagnosticsProfileStartup"></option>
|
|
213
|
-
<option value="device-representation" data-i18n="diagnosticsProfileDevices"></option>
|
|
214
|
-
<option value="control-state" data-i18n="diagnosticsProfileControl"></option>
|
|
215
|
-
<option value="live-media" data-i18n="diagnosticsProfileLiveMedia"></option>
|
|
216
|
-
<option value="hksv-recording" data-i18n="diagnosticsProfileRecording"></option>
|
|
217
|
-
<option value="dashboard-ui" data-i18n="diagnosticsProfileDashboard"></option>
|
|
218
|
-
<option value="other" data-i18n="diagnosticsProfileOther"></option>
|
|
219
|
-
</select>
|
|
220
|
-
</label>
|
|
207
|
+
<div class="diagnostics-devices" data-diagnostics-devices hidden>
|
|
208
|
+
<strong id="diagnostics-devices-heading" tabindex="-1" data-i18n="diagnosticsDevicesHeading"></strong>
|
|
209
|
+
<p data-i18n="diagnosticsDevicesHint"></p>
|
|
210
|
+
<div class="diagnostics-device-list" data-diagnostics-device-list role="group" aria-labelledby="diagnostics-devices-heading"></div>
|
|
221
211
|
<div class="diagnostics-choice-actions">
|
|
222
212
|
<button
|
|
223
213
|
type="button"
|
|
224
214
|
class="diagnostics-secondary-action"
|
|
225
|
-
data-diagnostics-
|
|
226
|
-
data-i18n="
|
|
215
|
+
data-diagnostics-devices-every
|
|
216
|
+
data-i18n="diagnosticsDevicesEvery"
|
|
227
217
|
></button>
|
|
228
|
-
<button type="button" data-diagnostics-
|
|
218
|
+
<button type="button" data-diagnostics-devices-chosen disabled data-i18n="diagnosticsDevicesChosen"></button>
|
|
229
219
|
</div>
|
|
230
220
|
</div>
|
|
231
221
|
<div class="diagnostics-frequency" data-diagnostics-frequency hidden>
|
|
@@ -288,20 +278,22 @@
|
|
|
288
278
|
<div class="diagnostics-result diagnostics-surface" data-diagnostics-result hidden>
|
|
289
279
|
<strong tabindex="-1" data-diagnostics-result-heading data-i18n="diagnosticsEvidenceReady"></strong>
|
|
290
280
|
<p data-i18n="diagnosticsArchiveReviewIntro"></p>
|
|
291
|
-
<button type="button" data-diagnostics-review data-i18n="diagnosticsArchiveReview"></button>
|
|
292
281
|
<div class="diagnostics-manifest" data-diagnostics-manifest hidden></div>
|
|
293
282
|
<label class="diagnostics-review-confirm" data-diagnostics-review-confirm-label hidden>
|
|
294
283
|
<input type="checkbox" data-diagnostics-review-confirm />
|
|
295
284
|
<span data-i18n="diagnosticsArchiveReviewConfirm"></span>
|
|
296
285
|
</label>
|
|
286
|
+
<p class="diagnostics-local-note" data-i18n="diagnosticsStaysLocal"></p>
|
|
297
287
|
<button type="button" data-diagnostics-export disabled hidden data-i18n="diagnosticsArchiveExport"></button>
|
|
298
288
|
<a
|
|
299
289
|
data-diagnostics-issue
|
|
300
290
|
hidden
|
|
301
291
|
target="_blank"
|
|
302
292
|
rel="noopener noreferrer"
|
|
293
|
+
aria-describedby="diagnostics-issue-hint"
|
|
303
294
|
data-i18n="diagnosticsOpenIssue"
|
|
304
295
|
></a>
|
|
296
|
+
<p class="diagnostics-step-hint" id="diagnostics-issue-hint" data-diagnostics-issue-hint></p>
|
|
305
297
|
<button
|
|
306
298
|
type="button"
|
|
307
299
|
class="diagnostics-secondary-action"
|
|
@@ -39,12 +39,11 @@ const diagnosticsClose = document.querySelector('[data-diagnostics-close]');
|
|
|
39
39
|
const diagnosticsWizardPanel = document.querySelector('[data-diagnostics-wizard]');
|
|
40
40
|
const diagnosticsQuestion = document.querySelector('[data-diagnostics-question]');
|
|
41
41
|
const diagnosticsQuestionText = document.querySelector('[data-diagnostics-question-text]');
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const diagnosticsDirectBack = document.querySelector('[data-diagnostics-direct-back]');
|
|
42
|
+
const diagnosticsTiles = [...document.querySelectorAll('[data-diagnostics-tile]')];
|
|
43
|
+
const diagnosticsDevices = document.querySelector('[data-diagnostics-devices]');
|
|
44
|
+
const diagnosticsDeviceList = document.querySelector('[data-diagnostics-device-list]');
|
|
45
|
+
const diagnosticsDevicesEvery = document.querySelector('[data-diagnostics-devices-every]');
|
|
46
|
+
const diagnosticsDevicesChosen = document.querySelector('[data-diagnostics-devices-chosen]');
|
|
48
47
|
const diagnosticsFrequency = document.querySelector('[data-diagnostics-frequency]');
|
|
49
48
|
const diagnosticsFrequencyHeading = document.querySelector('#diagnostics-frequency-heading');
|
|
50
49
|
const diagnosticsFrequencyNow = document.querySelector('[data-diagnostics-frequency-answer="now"]');
|
|
@@ -52,11 +51,11 @@ const diagnosticsFrequencyIntermittent = document.querySelector('[data-diagnosti
|
|
|
52
51
|
const diagnosticsFrequencyBack = document.querySelector('[data-diagnostics-frequency-back]');
|
|
53
52
|
const diagnosticsMatch = document.querySelector('[data-diagnostics-match]');
|
|
54
53
|
const diagnosticsReject = document.querySelector('[data-diagnostics-reject]');
|
|
55
|
-
const diagnosticsProfile = document.querySelector('[data-diagnostics-profile]');
|
|
56
54
|
const diagnosticsAuthorize = document.querySelector('[data-diagnostics-authorize]');
|
|
57
55
|
const diagnosticsReproduction = document.querySelector('[data-diagnostics-reproduction]');
|
|
58
56
|
const diagnosticsStatus = document.querySelector('[data-diagnostics-status]');
|
|
59
57
|
const diagnosticsIssue = document.querySelector('[data-diagnostics-issue]');
|
|
58
|
+
const diagnosticsIssueHint = document.querySelector('[data-diagnostics-issue-hint]');
|
|
60
59
|
const diagnosticsResult = document.querySelector('[data-diagnostics-result]');
|
|
61
60
|
const diagnosticsActions = document.querySelector('[data-diagnostics-actions]');
|
|
62
61
|
const diagnosticsGuidanceTitle = document.querySelector('[data-diagnostics-guidance-title]');
|
|
@@ -66,7 +65,6 @@ const diagnosticsPhaseTitle = document.querySelector('[data-diagnostics-phase-ti
|
|
|
66
65
|
const diagnosticsGuidanceBeforeSection = document.querySelector('[data-diagnostics-guidance-before-section]');
|
|
67
66
|
const diagnosticsGuidanceBefore = document.querySelector('[data-diagnostics-guidance-before]');
|
|
68
67
|
const diagnosticsGuidanceAction = document.querySelector('[data-diagnostics-guidance-action]');
|
|
69
|
-
const diagnosticsReview = document.querySelector('[data-diagnostics-review]');
|
|
70
68
|
const diagnosticsManifest = document.querySelector('[data-diagnostics-manifest]');
|
|
71
69
|
const diagnosticsReviewConfirm = document.querySelector('[data-diagnostics-review-confirm]');
|
|
72
70
|
const diagnosticsReviewConfirmLabel = document.querySelector('[data-diagnostics-review-confirm-label]');
|
|
@@ -100,6 +98,12 @@ let challenge = '';
|
|
|
100
98
|
let legacyNames = [];
|
|
101
99
|
let legacyAcknowledged = false;
|
|
102
100
|
let diagnosticsState = { status: 'inactive', missingEvidence: [] };
|
|
101
|
+
/** Whether the reporter already downloaded this session's archive, which is what the report offer follows. */
|
|
102
|
+
let diagnosticsArchiveDownloaded = false;
|
|
103
|
+
/** The session whose manifest is on screen, so it is fetched once rather than on every render. */
|
|
104
|
+
let diagnosticsReviewedCaseId = '';
|
|
105
|
+
/** Guards the one fetch, so a burst of renders does not assemble the manifest several times over. */
|
|
106
|
+
let diagnosticsReviewPending = false;
|
|
103
107
|
let diagnosticsReviewId = '';
|
|
104
108
|
let diagnosticsStartingAnother = false;
|
|
105
109
|
let panelReturn;
|
|
@@ -258,17 +262,82 @@ function renderDiagnosticsGuidance(profile) {
|
|
|
258
262
|
diagnosticsGuidanceAction.textContent = messages[guidance.action] ?? '';
|
|
259
263
|
}
|
|
260
264
|
|
|
265
|
+
/**
|
|
266
|
+
* The devices the reporter may name, one tile each.
|
|
267
|
+
*
|
|
268
|
+
* A tile carries the device's own name above its retained image where it has one, and the icon the devices
|
|
269
|
+
* view uses for a device where it does not. The image is capped at half the tile: it is there to be recognised
|
|
270
|
+
* at a glance, and a picture that fills the tile leaves the name it belongs to competing with it.
|
|
271
|
+
*
|
|
272
|
+
* Only the devices the chosen area's fault could be about are offered, so the answer cannot name a device that
|
|
273
|
+
* never had the thing being reported.
|
|
274
|
+
*
|
|
275
|
+
* The list is asked for when it is empty rather than relying on the devices view having been opened first, and
|
|
276
|
+
* a request that goes unanswered leaves the step able to say every device, which is a complete answer.
|
|
277
|
+
*/
|
|
278
|
+
async function renderDiagnosticsDeviceList() {
|
|
279
|
+
if (dashboardDevices.length === 0) {
|
|
280
|
+
try {
|
|
281
|
+
const snapshot = await requestWithinDeadline('/dashboard', { representationPreferences: {} }, 12000);
|
|
282
|
+
dashboardDevices = snapshot.devices ?? [];
|
|
283
|
+
} catch {
|
|
284
|
+
dashboardDevices = [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
diagnosticsDeviceList.replaceChildren();
|
|
288
|
+
for (const device of diagnosticsWizard.affectable(diagnosticsWizardState.profile, dashboardDevices)) {
|
|
289
|
+
const tile = document.createElement('button');
|
|
290
|
+
tile.type = 'button';
|
|
291
|
+
tile.dataset.serial = device.serial;
|
|
292
|
+
tile.setAttribute('aria-pressed', 'false');
|
|
293
|
+
const art = document.createElement('img');
|
|
294
|
+
art.alt = '';
|
|
295
|
+
if (device.artwork) {
|
|
296
|
+
art.className = 'device-photo';
|
|
297
|
+
art.loading = 'lazy';
|
|
298
|
+
art.src = device.artwork;
|
|
299
|
+
} else {
|
|
300
|
+
art.src = 'assets/icons/inventory.svg';
|
|
301
|
+
}
|
|
302
|
+
const name = document.createElement('span');
|
|
303
|
+
name.textContent = device.name;
|
|
304
|
+
tile.append(art, name);
|
|
305
|
+
tile.addEventListener('click', () => {
|
|
306
|
+
tile.setAttribute('aria-pressed', tile.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
|
|
307
|
+
renderDiagnosticsDeviceChoice();
|
|
308
|
+
});
|
|
309
|
+
diagnosticsDeviceList.append(tile);
|
|
310
|
+
}
|
|
311
|
+
renderDiagnosticsDeviceChoice();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* The two controls that follow the pressed tiles: whether continuing is possible, and what the escape does.
|
|
316
|
+
*
|
|
317
|
+
* With every tile pressed there is nothing left for a select-all to select, so the same control clears instead —
|
|
318
|
+
* otherwise the only way back from a full grid is pressing each tile again.
|
|
319
|
+
*/
|
|
320
|
+
function renderDiagnosticsDeviceChoice() {
|
|
321
|
+
const tiles = diagnosticsDeviceList.querySelectorAll('[data-serial]').length;
|
|
322
|
+
const chosen = chosenDiagnosticsDevices().length;
|
|
323
|
+
diagnosticsDevicesChosen.disabled = chosen === 0;
|
|
324
|
+
diagnosticsDevicesEvery.textContent =
|
|
325
|
+
messages[chosen > 0 && chosen === tiles ? 'diagnosticsDevicesNone' : 'diagnosticsDevicesEvery'] ?? '';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The serials pressed in the device step, in the order the grid drew them. */
|
|
329
|
+
function chosenDiagnosticsDevices() {
|
|
330
|
+
return [...diagnosticsDeviceList.querySelectorAll('[data-serial]')]
|
|
331
|
+
.filter((tile) => tile.getAttribute('aria-pressed') === 'true')
|
|
332
|
+
.map((tile) => tile.dataset.serial);
|
|
333
|
+
}
|
|
334
|
+
|
|
261
335
|
function renderDiagnosticsWizard() {
|
|
262
|
-
diagnosticsQuestion.hidden = diagnosticsWizardState.mode !== '
|
|
263
|
-
|
|
336
|
+
diagnosticsQuestion.hidden = diagnosticsWizardState.mode !== 'tiles';
|
|
337
|
+
diagnosticsDevices.hidden = diagnosticsWizardState.mode !== 'devices';
|
|
264
338
|
diagnosticsFrequency.hidden = diagnosticsWizardState.mode !== 'frequency';
|
|
265
339
|
diagnosticsMatch.hidden = diagnosticsWizardState.mode !== 'match';
|
|
266
|
-
if (diagnosticsWizardState.mode === 'questions') {
|
|
267
|
-
const question = diagnosticsWizard.questions[diagnosticsWizardState.questionIndex];
|
|
268
|
-
diagnosticsQuestionText.textContent = messages[question.message] ?? '';
|
|
269
|
-
}
|
|
270
340
|
if (diagnosticsWizardState.mode === 'match') {
|
|
271
|
-
diagnosticsProfile.value = diagnosticsWizardState.profile;
|
|
272
341
|
renderDiagnosticsGuidance(diagnosticsWizardState.profile);
|
|
273
342
|
diagnosticsModeSummary.textContent =
|
|
274
343
|
messages[
|
|
@@ -279,7 +348,21 @@ function renderDiagnosticsWizard() {
|
|
|
279
348
|
}
|
|
280
349
|
}
|
|
281
350
|
|
|
351
|
+
/**
|
|
352
|
+
* The report step, reachable or not.
|
|
353
|
+
*
|
|
354
|
+
* Without an archive there is nothing to attach, so the anchor carries no `href` — it is not a link — and
|
|
355
|
+
* `aria-disabled` states the same thing to a reader who cannot see it greyed. The hint says why, because a
|
|
356
|
+
* control that cannot be used and does not explain itself reads as broken rather than as not yet.
|
|
357
|
+
*/
|
|
358
|
+
function setIssueStepReachable(url) {
|
|
359
|
+
diagnosticsIssue.href = url;
|
|
360
|
+
diagnosticsIssue.setAttribute('aria-disabled', url ? 'false' : 'true');
|
|
361
|
+
diagnosticsIssueHint.textContent = messages[url ? 'diagnosticsIssueOpensTab' : 'diagnosticsIssueNeedsArchive'] ?? '';
|
|
362
|
+
}
|
|
363
|
+
|
|
282
364
|
function renderDiagnostics(state) {
|
|
365
|
+
if (state.supportCaseId !== diagnosticsState.supportCaseId) diagnosticsArchiveDownloaded = false;
|
|
283
366
|
diagnosticsState = state;
|
|
284
367
|
const screen = diagnosticsWizard.screen(state, diagnosticsStartingAnother);
|
|
285
368
|
const choosing = screen === 'choose';
|
|
@@ -307,17 +390,20 @@ function renderDiagnostics(state) {
|
|
|
307
390
|
: state.status === 'reproducing'
|
|
308
391
|
? messages.diagnosticsNowFinish
|
|
309
392
|
: messages.diagnosticsStartRecording;
|
|
310
|
-
diagnosticsIssue.hidden =
|
|
311
|
-
|
|
393
|
+
diagnosticsIssue.hidden = true;
|
|
394
|
+
setIssueStepReachable(diagnosticsArchiveDownloaded ? (state.issueUrl ?? '') : '');
|
|
312
395
|
diagnosticsResult.hidden = !reviewing;
|
|
313
|
-
|
|
314
|
-
diagnosticsManifest.hidden =
|
|
315
|
-
diagnosticsReviewConfirmLabel.hidden =
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
396
|
+
const reviewed = reviewing && diagnosticsReviewedCaseId === (state.supportCaseId ?? '');
|
|
397
|
+
diagnosticsManifest.hidden = !reviewed;
|
|
398
|
+
diagnosticsReviewConfirmLabel.hidden = !reviewed;
|
|
399
|
+
diagnosticsExport.hidden = !reviewed;
|
|
400
|
+
diagnosticsIssue.hidden = !reviewed;
|
|
401
|
+
if (!reviewed) {
|
|
402
|
+
diagnosticsReviewConfirm.checked = false;
|
|
403
|
+
diagnosticsExport.disabled = true;
|
|
404
|
+
diagnosticsReviewId = '';
|
|
405
|
+
if (reviewing) void ensureArchiveReview(state.supportCaseId ?? '');
|
|
406
|
+
}
|
|
321
407
|
if (screen === 'reproduce') {
|
|
322
408
|
renderDiagnosticsGuidance(state.profile);
|
|
323
409
|
if (reproductionMode === 'intermittent') {
|
|
@@ -349,6 +435,21 @@ function renderDiagnostics(state) {
|
|
|
349
435
|
).replace('{evidence}', state.missingEvidence?.join(', ') ?? '');
|
|
350
436
|
}
|
|
351
437
|
|
|
438
|
+
/**
|
|
439
|
+
* The catalogue entry that says, in a reader's words, what a never-collected class is.
|
|
440
|
+
*
|
|
441
|
+
* A class with no entry falls back to its own identifier, which is visible rather than absent, so a newly
|
|
442
|
+
* declared class shows up as something to translate instead of quietly disappearing from the list.
|
|
443
|
+
*/
|
|
444
|
+
const NEVER_COLLECTED_LABELS = {
|
|
445
|
+
'credentials-and-authentication': 'excludedCredentials',
|
|
446
|
+
'tokens-cookies-and-authorization': 'excludedTokens',
|
|
447
|
+
'session-and-push-stores': 'excludedSessions',
|
|
448
|
+
'private-and-symmetric-keys': 'excludedKeys',
|
|
449
|
+
'unconstrained-internal-objects': 'excludedInternalData',
|
|
450
|
+
'camera-images-talkback-and-raw-media': 'excludedMedia',
|
|
451
|
+
};
|
|
452
|
+
|
|
352
453
|
function renderArchiveManifest(manifest) {
|
|
353
454
|
diagnosticsManifest.replaceChildren();
|
|
354
455
|
const summary = document.createElement('p');
|
|
@@ -366,9 +467,16 @@ function renderArchiveManifest(manifest) {
|
|
|
366
467
|
row.textContent = `${item.evidence} · ${item.privacyClass} · ${item.status}${missing}${size}${truncated}${fields ? ` · ${messages.diagnosticsArchiveFields ?? ''} ${fields}` : ''}`;
|
|
367
468
|
evidence.append(row);
|
|
368
469
|
}
|
|
470
|
+
const detail = document.createElement('details');
|
|
471
|
+
const detailLabel = document.createElement('summary');
|
|
472
|
+
detailLabel.textContent = messages.diagnosticsArchiveDetail ?? '';
|
|
473
|
+
detail.append(detailLabel, summary, evidence);
|
|
369
474
|
const exclusions = document.createElement('p');
|
|
370
|
-
|
|
371
|
-
|
|
475
|
+
const neverCollected = (manifest.excludedClasses ?? []).map(
|
|
476
|
+
(excluded) => messages[NEVER_COLLECTED_LABELS[excluded]] ?? excluded,
|
|
477
|
+
);
|
|
478
|
+
exclusions.textContent = `${messages.diagnosticsArchiveExcluded ?? ''} ${neverCollected.join(', ')}.`;
|
|
479
|
+
diagnosticsManifest.append(detail, exclusions);
|
|
372
480
|
const uncovered = (manifest.evidence ?? []).filter((item) => item.coversReproduction === false);
|
|
373
481
|
if (uncovered.length > 0) {
|
|
374
482
|
const gap = document.createElement('p');
|
|
@@ -385,25 +493,32 @@ function renderArchiveManifest(manifest) {
|
|
|
385
493
|
diagnosticsManifest.hidden = false;
|
|
386
494
|
}
|
|
387
495
|
|
|
388
|
-
|
|
389
|
-
|
|
496
|
+
/**
|
|
497
|
+
* The manifest for the session on screen, fetched once.
|
|
498
|
+
*
|
|
499
|
+
* A completed reproduction that the reporter is looking at is a file they came for, so the manifest is
|
|
500
|
+
* read without being asked for. It is read once per session rather than per render, because assembling it
|
|
501
|
+
* reads every collected log. Exporting spends it, so the session is forgotten again and a second download
|
|
502
|
+
* asks for a fresh one.
|
|
503
|
+
*/
|
|
504
|
+
async function ensureArchiveReview(caseId) {
|
|
505
|
+
if (!caseId || diagnosticsReviewedCaseId === caseId || diagnosticsReviewPending) return;
|
|
506
|
+
diagnosticsReviewPending = true;
|
|
390
507
|
try {
|
|
391
508
|
const review = await requestWithinDeadline('/diagnostics/archive/review', undefined, 12000);
|
|
392
509
|
diagnosticsReviewId = review.reviewId;
|
|
510
|
+
diagnosticsReviewedCaseId = caseId;
|
|
393
511
|
renderArchiveManifest(review.manifest);
|
|
394
512
|
diagnosticsReviewConfirmLabel.hidden = false;
|
|
395
513
|
diagnosticsExport.hidden = false;
|
|
514
|
+
diagnosticsExport.disabled = !diagnosticsReviewConfirm.checked;
|
|
515
|
+
diagnosticsIssue.hidden = false;
|
|
396
516
|
} catch {
|
|
397
|
-
|
|
398
|
-
renderDiagnostics(await requestWithinDeadline('/diagnostics/status', undefined, 12000));
|
|
399
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
400
|
-
} catch {
|
|
401
|
-
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
402
|
-
}
|
|
517
|
+
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
403
518
|
} finally {
|
|
404
|
-
|
|
519
|
+
diagnosticsReviewPending = false;
|
|
405
520
|
}
|
|
406
|
-
}
|
|
521
|
+
}
|
|
407
522
|
|
|
408
523
|
diagnosticsReviewConfirm.addEventListener('change', () => {
|
|
409
524
|
diagnosticsExport.disabled = !diagnosticsReviewConfirm.checked || !diagnosticsReviewId;
|
|
@@ -425,7 +540,10 @@ diagnosticsExport.addEventListener('click', async () => {
|
|
|
425
540
|
download.click();
|
|
426
541
|
document.body.removeChild(download);
|
|
427
542
|
diagnosticsReviewId = '';
|
|
543
|
+
diagnosticsReviewedCaseId = '';
|
|
428
544
|
diagnosticsReviewConfirm.checked = false;
|
|
545
|
+
diagnosticsArchiveDownloaded = true;
|
|
546
|
+
setIssueStepReachable(diagnosticsState.issueUrl ?? '');
|
|
429
547
|
} catch {
|
|
430
548
|
diagnosticsStatus.textContent = messages.diagnosticsFailed ?? '';
|
|
431
549
|
}
|
|
@@ -441,6 +559,7 @@ diagnosticsAuthorize.addEventListener('click', async () => {
|
|
|
441
559
|
{
|
|
442
560
|
profile: diagnosticsWizardState.profile,
|
|
443
561
|
reproductionMode: diagnosticsWizardState.reproductionMode,
|
|
562
|
+
...(diagnosticsWizardState.devices === undefined ? {} : { affectedDevices: diagnosticsWizardState.devices }),
|
|
444
563
|
},
|
|
445
564
|
12000,
|
|
446
565
|
);
|
|
@@ -465,37 +584,34 @@ diagnosticsAuthorize.addEventListener('click', async () => {
|
|
|
465
584
|
}
|
|
466
585
|
});
|
|
467
586
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
renderDiagnosticsWizard();
|
|
477
|
-
if (diagnosticsWizardState.mode === 'frequency') diagnosticsFrequencyHeading.focus?.();
|
|
478
|
-
else diagnosticsQuestionText.focus?.();
|
|
479
|
-
});
|
|
587
|
+
for (const tile of diagnosticsTiles) {
|
|
588
|
+
tile.addEventListener('click', async () => {
|
|
589
|
+
diagnosticsWizardState = diagnosticsWizard.select(diagnosticsWizardState, tile.dataset.diagnosticsTile);
|
|
590
|
+
renderDiagnosticsWizard();
|
|
591
|
+
if (diagnosticsWizardState.mode === 'devices') await renderDiagnosticsDeviceList();
|
|
592
|
+
else diagnosticsFrequencyHeading.focus?.();
|
|
593
|
+
});
|
|
594
|
+
}
|
|
480
595
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
596
|
+
diagnosticsDevicesEvery.addEventListener('click', () => {
|
|
597
|
+
const tiles = [...diagnosticsDeviceList.querySelectorAll('[data-serial]')];
|
|
598
|
+
if (tiles.length === 0) {
|
|
599
|
+
diagnosticsWizardState = diagnosticsWizard.chooseDevices(diagnosticsWizardState, 'all');
|
|
600
|
+
renderDiagnosticsWizard();
|
|
601
|
+
diagnosticsFrequencyHeading.focus?.();
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const clearing = chosenDiagnosticsDevices().length === tiles.length;
|
|
605
|
+
for (const tile of tiles) tile.setAttribute('aria-pressed', clearing ? 'false' : 'true');
|
|
606
|
+
renderDiagnosticsDeviceChoice();
|
|
485
607
|
});
|
|
486
608
|
|
|
487
|
-
|
|
488
|
-
diagnosticsWizardState = diagnosticsWizard.
|
|
609
|
+
diagnosticsDevicesChosen.addEventListener('click', () => {
|
|
610
|
+
diagnosticsWizardState = diagnosticsWizard.chooseDevices(diagnosticsWizardState, chosenDiagnosticsDevices());
|
|
489
611
|
renderDiagnosticsWizard();
|
|
490
612
|
diagnosticsFrequencyHeading.focus?.();
|
|
491
613
|
});
|
|
492
614
|
|
|
493
|
-
diagnosticsDirectBack.addEventListener('click', () => {
|
|
494
|
-
diagnosticsWizardState = diagnosticsWizard.start();
|
|
495
|
-
renderDiagnosticsWizard();
|
|
496
|
-
diagnosticsQuestionText.focus?.();
|
|
497
|
-
});
|
|
498
|
-
|
|
499
615
|
function chooseDiagnosticsReproductionMode(reproductionMode) {
|
|
500
616
|
diagnosticsWizardState = diagnosticsWizard.chooseReproductionMode(diagnosticsWizardState, reproductionMode);
|
|
501
617
|
renderDiagnosticsWizard();
|
|
@@ -508,15 +624,13 @@ diagnosticsFrequencyIntermittent.addEventListener('click', () => chooseDiagnosti
|
|
|
508
624
|
diagnosticsFrequencyBack.addEventListener('click', () => {
|
|
509
625
|
diagnosticsWizardState = diagnosticsWizard.backFromFrequency(diagnosticsWizardState);
|
|
510
626
|
renderDiagnosticsWizard();
|
|
511
|
-
|
|
512
|
-
else diagnosticsQuestionText.focus?.();
|
|
627
|
+
diagnosticsQuestionText.focus?.();
|
|
513
628
|
});
|
|
514
629
|
|
|
515
630
|
diagnosticsReject.addEventListener('click', () => {
|
|
516
631
|
diagnosticsWizardState = diagnosticsWizard.reject(diagnosticsWizardState);
|
|
517
632
|
renderDiagnosticsWizard();
|
|
518
|
-
|
|
519
|
-
else diagnosticsQuestionText.focus?.();
|
|
633
|
+
diagnosticsQuestionText.focus?.();
|
|
520
634
|
});
|
|
521
635
|
|
|
522
636
|
diagnosticsStartAnother.addEventListener('click', () => {
|
|
@@ -618,7 +732,15 @@ mastheadDiagnostics.addEventListener('click', () => menuDiagnostics.click());
|
|
|
618
732
|
dashboardDiagnose.addEventListener('click', () => menuDiagnostics.click());
|
|
619
733
|
|
|
620
734
|
menuDiagnostics.addEventListener('click', async () => {
|
|
735
|
+
/*
|
|
736
|
+
* Opened from the sign-in screen, the area is not in question: what is going wrong is getting signed in. The
|
|
737
|
+
* answer is presumed rather than fixed, so the frequency step's way back still reaches the other six.
|
|
738
|
+
*/
|
|
739
|
+
const signingIn = !setupContent.hidden;
|
|
621
740
|
openDashboardPanel(diagnosticsPanel, menuDiagnostics);
|
|
741
|
+
if (signingIn) {
|
|
742
|
+
diagnosticsWizardState = diagnosticsWizard.select(diagnosticsWizard.start(), 'startup-authentication');
|
|
743
|
+
}
|
|
622
744
|
// Drawn on opening, because the wizard's first question is only ever put on the page by this call.
|
|
623
745
|
renderDiagnosticsWizard();
|
|
624
746
|
try {
|
|
@@ -8,12 +8,62 @@
|
|
|
8
8
|
.replaceAll("'", ''');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const EUFY_MODES = [
|
|
12
|
+
{ value: 'away', label: 'Away' },
|
|
13
|
+
{ value: 'home', label: 'Home' },
|
|
14
|
+
{ value: 'schedule', label: 'Schedule' },
|
|
15
|
+
{ value: 'custom1', label: 'Custom 1' },
|
|
16
|
+
{ value: 'custom2', label: 'Custom 2' },
|
|
17
|
+
{ value: 'custom3', label: 'Custom 3' },
|
|
18
|
+
{ value: 'off', label: 'Off' },
|
|
19
|
+
{ value: 'geo', label: 'Geofencing' },
|
|
20
|
+
{ value: 'disarmed', label: 'Disarmed' },
|
|
21
|
+
];
|
|
22
|
+
/** What each HomeKit state means where the household assigned nothing, as the adapter resolves it. */
|
|
23
|
+
const DEFAULT_ARMING_MODES = { home: 'home', away: 'away', off: 'disarmed' };
|
|
24
|
+
const ARMING_SLOTS = [
|
|
25
|
+
{ slot: 'home', label: 'armingSlotHome' },
|
|
26
|
+
{ slot: 'away', label: 'armingSlotAway' },
|
|
27
|
+
{ slot: 'night', label: 'armingSlotNight' },
|
|
28
|
+
{ slot: 'off', label: 'armingSlotOff' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The guard-mode map: four states HomeKit names against the nine a station reports, one pairing per row.
|
|
33
|
+
*
|
|
34
|
+
* Folded shut, because the four defaults are what nearly every household keeps and the panel has a camera's
|
|
35
|
+
* worth of settings above it. The summary carries the whole map as text, so the answer is readable without
|
|
36
|
+
* opening anything, and a household that assigned a state of its own finds it already open.
|
|
37
|
+
*
|
|
38
|
+
* A row is the whole control — the state names itself and the select beside it says what it means. An
|
|
39
|
+
* unassigned state shows a dash, which is what keeps Night out of HomeKit until it is given a mode.
|
|
40
|
+
*/
|
|
41
|
+
function armingModeControl(device, assigned, messages) {
|
|
42
|
+
const summary = ARMING_SLOTS.map(
|
|
43
|
+
({ slot, label }) => `${messages[label]} → ${EUFY_MODES.find((mode) => mode.value === assigned[slot])?.label ?? '—'}`,
|
|
44
|
+
).join(' · ');
|
|
45
|
+
const rows = ARMING_SLOTS.map(({ slot, label }) => {
|
|
46
|
+
const value = assigned[slot] ?? '';
|
|
47
|
+
const id = `arming-${escapeHtml(device.serial)}-${slot}`;
|
|
48
|
+
const options = EUFY_MODES.map(
|
|
49
|
+
(mode) =>
|
|
50
|
+
`<option value="${mode.value}"${value === mode.value ? ' selected' : ''}>${escapeHtml(mode.label)}</option>`,
|
|
51
|
+
).join('');
|
|
52
|
+
return `<tr><th scope="row"><label for="${id}">${escapeHtml(messages[label])}</label></th><td><select id="${id}" data-preference="armingModes" data-slot="${slot}" data-serial="${escapeHtml(device.serial)}" data-original="${escapeHtml(value)}"><option value=""${value === '' ? ' selected' : ''}>—</option>${options}</select></td></tr>`;
|
|
53
|
+
}).join('');
|
|
54
|
+
const assignedItself = ARMING_SLOTS.some(({ slot }) => assigned[slot] !== DEFAULT_ARMING_MODES[slot]);
|
|
55
|
+
return `<details class="arming-setting" data-setting${assignedItself ? ' open' : ''}${device.represented ? '' : ' hidden'} data-requires-representation><summary><span class="setting-label">${escapeHtml(messages.preferenceArmingModes)}</span><span class="arming-summary">${escapeHtml(summary)}</span></summary><table class="arming-table"><thead><tr><th scope="col">HomeKit</th><th scope="col">eufy</th></tr></thead><tbody>${rows}</tbody></table><p class="snapshot-help">${escapeHtml(messages.armingModesHelp)}</p></details>`;
|
|
56
|
+
}
|
|
57
|
+
|
|
11
58
|
function preferenceControl(device, key, preference, messages) {
|
|
12
59
|
const labels = {
|
|
13
60
|
represented: messages.preferenceRepresented,
|
|
14
61
|
audio: messages.preferenceAudio,
|
|
15
62
|
snapshotMode: messages.preferenceSnapshotMode,
|
|
16
63
|
};
|
|
64
|
+
if (key === 'armingModes') {
|
|
65
|
+
return armingModeControl(device, preference.armingModes, messages);
|
|
66
|
+
}
|
|
17
67
|
const represented = preference.represented;
|
|
18
68
|
const dependent = key === 'represented' ? '' : ` data-requires-representation${represented ? '' : ' hidden'}`;
|
|
19
69
|
if (key === 'snapshotMode') {
|
|
@@ -29,7 +79,7 @@
|
|
|
29
79
|
|
|
30
80
|
function markPendingPreference(control, key, value) {
|
|
31
81
|
if (!control?.classList || control.dataset.original === undefined) return;
|
|
32
|
-
const current = key === 'snapshotMode' ? String(value) : String(Boolean(value));
|
|
82
|
+
const current = key === 'snapshotMode' || key === 'armingModes' ? String(value) : String(Boolean(value));
|
|
33
83
|
const setting = control.closest('[data-setting]');
|
|
34
84
|
const changed = current !== control.dataset.original;
|
|
35
85
|
if (key === 'snapshotMode') {
|
|
@@ -261,6 +311,10 @@
|
|
|
261
311
|
const existing = getConfig();
|
|
262
312
|
if (!existing || !serial || !key) return;
|
|
263
313
|
const defaults = { represented: true, audio: true, snapshotMode: 'Refresh' };
|
|
314
|
+
if (key === 'armingModes') {
|
|
315
|
+
await saveArmingMode(control, serial, existing, saveConfig, getMessages, elements);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
264
318
|
const value = key === 'snapshotMode' ? control.value : control.checked;
|
|
265
319
|
markPendingPreference(control, key, value);
|
|
266
320
|
if (key === 'snapshotMode') {
|
|
@@ -295,6 +349,30 @@
|
|
|
295
349
|
});
|
|
296
350
|
}
|
|
297
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Records one HomeKit state's guard mode, keeping the map sparse: a state set back to the plugin's own choice
|
|
354
|
+
* is removed rather than pinned, so a later default reaches a household that never overrode it.
|
|
355
|
+
*/
|
|
356
|
+
async function saveArmingMode(control, serial, existing, saveConfig, getMessages, elements) {
|
|
357
|
+
const slot = control.dataset.slot;
|
|
358
|
+
const value = control.value;
|
|
359
|
+
markPendingPreference(control, 'armingModes', value);
|
|
360
|
+
const entityPreferences = { ...(existing.entityPreferences ?? {}) };
|
|
361
|
+
const preference = { ...(entityPreferences[serial] ?? {}) };
|
|
362
|
+
const armingModes = { ...(preference.armingModes ?? {}) };
|
|
363
|
+
if (value === '') delete armingModes[slot];
|
|
364
|
+
else armingModes[slot] = value;
|
|
365
|
+
if (Object.keys(armingModes).length === 0) delete preference.armingModes;
|
|
366
|
+
else preference.armingModes = armingModes;
|
|
367
|
+
if (Object.keys(preference).length === 0) delete entityPreferences[serial];
|
|
368
|
+
else entityPreferences[serial] = preference;
|
|
369
|
+
try {
|
|
370
|
+
await saveConfig({ ...existing, entityPreferences });
|
|
371
|
+
} catch {
|
|
372
|
+
elements.summary.textContent = getMessages().preferenceSaveFailed;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
298
376
|
/**
|
|
299
377
|
* Fills the settings page for one device, which is where its controls live rather than on the tile itself.
|
|
300
378
|
*
|
|
@@ -313,6 +391,7 @@
|
|
|
313
391
|
represented: preferences[device.serial]?.represented ?? true,
|
|
314
392
|
audio: preferences[device.serial]?.audio ?? true,
|
|
315
393
|
snapshotMode: preferences[device.serial]?.snapshotMode ?? 'Refresh',
|
|
394
|
+
armingModes: { ...DEFAULT_ARMING_MODES, ...preferences[device.serial]?.armingModes },
|
|
316
395
|
};
|
|
317
396
|
const controls = device.preferences
|
|
318
397
|
.map((key) => preferenceControl(device, key, preference, messages))
|