@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.
- package/config.schema.json +6 -0
- package/dist/configuration.js +2 -0
- package/dist/configuration.js.map +1 -1
- package/dist/diagnostics.js +86 -2
- 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 +310 -32
- package/dist/media/live-stream.js.map +1 -1
- package/dist/platform.js +33 -2
- 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 +29 -1
- package/homebridge-ui/public/i18n/fr.json +29 -1
- package/homebridge-ui/public/index.html +63 -0
- package/homebridge-ui/public/js/app.js +318 -3
- package/homebridge-ui/public/js/dashboard.js +5 -2
- package/homebridge-ui/public/js/setup-wizard.js +52 -0
- package/i18n/runtime/en.json +1 -0
- 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())
|
|
940
|
-
|
|
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
|
-
|
|
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/i18n/runtime/en.json
CHANGED
|
@@ -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