@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.32 → 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.
- package/dist/diagnostics.js +16 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapters/lock.js +21 -0
- package/dist/homekit/adapters/lock.js.map +1 -1
- package/dist/media/live-preview.js +96 -0
- package/dist/media/live-preview.js.map +1 -0
- package/dist/media/live-stream.js +5 -6
- package/dist/media/live-stream.js.map +1 -1
- package/dist/platform.js +17 -0
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +54 -1
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/owner.js +14 -1
- package/dist/runtime/owner.js.map +1 -1
- package/dist/ui/dashboard.js +4 -2
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/runtime-channel-client.js +46 -1
- package/dist/ui/runtime-channel-client.js.map +1 -1
- package/dist/ui/server.js +20 -0
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +181 -0
- package/homebridge-ui/public/assets/icons/auto_fix_high.svg +1 -0
- package/homebridge-ui/public/assets/icons/play_arrow.svg +1 -0
- package/homebridge-ui/public/assets/icons/stop.svg +1 -0
- package/homebridge-ui/public/i18n/en.json +27 -1
- package/homebridge-ui/public/i18n/fr.json +27 -1
- package/homebridge-ui/public/index.html +58 -0
- package/homebridge-ui/public/js/app.js +313 -3
- package/homebridge-ui/public/js/dashboard.js +5 -2
- package/homebridge-ui/public/js/setup-wizard.js +52 -0
- package/package.json +1 -1
|
@@ -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())
|
|
940
|
-
|
|
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
|
-
|
|
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