@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.35 → 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.
- package/CONTRIBUTING.md +9 -17
- package/config.schema.json +6 -0
- package/dist/account/ownership.js +16 -16
- package/dist/account/ownership.js.map +1 -1
- package/dist/account/persistence.js +24 -74
- package/dist/account/persistence.js.map +1 -1
- package/dist/account/temporary-authentication.js +2 -2
- package/dist/account/temporary-authentication.js.map +1 -1
- package/dist/configuration.js +18 -25
- package/dist/configuration.js.map +1 -1
- package/dist/device/member-evidence.js +1 -1
- package/dist/device/member-evidence.js.map +1 -1
- package/dist/device/member-trust.js +14 -32
- package/dist/device/member-trust.js.map +1 -1
- package/dist/device/snapshot.js +5 -17
- package/dist/device/snapshot.js.map +1 -1
- package/dist/diagnostics.js +246 -300
- package/dist/diagnostics.js.map +1 -1
- package/dist/homekit/adapter.js +4 -1
- package/dist/homekit/adapter.js.map +1 -1
- package/dist/homekit/adapters/battery.js +3 -3
- package/dist/homekit/adapters/battery.js.map +1 -1
- package/dist/homekit/adapters/camera-controls.js +15 -37
- package/dist/homekit/adapters/camera-controls.js.map +1 -1
- package/dist/homekit/adapters/camera-streaming.js +9 -19
- package/dist/homekit/adapters/camera-streaming.js.map +1 -1
- package/dist/homekit/adapters/contact.js +5 -5
- package/dist/homekit/adapters/contact.js.map +1 -1
- package/dist/homekit/adapters/doorbell.js +3 -15
- package/dist/homekit/adapters/doorbell.js.map +1 -1
- package/dist/homekit/adapters/lock.js +15 -214
- package/dist/homekit/adapters/lock.js.map +1 -1
- package/dist/homekit/adapters/motion.js +2 -2
- package/dist/homekit/adapters/motion.js.map +1 -1
- package/dist/homekit/adapters/package-sensor.js +2 -2
- package/dist/homekit/adapters/package-sensor.js.map +1 -1
- package/dist/homekit/adapters/security-system.js +11 -211
- package/dist/homekit/adapters/security-system.js.map +1 -1
- package/dist/homekit/adapters/siren.js +5 -33
- package/dist/homekit/adapters/siren.js.map +1 -1
- package/dist/homekit/adapters/smart-light.js +19 -217
- package/dist/homekit/adapters/smart-light.js.map +1 -1
- package/dist/homekit/coverage-matrix.js +8 -65
- package/dist/homekit/coverage-matrix.js.map +1 -1
- package/dist/homekit/device-control.js +249 -13
- package/dist/homekit/device-control.js.map +1 -1
- package/dist/homekit/reconciler.js +5 -5
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/last-successful-image.js +3 -20
- package/dist/media/last-successful-image.js.map +1 -1
- package/dist/media/live-preview.js +163 -42
- package/dist/media/live-preview.js.map +1 -1
- package/dist/media/live-stream.js +108 -67
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +37 -120
- package/dist/media/recording.js.map +1 -1
- package/dist/media/snapshot.js +1 -9
- package/dist/media/snapshot.js.map +1 -1
- package/dist/media/station-live-sessions.js +0 -15
- package/dist/media/station-live-sessions.js.map +1 -1
- package/dist/platform.js +10 -10
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +91 -69
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/owner.js +6 -6
- package/dist/runtime/owner.js.map +1 -1
- package/dist/runtime/sdk-client.js +3 -14
- package/dist/runtime/sdk-client.js.map +1 -1
- package/dist/runtime/tracker.js +8 -38
- package/dist/runtime/tracker.js.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/ui/runtime-channel-client.js +145 -282
- package/dist/ui/runtime-channel-client.js.map +1 -1
- package/dist/ui/server.js +39 -53
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +150 -78
- package/homebridge-ui/public/i18n/en.json +14 -2
- package/homebridge-ui/public/i18n/fr.json +14 -2
- package/homebridge-ui/public/index.html +59 -10
- package/homebridge-ui/public/js/app.js +206 -42
- package/homebridge-ui/public/js/dashboard.js +4 -19
- package/package.json +3 -7
- package/homebridge-ui/public/js/legacy-settings.js +0 -37
- package/homebridge-ui/public/js/profile-wizard.js +0 -31
- 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
|
|
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 =
|
|
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 (
|
|
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 ||
|
|
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(
|
|
874
|
-
if (pending) localStorage.setItem(
|
|
875
|
-
else localStorage.removeItem(
|
|
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 =
|
|
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
|
-
|
|
908
|
-
|
|
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
|
-
|
|
1007
|
+
const body = { serial, adaptation: wizardLiveAdaptation() };
|
|
1008
|
+
void requestWithinDeadline('/device/live/start', body, 12000).catch(() => endWizardLive(serial));
|
|
927
1009
|
}
|
|
928
1010
|
|
|
929
|
-
|
|
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
|
|
1014
|
+
const failure = unplayable ? messages.wizardLiveUnplayable : wizardLivePictures ? '' : messages.wizardLiveFailed;
|
|
932
1015
|
wizardLiveSerial = undefined;
|
|
933
1016
|
wizardLivePictures = 0;
|
|
934
|
-
wizardLiveStatus.textContent =
|
|
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
|
-
|
|
947
|
-
|
|
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
|
-
|
|
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 } =
|
|
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 =
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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 =
|
|
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 /
|
|
127
|
-
const icon =
|
|
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.
|
|
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": "
|
|
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.
|
|
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);
|