@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.32 → 5.0.0-beta.34

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 (35) hide show
  1. package/config.schema.json +6 -0
  2. package/dist/configuration.js +2 -0
  3. package/dist/configuration.js.map +1 -1
  4. package/dist/diagnostics.js +86 -2
  5. package/dist/diagnostics.js.map +1 -1
  6. package/dist/homekit/adapters/lock.js +21 -0
  7. package/dist/homekit/adapters/lock.js.map +1 -1
  8. package/dist/media/live-preview.js +96 -0
  9. package/dist/media/live-preview.js.map +1 -0
  10. package/dist/media/live-stream.js +310 -32
  11. package/dist/media/live-stream.js.map +1 -1
  12. package/dist/platform.js +33 -2
  13. package/dist/platform.js.map +1 -1
  14. package/dist/runtime/channel.js +54 -1
  15. package/dist/runtime/channel.js.map +1 -1
  16. package/dist/runtime/owner.js +14 -1
  17. package/dist/runtime/owner.js.map +1 -1
  18. package/dist/ui/dashboard.js +4 -2
  19. package/dist/ui/dashboard.js.map +1 -1
  20. package/dist/ui/runtime-channel-client.js +46 -1
  21. package/dist/ui/runtime-channel-client.js.map +1 -1
  22. package/dist/ui/server.js +20 -0
  23. package/dist/ui/server.js.map +1 -1
  24. package/homebridge-ui/public/app.css +181 -0
  25. package/homebridge-ui/public/assets/icons/auto_fix_high.svg +1 -0
  26. package/homebridge-ui/public/assets/icons/play_arrow.svg +1 -0
  27. package/homebridge-ui/public/assets/icons/stop.svg +1 -0
  28. package/homebridge-ui/public/i18n/en.json +29 -1
  29. package/homebridge-ui/public/i18n/fr.json +29 -1
  30. package/homebridge-ui/public/index.html +63 -0
  31. package/homebridge-ui/public/js/app.js +318 -3
  32. package/homebridge-ui/public/js/dashboard.js +5 -2
  33. package/homebridge-ui/public/js/setup-wizard.js +52 -0
  34. package/i18n/runtime/en.json +1 -0
  35. package/package.json +1 -1
@@ -11,6 +11,8 @@
11
11
  "advancedFfmpegLabel": "Chemin FFmpeg personnalisé",
12
12
  "advancedLiveVideoPassthroughHelp": "Activez cette option si la vidéo en direct ne s’affiche jamais, ou si le journal indique que FFmpeg s’est arrêté avant d’envoyer la vidéo. La vidéo des caméras qui enregistrent en H.264 est alors envoyée telle que la caméra l’a enregistrée, comme dans la version 4, au lieu d’être convertie selon ce que demande l’app Maison.",
13
13
  "advancedLiveVideoPassthroughLabel": "Envoyer la vidéo de la caméra sans conversion",
14
+ "advancedHardwareVideoEncoderHelp": "Utiliser le matériel vidéo de l’ordinateur : désactivez cette option si la vidéo en direct ne fonctionne plus depuis une mise à jour ou s’affiche mal.",
15
+ "advancedHardwareVideoEncoderLabel": "Encodage vidéo matériel",
14
16
  "advancedHdLiveVideoHelp": "Activez cette option si la vidéo en direct paraît floue dans l’app Maison. Les caméras ne proposent alors à l’app Maison que du 720p et du 1080p, aussi en données mobiles et dans la grille des caméras, ce qui consomme plus de données.",
15
17
  "advancedHdLiveVideoLabel": "Vidéo en direct toujours en HD",
16
18
  "advancedIgnoreHomeBitRateHelp": "Activez cette option si la vidéo en direct est nette quelques secondes, puis floue ou pixelisée. La vidéo est alors convertie à une qualité constante proche de celle de la caméra, au lieu du faible débit demandé par l’app Maison, ce qui utilise plus de bande passante.",
@@ -255,5 +257,31 @@
255
257
  "updatePendingManual": "Cette page montre ce que sait faire la version en cours. Redémarrez Homebridge pour utiliser la version installée.",
256
258
  "updatePendingSummary": "Cette page montre ce que sait faire la version en cours. Redémarrez pour utiliser la version installée.",
257
259
  "updatePendingTitle": "Une mise à jour attend",
258
- "updatePendingVersionLabel": "Version en cours"
260
+ "updatePendingVersionLabel": "Version en cours",
261
+ "menuWizard": "Assistant de configuration",
262
+ "closeWizard": "Retour aux appareils",
263
+ "wizardTitle": "Assistant de configuration",
264
+ "wizardProgress": "Appareil {current} sur {total}",
265
+ "wizardRestartSummary": "Redémarrez Homebridge pour connecter vos caméras. Cet assistant s’ouvrira de lui-même une fois le plugin lancé, pour passer chaque appareil en revue avec vous.",
266
+ "wizardBack": "Retour",
267
+ "wizardNext": "Suivant",
268
+ "wizardFinish": "Terminer",
269
+ "wizardClose": "Fermer",
270
+ "wizardLiveStarting": "Connexion à la caméra. Cela peut prendre jusqu’à 30 secondes.",
271
+ "wizardLiveFailed": "La caméra n’a pas envoyé de vidéo. Vérifiez qu’elle est allumée et en ligne, puis réessayez.",
272
+ "wizardEufySettings": "Réglages de la caméra dans l’app eufy",
273
+ "wizardMotionDetection": "Détection de mouvement",
274
+ "wizardNightVision": "Vision nocturne",
275
+ "wizardNotificationStyle": "Style de notification",
276
+ "wizardSettingSending": "Envoi à la caméra…",
277
+ "wizardSettingSent": "Envoyé. La caméra le confirme d’ici quelques minutes.",
278
+ "wizardSettingFailed": "La caméra n’a pas pris le changement. Réessayez.",
279
+ "wizardLivePlay": "Lancer la vidéo en direct",
280
+ "wizardLiveStop": "Arrêter la vidéo en direct",
281
+ "wizardHomeKitTitle": "Dans l’app Maison",
282
+ "wizardHomeKitHelp": "Ce que l’app Maison montre de cet appareil et comment il s’y comporte. Enregistré avec les réglages du plugin.",
283
+ "wizardEufyHelp": "Envoyé à la caméra elle-même, les mêmes réglages que dans l’app eufy.",
284
+ "wizardMapLabel": "Vos appareils",
285
+ "wizardMapBase": "HomeBase",
286
+ "wizardMapStandalone": "Autonomes"
259
287
  }
@@ -72,6 +72,9 @@
72
72
  <span class="diagnostics-name" data-i18n="menuDiagnostics"></span>
73
73
  <span class="diagnostics-capturing" data-i18n="diagnosticsCapturing"></span>
74
74
  </button>
75
+ <button type="button" class="dashboard-action" data-menu-wizard data-i18n-aria-label="menuWizard">
76
+ <img src="assets/icons/auto_fix_high.svg" alt="" />
77
+ </button>
75
78
  <button type="button" class="dashboard-action" data-menu-advanced data-i18n-aria-label="menuAdvanced">
76
79
  <img src="assets/icons/settings.svg" alt="" />
77
80
  </button>
@@ -176,6 +179,11 @@
176
179
  <input type="checkbox" data-advanced-live-video-passthrough />
177
180
  <small data-i18n="advancedLiveVideoPassthroughHelp"></small>
178
181
  </label>
182
+ <label>
183
+ <span data-i18n="advancedHardwareVideoEncoderLabel"></span>
184
+ <input type="checkbox" data-advanced-hardware-video-encoder />
185
+ <small data-i18n="advancedHardwareVideoEncoderHelp"></small>
186
+ </label>
179
187
  <label>
180
188
  <span data-i18n="advancedHdLiveVideoLabel"></span>
181
189
  <input type="checkbox" data-advanced-hd-live-video />
@@ -190,6 +198,60 @@
190
198
  <p class="advanced-device-help" data-i18n="advancedDeviceHelp"></p>
191
199
  <p class="advanced-status" role="status" aria-live="polite" data-advanced-status></p>
192
200
  </section>
201
+
202
+ <section class="dashboard-panel wizard-panel" data-setup-wizard hidden aria-labelledby="wizard-title">
203
+ <div class="dashboard-panel-header">
204
+ <button type="button" class="dashboard-page-back" data-wizard-close data-i18n-aria-label="closeWizard">
205
+ <span aria-hidden="true">←</span>
206
+ </button>
207
+ <img class="dashboard-page-icon" src="assets/icons/auto_fix_high.svg" alt="" />
208
+ <div>
209
+ <p class="eyebrow" data-wizard-progress></p>
210
+ <h2 id="wizard-title" data-wizard-title></h2>
211
+ <p class="summary" data-wizard-eyebrow></p>
212
+ </div>
213
+ </div>
214
+ <p class="summary" data-wizard-restart hidden data-i18n="wizardRestartSummary"></p>
215
+ <nav class="wizard-map" data-wizard-map data-i18n-aria-label="wizardMapLabel"></nav>
216
+ <div class="wizard-stage" data-wizard-stage>
217
+ <button type="button" class="wizard-arrow" data-wizard-back data-i18n-aria-label="wizardBack">
218
+ <img src="assets/icons/keyboard_arrow_left.svg" alt="" />
219
+ </button>
220
+ <div class="wizard-live" data-wizard-live>
221
+ <img data-wizard-live-image alt="" hidden />
222
+ <button type="button" class="wizard-live-play" data-wizard-live-play data-i18n-aria-label="wizardLivePlay">
223
+ <img src="assets/icons/play_arrow.svg" alt="" />
224
+ </button>
225
+ <button
226
+ type="button"
227
+ class="wizard-live-play wizard-live-stop"
228
+ data-wizard-live-stop
229
+ hidden
230
+ data-i18n-aria-label="wizardLiveStop"
231
+ >
232
+ <img src="assets/icons/stop.svg" alt="" />
233
+ </button>
234
+ <span class="wizard-live-spinner" data-wizard-live-spinner hidden aria-hidden="true"></span>
235
+ <p class="wizard-live-status" role="status" aria-live="polite" data-wizard-live-status></p>
236
+ </div>
237
+ <button type="button" class="wizard-arrow" data-wizard-next data-i18n-aria-label="wizardNext">
238
+ <img src="assets/icons/keyboard_arrow_right.svg" alt="" />
239
+ </button>
240
+ </div>
241
+ <section class="wizard-card" data-wizard-homekit aria-labelledby="wizard-homekit-title">
242
+ <h3 id="wizard-homekit-title" data-i18n="wizardHomeKitTitle"></h3>
243
+ <p class="preference-note" data-i18n="wizardHomeKitHelp"></p>
244
+ <div class="preference-grid" data-wizard-controls></div>
245
+ </section>
246
+ <section class="wizard-card" data-wizard-eufy hidden aria-labelledby="wizard-eufy-title">
247
+ <h3 id="wizard-eufy-title" data-i18n="wizardEufySettings"></h3>
248
+ <p class="preference-note" data-i18n="wizardEufyHelp"></p>
249
+ <div class="preference-grid" data-wizard-settings></div>
250
+ </section>
251
+ <div class="wizard-nav">
252
+ <button type="button" data-wizard-finish></button>
253
+ </div>
254
+ </section>
193
255
  </section>
194
256
 
195
257
  <section class="dashboard-panel diagnostics-panel" data-diagnostics hidden aria-labelledby="diagnostics-title">
@@ -383,4 +445,5 @@
383
445
  <script src="js/dashboard.js"></script>
384
446
  <script src="js/legacy-settings.js"></script>
385
447
  <script src="js/profile-wizard.js"></script>
448
+ <script src="js/setup-wizard.js"></script>
386
449
  <script src="js/app.js"></script>
@@ -77,6 +77,7 @@ const advancedConcurrentMedia = document.querySelector('[data-advanced-concurren
77
77
  const advancedFfmpeg = document.querySelector('[data-advanced-ffmpeg]');
78
78
  const advancedSmallVideoPackets = document.querySelector('[data-advanced-small-video-packets]');
79
79
  const advancedLiveVideoPassthrough = document.querySelector('[data-advanced-live-video-passthrough]');
80
+ const advancedHardwareVideoEncoder = document.querySelector('[data-advanced-hardware-video-encoder]');
80
81
  const advancedHdLiveVideo = document.querySelector('[data-advanced-hd-live-video]');
81
82
  const advancedIgnoreHomeBitRate = document.querySelector('[data-advanced-ignore-home-bit-rate]');
82
83
  const warmUpAvailable = document.querySelector('[data-warm-up-available]');
@@ -86,6 +87,28 @@ const warmUpAddAll = document.querySelector('[data-warm-up-add-all]');
86
87
  const warmUpRemove = document.querySelector('[data-warm-up-remove]');
87
88
  const warmUpRemoveAll = document.querySelector('[data-warm-up-remove-all]');
88
89
  const advancedStatus = document.querySelector('[data-advanced-status]');
90
+ const menuWizard = document.querySelector('[data-menu-wizard]');
91
+ const wizardPanel = document.querySelector('[data-setup-wizard]');
92
+ const wizardClose = document.querySelector('[data-wizard-close]');
93
+ const wizardProgress = document.querySelector('[data-wizard-progress]');
94
+ const wizardTitle = document.querySelector('[data-wizard-title]');
95
+ const wizardEyebrow = document.querySelector('[data-wizard-eyebrow]');
96
+ const wizardRestart = document.querySelector('[data-wizard-restart]');
97
+ const wizardLive = document.querySelector('[data-wizard-live]');
98
+ const wizardLiveImage = document.querySelector('[data-wizard-live-image]');
99
+ const wizardLiveStatus = document.querySelector('[data-wizard-live-status]');
100
+ const wizardLivePlay = document.querySelector('[data-wizard-live-play]');
101
+ const wizardLiveStop = document.querySelector('[data-wizard-live-stop]');
102
+ const wizardLiveSpinner = document.querySelector('[data-wizard-live-spinner]');
103
+ const wizardSettings = document.querySelector('[data-wizard-settings]');
104
+ const wizardControls = document.querySelector('[data-wizard-controls]');
105
+ const wizardBack = document.querySelector('[data-wizard-back]');
106
+ const wizardNext = document.querySelector('[data-wizard-next]');
107
+ const wizardFinish = document.querySelector('[data-wizard-finish]');
108
+ const wizardMap = document.querySelector('[data-wizard-map]');
109
+ const wizardStage = document.querySelector('[data-wizard-stage]');
110
+ const wizardHomeKit = document.querySelector('[data-wizard-homekit]');
111
+ const wizardEufy = document.querySelector('[data-wizard-eufy]');
89
112
 
90
113
  let messages = {};
91
114
  let pluginConfig = [];
@@ -109,6 +132,7 @@ let dashboardPanelTrigger;
109
132
  const dashboardView = window.HomebridgeEufyDashboard;
110
133
  const legacySettingsView = window.HomebridgeEufyLegacySettings;
111
134
  const diagnosticsWizard = window.HomebridgeEufyDiagnosticsWizard;
135
+ const setupWizard = window.HomebridgeEufySetupWizard;
112
136
  const dashboardElements = {
113
137
  dashboard,
114
138
  title: dashboardTitle,
@@ -118,6 +142,7 @@ const dashboardElements = {
118
142
  deviceSettingsTitle,
119
143
  deviceSettingsEyebrow,
120
144
  deviceSettingsControls,
145
+ preferenceContainers: [deviceSettingsControls, wizardControls],
121
146
  updatePending,
122
147
  updatePendingSummary,
123
148
  updatePendingVersion,
@@ -512,7 +537,7 @@ function openDashboardPanel(panel, trigger) {
512
537
  dashboardState.hidden = insideDashboard;
513
538
  dashboardSummary.hidden = insideDashboard;
514
539
  deviceGroups.hidden = insideDashboard;
515
- for (const other of [diagnosticsPanel, advancedPanel, devicePanel]) {
540
+ for (const other of [diagnosticsPanel, advancedPanel, devicePanel, wizardPanel]) {
516
541
  other.hidden = other !== panel;
517
542
  }
518
543
  panel.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
@@ -524,6 +549,8 @@ function closeDashboardPanel() {
524
549
  diagnosticsPanel.hidden = true;
525
550
  advancedPanel.hidden = true;
526
551
  devicePanel.hidden = true;
552
+ wizardPanel.hidden = true;
553
+ stopWizardLive();
527
554
  dashboardState.hidden = false;
528
555
  dashboardSummary.hidden = false;
529
556
  deviceGroups.hidden = false;
@@ -592,6 +619,7 @@ menuAdvanced.addEventListener('click', async () => {
592
619
  advancedFfmpeg.value = config.ffmpegPath ?? '';
593
620
  advancedSmallVideoPackets.checked = config.smallVideoPackets === true;
594
621
  advancedLiveVideoPassthrough.checked = config.liveVideoPassthrough === true;
622
+ advancedHardwareVideoEncoder.checked = config.hardwareVideoEncoder !== false;
595
623
  advancedHdLiveVideo.checked = config.hdLiveVideo === true;
596
624
  advancedIgnoreHomeBitRate.checked = config.ignoreHomeBitRate === true;
597
625
  openDashboardPanel(advancedPanel, menuAdvanced);
@@ -653,6 +681,8 @@ async function updateAdvancedSettings() {
653
681
  else delete next.smallVideoPackets;
654
682
  if (advancedLiveVideoPassthrough.checked) next.liveVideoPassthrough = true;
655
683
  else delete next.liveVideoPassthrough;
684
+ if (advancedHardwareVideoEncoder.checked) delete next.hardwareVideoEncoder;
685
+ else next.hardwareVideoEncoder = false;
656
686
  if (advancedHdLiveVideo.checked) next.hdLiveVideo = true;
657
687
  else delete next.hdLiveVideo;
658
688
  if (advancedIgnoreHomeBitRate.checked) next.ignoreHomeBitRate = true;
@@ -674,6 +704,7 @@ advancedConcurrentMedia.addEventListener('change', updateAdvancedSettings);
674
704
  advancedFfmpeg.addEventListener('change', updateAdvancedSettings);
675
705
  advancedSmallVideoPackets.addEventListener('change', updateAdvancedSettings);
676
706
  advancedLiveVideoPassthrough.addEventListener('change', updateAdvancedSettings);
707
+ advancedHardwareVideoEncoder.addEventListener('change', updateAdvancedSettings);
677
708
  advancedHdLiveVideo.addEventListener('change', updateAdvancedSettings);
678
709
  advancedIgnoreHomeBitRate.addEventListener('change', updateAdvancedSettings);
679
710
  warmUpAdd.addEventListener('click', () => moveWarmUp('available'));
@@ -810,9 +841,288 @@ dashboardView.bindPreferences(dashboardElements, configuredBlock, updateConfig,
810
841
  openDashboardPanel(devicePanel, trigger);
811
842
  });
812
843
 
844
+ /*
845
+ * The setup wizard walks every device once a sign-in has a running plugin to show it: each camera live, the eufy
846
+ * settings the plugin can change, and the HomeKit choices. A sign-in always leaves Homebridge to restart, so it
847
+ * marks the wizard pending in this browser and the next page opened on a ready plugin starts it.
848
+ */
849
+ let dashboardPluginState;
850
+ let wizardSteps = [];
851
+ let wizardIndex = 0;
852
+ let wizardLiveSerial;
853
+ let wizardLivePictures = 0;
854
+ const wizardElements = {
855
+ deviceSettingsTitle: wizardTitle,
856
+ deviceSettingsEyebrow: wizardEyebrow,
857
+ deviceSettingsControls: wizardControls,
858
+ };
859
+ /** The eufy settings the wizard offers, each with the label its control is named by. */
860
+ const EUFY_SETTING_LABELS = {
861
+ motionDetection: 'wizardMotionDetection',
862
+ nightVision: 'wizardNightVision',
863
+ notificationStyle: 'wizardNotificationStyle',
864
+ };
865
+
866
+ /** Whether a plugin is running to stream and change settings: a degraded one still holds its devices. */
867
+ function wizardPluginUp() {
868
+ return dashboardPluginState === 'ready' || dashboardPluginState === 'degraded';
869
+ }
870
+
871
+ function wizardPending(pending) {
872
+ try {
873
+ if (pending === undefined) return localStorage.getItem(setupWizard.PENDING_KEY) === 'pending';
874
+ if (pending) localStorage.setItem(setupWizard.PENDING_KEY, 'pending');
875
+ else localStorage.removeItem(setupWizard.PENDING_KEY);
876
+ } catch {
877
+ return false;
878
+ }
879
+ return pending;
880
+ }
881
+
882
+ async function openSetupWizard(trigger) {
883
+ wizardSteps = setupWizard.steps(dashboardDevices);
884
+ wizardIndex = 0;
885
+ openDashboardPanel(wizardPanel, trigger);
886
+ await renderWizardStep();
887
+ }
888
+
889
+ function stopWizardLive() {
890
+ if (!wizardLiveSerial) return;
891
+ wizardLiveSerial = undefined;
892
+ wizardLivePictures = 0;
893
+ wizardLivePlay.hidden = false;
894
+ wizardLiveStop.hidden = true;
895
+ wizardLiveSpinner.hidden = true;
896
+ wizardLiveStatus.textContent = '';
897
+ void requestWithinDeadline('/device/live/stop', {}, 12000).catch(() => undefined);
898
+ }
899
+
900
+ /**
901
+ * Shows a camera's box with its last picture behind the play button, so the step costs nothing until asked.
902
+ * A camera with no picture yet shows the empty box.
903
+ */
904
+ async function showWizardCamera(serial) {
905
+ wizardLive.hidden = false;
906
+ wizardLiveImage.hidden = true;
907
+ wizardLivePlay.hidden = false;
908
+ wizardLiveStatus.textContent = '';
909
+ let answer;
910
+ try {
911
+ answer = await requestWithinDeadline('/device/image', { serial }, 12000);
912
+ } catch {
913
+ answer = undefined;
914
+ }
915
+ if (!answer?.image || wizardSteps[wizardIndex]?.serial !== serial || wizardLiveSerial) return;
916
+ wizardLiveImage.src = `data:image/jpeg;base64,${answer.image}`;
917
+ wizardLiveImage.hidden = false;
918
+ }
919
+
920
+ function startWizardLive(serial) {
921
+ stopWizardLive();
922
+ wizardLiveSerial = serial;
923
+ wizardLivePlay.hidden = true;
924
+ wizardLiveSpinner.hidden = false;
925
+ wizardLiveStatus.textContent = messages.wizardLiveStarting ?? '';
926
+ void requestWithinDeadline('/device/live/start', { serial }, 12000).catch(() => endWizardLive(serial));
927
+ }
928
+
929
+ function endWizardLive(serial) {
930
+ if (serial !== wizardLiveSerial) return;
931
+ const watched = wizardLivePictures > 0;
932
+ wizardLiveSerial = undefined;
933
+ wizardLivePictures = 0;
934
+ wizardLiveStatus.textContent = watched ? '' : (messages.wizardLiveFailed ?? '');
935
+ wizardLivePlay.hidden = false;
936
+ wizardLiveStop.hidden = true;
937
+ wizardLiveSpinner.hidden = true;
938
+ }
939
+
940
+ homebridge.addEventListener('device-live', (event) => {
941
+ if (!event.data || event.data.serial !== wizardLiveSerial) return;
942
+ wizardLivePictures += 1;
943
+ // The stop is offered once there is video to stop; until then the spinner says it is on its way.
944
+ wizardLiveSpinner.hidden = true;
945
+ wizardLiveStop.hidden = false;
946
+ wizardLiveImage.src = `data:image/jpeg;base64,${event.data.image}`;
947
+ wizardLiveImage.hidden = false;
948
+ wizardLiveStatus.textContent = '';
949
+ });
950
+ homebridge.addEventListener('device-live-end', (event) => endWizardLive(event.data?.serial));
951
+
952
+ /** One control per eufy setting the device states and can change, named and valued the way the camera describes it. */
953
+ function eufySettingControl(setting) {
954
+ const label = messages[EUFY_SETTING_LABELS[setting.setting]] ?? setting.setting;
955
+ const wrapper = document.createElement('label');
956
+ wrapper.className = typeof setting.value === 'boolean' ? 'toggle' : 'wizard-setting';
957
+ wrapper.dataset.setting = '';
958
+ const name = document.createElement('span');
959
+ name.className = 'setting-label';
960
+ name.textContent = label;
961
+ let control;
962
+ if (typeof setting.value === 'boolean') {
963
+ control = document.createElement('input');
964
+ control.type = 'checkbox';
965
+ control.checked = setting.value;
966
+ wrapper.append(control, name);
967
+ } else {
968
+ control = document.createElement('select');
969
+ for (const [value, text] of Object.entries(setting.labels ?? { [setting.value]: String(setting.value) })) {
970
+ const option = document.createElement('option');
971
+ option.value = value;
972
+ option.textContent = text;
973
+ option.selected = Number(value) === setting.value;
974
+ control.append(option);
975
+ }
976
+ wrapper.append(name, control);
977
+ }
978
+ control.dataset.eufySetting = setting.setting;
979
+ return wrapper;
980
+ }
981
+
982
+ async function renderWizardSettings(serial) {
983
+ let answer;
984
+ try {
985
+ answer = await requestWithinDeadline('/device/settings', { serial }, 12000);
986
+ } catch {
987
+ answer = undefined;
988
+ }
989
+ if (wizardSteps[wizardIndex]?.serial !== serial) return;
990
+ const writable = (answer?.settings ?? []).filter((setting) => setting.writable);
991
+ if (writable.length === 0) return;
992
+ const status = document.createElement('p');
993
+ status.className = 'preference-note';
994
+ status.setAttribute('role', 'status');
995
+ status.dataset.wizardSettingsStatus = '';
996
+ wizardSettings.replaceChildren(...writable.map(eufySettingControl), status);
997
+ wizardEufy.hidden = false;
998
+ }
999
+
1000
+ /** One device on the map: a button that opens its step, marked as the current one where it is. */
1001
+ function wizardMapEntry(device, current) {
1002
+ const entry = document.createElement('button');
1003
+ entry.type = 'button';
1004
+ entry.className = 'wizard-map-device';
1005
+ entry.textContent = device.name;
1006
+ entry.dataset.serial = device.serial;
1007
+ if (device.serial === current) entry.setAttribute('aria-current', 'step');
1008
+ return entry;
1009
+ }
1010
+
1011
+ /**
1012
+ * Draws the household: each HomeBase with the devices that hang off it, then the cameras that stand alone. A base
1013
+ * is a step too where it has HomeKit choices, so its name opens it like any other device.
1014
+ */
1015
+ function renderWizardMap(current) {
1016
+ const { bases, standalone } = setupWizard.topology(wizardSteps, dashboardDevices);
1017
+ const group = (title, members, base) => {
1018
+ const section = document.createElement('div');
1019
+ section.className = 'wizard-map-group';
1020
+ const heading = base && wizardSteps.includes(base) ? wizardMapEntry(base, current) : document.createElement('p');
1021
+ heading.classList?.add('wizard-map-base');
1022
+ heading.textContent = title;
1023
+ const list = document.createElement('div');
1024
+ list.className = 'wizard-map-members';
1025
+ list.append(...members.map((device) => wizardMapEntry(device, current)));
1026
+ section.append(heading, list);
1027
+ return section;
1028
+ };
1029
+ wizardMap.replaceChildren(
1030
+ ...bases.map(({ base, members }) => group(base?.name ?? messages.wizardMapBase ?? '', members, base)),
1031
+ ...(standalone.length > 0 ? [group(messages.wizardMapStandalone ?? '', standalone)] : []),
1032
+ );
1033
+ }
1034
+
1035
+ wizardSettings.addEventListener('change', async (event) => {
1036
+ const control = event.target;
1037
+ const setting = control?.dataset?.eufySetting;
1038
+ const device = wizardSteps[wizardIndex];
1039
+ if (!setting || !device) return;
1040
+ const value = control.type === 'checkbox' ? control.checked : Number(control.value);
1041
+ const status = wizardSettings.querySelector('[data-wizard-settings-status]');
1042
+ control.disabled = true;
1043
+ status.textContent = messages.wizardSettingSending ?? '';
1044
+ try {
1045
+ const answer = await requestWithinDeadline('/device/settings/write', { serial: device.serial, setting, value }, 30000);
1046
+ // A write that reached the camera is not yet a camera that changed, so the step says sent rather than done.
1047
+ status.textContent = answer?.ok ? (messages.wizardSettingSent ?? '') : (messages.wizardSettingFailed ?? '');
1048
+ } catch {
1049
+ status.textContent = messages.wizardSettingFailed ?? '';
1050
+ } finally {
1051
+ control.disabled = false;
1052
+ }
1053
+ });
1054
+
1055
+ /** A device that is not a camera shows its product picture in the box, with nothing to play. */
1056
+ function showWizardArtwork(device) {
1057
+ wizardLivePlay.hidden = true;
1058
+ wizardLiveStop.hidden = true;
1059
+ wizardLiveStatus.textContent = '';
1060
+ wizardLiveImage.hidden = !device.artwork;
1061
+ if (device.artwork) wizardLiveImage.src = device.artwork;
1062
+ }
1063
+
1064
+ async function renderWizardStep() {
1065
+ stopWizardLive();
1066
+ wizardSettings.replaceChildren();
1067
+ wizardControls.replaceChildren();
1068
+ wizardEufy.hidden = true;
1069
+ const restart = !wizardPluginUp();
1070
+ const empty = restart || wizardSteps.length === 0;
1071
+ wizardRestart.hidden = !restart;
1072
+ wizardMap.hidden = empty;
1073
+ wizardStage.hidden = empty;
1074
+ wizardHomeKit.hidden = empty;
1075
+ wizardFinish.textContent = restart ? (messages.wizardClose ?? '') : (messages.wizardFinish ?? '');
1076
+ if (empty) {
1077
+ wizardProgress.textContent = '';
1078
+ wizardTitle.textContent = messages.wizardTitle ?? '';
1079
+ wizardEyebrow.textContent = '';
1080
+ return;
1081
+ }
1082
+ const device = wizardSteps[wizardIndex];
1083
+ renderWizardMap(device.serial);
1084
+ dashboardView.renderDeviceSettings(device, configuredBlock() ?? {}, messages, wizardElements);
1085
+ wizardHomeKit.hidden = !(device.preferences?.length > 0);
1086
+ wizardProgress.textContent = setupWizard.progress(messages.wizardProgress, wizardIndex, wizardSteps.length);
1087
+ wizardBack.disabled = wizardIndex === 0;
1088
+ wizardNext.disabled = wizardIndex === wizardSteps.length - 1;
1089
+ if (device.deviceClass === 'camera') void showWizardCamera(device.serial);
1090
+ else showWizardArtwork(device);
1091
+ await renderWizardSettings(device.serial);
1092
+ }
1093
+
1094
+ function goToWizardStep(index) {
1095
+ if (index < 0 || index >= wizardSteps.length || index === wizardIndex) return;
1096
+ wizardIndex = index;
1097
+ void renderWizardStep();
1098
+ }
1099
+
1100
+ menuWizard.addEventListener('click', () => openSetupWizard(menuWizard));
1101
+ wizardClose.addEventListener('click', () => closeDashboardPanel());
1102
+ wizardLiveStop.addEventListener('click', () => stopWizardLive());
1103
+ wizardLivePlay.addEventListener('click', () => {
1104
+ const device = wizardSteps[wizardIndex];
1105
+ if (device) startWizardLive(device.serial);
1106
+ });
1107
+ wizardBack.addEventListener('click', () => goToWizardStep(wizardIndex - 1));
1108
+ wizardNext.addEventListener('click', () => goToWizardStep(wizardIndex + 1));
1109
+ wizardMap.addEventListener('click', (event) => {
1110
+ const serial = event.target?.closest?.('[data-serial]')?.dataset?.serial;
1111
+ goToWizardStep(wizardSteps.findIndex((device) => device.serial === serial));
1112
+ });
1113
+ // Closing a wizard that waits for a restart keeps it due; finishing one that walked the devices does not.
1114
+ wizardFinish.addEventListener('click', () => {
1115
+ if (wizardPluginUp()) wizardPending(false);
1116
+ closeDashboardPanel();
1117
+ });
1118
+ window.addEventListener?.('pagehide', () => stopWizardLive());
1119
+
813
1120
  function openAuthentication() {
814
1121
  diagnosticsPanel.hidden = true;
815
1122
  advancedPanel.hidden = true;
1123
+ devicePanel.hidden = true;
1124
+ wizardPanel.hidden = true;
1125
+ stopWizardLive();
816
1126
  dashboard.hidden = true;
817
1127
  setupContent.hidden = false;
818
1128
  masthead.hidden = false;
@@ -881,6 +1191,7 @@ async function showDashboard() {
881
1191
  warmUpCandidates = Array.isArray(snapshot.warmUpCandidates) ? snapshot.warmUpCandidates : [];
882
1192
  // Kept beside the render that drew them, so opening a tile finds the device that tile stands for.
883
1193
  dashboardDevices = snapshot.devices ?? [];
1194
+ dashboardPluginState = snapshot.state;
884
1195
  dashboardAttention = dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
885
1196
  if (snapshot.state === 'authentication-required' && snapshot.devices?.length > 0 && !reauthDialog.open) {
886
1197
  reauthDialog.showModal?.();
@@ -936,8 +1247,11 @@ async function handleResult(result) {
936
1247
  authForm.hidden = true;
937
1248
  authStatus.textContent = messages.authSuccess ?? '';
938
1249
  // The devices this sign-in discovered are already recorded, so the flow ends on them rather than on a notice.
939
- if (await saveAuthenticatedConfig()) await showDashboard();
940
- else authStatus.textContent = messages.authSaveFailed ?? '';
1250
+ if (await saveAuthenticatedConfig()) {
1251
+ await showDashboard();
1252
+ wizardPending(true);
1253
+ await openSetupWizard();
1254
+ } else authStatus.textContent = messages.authSaveFailed ?? '';
941
1255
  } else {
942
1256
  authForm.hidden = false;
943
1257
  authStatus.textContent = messages[AUTH_OUTCOMES[result.status] ?? 'authFailed'] ?? '';
@@ -1056,5 +1370,6 @@ homebridge.addEventListener('ready', async () => {
1056
1370
  trustedDeviceInput.value = configured?.trustedDeviceName ?? 'Homebridge Eufy';
1057
1371
  if (configured) {
1058
1372
  await showDashboard();
1373
+ if (wizardPending() && wizardPluginUp()) await openSetupWizard();
1059
1374
  }
1060
1375
  });
@@ -346,7 +346,7 @@
346
346
  },
347
347
  true,
348
348
  );
349
- (elements.deviceSettingsControls ?? elements.groups).addEventListener('change', async (event) => {
349
+ const onPreferenceChange = async (event) => {
350
350
  const control = event.target;
351
351
  const serial = control?.dataset?.serial;
352
352
  const key = control?.dataset?.preference;
@@ -395,7 +395,10 @@
395
395
  } catch {
396
396
  elements.summary.textContent = getMessages().preferenceSaveFailed;
397
397
  }
398
- });
398
+ };
399
+ for (const container of elements.preferenceContainers ?? [elements.deviceSettingsControls ?? elements.groups]) {
400
+ container.addEventListener('change', onPreferenceChange);
401
+ }
399
402
  }
400
403
 
401
404
  /**
@@ -0,0 +1,52 @@
1
+ (function (root) {
2
+ 'use strict';
3
+
4
+ /** The browser key marking a sign-in whose devices have not been walked through yet. */
5
+ const PENDING_KEY = 'homebridge-eufy.setup-wizard-pending';
6
+
7
+ /**
8
+ * The devices the wizard walks through, in the map's order so the arrows follow it: each HomeBase then what hangs
9
+ * off it, then the devices that stand alone. Every camera is one, and every other device with a HomeKit choice to
10
+ * make; a device the plugin only diagnoses has nothing to set.
11
+ */
12
+ function steps(devices) {
13
+ const walkable = (devices ?? []).filter(
14
+ (device) => !device.diagnosticOnly && (device.deviceClass === 'camera' || device.preferences?.length > 0),
15
+ );
16
+ const { bases, standalone } = topology(walkable, devices);
17
+ return [
18
+ ...bases.flatMap(({ base, members }) => [...(base && walkable.includes(base) ? [base] : []), ...members]),
19
+ ...standalone,
20
+ ];
21
+ }
22
+
23
+ /** The progress line for one step, from a template naming `{current}` and `{total}`. */
24
+ function progress(template, index, total) {
25
+ return (template ?? '').replace('{current}', String(index + 1)).replace('{total}', String(total));
26
+ }
27
+
28
+ /**
29
+ * The household as the map draws it: each HomeBase with the devices that hang off it, then the devices that are
30
+ * their own station. A base is named by its own entry where the dashboard lists one; a device whose base it does
31
+ * not list stays with that base all the same, under no name.
32
+ */
33
+ function topology(steps, devices) {
34
+ const bases = new Map();
35
+ const standalone = [];
36
+ for (const device of devices ?? []) {
37
+ if (device.deviceClass === 'homebase') bases.set(device.serial, { base: device, members: [] });
38
+ }
39
+ for (const device of steps) {
40
+ if (device.deviceClass === 'homebase') continue;
41
+ if (!device.station) {
42
+ standalone.push(device);
43
+ continue;
44
+ }
45
+ if (!bases.has(device.station)) bases.set(device.station, { base: undefined, members: [] });
46
+ bases.get(device.station).members.push(device);
47
+ }
48
+ return { bases: [...bases.values()], standalone };
49
+ }
50
+
51
+ root.HomebridgeEufySetupWizard = { PENDING_KEY, steps, progress, topology };
52
+ })(window);
@@ -44,6 +44,7 @@
44
44
  "log.condition.affectedOne": "1 accessory is affected.",
45
45
  "log.condition.recovered": "Recovered: {summary}.",
46
46
  "log.diagnosticFileFailed": "Diagnostic file logging failed; check storage permissions.",
47
+ "log.liveVideoEncoders": "Live video encoders, in order of use: {encoders}.",
47
48
  "log.discardedSettings": "Some legacy settings were discarded. Review and acknowledge them in the Homebridge Eufy dashboard.",
48
49
  "log.snapshotCacheInvalid": "An invalid retained camera image was removed",
49
50
  "log.homekit.batteryCapabilityUnavailable": "Battery information is unavailable",
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.32",
4
+ "version": "5.0.0-beta.34",
5
5
  "description": "Control Eufy Security from homebridge.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",