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

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 (86) hide show
  1. package/CONTRIBUTING.md +9 -17
  2. package/config.schema.json +6 -0
  3. package/dist/account/ownership.js +16 -16
  4. package/dist/account/ownership.js.map +1 -1
  5. package/dist/account/persistence.js +24 -74
  6. package/dist/account/persistence.js.map +1 -1
  7. package/dist/account/temporary-authentication.js +2 -2
  8. package/dist/account/temporary-authentication.js.map +1 -1
  9. package/dist/configuration.js +18 -25
  10. package/dist/configuration.js.map +1 -1
  11. package/dist/device/member-evidence.js +1 -1
  12. package/dist/device/member-evidence.js.map +1 -1
  13. package/dist/device/member-trust.js +14 -32
  14. package/dist/device/member-trust.js.map +1 -1
  15. package/dist/device/snapshot.js +5 -17
  16. package/dist/device/snapshot.js.map +1 -1
  17. package/dist/diagnostics.js +246 -300
  18. package/dist/diagnostics.js.map +1 -1
  19. package/dist/homekit/adapter.js +4 -1
  20. package/dist/homekit/adapter.js.map +1 -1
  21. package/dist/homekit/adapters/battery.js +3 -3
  22. package/dist/homekit/adapters/battery.js.map +1 -1
  23. package/dist/homekit/adapters/camera-controls.js +15 -37
  24. package/dist/homekit/adapters/camera-controls.js.map +1 -1
  25. package/dist/homekit/adapters/camera-streaming.js +9 -19
  26. package/dist/homekit/adapters/camera-streaming.js.map +1 -1
  27. package/dist/homekit/adapters/contact.js +5 -5
  28. package/dist/homekit/adapters/contact.js.map +1 -1
  29. package/dist/homekit/adapters/doorbell.js +3 -15
  30. package/dist/homekit/adapters/doorbell.js.map +1 -1
  31. package/dist/homekit/adapters/lock.js +15 -214
  32. package/dist/homekit/adapters/lock.js.map +1 -1
  33. package/dist/homekit/adapters/motion.js +2 -2
  34. package/dist/homekit/adapters/motion.js.map +1 -1
  35. package/dist/homekit/adapters/package-sensor.js +2 -2
  36. package/dist/homekit/adapters/package-sensor.js.map +1 -1
  37. package/dist/homekit/adapters/security-system.js +11 -211
  38. package/dist/homekit/adapters/security-system.js.map +1 -1
  39. package/dist/homekit/adapters/siren.js +5 -33
  40. package/dist/homekit/adapters/siren.js.map +1 -1
  41. package/dist/homekit/adapters/smart-light.js +19 -217
  42. package/dist/homekit/adapters/smart-light.js.map +1 -1
  43. package/dist/homekit/coverage-matrix.js +8 -65
  44. package/dist/homekit/coverage-matrix.js.map +1 -1
  45. package/dist/homekit/device-control.js +249 -13
  46. package/dist/homekit/device-control.js.map +1 -1
  47. package/dist/homekit/reconciler.js +5 -5
  48. package/dist/homekit/reconciler.js.map +1 -1
  49. package/dist/media/last-successful-image.js +3 -20
  50. package/dist/media/last-successful-image.js.map +1 -1
  51. package/dist/media/live-preview.js +163 -42
  52. package/dist/media/live-preview.js.map +1 -1
  53. package/dist/media/live-stream.js +108 -67
  54. package/dist/media/live-stream.js.map +1 -1
  55. package/dist/media/recording.js +37 -120
  56. package/dist/media/recording.js.map +1 -1
  57. package/dist/media/snapshot.js +1 -9
  58. package/dist/media/snapshot.js.map +1 -1
  59. package/dist/media/station-live-sessions.js +0 -15
  60. package/dist/media/station-live-sessions.js.map +1 -1
  61. package/dist/platform.js +10 -10
  62. package/dist/platform.js.map +1 -1
  63. package/dist/runtime/channel.js +91 -69
  64. package/dist/runtime/channel.js.map +1 -1
  65. package/dist/runtime/owner.js +6 -6
  66. package/dist/runtime/owner.js.map +1 -1
  67. package/dist/runtime/sdk-client.js +3 -14
  68. package/dist/runtime/sdk-client.js.map +1 -1
  69. package/dist/runtime/tracker.js +8 -38
  70. package/dist/runtime/tracker.js.map +1 -1
  71. package/dist/storage.js +1 -1
  72. package/dist/storage.js.map +1 -1
  73. package/dist/ui/runtime-channel-client.js +145 -282
  74. package/dist/ui/runtime-channel-client.js.map +1 -1
  75. package/dist/ui/server.js +39 -53
  76. package/dist/ui/server.js.map +1 -1
  77. package/homebridge-ui/public/app.css +150 -78
  78. package/homebridge-ui/public/i18n/en.json +14 -2
  79. package/homebridge-ui/public/i18n/fr.json +14 -2
  80. package/homebridge-ui/public/index.html +59 -10
  81. package/homebridge-ui/public/js/app.js +206 -42
  82. package/homebridge-ui/public/js/dashboard.js +4 -19
  83. package/package.json +3 -7
  84. package/homebridge-ui/public/js/legacy-settings.js +0 -37
  85. package/homebridge-ui/public/js/profile-wizard.js +0 -31
  86. package/homebridge-ui/public/js/setup-wizard.js +0 -52
@@ -19,7 +19,6 @@ const challengeSubmit = document.querySelector('[data-challenge-submit]');
19
19
  const dashboard = document.querySelector('[data-dashboard]');
20
20
  const dashboardState = document.querySelector('[data-dashboard-state]');
21
21
  const dashboardTitle = document.querySelector('[data-dashboard-title]');
22
- const dashboardBadge = document.querySelector('[data-dashboard-badge]');
23
22
  const dashboardSummary = document.querySelector('[data-dashboard-summary]');
24
23
  const dashboardAuthenticate = document.querySelector('[data-dashboard-authenticate]');
25
24
  const reauthDialog = document.querySelector('[data-reauth-dialog]');
@@ -96,14 +95,20 @@ const wizardEyebrow = document.querySelector('[data-wizard-eyebrow]');
96
95
  const wizardRestart = document.querySelector('[data-wizard-restart]');
97
96
  const wizardLive = document.querySelector('[data-wizard-live]');
98
97
  const wizardLiveImage = document.querySelector('[data-wizard-live-image]');
98
+ const wizardLiveHomeKit = document.querySelector('[data-wizard-live-homekit]');
99
+ const wizardLiveCamera = document.querySelector('[data-wizard-live-camera]');
99
100
  const wizardLiveStatus = document.querySelector('[data-wizard-live-status]');
100
101
  const wizardLivePlay = document.querySelector('[data-wizard-live-play]');
101
102
  const wizardLiveStop = document.querySelector('[data-wizard-live-stop]');
102
103
  const wizardLiveSpinner = document.querySelector('[data-wizard-live-spinner]');
104
+ const wizardLiveHomeKitSpinner = document.querySelector('[data-wizard-live-homekit-spinner]');
105
+ const wizardLiveControls = document.querySelector('[data-wizard-live-controls]');
106
+ const wizardLiveSave = document.querySelector('[data-wizard-live-save]');
107
+ const wizardLiveEncoder = document.querySelector('[data-wizard-live-encoder]');
108
+ const wizardLiveDiagnose = document.querySelector('[data-wizard-live-diagnose]');
109
+ const wizardLiveDiagnoseStart = document.querySelector('[data-wizard-live-diagnose-start]');
103
110
  const wizardSettings = document.querySelector('[data-wizard-settings]');
104
111
  const wizardControls = document.querySelector('[data-wizard-controls]');
105
- const wizardBack = document.querySelector('[data-wizard-back]');
106
- const wizardNext = document.querySelector('[data-wizard-next]');
107
112
  const wizardFinish = document.querySelector('[data-wizard-finish]');
108
113
  const wizardMap = document.querySelector('[data-wizard-map]');
109
114
  const wizardStage = document.querySelector('[data-wizard-stage]');
@@ -130,9 +135,6 @@ let panelReturn;
130
135
  let dashboardAttention;
131
136
  let dashboardPanelTrigger;
132
137
  const dashboardView = window.HomebridgeEufyDashboard;
133
- const legacySettingsView = window.HomebridgeEufyLegacySettings;
134
- const diagnosticsWizard = window.HomebridgeEufyDiagnosticsWizard;
135
- const setupWizard = window.HomebridgeEufySetupWizard;
136
138
  const dashboardElements = {
137
139
  dashboard,
138
140
  title: dashboardTitle,
@@ -168,7 +170,7 @@ function requestWithinDeadline(path, body, timeoutMs = 320000) {
168
170
  }
169
171
 
170
172
  function isDashboardUiReproducing(state = diagnosticsState) {
171
- return diagnosticsWizard.backgroundActive(state);
173
+ return state.status === 'reproducing' && state.profile === 'dashboard-ui';
172
174
  }
173
175
 
174
176
  async function recordActiveUiEvent(event) {
@@ -252,6 +254,16 @@ const diagnosticsProfiles = {
252
254
  other: { title: 'diagnosticsProfileOther', action: 'diagnosticsOtherAction' },
253
255
  };
254
256
 
257
+ /**
258
+ * Which screen a session puts on the panel: its file while one can be downloaded, its capture while one runs, and
259
+ * otherwise the question, since picking an area replaces the session with a capture of that area.
260
+ */
261
+ function diagnosticsScreen(session) {
262
+ if (session.partialExportAvailable) return 'review';
263
+ if (session.status === 'reproducing') return 'reproduce';
264
+ return 'choose';
265
+ }
266
+
255
267
  function renderDiagnosticsGuidance(profile) {
256
268
  const guidance = diagnosticsProfiles[profile] ?? diagnosticsProfiles.other;
257
269
  diagnosticsPhaseTitle.textContent = messages[guidance.title] ?? '';
@@ -286,7 +298,7 @@ async function endDiagnosticsCase() {
286
298
  function renderDiagnostics(state) {
287
299
  if (state.supportCaseId !== diagnosticsState.supportCaseId) diagnosticsArchiveDownloaded = false;
288
300
  diagnosticsState = state;
289
- const screen = diagnosticsWizard.screen(state);
301
+ const screen = diagnosticsScreen(state);
290
302
  const choosing = screen === 'choose';
291
303
  const reviewing = screen === 'review';
292
304
  renderDiagnosticsEntries();
@@ -583,7 +595,7 @@ async function openDiagnostics(preset) {
583
595
  recordActiveUiEventBestEffort('request-failed');
584
596
  return;
585
597
  }
586
- if (diagnosticsWizard.screen(diagnosticsState) !== 'choose') return;
598
+ if (diagnosticsScreen(diagnosticsState) !== 'choose') return;
587
599
  if (preset) await startDiagnosticsCapture(preset.profile);
588
600
  else if (signingIn) {
589
601
  diagnosticsTiles.find((tile) => tile.dataset.diagnosticsTile === 'startup-authentication')?.focus?.();
@@ -594,7 +606,7 @@ mastheadDiagnostics.addEventListener('click', () => openDiagnostics());
594
606
 
595
607
  /** While the dashboard shows a problem and no session is under way, the action names it before opening the wizard. */
596
608
  menuDiagnostics.addEventListener('click', () => {
597
- if (!menuDiagnostics.dataset.attention || diagnosticsWizard.screen(diagnosticsState) !== 'choose') {
609
+ if (!menuDiagnostics.dataset.attention || diagnosticsScreen(diagnosticsState) !== 'choose') {
598
610
  return openDiagnostics();
599
611
  }
600
612
  attentionTitle.textContent = dashboardAttention.title;
@@ -868,25 +880,89 @@ function wizardPluginUp() {
868
880
  return dashboardPluginState === 'ready' || dashboardPluginState === 'degraded';
869
881
  }
870
882
 
883
+ /** The browser key marking a sign-in whose devices have not been walked through yet. */
884
+ const WIZARD_PENDING_KEY = 'homebridge-eufy.setup-wizard-pending';
885
+
871
886
  function wizardPending(pending) {
872
887
  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);
888
+ if (pending === undefined) return localStorage.getItem(WIZARD_PENDING_KEY) === 'pending';
889
+ if (pending) localStorage.setItem(WIZARD_PENDING_KEY, 'pending');
890
+ else localStorage.removeItem(WIZARD_PENDING_KEY);
876
891
  } catch {
877
892
  return false;
878
893
  }
879
894
  return pending;
880
895
  }
881
896
 
897
+ /**
898
+ * The devices the wizard walks through, in the map's order so the arrows follow it: each HomeBase then what hangs
899
+ * off it, then the devices that stand alone. Every camera is one, and every other device with a HomeKit choice to
900
+ * make; a device the plugin only diagnoses has nothing to set.
901
+ */
902
+ function setupWizardSteps(devices) {
903
+ const walkable = (devices ?? []).filter(
904
+ (device) => !device.diagnosticOnly && (device.deviceClass === 'camera' || device.preferences?.length > 0),
905
+ );
906
+ const { bases, standalone } = setupWizardTopology(walkable, devices);
907
+ return [
908
+ ...bases.flatMap(({ base, members }) => [...(base && walkable.includes(base) ? [base] : []), ...members]),
909
+ ...standalone,
910
+ ];
911
+ }
912
+
913
+ /**
914
+ * The household as the map draws it: each HomeBase with the devices that hang off it, then the devices that are
915
+ * their own station. A base is named by its own entry where the dashboard lists one; a device whose base it does
916
+ * not list stays with that base all the same, under no name.
917
+ */
918
+ function setupWizardTopology(steps, devices) {
919
+ const bases = new Map();
920
+ const standalone = [];
921
+ for (const device of devices ?? []) {
922
+ if (device.deviceClass === 'homebase') bases.set(device.serial, { base: device, members: [] });
923
+ }
924
+ for (const device of steps) {
925
+ if (device.deviceClass === 'homebase') continue;
926
+ if (!device.station) {
927
+ standalone.push(device);
928
+ continue;
929
+ }
930
+ if (!bases.has(device.station)) bases.set(device.station, { base: undefined, members: [] });
931
+ bases.get(device.station).members.push(device);
932
+ }
933
+ return { bases: [...bases.values()], standalone };
934
+ }
935
+
882
936
  async function openSetupWizard(trigger) {
883
- wizardSteps = setupWizard.steps(dashboardDevices);
937
+ wizardSteps = setupWizardSteps(dashboardDevices);
884
938
  wizardIndex = 0;
885
939
  openDashboardPanel(wizardPanel, trigger);
886
940
  await renderWizardStep();
887
941
  }
888
942
 
943
+ /**
944
+ * The advanced form's live video settings by name. A camera may keep its own value of those in
945
+ * {@link WIZARD_LIVE_CAMERA}; the others are general, so the card's toggle for one is the advanced form's own.
946
+ */
947
+ const wizardLiveSettings = {
948
+ liveVideoPassthrough: advancedLiveVideoPassthrough,
949
+ ignoreHomeBitRate: advancedIgnoreHomeBitRate,
950
+ hardwareVideoEncoder: advancedHardwareVideoEncoder,
951
+ smallVideoPackets: advancedSmallVideoPackets,
952
+ };
953
+ const WIZARD_LIVE_CAMERA = ['liveVideoPassthrough', 'ignoreHomeBitRate'];
954
+
955
+ /** The ask and settings the preview's HomeKit side is coded with, as its controls stand. Never saved. */
956
+ function wizardLiveAdaptation() {
957
+ const fields = wizardLiveControls.elements;
958
+ const settings = Object.keys(wizardLiveSettings).map((name) => [name, fields[name].checked]);
959
+ return { ask: fields.ask.value, ...Object.fromEntries(settings) };
960
+ }
961
+
889
962
  function stopWizardLive() {
963
+ wizardLiveHomeKitSpinner.hidden = true;
964
+ wizardLiveEncoder.textContent = messages.wizardLiveHomeKit ?? '';
965
+ wizardLiveDiagnose.hidden = true;
890
966
  if (!wizardLiveSerial) return;
891
967
  wizardLiveSerial = undefined;
892
968
  wizardLivePictures = 0;
@@ -901,11 +977,16 @@ function stopWizardLive() {
901
977
  * Shows a camera's box with its last picture behind the play button, so the step costs nothing until asked.
902
978
  * A camera with no picture yet shows the empty box.
903
979
  */
904
- async function showWizardCamera(serial) {
980
+ async function showWizardCamera(serial, enabled) {
905
981
  wizardLive.hidden = false;
906
982
  wizardLiveImage.hidden = true;
907
- wizardLivePlay.hidden = false;
908
- wizardLiveStatus.textContent = '';
983
+ const own = configuredBlock()?.entityPreferences?.[serial] ?? {};
984
+ for (const [name, input] of Object.entries(wizardLiveSettings)) {
985
+ wizardLiveControls.elements[name].checked = own[name] ?? input.checked;
986
+ }
987
+ // A camera switched off in the eufy app has nothing to stream, so it offers no play, only the reason.
988
+ wizardLivePlay.hidden = enabled === false;
989
+ wizardLiveStatus.textContent = enabled === false ? (messages.deviceStatusSwitchedOff ?? '') : '';
909
990
  let answer;
910
991
  try {
911
992
  answer = await requestWithinDeadline('/device/image', { serial }, 12000);
@@ -923,18 +1004,22 @@ function startWizardLive(serial) {
923
1004
  wizardLivePlay.hidden = true;
924
1005
  wizardLiveSpinner.hidden = false;
925
1006
  wizardLiveStatus.textContent = messages.wizardLiveStarting ?? '';
926
- void requestWithinDeadline('/device/live/start', { serial }, 12000).catch(() => endWizardLive(serial));
1007
+ const body = { serial, adaptation: wizardLiveAdaptation() };
1008
+ void requestWithinDeadline('/device/live/start', body, 12000).catch(() => endWizardLive(serial));
927
1009
  }
928
1010
 
929
- function endWizardLive(serial) {
1011
+ /** Ends the stream on screen, saying why HomeKit got no picture: coded video that decoded to nothing, or no video. */
1012
+ function endWizardLive(serial, unplayable) {
930
1013
  if (serial !== wizardLiveSerial) return;
931
- const watched = wizardLivePictures > 0;
1014
+ const failure = unplayable ? messages.wizardLiveUnplayable : wizardLivePictures ? '' : messages.wizardLiveFailed;
932
1015
  wizardLiveSerial = undefined;
933
1016
  wizardLivePictures = 0;
934
- wizardLiveStatus.textContent = watched ? '' : (messages.wizardLiveFailed ?? '');
1017
+ wizardLiveStatus.textContent = failure ?? '';
1018
+ wizardLiveDiagnose.hidden = !failure;
935
1019
  wizardLivePlay.hidden = false;
936
1020
  wizardLiveStop.hidden = true;
937
1021
  wizardLiveSpinner.hidden = true;
1022
+ wizardLiveHomeKitSpinner.hidden = true;
938
1023
  }
939
1024
 
940
1025
  homebridge.addEventListener('device-live', (event) => {
@@ -943,11 +1028,64 @@ homebridge.addEventListener('device-live', (event) => {
943
1028
  // The stop is offered once there is video to stop; until then the spinner says it is on its way.
944
1029
  wizardLiveSpinner.hidden = true;
945
1030
  wizardLiveStop.hidden = false;
946
- wizardLiveImage.src = `data:image/jpeg;base64,${event.data.image}`;
947
- wizardLiveImage.hidden = false;
1031
+ const homekit = event.data.side === 'homekit';
1032
+ // The first picture clears the connecting text; after that only the HomeKit side changes what the status says.
1033
+ if (homekit || wizardLivePictures === 1) wizardLiveStatus.textContent = '';
1034
+ if (homekit) wizardLiveHomeKitSpinner.hidden = true;
1035
+ // An empty HomeKit picture is a run that coded nothing watchable while the camera plays on.
1036
+ if (homekit && !event.data.image) {
1037
+ wizardLiveHomeKit.hidden = true;
1038
+ wizardLiveStatus.textContent = messages.wizardLiveUnplayable ?? '';
1039
+ wizardLiveDiagnose.hidden = false;
1040
+ return;
1041
+ }
1042
+ const image = homekit ? wizardLiveHomeKit : wizardLiveImage;
1043
+ image.src = `data:image/jpeg;base64,${event.data.image}`;
1044
+ image.hidden = false;
1045
+ if (!homekit || typeof event.data.kbps !== 'number') return;
1046
+ // The rate is the encoder's own output, the video HomeKit would be sent, not these pictures.
1047
+ const encoder = event.data.encoder === 'copy' ? messages.wizardLiveCamera : event.data.encoder;
1048
+ wizardLiveEncoder.textContent = (messages.wizardLiveEncoder ?? '')
1049
+ .replace('{encoder}', encoder ?? '')
1050
+ .replace('{kbps}', String(event.data.kbps));
1051
+ });
1052
+ // Only the HomeKit side restarts: its half waits under a spinner while the camera plays on.
1053
+ wizardLiveControls.addEventListener('change', (event) => {
1054
+ const name = event.target?.name;
1055
+ if (wizardLiveSettings[name] && !WIZARD_LIVE_CAMERA.includes(name)) {
1056
+ wizardLiveSettings[name].checked = event.target.checked;
1057
+ void updateAdvancedSettings();
1058
+ }
1059
+ if (!wizardLiveSerial) return;
1060
+ wizardLiveHomeKit.hidden = false;
1061
+ wizardLiveHomeKitSpinner.hidden = false;
948
1062
  wizardLiveStatus.textContent = '';
1063
+ wizardLiveDiagnose.hidden = true;
1064
+ const body = { serial: wizardLiveSerial, adaptation: wizardLiveAdaptation() };
1065
+ void requestWithinDeadline('/device/live/adapt', body, 12000).catch(() => undefined);
949
1066
  });
950
- homebridge.addEventListener('device-live-end', (event) => endWizardLive(event.data?.serial));
1067
+ // Keeps this camera's own ticks, as its preference wherever they differ from the general setting.
1068
+ wizardLiveSave.addEventListener('click', async () => {
1069
+ const serial = wizardSteps[wizardIndex]?.serial;
1070
+ const existing = configuredBlock();
1071
+ if (!serial || !existing) return;
1072
+ const entityPreferences = { ...(existing.entityPreferences ?? {}) };
1073
+ const preference = { ...(entityPreferences[serial] ?? {}) };
1074
+ for (const name of WIZARD_LIVE_CAMERA) {
1075
+ const checked = wizardLiveControls.elements[name].checked;
1076
+ if (checked === wizardLiveSettings[name].checked) delete preference[name];
1077
+ else preference[name] = checked;
1078
+ }
1079
+ if (Object.keys(preference).length === 0) delete entityPreferences[serial];
1080
+ else entityPreferences[serial] = preference;
1081
+ try {
1082
+ await updateConfig({ ...existing, entityPreferences });
1083
+ } catch {
1084
+ wizardLiveStatus.textContent = messages.preferenceSaveFailed ?? '';
1085
+ }
1086
+ });
1087
+ wizardLiveDiagnoseStart.addEventListener('click', () => openDiagnostics({ profile: 'live-media' }));
1088
+ homebridge.addEventListener('device-live-end', (event) => endWizardLive(event.data?.serial, event.data?.unplayable));
951
1089
 
952
1090
  /** One control per eufy setting the device states and can change, named and valued the way the camera describes it. */
953
1091
  function eufySettingControl(setting) {
@@ -1013,7 +1151,7 @@ function wizardMapEntry(device, current) {
1013
1151
  * is a step too where it has HomeKit choices, so its name opens it like any other device.
1014
1152
  */
1015
1153
  function renderWizardMap(current) {
1016
- const { bases, standalone } = setupWizard.topology(wizardSteps, dashboardDevices);
1154
+ const { bases, standalone } = setupWizardTopology(wizardSteps, dashboardDevices);
1017
1155
  const group = (title, members, base) => {
1018
1156
  const section = document.createElement('div');
1019
1157
  section.className = 'wizard-map-group';
@@ -1052,17 +1190,9 @@ wizardSettings.addEventListener('change', async (event) => {
1052
1190
  }
1053
1191
  });
1054
1192
 
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
1193
  async function renderWizardStep() {
1065
1194
  stopWizardLive();
1195
+ wizardLiveHomeKit.hidden = true;
1066
1196
  wizardSettings.replaceChildren();
1067
1197
  wizardControls.replaceChildren();
1068
1198
  wizardEufy.hidden = true;
@@ -1083,11 +1213,13 @@ async function renderWizardStep() {
1083
1213
  renderWizardMap(device.serial);
1084
1214
  dashboardView.renderDeviceSettings(device, configuredBlock() ?? {}, messages, wizardElements);
1085
1215
  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);
1216
+ wizardProgress.textContent = (messages.wizardProgress ?? '')
1217
+ .replace('{current}', String(wizardIndex + 1))
1218
+ .replace('{total}', String(wizardSteps.length));
1219
+ // Only a camera has a picture to show; any other device's step is its settings alone.
1220
+ wizardLive.hidden = device.deviceClass !== 'camera';
1221
+ wizardLiveControls.hidden = wizardLive.hidden;
1222
+ if (!wizardLive.hidden) void showWizardCamera(device.serial, device.enabled);
1091
1223
  await renderWizardSettings(device.serial);
1092
1224
  }
1093
1225
 
@@ -1104,8 +1236,6 @@ wizardLivePlay.addEventListener('click', () => {
1104
1236
  const device = wizardSteps[wizardIndex];
1105
1237
  if (device) startWizardLive(device.serial);
1106
1238
  });
1107
- wizardBack.addEventListener('click', () => goToWizardStep(wizardIndex - 1));
1108
- wizardNext.addEventListener('click', () => goToWizardStep(wizardIndex + 1));
1109
1239
  wizardMap.addEventListener('click', (event) => {
1110
1240
  const serial = event.target?.closest?.('[data-serial]')?.dataset?.serial;
1111
1241
  goToWizardStep(wizardSteps.findIndex((device) => device.serial === serial));
@@ -1331,6 +1461,40 @@ if (updateRestart) {
1331
1461
  });
1332
1462
  }
1333
1463
 
1464
+ /** The V4 settings a legacy block sets to something other than its default, which V5 no longer reads. */
1465
+ function legacySettingNames(block) {
1466
+ const defaults = { CameraMaxLivestreamDuration: 30, hkHome: 1, hkAway: 0, hkNight: 3, hkOff: 63 };
1467
+ const effectful = (key, value) => {
1468
+ if (Object.hasOwn(defaults, key)) return value !== defaults[key];
1469
+ if (Array.isArray(value)) return value.length > 0;
1470
+ if (value && typeof value === 'object') return Object.keys(value).length > 0;
1471
+ return Boolean(value);
1472
+ };
1473
+ return [
1474
+ 'CameraMaxLivestreamDuration',
1475
+ 'autoSyncStation',
1476
+ 'cameras',
1477
+ 'cleanCache',
1478
+ 'debugLivestream',
1479
+ 'enableDetailedLogging',
1480
+ 'enableEmbeddedPKCS1Support',
1481
+ 'experimentalMode',
1482
+ 'hkAway',
1483
+ 'hkHome',
1484
+ 'hkNight',
1485
+ 'hkOff',
1486
+ 'ignoreDevices',
1487
+ 'ignoreMultipleDevicesWarning',
1488
+ 'ignoreStations',
1489
+ 'omitLogFiles',
1490
+ 'stations',
1491
+ 'syncStationModes',
1492
+ 'useEmbeddedPKCS1Support',
1493
+ ]
1494
+ .filter((key) => Object.hasOwn(block, key) && effectful(key, block[key]))
1495
+ .sort();
1496
+ }
1497
+
1334
1498
  homebridge.addEventListener('ready', async () => {
1335
1499
  homebridge.disableSaveButton();
1336
1500
  shell.dataset.theme = await homebridge.userCurrentLightingMode();
@@ -1354,7 +1518,7 @@ homebridge.addEventListener('ready', async () => {
1354
1518
  } else {
1355
1519
  const legacyBlock = pluginConfig.find((block) => block.platform === 'EufySecurity');
1356
1520
  if (legacyBlock) {
1357
- legacyNames = legacySettingsView.names(legacyBlock);
1521
+ legacyNames = legacySettingNames(legacyBlock);
1358
1522
  }
1359
1523
  }
1360
1524
  legacyNotice.hidden = legacyNames.length === 0 || legacyAcknowledged;
@@ -96,35 +96,20 @@
96
96
  tile?.classList.toggle('device-tile-changed', Boolean(tile.querySelector('.preference-control-changed')));
97
97
  }
98
98
 
99
- /**
100
- * The eight battery icons the shipped set draws, from empty to full.
101
- *
102
- * Ordered, because the level picks one by its position: eight steps of twelve and a half percent each.
103
- */
104
- const BATTERY_STEPS = [
105
- 'battery_0',
106
- 'battery_1',
107
- 'battery_2',
108
- 'battery_3',
109
- 'battery_4',
110
- 'battery_5',
111
- 'battery_6',
112
- 'battery_full',
113
- ];
114
-
115
99
  /**
116
100
  * The battery badge a device gets, where it reports a level, and nothing where it does not.
117
101
  *
118
102
  * The icon carries the level and the exact percentage goes to the label: a tile is read at a glance, and a
119
103
  * number on every tile would be read deliberately or not at all. A device on mains power reports no level, and
120
- * neither does one nothing is observing, so both are left without a badge rather than shown an empty one.
104
+ * neither does one nothing is observing, so both are left without a badge rather than shown an empty one. The
105
+ * shipped set draws eight icons, `battery_0` to `battery_6` then `battery_full`, one per twelve and a half percent.
121
106
  */
122
107
  function batteryBadge(device, messages) {
123
108
  if (typeof device.battery !== 'number') {
124
109
  return undefined;
125
110
  }
126
- const step = Math.floor(device.battery / (100 / BATTERY_STEPS.length));
127
- const icon = BATTERY_STEPS[Math.min(BATTERY_STEPS.length - 1, Math.max(0, step))];
111
+ const step = Math.min(7, Math.max(0, Math.floor(device.battery / 12.5)));
112
+ const icon = step === 7 ? 'battery_full' : `battery_${step}`;
128
113
  const label = (messages.deviceBattery ?? '').replace('{level}', String(Math.round(device.battery)));
129
114
  return label ? { icon, label, variant: 'battery' } : undefined;
130
115
  }
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.34",
4
+ "version": "5.0.0-beta.36",
5
5
  "description": "Control Eufy Security from homebridge.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -26,12 +26,9 @@
26
26
  "scripts": {
27
27
  "lint": "prettier --check 'src/**/*.ts' 'test/**/*.ts'",
28
28
  "lint-fix": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
29
- "build": "npm run build-plugin",
30
- "build-plugin": "rimraf ./dist && tsc",
29
+ "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tsc",
31
30
  "guard:no-ecs": "bash scripts/guard-no-ecs.sh",
32
31
  "qualify:release": "node scripts/qualify-release.mjs",
33
- "docs:dev": "npm -C docs run dev",
34
- "docs:build": "npm -C docs run build",
35
32
  "test": "vitest run",
36
33
  "verify": "npm run lint && npm run guard:no-ecs && npm run build && npm test",
37
34
  "prepublishOnly": "npm run build && npm run qualify:release && npm run verify"
@@ -47,7 +44,7 @@
47
44
  ],
48
45
  "dependencies": {
49
46
  "@homebridge/plugin-ui-utils": "^2.2.6",
50
- "@mega-yfue/eufy-sdk": "0.5.0-beta.58",
47
+ "@mega-yfue/eufy-sdk": "0.5.0-beta.92",
51
48
  "ffmpeg-for-homebridge": "2.2.2"
52
49
  },
53
50
  "devDependencies": {
@@ -55,7 +52,6 @@
55
52
  "@types/node": "^26.6.4",
56
53
  "homebridge": "^2.4.0",
57
54
  "prettier": "^3.9.9",
58
- "rimraf": "^6.1.3",
59
55
  "typescript": "^7.0.2",
60
56
  "vitest": "^5.0.3"
61
57
  },
@@ -1,37 +0,0 @@
1
- (function legacySettingsModule(global) {
2
- const keys = [
3
- 'CameraMaxLivestreamDuration',
4
- 'autoSyncStation',
5
- 'cameras',
6
- 'cleanCache',
7
- 'debugLivestream',
8
- 'enableDetailedLogging',
9
- 'enableEmbeddedPKCS1Support',
10
- 'experimentalMode',
11
- 'hkAway',
12
- 'hkHome',
13
- 'hkNight',
14
- 'hkOff',
15
- 'ignoreDevices',
16
- 'ignoreMultipleDevicesWarning',
17
- 'ignoreStations',
18
- 'omitLogFiles',
19
- 'stations',
20
- 'syncStationModes',
21
- 'useEmbeddedPKCS1Support',
22
- ];
23
-
24
- function effectful(key, value) {
25
- const defaults = { CameraMaxLivestreamDuration: 30, hkHome: 1, hkAway: 0, hkNight: 3, hkOff: 63 };
26
- if (Object.hasOwn(defaults, key)) return value !== defaults[key];
27
- if (Array.isArray(value)) return value.length > 0;
28
- if (value && typeof value === 'object') return Object.keys(value).length > 0;
29
- return Boolean(value);
30
- }
31
-
32
- function names(block) {
33
- return keys.filter((key) => Object.hasOwn(block, key) && effectful(key, block[key])).sort();
34
- }
35
-
36
- global.HomebridgeEufyLegacySettings = { names };
37
- })(window);
@@ -1,31 +0,0 @@
1
- (function attachDiagnosticsWizard(global) {
2
- const profiles = [
3
- 'startup-authentication',
4
- 'device-representation',
5
- 'control-state',
6
- 'live-media',
7
- 'hksv-recording',
8
- 'dashboard-ui',
9
- 'other',
10
- ];
11
-
12
- /**
13
- * Which screen a session puts on the panel: its file while one can be downloaded, its capture while one runs, and
14
- * otherwise the question, since picking an area replaces the session with a capture of that area.
15
- */
16
- function screen(session) {
17
- if (session.partialExportAvailable) return 'review';
18
- if (session.status === 'reproducing') return 'reproduce';
19
- return 'choose';
20
- }
21
-
22
- function backgroundActive(session) {
23
- return session.status === 'reproducing' && session.profile === 'dashboard-ui';
24
- }
25
-
26
- global.HomebridgeEufyDiagnosticsWizard = {
27
- backgroundActive,
28
- profiles,
29
- screen,
30
- };
31
- })(window);
@@ -1,52 +0,0 @@
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);