@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.4 → 5.0.0-beta.40

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 (121) hide show
  1. package/CONTRIBUTING.md +9 -17
  2. package/config.schema.json +60 -0
  3. package/dist/account/ownership.js +16 -16
  4. package/dist/account/ownership.js.map +1 -1
  5. package/dist/account/persistence.js +24 -74
  6. package/dist/account/persistence.js.map +1 -1
  7. package/dist/account/temporary-authentication.js +5 -3
  8. package/dist/account/temporary-authentication.js.map +1 -1
  9. package/dist/configuration.js +50 -13
  10. package/dist/configuration.js.map +1 -1
  11. package/dist/device/member-evidence.js +11 -4
  12. package/dist/device/member-evidence.js.map +1 -1
  13. package/dist/device/member-trust.js +14 -32
  14. package/dist/device/member-trust.js.map +1 -1
  15. package/dist/device/snapshot.js +6 -18
  16. package/dist/device/snapshot.js.map +1 -1
  17. package/dist/diagnostics.js +951 -334
  18. package/dist/diagnostics.js.map +1 -1
  19. package/dist/homekit/adapter.js +4 -1
  20. package/dist/homekit/adapter.js.map +1 -1
  21. package/dist/homekit/adapters/battery.js +3 -3
  22. package/dist/homekit/adapters/battery.js.map +1 -1
  23. package/dist/homekit/adapters/camera-controls.js +15 -37
  24. package/dist/homekit/adapters/camera-controls.js.map +1 -1
  25. package/dist/homekit/adapters/camera-streaming.js +74 -48
  26. package/dist/homekit/adapters/camera-streaming.js.map +1 -1
  27. package/dist/homekit/adapters/contact.js +5 -5
  28. package/dist/homekit/adapters/contact.js.map +1 -1
  29. package/dist/homekit/adapters/doorbell.js +3 -15
  30. package/dist/homekit/adapters/doorbell.js.map +1 -1
  31. package/dist/homekit/adapters/lock.js +72 -221
  32. package/dist/homekit/adapters/lock.js.map +1 -1
  33. package/dist/homekit/adapters/motion.js +3 -3
  34. package/dist/homekit/adapters/motion.js.map +1 -1
  35. package/dist/homekit/adapters/package-sensor.js +63 -0
  36. package/dist/homekit/adapters/package-sensor.js.map +1 -0
  37. package/dist/homekit/adapters/registry.js +2 -0
  38. package/dist/homekit/adapters/registry.js.map +1 -1
  39. package/dist/homekit/adapters/security-system.js +106 -250
  40. package/dist/homekit/adapters/security-system.js.map +1 -1
  41. package/dist/homekit/adapters/siren.js +5 -33
  42. package/dist/homekit/adapters/siren.js.map +1 -1
  43. package/dist/homekit/adapters/smart-light.js +19 -217
  44. package/dist/homekit/adapters/smart-light.js.map +1 -1
  45. package/dist/homekit/coverage-matrix.js +8 -67
  46. package/dist/homekit/coverage-matrix.js.map +1 -1
  47. package/dist/homekit/device-control.js +249 -13
  48. package/dist/homekit/device-control.js.map +1 -1
  49. package/dist/homekit/reconciler.js +25 -9
  50. package/dist/homekit/reconciler.js.map +1 -1
  51. package/dist/media/last-successful-image.js +8 -23
  52. package/dist/media/last-successful-image.js.map +1 -1
  53. package/dist/media/live-preview.js +217 -0
  54. package/dist/media/live-preview.js.map +1 -0
  55. package/dist/media/live-stream.js +689 -160
  56. package/dist/media/live-stream.js.map +1 -1
  57. package/dist/media/recording.js +41 -118
  58. package/dist/media/recording.js.map +1 -1
  59. package/dist/media/snapshot.js +22 -17
  60. package/dist/media/snapshot.js.map +1 -1
  61. package/dist/media/station-live-sessions.js +40 -51
  62. package/dist/media/station-live-sessions.js.map +1 -1
  63. package/dist/platform.js +61 -10
  64. package/dist/platform.js.map +1 -1
  65. package/dist/runtime/channel.js +227 -40
  66. package/dist/runtime/channel.js.map +1 -1
  67. package/dist/runtime/device-settings.js +88 -0
  68. package/dist/runtime/device-settings.js.map +1 -0
  69. package/dist/runtime/owner.js +30 -5
  70. package/dist/runtime/owner.js.map +1 -1
  71. package/dist/runtime/sdk-client.js +21 -17
  72. package/dist/runtime/sdk-client.js.map +1 -1
  73. package/dist/runtime/tracker.js +8 -38
  74. package/dist/runtime/tracker.js.map +1 -1
  75. package/dist/storage.js +1 -1
  76. package/dist/storage.js.map +1 -1
  77. package/dist/ui/dashboard.js +36 -16
  78. package/dist/ui/dashboard.js.map +1 -1
  79. package/dist/ui/runtime-channel-client.js +155 -184
  80. package/dist/ui/runtime-channel-client.js.map +1 -1
  81. package/dist/ui/server.js +81 -38
  82. package/dist/ui/server.js.map +1 -1
  83. package/homebridge-ui/public/app.css +531 -239
  84. package/homebridge-ui/public/assets/icons/auto_fix_high.svg +1 -0
  85. package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
  86. package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
  87. package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
  88. package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
  89. package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
  90. package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
  91. package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
  92. package/homebridge-ui/public/assets/icons/battery_full.svg +1 -0
  93. package/homebridge-ui/public/assets/icons/contact_support.svg +1 -0
  94. package/homebridge-ui/public/assets/icons/delete.svg +1 -0
  95. package/homebridge-ui/public/assets/icons/download.svg +1 -0
  96. package/homebridge-ui/public/assets/icons/extension_off.svg +1 -0
  97. package/homebridge-ui/public/assets/icons/info.svg +1 -0
  98. package/homebridge-ui/public/assets/icons/key_off.svg +1 -0
  99. package/homebridge-ui/public/assets/icons/keyboard_arrow_left.svg +1 -0
  100. package/homebridge-ui/public/assets/icons/keyboard_arrow_right.svg +1 -0
  101. package/homebridge-ui/public/assets/icons/keyboard_double_arrow_left.svg +1 -0
  102. package/homebridge-ui/public/assets/icons/keyboard_double_arrow_right.svg +1 -0
  103. package/homebridge-ui/public/assets/icons/movie_off.svg +1 -0
  104. package/homebridge-ui/public/assets/icons/play_arrow.svg +1 -0
  105. package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
  106. package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
  107. package/homebridge-ui/public/assets/icons/search_off.svg +1 -0
  108. package/homebridge-ui/public/assets/icons/sensors_off.svg +1 -0
  109. package/homebridge-ui/public/assets/icons/settings_photo_camera.svg +1 -0
  110. package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
  111. package/homebridge-ui/public/assets/icons/stop.svg +1 -0
  112. package/homebridge-ui/public/assets/icons/videocam_off.svg +1 -0
  113. package/homebridge-ui/public/i18n/en.json +160 -82
  114. package/homebridge-ui/public/i18n/fr.json +161 -83
  115. package/homebridge-ui/public/index.html +225 -96
  116. package/homebridge-ui/public/js/app.js +814 -435
  117. package/homebridge-ui/public/js/dashboard.js +148 -31
  118. package/i18n/runtime/en.json +10 -3
  119. package/package.json +10 -11
  120. package/homebridge-ui/public/js/legacy-settings.js +0 -37
  121. package/homebridge-ui/public/js/profile-wizard.js +0 -61
@@ -8,12 +8,65 @@
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
+ * It is withheld while the security system it maps is turned off, and the stored map is kept for when it is back.
42
+ */
43
+ function armingModeControl(device, assigned, messages, securitySystem) {
44
+ const summary = ARMING_SLOTS.map(
45
+ ({ slot, label }) => `${messages[label]} → ${EUFY_MODES.find((mode) => mode.value === assigned[slot])?.label ?? '—'}`,
46
+ ).join(' · ');
47
+ const rows = ARMING_SLOTS.map(({ slot, label }) => {
48
+ const value = assigned[slot] ?? '';
49
+ const id = `arming-${escapeHtml(device.serial)}-${slot}`;
50
+ const options = EUFY_MODES.map(
51
+ (mode) =>
52
+ `<option value="${mode.value}"${value === mode.value ? ' selected' : ''}>${escapeHtml(mode.label)}</option>`,
53
+ ).join('');
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>`;
55
+ }).join('');
56
+ const assignedItself = ARMING_SLOTS.some(({ slot }) => assigned[slot] !== DEFAULT_ARMING_MODES[slot]);
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>`;
58
+ }
59
+
11
60
  function preferenceControl(device, key, preference, messages) {
12
61
  const labels = {
13
62
  represented: messages.preferenceRepresented,
14
63
  audio: messages.preferenceAudio,
64
+ securitySystem: messages.preferenceSecuritySystem,
15
65
  snapshotMode: messages.preferenceSnapshotMode,
16
66
  };
67
+ if (key === 'armingModes') {
68
+ return armingModeControl(device, preference.armingModes, messages, preference.securitySystem);
69
+ }
17
70
  const represented = preference.represented;
18
71
  const dependent = key === 'represented' ? '' : ` data-requires-representation${represented ? '' : ' hidden'}`;
19
72
  if (key === 'snapshotMode') {
@@ -29,7 +82,7 @@
29
82
 
30
83
  function markPendingPreference(control, key, value) {
31
84
  if (!control?.classList || control.dataset.original === undefined) return;
32
- const current = key === 'snapshotMode' ? String(value) : String(Boolean(value));
85
+ const current = key === 'snapshotMode' || key === 'armingModes' ? String(value) : String(Boolean(value));
33
86
  const setting = control.closest('[data-setting]');
34
87
  const changed = current !== control.dataset.original;
35
88
  if (key === 'snapshotMode') {
@@ -43,35 +96,20 @@
43
96
  tile?.classList.toggle('device-tile-changed', Boolean(tile.querySelector('.preference-control-changed')));
44
97
  }
45
98
 
46
- /**
47
- * The eight battery icons the shipped set draws, from empty to full.
48
- *
49
- * Ordered, because the level picks one by its position: eight steps of twelve and a half percent each.
50
- */
51
- const BATTERY_STEPS = [
52
- 'battery_0',
53
- 'battery_1',
54
- 'battery_2',
55
- 'battery_3',
56
- 'battery_4',
57
- 'battery_5',
58
- 'battery_6',
59
- 'battery_full',
60
- ];
61
-
62
99
  /**
63
100
  * The battery badge a device gets, where it reports a level, and nothing where it does not.
64
101
  *
65
102
  * The icon carries the level and the exact percentage goes to the label: a tile is read at a glance, and a
66
103
  * number on every tile would be read deliberately or not at all. A device on mains power reports no level, and
67
- * neither does one nothing is observing, so both are left without a badge rather than shown an empty one.
104
+ * neither does one nothing is observing, so both are left without a badge rather than shown an empty one. The
105
+ * shipped set draws eight icons, `battery_0` to `battery_6` then `battery_full`, one per twelve and a half percent.
68
106
  */
69
107
  function batteryBadge(device, messages) {
70
108
  if (typeof device.battery !== 'number') {
71
109
  return undefined;
72
110
  }
73
- const step = Math.floor(device.battery / (100 / BATTERY_STEPS.length));
74
- const icon = BATTERY_STEPS[Math.min(BATTERY_STEPS.length - 1, Math.max(0, step))];
111
+ const step = Math.min(7, Math.max(0, Math.floor(device.battery / 12.5)));
112
+ const icon = step === 7 ? 'battery_full' : `battery_${step}`;
75
113
  const label = (messages.deviceBattery ?? '').replace('{level}', String(Math.round(device.battery)));
76
114
  return label ? { icon, label, variant: 'battery' } : undefined;
77
115
  }
@@ -204,21 +242,59 @@
204
242
  }
205
243
 
206
244
  /*
207
- * The states a diagnostic run can shed light on. Sign-in, another process holding the session, and a pending
208
- * restart each have their own remedy stated on the page, so sending those to the wizard would be a detour.
245
+ * The states a diagnostic run can shed light on. Sign-in, another process holding the session, a pending
246
+ * restart, a plugin that stopped on an error, a page that could not load, and a connection that recovers on its
247
+ * own each have their own remedy stated on the page, so sending those to the wizard would be a detour.
209
248
  */
210
- const DIAGNOSABLE = new Set(['degraded', 'incomplete', 'missing', 'stale']);
249
+ const DIAGNOSABLE = new Set(['incomplete', 'missing', 'stale']);
250
+
251
+ /** The diagnostics area a condition's capture belongs to, by its code. A code no entry matches is `other`. */
252
+ const CONDITION_AREAS = [
253
+ [/^camera-recording/, 'hksv-recording'],
254
+ [/^camera-(?:live|media|streaming|snapshot|talkback)/, 'live-media'],
255
+ [/lock|light|arming|siren|contact|camera-control/, 'control-state'],
256
+ [/transport-degraded/, 'startup-authentication'],
257
+ ];
211
258
 
259
+ /**
260
+ * The problem the diagnostics action points at, or nothing: a state a run explains, then the first active condition
261
+ * this page has words for, then the first device nothing can reach. Each names the diagnostics area a run on it is
262
+ * about. A condition is worded under its log keys; the ones left unworded are those the page answers on its own,
263
+ * such as a sign-in or another owner, and the devices no action makes supported.
264
+ */
265
+ function attention(result, state, messages) {
266
+ if (DIAGNOSABLE.has(result.state)) return { ...state, profile: 'startup-authentication' };
267
+ const condition = (result.conditions ?? []).find(
268
+ ({ summaryKey, actionKey }) => messages[summaryKey] && messages[actionKey],
269
+ );
270
+ if (condition) {
271
+ return {
272
+ title: messages[condition.summaryKey],
273
+ summary: messages[condition.actionKey],
274
+ profile: CONDITION_AREAS.find(([code]) => code.test(condition.code))?.[1] ?? 'other',
275
+ };
276
+ }
277
+ const device = result.devices.find((candidate) => candidate.availability === 'unavailable');
278
+ return (
279
+ device && {
280
+ title: (messages.attentionUnreachableTitle ?? '').replace('{device}', device.name),
281
+ summary: messages.attentionUnreachableSummary ?? '',
282
+ profile: 'device-representation',
283
+ }
284
+ );
285
+ }
286
+
287
+ /** Draws the dashboard, and answers the problem it shows the diagnostics action, if any. */
212
288
  function render(result, config, messages, elements) {
213
289
  const suffix = result.state
214
290
  .split('-')
215
291
  .map((part) => part[0].toUpperCase() + part.slice(1))
216
292
  .join('');
217
- elements.title.textContent = messages[`dashboard${suffix}Title`] ?? messages.dashboardIncompleteTitle;
218
- elements.summary.textContent = messages[`dashboard${suffix}Summary`] ?? messages.dashboardIncompleteSummary;
293
+ const title = messages[`dashboard${suffix}Title`] ?? messages.dashboardIncompleteTitle;
294
+ const summary = messages[`dashboard${suffix}Summary`] ?? messages.dashboardIncompleteSummary;
295
+ elements.title.textContent = title;
296
+ elements.summary.textContent = summary;
219
297
  elements.dashboard.dataset.state = result.state;
220
- // Offered rather than announced: a run is worth suggesting only where the state is one a run explains.
221
- if (elements.diagnose) elements.diagnose.hidden = !DIAGNOSABLE.has(result.state);
222
298
  elements.dashboard.hidden = false;
223
299
  elements.masthead.hidden = true;
224
300
  renderUpdatePending(result, messages, elements);
@@ -230,6 +306,7 @@
230
306
  } else if (result.state === 'authentication-required') {
231
307
  elements.setup.hidden = false;
232
308
  }
309
+ return attention(result, { title, summary }, messages);
233
310
  }
234
311
 
235
312
  function bindPreferences(elements, getConfig, saveConfig, getMessages, openDevice) {
@@ -254,13 +331,17 @@
254
331
  },
255
332
  true,
256
333
  );
257
- (elements.deviceSettingsControls ?? elements.groups).addEventListener('change', async (event) => {
334
+ const onPreferenceChange = async (event) => {
258
335
  const control = event.target;
259
336
  const serial = control?.dataset?.serial;
260
337
  const key = control?.dataset?.preference;
261
338
  const existing = getConfig();
262
339
  if (!existing || !serial || !key) return;
263
- const defaults = { represented: true, audio: true, snapshotMode: 'Refresh' };
340
+ const defaults = { represented: true, audio: true, securitySystem: true, snapshotMode: 'Refresh' };
341
+ if (key === 'armingModes') {
342
+ await saveArmingMode(control, serial, existing, saveConfig, getMessages, elements);
343
+ return;
344
+ }
264
345
  const value = key === 'snapshotMode' ? control.value : control.checked;
265
346
  markPendingPreference(control, key, value);
266
347
  if (key === 'snapshotMode') {
@@ -282,9 +363,16 @@
282
363
  (candidate) => candidate.dataset.serial === serial,
283
364
  );
284
365
  tile?.classList.toggle('device-tile-disabled', !value);
366
+ const grid = control.closest('.preference-grid');
367
+ const securityOff = grid?.querySelector('[data-preference="securitySystem"]')?.checked === false;
368
+ grid?.querySelectorAll('[data-requires-representation]').forEach((setting) => {
369
+ setting.hidden = !value || (securityOff && setting.hasAttribute('data-requires-security-system'));
370
+ });
371
+ }
372
+ if (key === 'securitySystem') {
285
373
  control
286
374
  .closest('.preference-grid')
287
- ?.querySelectorAll('[data-requires-representation]')
375
+ ?.querySelectorAll('[data-requires-security-system]')
288
376
  .forEach((setting) => {
289
377
  setting.hidden = !value;
290
378
  });
@@ -292,7 +380,34 @@
292
380
  } catch {
293
381
  elements.summary.textContent = getMessages().preferenceSaveFailed;
294
382
  }
295
- });
383
+ };
384
+ for (const container of elements.preferenceContainers ?? [elements.deviceSettingsControls ?? elements.groups]) {
385
+ container.addEventListener('change', onPreferenceChange);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Records one HomeKit state's guard mode, keeping the map sparse: a state set back to the plugin's own choice
391
+ * is removed rather than pinned, so a later default reaches a household that never overrode it.
392
+ */
393
+ async function saveArmingMode(control, serial, existing, saveConfig, getMessages, elements) {
394
+ const slot = control.dataset.slot;
395
+ const value = control.value;
396
+ markPendingPreference(control, 'armingModes', value);
397
+ const entityPreferences = { ...(existing.entityPreferences ?? {}) };
398
+ const preference = { ...(entityPreferences[serial] ?? {}) };
399
+ const armingModes = { ...(preference.armingModes ?? {}) };
400
+ if (value === '') delete armingModes[slot];
401
+ else armingModes[slot] = value;
402
+ if (Object.keys(armingModes).length === 0) delete preference.armingModes;
403
+ else preference.armingModes = armingModes;
404
+ if (Object.keys(preference).length === 0) delete entityPreferences[serial];
405
+ else entityPreferences[serial] = preference;
406
+ try {
407
+ await saveConfig({ ...existing, entityPreferences });
408
+ } catch {
409
+ elements.summary.textContent = getMessages().preferenceSaveFailed;
410
+ }
296
411
  }
297
412
 
298
413
  /**
@@ -312,7 +427,9 @@
312
427
  const preference = {
313
428
  represented: preferences[device.serial]?.represented ?? true,
314
429
  audio: preferences[device.serial]?.audio ?? true,
430
+ securitySystem: preferences[device.serial]?.securitySystem ?? true,
315
431
  snapshotMode: preferences[device.serial]?.snapshotMode ?? 'Refresh',
432
+ armingModes: { ...DEFAULT_ARMING_MODES, ...preferences[device.serial]?.armingModes },
316
433
  };
317
434
  const controls = device.preferences
318
435
  .map((key) => preferenceControl(device, key, preference, messages))
@@ -4,22 +4,28 @@
4
4
  "log.action.checkPhysicalArmingMode": "Check the physical security-system mode before retrying",
5
5
  "log.action.checkPhysicalLight": "Check the physical smart-light state before retrying",
6
6
  "log.action.checkPhysicalLock": "Check the physical lock state before retrying",
7
+ "log.action.controlTimedOut": "The device did not answer in time; use the Eufy app for this control, and attach a diagnostics file to your issue if it keeps happening",
7
8
  "log.action.checkCameraSnapshot": "Check that the camera is reachable and can produce an image, then open the camera again",
8
9
  "log.action.waitCameraControl": "No action is needed; the control returns when the camera reports it again",
9
10
  "log.action.checkCameraControl": "Check the camera in the Eufy app; this control is unavailable until it reports its state",
10
11
  "log.action.retryCameraControl": "Try the control again; the camera did not confirm the change",
11
12
  "log.action.enableCamera": "Turn the camera on in the Eufy app, then open live view again",
12
- "log.action.openDashboard": "Review device representation in the Homebridge Eufy dashboard",
13
+ "log.action.notSupportedYet": "No action is needed; these devices are not supported in HomeKit yet",
13
14
  "log.action.reduceConcurrentMedia": "Open live view again; if it keeps happening, close a camera already open or raise the concurrent camera media limit in the plugin settings",
14
15
  "log.action.reauthenticate": "Authenticate again in the Homebridge Eufy dashboard, then restart Homebridge",
15
16
  "log.action.retryArming": "Retry the security-system control manually",
16
17
  "log.action.retryLight": "Retry the smart-light control manually",
17
18
  "log.action.retryLiveView": "Open live view again, and check network access between Homebridge and the camera",
19
+ "log.action.setFfmpegPath": "Set a custom FFmpeg path in Advanced settings in the Homebridge Eufy dashboard",
20
+ "log.action.checkFfmpegPath": "FFmpeg could not be started; check the custom FFmpeg path in Advanced settings in the Homebridge Eufy dashboard",
21
+ "log.action.checkStation": "Check that the station or HomeBase this camera uses is powered and online, and network access between Homebridge and it, then open live view again",
22
+ "log.action.retryLiveViewNoPicture": "The camera answered but sent no picture in time. Open live view again; if it keeps happening, send a diagnostics file",
18
23
  "log.action.retryLock": "Retry the lock control manually",
19
24
  "log.action.retryTalkback": "Close and reopen the camera, then try speaking again",
25
+ "log.action.trySmallVideoPackets": "If the live view never shows in the Home app, turn on Smaller live video packets in Advanced settings in the Homebridge Eufy dashboard",
20
26
  "log.action.reviewRuntime": "Review Homebridge Eufy health in the dashboard and restart Homebridge",
21
27
  "log.action.stopOtherOwner": "Stop the other Homebridge Eufy owner before restarting Homebridge",
22
- "log.action.selectSupportedArmingMode": "Select Stay, Away, or Disarm to leave the unsupported mode",
28
+ "log.action.mapArmingMode": "Assign this mode to a HomeKit state under HomeKit guard modes in the device's settings in the Homebridge Eufy dashboard",
23
29
  "log.action.waitBattery": "Wait for complete battery capability evidence",
24
30
  "log.action.waitArming": "Wait for complete arming capability evidence",
25
31
  "log.action.waitBatteryObservation": "Wait for a valid battery observation",
@@ -39,6 +45,7 @@
39
45
  "log.condition.affectedOne": "1 accessory is affected.",
40
46
  "log.condition.recovered": "Recovered: {summary}.",
41
47
  "log.diagnosticFileFailed": "Diagnostic file logging failed; check storage permissions.",
48
+ "log.liveVideoEncoders": "Live video encoders, in order of use: {encoders}.",
42
49
  "log.discardedSettings": "Some legacy settings were discarded. Review and acknowledge them in the Homebridge Eufy dashboard.",
43
50
  "log.snapshotCacheInvalid": "An invalid retained camera image was removed",
44
51
  "log.homekit.batteryCapabilityUnavailable": "Battery information is unavailable",
@@ -85,7 +92,7 @@
85
92
  "log.notice.stateSubscriberFailed": "A runtime state consumer failed; review Homebridge Eufy health in the dashboard.",
86
93
  "log.notice.statusPublicationFailed": "Runtime status could not be published; dashboard status may be stale.",
87
94
  "log.notice.channelServeFailed": "Live runtime status could not be served to the plugin settings page; it will show the last published status instead.",
88
- "log.notice.diagnosticsAuthorizationArmed": "Diagnostic evidence collection is now active in the plugin runtime.",
95
+ "log.notice.diagnosticsAuthorizationArmed": "A diagnostics capture started; finish it from the plugin settings before {finishBy}.",
89
96
  "log.notice.standDownNotRearmed": "Homebridge Eufy stood down for an authentication and could not take the account session back; restart Homebridge to bring it back.",
90
97
  "log.notice.standDownAccountReplaced": "Homebridge Eufy stood down for an authentication that replaced the account; restart Homebridge to use the new account.",
91
98
  "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.4",
4
+ "version": "5.0.0-beta.40",
5
5
  "description": "Control Eufy Security from homebridge.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -26,12 +26,9 @@
26
26
  "scripts": {
27
27
  "lint": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
28
28
  "lint-fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
29
- "build": "npm run build-plugin",
30
- "build-plugin": "rimraf ./dist && tsc",
29
+ "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc",
31
30
  "guard:no-ecs": "bash scripts/guard-no-ecs.sh",
32
31
  "qualify:release": "node scripts/qualify-release.mjs",
33
- "docs:dev": "npm -C docs run dev",
34
- "docs:build": "npm -C docs run build",
35
32
  "test": "vitest run",
36
33
  "verify": "npm run lint && npm run guard:no-ecs && npm run build && npm test",
37
34
  "prepublishOnly": "npm run build && npm run qualify:release && npm run verify"
@@ -47,21 +44,23 @@
47
44
  ],
48
45
  "dependencies": {
49
46
  "@homebridge/plugin-ui-utils": "^2.2.6",
50
- "@mega-yfue/eufy-sdk": "0.1.0",
47
+ "@mega-yfue/eufy-sdk": "0.6.0-beta.26",
51
48
  "ffmpeg-for-homebridge": "2.2.2"
52
49
  },
53
50
  "devDependencies": {
54
- "@homebridge/hap-nodejs": "2.2.2",
55
- "@types/node": "^26.4.1",
51
+ "@homebridge/hap-nodejs": "2.2.3",
52
+ "@types/node": "^26.6.5",
56
53
  "homebridge": "^2.4.0",
57
- "prettier": "^3.9.6",
58
- "rimraf": "^6.1.3",
54
+ "prettier": "^3.9.10",
59
55
  "typescript": "^7.0.2",
60
- "vitest": "^5.0.0"
56
+ "vitest": "^5.0.5"
61
57
  },
62
58
  "homepage": "https://homebridge-plugins.github.io/homebridge-eufy/",
63
59
  "author": "homebridge-eufy",
64
60
  "allowScripts": {
65
61
  "ffmpeg-for-homebridge@2.2.2": true
62
+ },
63
+ "optionalDependencies": {
64
+ "node-datachannel": "0.33.5"
66
65
  }
67
66
  }
@@ -1,37 +0,0 @@
1
- (function legacySettingsModule(global) {
2
- const keys = [
3
- 'CameraMaxLivestreamDuration',
4
- 'autoSyncStation',
5
- 'cameras',
6
- 'cleanCache',
7
- 'debugLivestream',
8
- 'enableDetailedLogging',
9
- 'enableEmbeddedPKCS1Support',
10
- 'experimentalMode',
11
- 'hkAway',
12
- 'hkHome',
13
- 'hkNight',
14
- 'hkOff',
15
- 'ignoreDevices',
16
- 'ignoreMultipleDevicesWarning',
17
- 'ignoreStations',
18
- 'omitLogFiles',
19
- 'stations',
20
- 'syncStationModes',
21
- 'useEmbeddedPKCS1Support',
22
- ];
23
-
24
- function effectful(key, value) {
25
- const defaults = { CameraMaxLivestreamDuration: 30, hkHome: 1, hkAway: 0, hkNight: 3, hkOff: 63 };
26
- if (Object.hasOwn(defaults, key)) return value !== defaults[key];
27
- if (Array.isArray(value)) return value.length > 0;
28
- if (value && typeof value === 'object') return Object.keys(value).length > 0;
29
- return Boolean(value);
30
- }
31
-
32
- function names(block) {
33
- return keys.filter((key) => Object.hasOwn(block, key) && effectful(key, block[key])).sort();
34
- }
35
-
36
- global.HomebridgeEufyLegacySettings = { names };
37
- })(window);
@@ -1,61 +0,0 @@
1
- (function attachDiagnosticsWizard(global) {
2
- const profiles = [
3
- 'startup-authentication',
4
- 'device-representation',
5
- 'control-state',
6
- 'live-media',
7
- 'hksv-recording',
8
- 'dashboard-ui',
9
- 'other',
10
- ];
11
-
12
- const deviceProfiles = ['device-representation', 'control-state', 'live-media', 'hksv-recording'];
13
-
14
- function start() {
15
- return { mode: 'tiles', profile: undefined };
16
- }
17
-
18
- function select(state, profile) {
19
- return { ...state, mode: deviceProfiles.includes(profile) ? 'devices' : 'frequency', profile };
20
- }
21
-
22
- function chooseDevices(state, devices) {
23
- return { ...state, mode: 'frequency', devices };
24
- }
25
-
26
- function reject() {
27
- return start();
28
- }
29
-
30
- function chooseReproductionMode(state, reproductionMode) {
31
- return { ...state, mode: 'match', reproductionMode };
32
- }
33
-
34
- function backFromFrequency(state) {
35
- return deviceProfiles.includes(state.profile) ? { ...state, mode: 'devices' } : start();
36
- }
37
-
38
- function screen(session, startingAnother) {
39
- if (session.partialExportAvailable && !startingAnother) return 'review';
40
- if (startingAnother || session.status === 'inactive' || session.status === 'expired') return 'choose';
41
- if (session.status === 'authorized' || session.status === 'reproducing') return 'reproduce';
42
- return 'status';
43
- }
44
-
45
- function backgroundActive(session) {
46
- return session.status === 'reproducing' && session.profile === 'dashboard-ui';
47
- }
48
-
49
- global.HomebridgeEufyDiagnosticsWizard = {
50
- backFromFrequency,
51
- backgroundActive,
52
- chooseDevices,
53
- chooseReproductionMode,
54
- deviceProfiles,
55
- profiles,
56
- reject,
57
- screen,
58
- select,
59
- start,
60
- };
61
- })(window);