@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.15 → 5.0.0-beta.17

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.
Files changed (36) hide show
  1. package/config.schema.json +4 -0
  2. package/dist/configuration.js +11 -1
  3. package/dist/configuration.js.map +1 -1
  4. package/dist/diagnostics.js +105 -19
  5. package/dist/diagnostics.js.map +1 -1
  6. package/dist/homekit/adapters/lock.js +27 -11
  7. package/dist/homekit/adapters/lock.js.map +1 -1
  8. package/dist/homekit/adapters/package-sensor.js +63 -0
  9. package/dist/homekit/adapters/package-sensor.js.map +1 -0
  10. package/dist/homekit/adapters/registry.js +2 -0
  11. package/dist/homekit/adapters/registry.js.map +1 -1
  12. package/dist/homekit/adapters/security-system.js +10 -0
  13. package/dist/homekit/adapters/security-system.js.map +1 -1
  14. package/dist/homekit/coverage-matrix.js +0 -2
  15. package/dist/homekit/coverage-matrix.js.map +1 -1
  16. package/dist/homekit/reconciler.js +1 -0
  17. package/dist/homekit/reconciler.js.map +1 -1
  18. package/dist/platform.js +1 -0
  19. package/dist/platform.js.map +1 -1
  20. package/dist/runtime/channel.js +6 -2
  21. package/dist/runtime/channel.js.map +1 -1
  22. package/dist/runtime/owner.js +3 -0
  23. package/dist/runtime/owner.js.map +1 -1
  24. package/dist/ui/dashboard.js +7 -1
  25. package/dist/ui/dashboard.js.map +1 -1
  26. package/dist/ui/server.js +1 -0
  27. package/dist/ui/server.js.map +1 -1
  28. package/homebridge-ui/public/app.css +117 -229
  29. package/homebridge-ui/public/i18n/en.json +86 -79
  30. package/homebridge-ui/public/i18n/fr.json +87 -80
  31. package/homebridge-ui/public/index.html +84 -94
  32. package/homebridge-ui/public/js/app.js +246 -423
  33. package/homebridge-ui/public/js/dashboard.js +58 -9
  34. package/homebridge-ui/public/js/profile-wizard.js +6 -55
  35. package/i18n/runtime/en.json +1 -1
  36. package/package.json +1 -1
@@ -37,8 +37,10 @@
37
37
  *
38
38
  * A row is the whole control — the state names itself and the select beside it says what it means. An
39
39
  * unassigned state shows a dash, which is what keeps Night out of HomeKit until it is given a mode.
40
+ *
41
+ * It is withheld while the security system it maps is turned off, and the stored map is kept for when it is back.
40
42
  */
41
- function armingModeControl(device, assigned, messages) {
43
+ function armingModeControl(device, assigned, messages, securitySystem) {
42
44
  const summary = ARMING_SLOTS.map(
43
45
  ({ slot, label }) => `${messages[label]} → ${EUFY_MODES.find((mode) => mode.value === assigned[slot])?.label ?? '—'}`,
44
46
  ).join(' · ');
@@ -52,17 +54,18 @@
52
54
  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
55
  }).join('');
54
56
  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>`;
57
+ return `<details class="arming-setting" data-setting${assignedItself ? ' open' : ''}${device.represented && securitySystem ? '' : ' hidden'} data-requires-representation data-requires-security-system><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
58
  }
57
59
 
58
60
  function preferenceControl(device, key, preference, messages) {
59
61
  const labels = {
60
62
  represented: messages.preferenceRepresented,
61
63
  audio: messages.preferenceAudio,
64
+ securitySystem: messages.preferenceSecuritySystem,
62
65
  snapshotMode: messages.preferenceSnapshotMode,
63
66
  };
64
67
  if (key === 'armingModes') {
65
- return armingModeControl(device, preference.armingModes, messages);
68
+ return armingModeControl(device, preference.armingModes, messages, preference.securitySystem);
66
69
  }
67
70
  const represented = preference.represented;
68
71
  const dependent = key === 'represented' ? '' : ` data-requires-representation${represented ? '' : ' hidden'}`;
@@ -260,16 +263,53 @@
260
263
  */
261
264
  const DIAGNOSABLE = new Set(['incomplete', 'missing', 'stale']);
262
265
 
266
+ /** The diagnostics area a condition's capture belongs to, by its code. A code no entry matches is `other`. */
267
+ const CONDITION_AREAS = [
268
+ [/^camera-recording/, 'hksv-recording'],
269
+ [/^camera-(?:live|media|streaming|snapshot|talkback)/, 'live-media'],
270
+ [/lock|light|arming|siren|contact|camera-control/, 'control-state'],
271
+ [/transport-degraded/, 'startup-authentication'],
272
+ ];
273
+
274
+ /**
275
+ * The problem the diagnostics action points at, or nothing: a state a run explains, then the first active condition
276
+ * this page has words for, then the first device nothing can reach. Each names the diagnostics area a run on it is
277
+ * about. A condition is worded under its log keys; the ones left unworded are those the page answers on its own,
278
+ * such as a sign-in or another owner, and the devices no action makes supported.
279
+ */
280
+ function attention(result, state, messages) {
281
+ if (DIAGNOSABLE.has(result.state)) return { ...state, profile: 'startup-authentication' };
282
+ const condition = (result.conditions ?? []).find(
283
+ ({ summaryKey, actionKey }) => messages[summaryKey] && messages[actionKey],
284
+ );
285
+ if (condition) {
286
+ return {
287
+ title: messages[condition.summaryKey],
288
+ summary: messages[condition.actionKey],
289
+ profile: CONDITION_AREAS.find(([code]) => code.test(condition.code))?.[1] ?? 'other',
290
+ };
291
+ }
292
+ const device = result.devices.find((candidate) => candidate.availability === 'unavailable');
293
+ return (
294
+ device && {
295
+ title: (messages.attentionUnreachableTitle ?? '').replace('{device}', device.name),
296
+ summary: messages.attentionUnreachableSummary ?? '',
297
+ profile: 'device-representation',
298
+ }
299
+ );
300
+ }
301
+
302
+ /** Draws the dashboard, and answers the problem it shows the diagnostics action, if any. */
263
303
  function render(result, config, messages, elements) {
264
304
  const suffix = result.state
265
305
  .split('-')
266
306
  .map((part) => part[0].toUpperCase() + part.slice(1))
267
307
  .join('');
268
- elements.title.textContent = messages[`dashboard${suffix}Title`] ?? messages.dashboardIncompleteTitle;
269
- elements.summary.textContent = messages[`dashboard${suffix}Summary`] ?? messages.dashboardIncompleteSummary;
308
+ const title = messages[`dashboard${suffix}Title`] ?? messages.dashboardIncompleteTitle;
309
+ const summary = messages[`dashboard${suffix}Summary`] ?? messages.dashboardIncompleteSummary;
310
+ elements.title.textContent = title;
311
+ elements.summary.textContent = summary;
270
312
  elements.dashboard.dataset.state = result.state;
271
- // Offered rather than announced: a run is worth suggesting only where the state is one a run explains.
272
- if (elements.diagnose) elements.diagnose.hidden = !DIAGNOSABLE.has(result.state);
273
313
  elements.dashboard.hidden = false;
274
314
  elements.masthead.hidden = true;
275
315
  renderUpdatePending(result, messages, elements);
@@ -281,6 +321,7 @@
281
321
  } else if (result.state === 'authentication-required') {
282
322
  elements.setup.hidden = false;
283
323
  }
324
+ return attention(result, { title, summary }, messages);
284
325
  }
285
326
 
286
327
  function bindPreferences(elements, getConfig, saveConfig, getMessages, openDevice) {
@@ -311,7 +352,7 @@
311
352
  const key = control?.dataset?.preference;
312
353
  const existing = getConfig();
313
354
  if (!existing || !serial || !key) return;
314
- const defaults = { represented: true, audio: true, snapshotMode: 'Refresh' };
355
+ const defaults = { represented: true, audio: true, securitySystem: true, snapshotMode: 'Refresh' };
315
356
  if (key === 'armingModes') {
316
357
  await saveArmingMode(control, serial, existing, saveConfig, getMessages, elements);
317
358
  return;
@@ -337,9 +378,16 @@
337
378
  (candidate) => candidate.dataset.serial === serial,
338
379
  );
339
380
  tile?.classList.toggle('device-tile-disabled', !value);
381
+ const grid = control.closest('.preference-grid');
382
+ const securityOff = grid?.querySelector('[data-preference="securitySystem"]')?.checked === false;
383
+ grid?.querySelectorAll('[data-requires-representation]').forEach((setting) => {
384
+ setting.hidden = !value || (securityOff && setting.hasAttribute('data-requires-security-system'));
385
+ });
386
+ }
387
+ if (key === 'securitySystem') {
340
388
  control
341
389
  .closest('.preference-grid')
342
- ?.querySelectorAll('[data-requires-representation]')
390
+ ?.querySelectorAll('[data-requires-security-system]')
343
391
  .forEach((setting) => {
344
392
  setting.hidden = !value;
345
393
  });
@@ -391,6 +439,7 @@
391
439
  const preference = {
392
440
  represented: preferences[device.serial]?.represented ?? true,
393
441
  audio: preferences[device.serial]?.audio ?? true,
442
+ securitySystem: preferences[device.serial]?.securitySystem ?? true,
394
443
  snapshotMode: preferences[device.serial]?.snapshotMode ?? 'Refresh',
395
444
  armingModes: { ...DEFAULT_ARMING_MODES, ...preferences[device.serial]?.armingModes },
396
445
  };
@@ -9,55 +9,14 @@
9
9
  'other',
10
10
  ];
11
11
 
12
- const deviceProfiles = ['device-representation', 'control-state', 'live-media', 'hksv-recording'];
13
-
14
12
  /**
15
- * Whether a fault of a given area could be about a given device, per area that can tell.
16
- *
17
- * A device the fault cannot happen on is not an answer to which ones are affected: offering a HomeBase for a
18
- * live view fault invites a report about a stream that device never had. Missing from the list means every
19
- * device qualifies, which is what a device missing from HomeKit needs — the ones with no representation at
20
- * all are exactly the subject there.
13
+ * Which screen a session puts on the panel: its file while one can be downloaded, its capture while one runs, and
14
+ * otherwise the question, since picking an area replaces the session with a capture of that area.
21
15
  */
22
- const affected = {
23
- 'control-state': (device) => device.controllable,
24
- 'live-media': (device) => device.representation.includes('camera.streaming'),
25
- 'hksv-recording': (device) => device.representation.includes('camera.streaming'),
26
- };
27
-
28
- function affectable(profile, devices) {
29
- return devices.filter(affected[profile] ?? (() => true));
30
- }
31
-
32
- function start() {
33
- return { mode: 'tiles', profile: undefined };
34
- }
35
-
36
- function select(state, profile) {
37
- return { ...state, mode: deviceProfiles.includes(profile) ? 'devices' : 'frequency', profile };
38
- }
39
-
40
- function chooseDevices(state, devices) {
41
- return { ...state, mode: 'frequency', devices };
42
- }
43
-
44
- function reject() {
45
- return start();
46
- }
47
-
48
- function chooseReproductionMode(state, reproductionMode) {
49
- return { ...state, mode: 'match', reproductionMode };
50
- }
51
-
52
- function backFromFrequency(state) {
53
- return deviceProfiles.includes(state.profile) ? { ...state, mode: 'devices' } : start();
54
- }
55
-
56
- function screen(session, startingAnother) {
57
- if (session.partialExportAvailable && !startingAnother) return 'review';
58
- if (startingAnother || session.status === 'inactive' || session.status === 'expired') return 'choose';
59
- if (session.status === 'authorized' || session.status === 'reproducing') return 'reproduce';
60
- return 'status';
16
+ function screen(session) {
17
+ if (session.partialExportAvailable) return 'review';
18
+ if (session.status === 'reproducing') return 'reproduce';
19
+ return 'choose';
61
20
  }
62
21
 
63
22
  function backgroundActive(session) {
@@ -65,16 +24,8 @@
65
24
  }
66
25
 
67
26
  global.HomebridgeEufyDiagnosticsWizard = {
68
- affectable,
69
- backFromFrequency,
70
27
  backgroundActive,
71
- chooseDevices,
72
- chooseReproductionMode,
73
- deviceProfiles,
74
28
  profiles,
75
- reject,
76
29
  screen,
77
- select,
78
- start,
79
30
  };
80
31
  })(window);
@@ -87,7 +87,7 @@
87
87
  "log.notice.stateSubscriberFailed": "A runtime state consumer failed; review Homebridge Eufy health in the dashboard.",
88
88
  "log.notice.statusPublicationFailed": "Runtime status could not be published; dashboard status may be stale.",
89
89
  "log.notice.channelServeFailed": "Live runtime status could not be served to the plugin settings page; it will show the last published status instead.",
90
- "log.notice.diagnosticsAuthorizationArmed": "Diagnostic evidence collection is now active in the plugin runtime.",
90
+ "log.notice.diagnosticsAuthorizationArmed": "A diagnostics capture started; finish it from the plugin settings before {finishBy}.",
91
91
  "log.notice.standDownNotRearmed": "Homebridge Eufy stood down for an authentication and could not take the account session back; restart Homebridge to bring it back.",
92
92
  "log.notice.standDownAccountReplaced": "Homebridge Eufy stood down for an authentication that replaced the account; restart Homebridge to use the new account.",
93
93
  "log.runtime.authenticationRequired": "Eufy authentication is required",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "Homebridge Eufy",
3
3
  "name": "@homebridge-plugins/homebridge-eufy-security",
4
- "version": "5.0.0-beta.15",
4
+ "version": "5.0.0-beta.17",
5
5
  "description": "Control Eufy Security from homebridge.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",