@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.31 → 5.0.0-beta.33

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 (33) hide show
  1. package/dist/diagnostics.js +18 -2
  2. package/dist/diagnostics.js.map +1 -1
  3. package/dist/homekit/adapters/camera-streaming.js +9 -2
  4. package/dist/homekit/adapters/camera-streaming.js.map +1 -1
  5. package/dist/homekit/adapters/lock.js +21 -0
  6. package/dist/homekit/adapters/lock.js.map +1 -1
  7. package/dist/media/live-preview.js +96 -0
  8. package/dist/media/live-preview.js.map +1 -0
  9. package/dist/media/live-stream.js +99 -19
  10. package/dist/media/live-stream.js.map +1 -1
  11. package/dist/platform.js +17 -0
  12. package/dist/platform.js.map +1 -1
  13. package/dist/runtime/channel.js +54 -1
  14. package/dist/runtime/channel.js.map +1 -1
  15. package/dist/runtime/owner.js +14 -1
  16. package/dist/runtime/owner.js.map +1 -1
  17. package/dist/ui/dashboard.js +4 -2
  18. package/dist/ui/dashboard.js.map +1 -1
  19. package/dist/ui/runtime-channel-client.js +46 -1
  20. package/dist/ui/runtime-channel-client.js.map +1 -1
  21. package/dist/ui/server.js +20 -0
  22. package/dist/ui/server.js.map +1 -1
  23. package/homebridge-ui/public/app.css +181 -0
  24. package/homebridge-ui/public/assets/icons/auto_fix_high.svg +1 -0
  25. package/homebridge-ui/public/assets/icons/play_arrow.svg +1 -0
  26. package/homebridge-ui/public/assets/icons/stop.svg +1 -0
  27. package/homebridge-ui/public/i18n/en.json +27 -1
  28. package/homebridge-ui/public/i18n/fr.json +27 -1
  29. package/homebridge-ui/public/index.html +58 -0
  30. package/homebridge-ui/public/js/app.js +313 -3
  31. package/homebridge-ui/public/js/dashboard.js +5 -2
  32. package/homebridge-ui/public/js/setup-wizard.js +52 -0
  33. package/package.json +2 -2
@@ -255,5 +255,31 @@
255
255
  "updatePendingManual": "This page shows what the running version can do. Restart Homebridge to use the version you installed.",
256
256
  "updatePendingSummary": "This page shows what the running version can do. Restart to use the version you installed.",
257
257
  "updatePendingTitle": "An update is waiting",
258
- "updatePendingVersionLabel": "Running now"
258
+ "updatePendingVersionLabel": "Running now",
259
+ "menuWizard": "Setup assistant",
260
+ "closeWizard": "Back to devices",
261
+ "wizardTitle": "Setup assistant",
262
+ "wizardProgress": "Device {current} of {total}",
263
+ "wizardRestartSummary": "Restart Homebridge to connect your cameras. This assistant opens again on its own once the plugin is running, to go through each device with you.",
264
+ "wizardBack": "Back",
265
+ "wizardNext": "Next",
266
+ "wizardFinish": "Finish",
267
+ "wizardClose": "Close",
268
+ "wizardLiveStarting": "Connecting to the camera. This can take up to 30 seconds.",
269
+ "wizardLiveFailed": "The camera did not send video. Check it is on and online, then try again.",
270
+ "wizardEufySettings": "Camera settings in the eufy app",
271
+ "wizardMotionDetection": "Motion detection",
272
+ "wizardNightVision": "Night vision",
273
+ "wizardNotificationStyle": "Notification style",
274
+ "wizardSettingSending": "Sending to the camera…",
275
+ "wizardSettingSent": "Sent. The camera confirms within a few minutes.",
276
+ "wizardSettingFailed": "The camera did not take the change. Try again.",
277
+ "wizardLivePlay": "Play live video",
278
+ "wizardLiveStop": "Stop live video",
279
+ "wizardHomeKitTitle": "In the Home app",
280
+ "wizardHomeKitHelp": "What the Home app shows of this device and how it behaves there. Saved with the plugin settings.",
281
+ "wizardEufyHelp": "Sent to the camera itself, the same settings as in the eufy app.",
282
+ "wizardMapLabel": "Your devices",
283
+ "wizardMapBase": "HomeBase",
284
+ "wizardMapStandalone": "Standalone"
259
285
  }
@@ -255,5 +255,31 @@
255
255
  "updatePendingManual": "Cette page montre ce que sait faire la version en cours. Redémarrez Homebridge pour utiliser la version installée.",
256
256
  "updatePendingSummary": "Cette page montre ce que sait faire la version en cours. Redémarrez pour utiliser la version installée.",
257
257
  "updatePendingTitle": "Une mise à jour attend",
258
- "updatePendingVersionLabel": "Version en cours"
258
+ "updatePendingVersionLabel": "Version en cours",
259
+ "menuWizard": "Assistant de configuration",
260
+ "closeWizard": "Retour aux appareils",
261
+ "wizardTitle": "Assistant de configuration",
262
+ "wizardProgress": "Appareil {current} sur {total}",
263
+ "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.",
264
+ "wizardBack": "Retour",
265
+ "wizardNext": "Suivant",
266
+ "wizardFinish": "Terminer",
267
+ "wizardClose": "Fermer",
268
+ "wizardLiveStarting": "Connexion à la caméra. Cela peut prendre jusqu’à 30 secondes.",
269
+ "wizardLiveFailed": "La caméra n’a pas envoyé de vidéo. Vérifiez qu’elle est allumée et en ligne, puis réessayez.",
270
+ "wizardEufySettings": "Réglages de la caméra dans l’app eufy",
271
+ "wizardMotionDetection": "Détection de mouvement",
272
+ "wizardNightVision": "Vision nocturne",
273
+ "wizardNotificationStyle": "Style de notification",
274
+ "wizardSettingSending": "Envoi à la caméra…",
275
+ "wizardSettingSent": "Envoyé. La caméra le confirme d’ici quelques minutes.",
276
+ "wizardSettingFailed": "La caméra n’a pas pris le changement. Réessayez.",
277
+ "wizardLivePlay": "Lancer la vidéo en direct",
278
+ "wizardLiveStop": "Arrêter la vidéo en direct",
279
+ "wizardHomeKitTitle": "Dans l’app Maison",
280
+ "wizardHomeKitHelp": "Ce que l’app Maison montre de cet appareil et comment il s’y comporte. Enregistré avec les réglages du plugin.",
281
+ "wizardEufyHelp": "Envoyé à la caméra elle-même, les mêmes réglages que dans l’app eufy.",
282
+ "wizardMapLabel": "Vos appareils",
283
+ "wizardMapBase": "HomeBase",
284
+ "wizardMapStandalone": "Autonomes"
259
285
  }
@@ -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>
@@ -190,6 +193,60 @@
190
193
  <p class="advanced-device-help" data-i18n="advancedDeviceHelp"></p>
191
194
  <p class="advanced-status" role="status" aria-live="polite" data-advanced-status></p>
192
195
  </section>
196
+
197
+ <section class="dashboard-panel wizard-panel" data-setup-wizard hidden aria-labelledby="wizard-title">
198
+ <div class="dashboard-panel-header">
199
+ <button type="button" class="dashboard-page-back" data-wizard-close data-i18n-aria-label="closeWizard">
200
+ <span aria-hidden="true">←</span>
201
+ </button>
202
+ <img class="dashboard-page-icon" src="assets/icons/auto_fix_high.svg" alt="" />
203
+ <div>
204
+ <p class="eyebrow" data-wizard-progress></p>
205
+ <h2 id="wizard-title" data-wizard-title></h2>
206
+ <p class="summary" data-wizard-eyebrow></p>
207
+ </div>
208
+ </div>
209
+ <p class="summary" data-wizard-restart hidden data-i18n="wizardRestartSummary"></p>
210
+ <nav class="wizard-map" data-wizard-map data-i18n-aria-label="wizardMapLabel"></nav>
211
+ <div class="wizard-stage" data-wizard-stage>
212
+ <button type="button" class="wizard-arrow" data-wizard-back data-i18n-aria-label="wizardBack">
213
+ <img src="assets/icons/keyboard_arrow_left.svg" alt="" />
214
+ </button>
215
+ <div class="wizard-live" data-wizard-live>
216
+ <img data-wizard-live-image alt="" hidden />
217
+ <button type="button" class="wizard-live-play" data-wizard-live-play data-i18n-aria-label="wizardLivePlay">
218
+ <img src="assets/icons/play_arrow.svg" alt="" />
219
+ </button>
220
+ <button
221
+ type="button"
222
+ class="wizard-live-play wizard-live-stop"
223
+ data-wizard-live-stop
224
+ hidden
225
+ data-i18n-aria-label="wizardLiveStop"
226
+ >
227
+ <img src="assets/icons/stop.svg" alt="" />
228
+ </button>
229
+ <span class="wizard-live-spinner" data-wizard-live-spinner hidden aria-hidden="true"></span>
230
+ <p class="wizard-live-status" role="status" aria-live="polite" data-wizard-live-status></p>
231
+ </div>
232
+ <button type="button" class="wizard-arrow" data-wizard-next data-i18n-aria-label="wizardNext">
233
+ <img src="assets/icons/keyboard_arrow_right.svg" alt="" />
234
+ </button>
235
+ </div>
236
+ <section class="wizard-card" data-wizard-homekit aria-labelledby="wizard-homekit-title">
237
+ <h3 id="wizard-homekit-title" data-i18n="wizardHomeKitTitle"></h3>
238
+ <p class="preference-note" data-i18n="wizardHomeKitHelp"></p>
239
+ <div class="preference-grid" data-wizard-controls></div>
240
+ </section>
241
+ <section class="wizard-card" data-wizard-eufy hidden aria-labelledby="wizard-eufy-title">
242
+ <h3 id="wizard-eufy-title" data-i18n="wizardEufySettings"></h3>
243
+ <p class="preference-note" data-i18n="wizardEufyHelp"></p>
244
+ <div class="preference-grid" data-wizard-settings></div>
245
+ </section>
246
+ <div class="wizard-nav">
247
+ <button type="button" data-wizard-finish></button>
248
+ </div>
249
+ </section>
193
250
  </section>
194
251
 
195
252
  <section class="dashboard-panel diagnostics-panel" data-diagnostics hidden aria-labelledby="diagnostics-title">
@@ -383,4 +440,5 @@
383
440
  <script src="js/dashboard.js"></script>
384
441
  <script src="js/legacy-settings.js"></script>
385
442
  <script src="js/profile-wizard.js"></script>
443
+ <script src="js/setup-wizard.js"></script>
386
444
  <script src="js/app.js"></script>
@@ -86,6 +86,28 @@ const warmUpAddAll = document.querySelector('[data-warm-up-add-all]');
86
86
  const warmUpRemove = document.querySelector('[data-warm-up-remove]');
87
87
  const warmUpRemoveAll = document.querySelector('[data-warm-up-remove-all]');
88
88
  const advancedStatus = document.querySelector('[data-advanced-status]');
89
+ const menuWizard = document.querySelector('[data-menu-wizard]');
90
+ const wizardPanel = document.querySelector('[data-setup-wizard]');
91
+ const wizardClose = document.querySelector('[data-wizard-close]');
92
+ const wizardProgress = document.querySelector('[data-wizard-progress]');
93
+ const wizardTitle = document.querySelector('[data-wizard-title]');
94
+ const wizardEyebrow = document.querySelector('[data-wizard-eyebrow]');
95
+ const wizardRestart = document.querySelector('[data-wizard-restart]');
96
+ const wizardLive = document.querySelector('[data-wizard-live]');
97
+ const wizardLiveImage = document.querySelector('[data-wizard-live-image]');
98
+ const wizardLiveStatus = document.querySelector('[data-wizard-live-status]');
99
+ const wizardLivePlay = document.querySelector('[data-wizard-live-play]');
100
+ const wizardLiveStop = document.querySelector('[data-wizard-live-stop]');
101
+ const wizardLiveSpinner = document.querySelector('[data-wizard-live-spinner]');
102
+ const wizardSettings = document.querySelector('[data-wizard-settings]');
103
+ const wizardControls = document.querySelector('[data-wizard-controls]');
104
+ const wizardBack = document.querySelector('[data-wizard-back]');
105
+ const wizardNext = document.querySelector('[data-wizard-next]');
106
+ const wizardFinish = document.querySelector('[data-wizard-finish]');
107
+ const wizardMap = document.querySelector('[data-wizard-map]');
108
+ const wizardStage = document.querySelector('[data-wizard-stage]');
109
+ const wizardHomeKit = document.querySelector('[data-wizard-homekit]');
110
+ const wizardEufy = document.querySelector('[data-wizard-eufy]');
89
111
 
90
112
  let messages = {};
91
113
  let pluginConfig = [];
@@ -109,6 +131,7 @@ let dashboardPanelTrigger;
109
131
  const dashboardView = window.HomebridgeEufyDashboard;
110
132
  const legacySettingsView = window.HomebridgeEufyLegacySettings;
111
133
  const diagnosticsWizard = window.HomebridgeEufyDiagnosticsWizard;
134
+ const setupWizard = window.HomebridgeEufySetupWizard;
112
135
  const dashboardElements = {
113
136
  dashboard,
114
137
  title: dashboardTitle,
@@ -118,6 +141,7 @@ const dashboardElements = {
118
141
  deviceSettingsTitle,
119
142
  deviceSettingsEyebrow,
120
143
  deviceSettingsControls,
144
+ preferenceContainers: [deviceSettingsControls, wizardControls],
121
145
  updatePending,
122
146
  updatePendingSummary,
123
147
  updatePendingVersion,
@@ -512,7 +536,7 @@ function openDashboardPanel(panel, trigger) {
512
536
  dashboardState.hidden = insideDashboard;
513
537
  dashboardSummary.hidden = insideDashboard;
514
538
  deviceGroups.hidden = insideDashboard;
515
- for (const other of [diagnosticsPanel, advancedPanel, devicePanel]) {
539
+ for (const other of [diagnosticsPanel, advancedPanel, devicePanel, wizardPanel]) {
516
540
  other.hidden = other !== panel;
517
541
  }
518
542
  panel.scrollIntoView?.({ behavior: 'smooth', block: 'start' });
@@ -524,6 +548,8 @@ function closeDashboardPanel() {
524
548
  diagnosticsPanel.hidden = true;
525
549
  advancedPanel.hidden = true;
526
550
  devicePanel.hidden = true;
551
+ wizardPanel.hidden = true;
552
+ stopWizardLive();
527
553
  dashboardState.hidden = false;
528
554
  dashboardSummary.hidden = false;
529
555
  deviceGroups.hidden = false;
@@ -810,9 +836,288 @@ dashboardView.bindPreferences(dashboardElements, configuredBlock, updateConfig,
810
836
  openDashboardPanel(devicePanel, trigger);
811
837
  });
812
838
 
839
+ /*
840
+ * The setup wizard walks every device once a sign-in has a running plugin to show it: each camera live, the eufy
841
+ * settings the plugin can change, and the HomeKit choices. A sign-in always leaves Homebridge to restart, so it
842
+ * marks the wizard pending in this browser and the next page opened on a ready plugin starts it.
843
+ */
844
+ let dashboardPluginState;
845
+ let wizardSteps = [];
846
+ let wizardIndex = 0;
847
+ let wizardLiveSerial;
848
+ let wizardLivePictures = 0;
849
+ const wizardElements = {
850
+ deviceSettingsTitle: wizardTitle,
851
+ deviceSettingsEyebrow: wizardEyebrow,
852
+ deviceSettingsControls: wizardControls,
853
+ };
854
+ /** The eufy settings the wizard offers, each with the label its control is named by. */
855
+ const EUFY_SETTING_LABELS = {
856
+ motionDetection: 'wizardMotionDetection',
857
+ nightVision: 'wizardNightVision',
858
+ notificationStyle: 'wizardNotificationStyle',
859
+ };
860
+
861
+ /** Whether a plugin is running to stream and change settings: a degraded one still holds its devices. */
862
+ function wizardPluginUp() {
863
+ return dashboardPluginState === 'ready' || dashboardPluginState === 'degraded';
864
+ }
865
+
866
+ function wizardPending(pending) {
867
+ try {
868
+ if (pending === undefined) return localStorage.getItem(setupWizard.PENDING_KEY) === 'pending';
869
+ if (pending) localStorage.setItem(setupWizard.PENDING_KEY, 'pending');
870
+ else localStorage.removeItem(setupWizard.PENDING_KEY);
871
+ } catch {
872
+ return false;
873
+ }
874
+ return pending;
875
+ }
876
+
877
+ async function openSetupWizard(trigger) {
878
+ wizardSteps = setupWizard.steps(dashboardDevices);
879
+ wizardIndex = 0;
880
+ openDashboardPanel(wizardPanel, trigger);
881
+ await renderWizardStep();
882
+ }
883
+
884
+ function stopWizardLive() {
885
+ if (!wizardLiveSerial) return;
886
+ wizardLiveSerial = undefined;
887
+ wizardLivePictures = 0;
888
+ wizardLivePlay.hidden = false;
889
+ wizardLiveStop.hidden = true;
890
+ wizardLiveSpinner.hidden = true;
891
+ wizardLiveStatus.textContent = '';
892
+ void requestWithinDeadline('/device/live/stop', {}, 12000).catch(() => undefined);
893
+ }
894
+
895
+ /**
896
+ * Shows a camera's box with its last picture behind the play button, so the step costs nothing until asked.
897
+ * A camera with no picture yet shows the empty box.
898
+ */
899
+ async function showWizardCamera(serial) {
900
+ wizardLive.hidden = false;
901
+ wizardLiveImage.hidden = true;
902
+ wizardLivePlay.hidden = false;
903
+ wizardLiveStatus.textContent = '';
904
+ let answer;
905
+ try {
906
+ answer = await requestWithinDeadline('/device/image', { serial }, 12000);
907
+ } catch {
908
+ answer = undefined;
909
+ }
910
+ if (!answer?.image || wizardSteps[wizardIndex]?.serial !== serial || wizardLiveSerial) return;
911
+ wizardLiveImage.src = `data:image/jpeg;base64,${answer.image}`;
912
+ wizardLiveImage.hidden = false;
913
+ }
914
+
915
+ function startWizardLive(serial) {
916
+ stopWizardLive();
917
+ wizardLiveSerial = serial;
918
+ wizardLivePlay.hidden = true;
919
+ wizardLiveSpinner.hidden = false;
920
+ wizardLiveStatus.textContent = messages.wizardLiveStarting ?? '';
921
+ void requestWithinDeadline('/device/live/start', { serial }, 12000).catch(() => endWizardLive(serial));
922
+ }
923
+
924
+ function endWizardLive(serial) {
925
+ if (serial !== wizardLiveSerial) return;
926
+ const watched = wizardLivePictures > 0;
927
+ wizardLiveSerial = undefined;
928
+ wizardLivePictures = 0;
929
+ wizardLiveStatus.textContent = watched ? '' : (messages.wizardLiveFailed ?? '');
930
+ wizardLivePlay.hidden = false;
931
+ wizardLiveStop.hidden = true;
932
+ wizardLiveSpinner.hidden = true;
933
+ }
934
+
935
+ homebridge.addEventListener('device-live', (event) => {
936
+ if (!event.data || event.data.serial !== wizardLiveSerial) return;
937
+ wizardLivePictures += 1;
938
+ // The stop is offered once there is video to stop; until then the spinner says it is on its way.
939
+ wizardLiveSpinner.hidden = true;
940
+ wizardLiveStop.hidden = false;
941
+ wizardLiveImage.src = `data:image/jpeg;base64,${event.data.image}`;
942
+ wizardLiveImage.hidden = false;
943
+ wizardLiveStatus.textContent = '';
944
+ });
945
+ homebridge.addEventListener('device-live-end', (event) => endWizardLive(event.data?.serial));
946
+
947
+ /** One control per eufy setting the device states and can change, named and valued the way the camera describes it. */
948
+ function eufySettingControl(setting) {
949
+ const label = messages[EUFY_SETTING_LABELS[setting.setting]] ?? setting.setting;
950
+ const wrapper = document.createElement('label');
951
+ wrapper.className = typeof setting.value === 'boolean' ? 'toggle' : 'wizard-setting';
952
+ wrapper.dataset.setting = '';
953
+ const name = document.createElement('span');
954
+ name.className = 'setting-label';
955
+ name.textContent = label;
956
+ let control;
957
+ if (typeof setting.value === 'boolean') {
958
+ control = document.createElement('input');
959
+ control.type = 'checkbox';
960
+ control.checked = setting.value;
961
+ wrapper.append(control, name);
962
+ } else {
963
+ control = document.createElement('select');
964
+ for (const [value, text] of Object.entries(setting.labels ?? { [setting.value]: String(setting.value) })) {
965
+ const option = document.createElement('option');
966
+ option.value = value;
967
+ option.textContent = text;
968
+ option.selected = Number(value) === setting.value;
969
+ control.append(option);
970
+ }
971
+ wrapper.append(name, control);
972
+ }
973
+ control.dataset.eufySetting = setting.setting;
974
+ return wrapper;
975
+ }
976
+
977
+ async function renderWizardSettings(serial) {
978
+ let answer;
979
+ try {
980
+ answer = await requestWithinDeadline('/device/settings', { serial }, 12000);
981
+ } catch {
982
+ answer = undefined;
983
+ }
984
+ if (wizardSteps[wizardIndex]?.serial !== serial) return;
985
+ const writable = (answer?.settings ?? []).filter((setting) => setting.writable);
986
+ if (writable.length === 0) return;
987
+ const status = document.createElement('p');
988
+ status.className = 'preference-note';
989
+ status.setAttribute('role', 'status');
990
+ status.dataset.wizardSettingsStatus = '';
991
+ wizardSettings.replaceChildren(...writable.map(eufySettingControl), status);
992
+ wizardEufy.hidden = false;
993
+ }
994
+
995
+ /** One device on the map: a button that opens its step, marked as the current one where it is. */
996
+ function wizardMapEntry(device, current) {
997
+ const entry = document.createElement('button');
998
+ entry.type = 'button';
999
+ entry.className = 'wizard-map-device';
1000
+ entry.textContent = device.name;
1001
+ entry.dataset.serial = device.serial;
1002
+ if (device.serial === current) entry.setAttribute('aria-current', 'step');
1003
+ return entry;
1004
+ }
1005
+
1006
+ /**
1007
+ * Draws the household: each HomeBase with the devices that hang off it, then the cameras that stand alone. A base
1008
+ * is a step too where it has HomeKit choices, so its name opens it like any other device.
1009
+ */
1010
+ function renderWizardMap(current) {
1011
+ const { bases, standalone } = setupWizard.topology(wizardSteps, dashboardDevices);
1012
+ const group = (title, members, base) => {
1013
+ const section = document.createElement('div');
1014
+ section.className = 'wizard-map-group';
1015
+ const heading = base && wizardSteps.includes(base) ? wizardMapEntry(base, current) : document.createElement('p');
1016
+ heading.classList?.add('wizard-map-base');
1017
+ heading.textContent = title;
1018
+ const list = document.createElement('div');
1019
+ list.className = 'wizard-map-members';
1020
+ list.append(...members.map((device) => wizardMapEntry(device, current)));
1021
+ section.append(heading, list);
1022
+ return section;
1023
+ };
1024
+ wizardMap.replaceChildren(
1025
+ ...bases.map(({ base, members }) => group(base?.name ?? messages.wizardMapBase ?? '', members, base)),
1026
+ ...(standalone.length > 0 ? [group(messages.wizardMapStandalone ?? '', standalone)] : []),
1027
+ );
1028
+ }
1029
+
1030
+ wizardSettings.addEventListener('change', async (event) => {
1031
+ const control = event.target;
1032
+ const setting = control?.dataset?.eufySetting;
1033
+ const device = wizardSteps[wizardIndex];
1034
+ if (!setting || !device) return;
1035
+ const value = control.type === 'checkbox' ? control.checked : Number(control.value);
1036
+ const status = wizardSettings.querySelector('[data-wizard-settings-status]');
1037
+ control.disabled = true;
1038
+ status.textContent = messages.wizardSettingSending ?? '';
1039
+ try {
1040
+ const answer = await requestWithinDeadline('/device/settings/write', { serial: device.serial, setting, value }, 30000);
1041
+ // A write that reached the camera is not yet a camera that changed, so the step says sent rather than done.
1042
+ status.textContent = answer?.ok ? (messages.wizardSettingSent ?? '') : (messages.wizardSettingFailed ?? '');
1043
+ } catch {
1044
+ status.textContent = messages.wizardSettingFailed ?? '';
1045
+ } finally {
1046
+ control.disabled = false;
1047
+ }
1048
+ });
1049
+
1050
+ /** A device that is not a camera shows its product picture in the box, with nothing to play. */
1051
+ function showWizardArtwork(device) {
1052
+ wizardLivePlay.hidden = true;
1053
+ wizardLiveStop.hidden = true;
1054
+ wizardLiveStatus.textContent = '';
1055
+ wizardLiveImage.hidden = !device.artwork;
1056
+ if (device.artwork) wizardLiveImage.src = device.artwork;
1057
+ }
1058
+
1059
+ async function renderWizardStep() {
1060
+ stopWizardLive();
1061
+ wizardSettings.replaceChildren();
1062
+ wizardControls.replaceChildren();
1063
+ wizardEufy.hidden = true;
1064
+ const restart = !wizardPluginUp();
1065
+ const empty = restart || wizardSteps.length === 0;
1066
+ wizardRestart.hidden = !restart;
1067
+ wizardMap.hidden = empty;
1068
+ wizardStage.hidden = empty;
1069
+ wizardHomeKit.hidden = empty;
1070
+ wizardFinish.textContent = restart ? (messages.wizardClose ?? '') : (messages.wizardFinish ?? '');
1071
+ if (empty) {
1072
+ wizardProgress.textContent = '';
1073
+ wizardTitle.textContent = messages.wizardTitle ?? '';
1074
+ wizardEyebrow.textContent = '';
1075
+ return;
1076
+ }
1077
+ const device = wizardSteps[wizardIndex];
1078
+ renderWizardMap(device.serial);
1079
+ dashboardView.renderDeviceSettings(device, configuredBlock() ?? {}, messages, wizardElements);
1080
+ wizardHomeKit.hidden = !(device.preferences?.length > 0);
1081
+ wizardProgress.textContent = setupWizard.progress(messages.wizardProgress, wizardIndex, wizardSteps.length);
1082
+ wizardBack.disabled = wizardIndex === 0;
1083
+ wizardNext.disabled = wizardIndex === wizardSteps.length - 1;
1084
+ if (device.deviceClass === 'camera') void showWizardCamera(device.serial);
1085
+ else showWizardArtwork(device);
1086
+ await renderWizardSettings(device.serial);
1087
+ }
1088
+
1089
+ function goToWizardStep(index) {
1090
+ if (index < 0 || index >= wizardSteps.length || index === wizardIndex) return;
1091
+ wizardIndex = index;
1092
+ void renderWizardStep();
1093
+ }
1094
+
1095
+ menuWizard.addEventListener('click', () => openSetupWizard(menuWizard));
1096
+ wizardClose.addEventListener('click', () => closeDashboardPanel());
1097
+ wizardLiveStop.addEventListener('click', () => stopWizardLive());
1098
+ wizardLivePlay.addEventListener('click', () => {
1099
+ const device = wizardSteps[wizardIndex];
1100
+ if (device) startWizardLive(device.serial);
1101
+ });
1102
+ wizardBack.addEventListener('click', () => goToWizardStep(wizardIndex - 1));
1103
+ wizardNext.addEventListener('click', () => goToWizardStep(wizardIndex + 1));
1104
+ wizardMap.addEventListener('click', (event) => {
1105
+ const serial = event.target?.closest?.('[data-serial]')?.dataset?.serial;
1106
+ goToWizardStep(wizardSteps.findIndex((device) => device.serial === serial));
1107
+ });
1108
+ // Closing a wizard that waits for a restart keeps it due; finishing one that walked the devices does not.
1109
+ wizardFinish.addEventListener('click', () => {
1110
+ if (wizardPluginUp()) wizardPending(false);
1111
+ closeDashboardPanel();
1112
+ });
1113
+ window.addEventListener?.('pagehide', () => stopWizardLive());
1114
+
813
1115
  function openAuthentication() {
814
1116
  diagnosticsPanel.hidden = true;
815
1117
  advancedPanel.hidden = true;
1118
+ devicePanel.hidden = true;
1119
+ wizardPanel.hidden = true;
1120
+ stopWizardLive();
816
1121
  dashboard.hidden = true;
817
1122
  setupContent.hidden = false;
818
1123
  masthead.hidden = false;
@@ -881,6 +1186,7 @@ async function showDashboard() {
881
1186
  warmUpCandidates = Array.isArray(snapshot.warmUpCandidates) ? snapshot.warmUpCandidates : [];
882
1187
  // Kept beside the render that drew them, so opening a tile finds the device that tile stands for.
883
1188
  dashboardDevices = snapshot.devices ?? [];
1189
+ dashboardPluginState = snapshot.state;
884
1190
  dashboardAttention = dashboardView.render(snapshot, configuredBlock() ?? {}, messages, dashboardElements);
885
1191
  if (snapshot.state === 'authentication-required' && snapshot.devices?.length > 0 && !reauthDialog.open) {
886
1192
  reauthDialog.showModal?.();
@@ -936,8 +1242,11 @@ async function handleResult(result) {
936
1242
  authForm.hidden = true;
937
1243
  authStatus.textContent = messages.authSuccess ?? '';
938
1244
  // 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 ?? '';
1245
+ if (await saveAuthenticatedConfig()) {
1246
+ await showDashboard();
1247
+ wizardPending(true);
1248
+ await openSetupWizard();
1249
+ } else authStatus.textContent = messages.authSaveFailed ?? '';
941
1250
  } else {
942
1251
  authForm.hidden = false;
943
1252
  authStatus.textContent = messages[AUTH_OUTCOMES[result.status] ?? 'authFailed'] ?? '';
@@ -1056,5 +1365,6 @@ homebridge.addEventListener('ready', async () => {
1056
1365
  trustedDeviceInput.value = configured?.trustedDeviceName ?? 'Homebridge Eufy';
1057
1366
  if (configured) {
1058
1367
  await showDashboard();
1368
+ if (wizardPending() && wizardPluginUp()) await openSetupWizard();
1059
1369
  }
1060
1370
  });
@@ -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);
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.31",
4
+ "version": "5.0.0-beta.33",
5
5
  "description": "Control Eufy Security from homebridge.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -47,7 +47,7 @@
47
47
  ],
48
48
  "dependencies": {
49
49
  "@homebridge/plugin-ui-utils": "^2.2.6",
50
- "@mega-yfue/eufy-sdk": "0.5.0-beta.25",
50
+ "@mega-yfue/eufy-sdk": "0.5.0-beta.58",
51
51
  "ffmpeg-for-homebridge": "2.2.2"
52
52
  },
53
53
  "devDependencies": {