@homebridge-plugins/homebridge-eufy-security 5.0.0-beta.4 → 5.0.0-beta.41
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 +60 -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 +5 -3
- package/dist/account/temporary-authentication.js.map +1 -1
- package/dist/configuration.js +50 -13
- package/dist/configuration.js.map +1 -1
- package/dist/device/member-evidence.js +11 -4
- 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 +6 -18
- package/dist/device/snapshot.js.map +1 -1
- package/dist/diagnostics.js +966 -337
- 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 +74 -48
- 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 +72 -221
- package/dist/homekit/adapters/lock.js.map +1 -1
- package/dist/homekit/adapters/motion.js +3 -3
- package/dist/homekit/adapters/motion.js.map +1 -1
- package/dist/homekit/adapters/package-sensor.js +63 -0
- package/dist/homekit/adapters/package-sensor.js.map +1 -0
- package/dist/homekit/adapters/registry.js +2 -0
- package/dist/homekit/adapters/registry.js.map +1 -1
- package/dist/homekit/adapters/security-system.js +106 -250
- 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 -67
- 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 +25 -9
- package/dist/homekit/reconciler.js.map +1 -1
- package/dist/media/last-successful-image.js +8 -23
- package/dist/media/last-successful-image.js.map +1 -1
- package/dist/media/live-preview.js +217 -0
- package/dist/media/live-preview.js.map +1 -0
- package/dist/media/live-stream.js +689 -160
- package/dist/media/live-stream.js.map +1 -1
- package/dist/media/recording.js +41 -118
- package/dist/media/recording.js.map +1 -1
- package/dist/media/snapshot.js +22 -17
- package/dist/media/snapshot.js.map +1 -1
- package/dist/media/station-live-sessions.js +40 -51
- package/dist/media/station-live-sessions.js.map +1 -1
- package/dist/platform.js +61 -10
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +227 -40
- package/dist/runtime/channel.js.map +1 -1
- package/dist/runtime/device-settings.js +88 -0
- package/dist/runtime/device-settings.js.map +1 -0
- package/dist/runtime/owner.js +30 -5
- package/dist/runtime/owner.js.map +1 -1
- package/dist/runtime/sdk-client.js +21 -17
- 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/dashboard.js +36 -16
- package/dist/ui/dashboard.js.map +1 -1
- package/dist/ui/runtime-channel-client.js +155 -184
- package/dist/ui/runtime-channel-client.js.map +1 -1
- package/dist/ui/server.js +94 -41
- package/dist/ui/server.js.map +1 -1
- package/homebridge-ui/public/app.css +531 -239
- package/homebridge-ui/public/assets/icons/auto_fix_high.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_full.svg +1 -0
- package/homebridge-ui/public/assets/icons/contact_support.svg +1 -0
- package/homebridge-ui/public/assets/icons/delete.svg +1 -0
- package/homebridge-ui/public/assets/icons/download.svg +1 -0
- package/homebridge-ui/public/assets/icons/extension_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/info.svg +1 -0
- package/homebridge-ui/public/assets/icons/key_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_arrow_left.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_arrow_right.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_double_arrow_left.svg +1 -0
- package/homebridge-ui/public/assets/icons/keyboard_double_arrow_right.svg +1 -0
- package/homebridge-ui/public/assets/icons/movie_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/play_arrow.svg +1 -0
- package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
- package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
- package/homebridge-ui/public/assets/icons/search_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/sensors_off.svg +1 -0
- package/homebridge-ui/public/assets/icons/settings_photo_camera.svg +1 -0
- package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
- package/homebridge-ui/public/assets/icons/stop.svg +1 -0
- package/homebridge-ui/public/assets/icons/videocam_off.svg +1 -0
- package/homebridge-ui/public/i18n/en.json +161 -82
- package/homebridge-ui/public/i18n/fr.json +162 -83
- package/homebridge-ui/public/index.html +224 -96
- package/homebridge-ui/public/js/app.js +821 -435
- package/homebridge-ui/public/js/dashboard.js +148 -31
- package/i18n/runtime/en.json +10 -3
- package/package.json +10 -11
- package/homebridge-ui/public/js/legacy-settings.js +0 -37
- package/homebridge-ui/public/js/profile-wizard.js +0 -61
|
@@ -8,12 +8,65 @@
|
|
|
8
8
|
.replaceAll("'", ''');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
const EUFY_MODES = [
|
|
12
|
+
{ value: 'away', label: 'Away' },
|
|
13
|
+
{ value: 'home', label: 'Home' },
|
|
14
|
+
{ value: 'schedule', label: 'Schedule' },
|
|
15
|
+
{ value: 'custom1', label: 'Custom 1' },
|
|
16
|
+
{ value: 'custom2', label: 'Custom 2' },
|
|
17
|
+
{ value: 'custom3', label: 'Custom 3' },
|
|
18
|
+
{ value: 'off', label: 'Off' },
|
|
19
|
+
{ value: 'geo', label: 'Geofencing' },
|
|
20
|
+
{ value: 'disarmed', label: 'Disarmed' },
|
|
21
|
+
];
|
|
22
|
+
/** What each HomeKit state means where the household assigned nothing, as the adapter resolves it. */
|
|
23
|
+
const DEFAULT_ARMING_MODES = { home: 'home', away: 'away', off: 'disarmed' };
|
|
24
|
+
const ARMING_SLOTS = [
|
|
25
|
+
{ slot: 'home', label: 'armingSlotHome' },
|
|
26
|
+
{ slot: 'away', label: 'armingSlotAway' },
|
|
27
|
+
{ slot: 'night', label: 'armingSlotNight' },
|
|
28
|
+
{ slot: 'off', label: 'armingSlotOff' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The guard-mode map: four states HomeKit names against the nine a station reports, one pairing per row.
|
|
33
|
+
*
|
|
34
|
+
* Folded shut, because the four defaults are what nearly every household keeps and the panel has a camera's
|
|
35
|
+
* worth of settings above it. The summary carries the whole map as text, so the answer is readable without
|
|
36
|
+
* opening anything, and a household that assigned a state of its own finds it already open.
|
|
37
|
+
*
|
|
38
|
+
* A row is the whole control — the state names itself and the select beside it says what it means. An
|
|
39
|
+
* unassigned state shows a dash, which is what keeps Night out of HomeKit until it is given a mode.
|
|
40
|
+
*
|
|
41
|
+
* It is withheld while the security system it maps is turned off, and the stored map is kept for when it is back.
|
|
42
|
+
*/
|
|
43
|
+
function armingModeControl(device, assigned, messages, securitySystem) {
|
|
44
|
+
const summary = ARMING_SLOTS.map(
|
|
45
|
+
({ slot, label }) => `${messages[label]} → ${EUFY_MODES.find((mode) => mode.value === assigned[slot])?.label ?? '—'}`,
|
|
46
|
+
).join(' · ');
|
|
47
|
+
const rows = ARMING_SLOTS.map(({ slot, label }) => {
|
|
48
|
+
const value = assigned[slot] ?? '';
|
|
49
|
+
const id = `arming-${escapeHtml(device.serial)}-${slot}`;
|
|
50
|
+
const options = EUFY_MODES.map(
|
|
51
|
+
(mode) =>
|
|
52
|
+
`<option value="${mode.value}"${value === mode.value ? ' selected' : ''}>${escapeHtml(mode.label)}</option>`,
|
|
53
|
+
).join('');
|
|
54
|
+
return `<tr><th scope="row"><label for="${id}">${escapeHtml(messages[label])}</label></th><td><select id="${id}" data-preference="armingModes" data-slot="${slot}" data-serial="${escapeHtml(device.serial)}" data-original="${escapeHtml(value)}"><option value=""${value === '' ? ' selected' : ''}>—</option>${options}</select></td></tr>`;
|
|
55
|
+
}).join('');
|
|
56
|
+
const assignedItself = ARMING_SLOTS.some(({ slot }) => assigned[slot] !== DEFAULT_ARMING_MODES[slot]);
|
|
57
|
+
return `<details class="arming-setting" data-setting${assignedItself ? ' open' : ''}${device.represented && securitySystem ? '' : ' hidden'} data-requires-representation data-requires-security-system><summary><span class="setting-label">${escapeHtml(messages.preferenceArmingModes)}</span><span class="arming-summary">${escapeHtml(summary)}</span></summary><table class="arming-table"><thead><tr><th scope="col">HomeKit</th><th scope="col">eufy</th></tr></thead><tbody>${rows}</tbody></table><p class="snapshot-help">${escapeHtml(messages.armingModesHelp)}</p></details>`;
|
|
58
|
+
}
|
|
59
|
+
|
|
11
60
|
function preferenceControl(device, key, preference, messages) {
|
|
12
61
|
const labels = {
|
|
13
62
|
represented: messages.preferenceRepresented,
|
|
14
63
|
audio: messages.preferenceAudio,
|
|
64
|
+
securitySystem: messages.preferenceSecuritySystem,
|
|
15
65
|
snapshotMode: messages.preferenceSnapshotMode,
|
|
16
66
|
};
|
|
67
|
+
if (key === 'armingModes') {
|
|
68
|
+
return armingModeControl(device, preference.armingModes, messages, preference.securitySystem);
|
|
69
|
+
}
|
|
17
70
|
const represented = preference.represented;
|
|
18
71
|
const dependent = key === 'represented' ? '' : ` data-requires-representation${represented ? '' : ' hidden'}`;
|
|
19
72
|
if (key === 'snapshotMode') {
|
|
@@ -29,7 +82,7 @@
|
|
|
29
82
|
|
|
30
83
|
function markPendingPreference(control, key, value) {
|
|
31
84
|
if (!control?.classList || control.dataset.original === undefined) return;
|
|
32
|
-
const current = key === 'snapshotMode' ? String(value) : String(Boolean(value));
|
|
85
|
+
const current = key === 'snapshotMode' || key === 'armingModes' ? String(value) : String(Boolean(value));
|
|
33
86
|
const setting = control.closest('[data-setting]');
|
|
34
87
|
const changed = current !== control.dataset.original;
|
|
35
88
|
if (key === 'snapshotMode') {
|
|
@@ -43,35 +96,20 @@
|
|
|
43
96
|
tile?.classList.toggle('device-tile-changed', Boolean(tile.querySelector('.preference-control-changed')));
|
|
44
97
|
}
|
|
45
98
|
|
|
46
|
-
/**
|
|
47
|
-
* The eight battery icons the shipped set draws, from empty to full.
|
|
48
|
-
*
|
|
49
|
-
* Ordered, because the level picks one by its position: eight steps of twelve and a half percent each.
|
|
50
|
-
*/
|
|
51
|
-
const BATTERY_STEPS = [
|
|
52
|
-
'battery_0',
|
|
53
|
-
'battery_1',
|
|
54
|
-
'battery_2',
|
|
55
|
-
'battery_3',
|
|
56
|
-
'battery_4',
|
|
57
|
-
'battery_5',
|
|
58
|
-
'battery_6',
|
|
59
|
-
'battery_full',
|
|
60
|
-
];
|
|
61
|
-
|
|
62
99
|
/**
|
|
63
100
|
* The battery badge a device gets, where it reports a level, and nothing where it does not.
|
|
64
101
|
*
|
|
65
102
|
* The icon carries the level and the exact percentage goes to the label: a tile is read at a glance, and a
|
|
66
103
|
* number on every tile would be read deliberately or not at all. A device on mains power reports no level, and
|
|
67
|
-
* 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.
|
|
68
106
|
*/
|
|
69
107
|
function batteryBadge(device, messages) {
|
|
70
108
|
if (typeof device.battery !== 'number') {
|
|
71
109
|
return undefined;
|
|
72
110
|
}
|
|
73
|
-
const step = Math.floor(device.battery /
|
|
74
|
-
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}`;
|
|
75
113
|
const label = (messages.deviceBattery ?? '').replace('{level}', String(Math.round(device.battery)));
|
|
76
114
|
return label ? { icon, label, variant: 'battery' } : undefined;
|
|
77
115
|
}
|
|
@@ -204,21 +242,59 @@
|
|
|
204
242
|
}
|
|
205
243
|
|
|
206
244
|
/*
|
|
207
|
-
* The states a diagnostic run can shed light on. Sign-in, another process holding the session,
|
|
208
|
-
* restart
|
|
245
|
+
* The states a diagnostic run can shed light on. Sign-in, another process holding the session, a pending
|
|
246
|
+
* restart, a plugin that stopped on an error, a page that could not load, and a connection that recovers on its
|
|
247
|
+
* own each have their own remedy stated on the page, so sending those to the wizard would be a detour.
|
|
209
248
|
*/
|
|
210
|
-
const DIAGNOSABLE = new Set(['
|
|
249
|
+
const DIAGNOSABLE = new Set(['incomplete', 'missing', 'stale']);
|
|
250
|
+
|
|
251
|
+
/** The diagnostics area a condition's capture belongs to, by its code. A code no entry matches is `other`. */
|
|
252
|
+
const CONDITION_AREAS = [
|
|
253
|
+
[/^camera-recording/, 'hksv-recording'],
|
|
254
|
+
[/^camera-(?:live|media|streaming|snapshot|talkback)/, 'live-media'],
|
|
255
|
+
[/lock|light|arming|siren|contact|camera-control/, 'control-state'],
|
|
256
|
+
[/transport-degraded/, 'startup-authentication'],
|
|
257
|
+
];
|
|
211
258
|
|
|
259
|
+
/**
|
|
260
|
+
* The problem the diagnostics action points at, or nothing: a state a run explains, then the first active condition
|
|
261
|
+
* this page has words for, then the first device nothing can reach. Each names the diagnostics area a run on it is
|
|
262
|
+
* about. A condition is worded under its log keys; the ones left unworded are those the page answers on its own,
|
|
263
|
+
* such as a sign-in or another owner, and the devices no action makes supported.
|
|
264
|
+
*/
|
|
265
|
+
function attention(result, state, messages) {
|
|
266
|
+
if (DIAGNOSABLE.has(result.state)) return { ...state, profile: 'startup-authentication' };
|
|
267
|
+
const condition = (result.conditions ?? []).find(
|
|
268
|
+
({ summaryKey, actionKey }) => messages[summaryKey] && messages[actionKey],
|
|
269
|
+
);
|
|
270
|
+
if (condition) {
|
|
271
|
+
return {
|
|
272
|
+
title: messages[condition.summaryKey],
|
|
273
|
+
summary: messages[condition.actionKey],
|
|
274
|
+
profile: CONDITION_AREAS.find(([code]) => code.test(condition.code))?.[1] ?? 'other',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const device = result.devices.find((candidate) => candidate.availability === 'unavailable');
|
|
278
|
+
return (
|
|
279
|
+
device && {
|
|
280
|
+
title: (messages.attentionUnreachableTitle ?? '').replace('{device}', device.name),
|
|
281
|
+
summary: messages.attentionUnreachableSummary ?? '',
|
|
282
|
+
profile: 'device-representation',
|
|
283
|
+
}
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Draws the dashboard, and answers the problem it shows the diagnostics action, if any. */
|
|
212
288
|
function render(result, config, messages, elements) {
|
|
213
289
|
const suffix = result.state
|
|
214
290
|
.split('-')
|
|
215
291
|
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
216
292
|
.join('');
|
|
217
|
-
|
|
218
|
-
|
|
293
|
+
const title = messages[`dashboard${suffix}Title`] ?? messages.dashboardIncompleteTitle;
|
|
294
|
+
const summary = messages[`dashboard${suffix}Summary`] ?? messages.dashboardIncompleteSummary;
|
|
295
|
+
elements.title.textContent = title;
|
|
296
|
+
elements.summary.textContent = summary;
|
|
219
297
|
elements.dashboard.dataset.state = result.state;
|
|
220
|
-
// Offered rather than announced: a run is worth suggesting only where the state is one a run explains.
|
|
221
|
-
if (elements.diagnose) elements.diagnose.hidden = !DIAGNOSABLE.has(result.state);
|
|
222
298
|
elements.dashboard.hidden = false;
|
|
223
299
|
elements.masthead.hidden = true;
|
|
224
300
|
renderUpdatePending(result, messages, elements);
|
|
@@ -230,6 +306,7 @@
|
|
|
230
306
|
} else if (result.state === 'authentication-required') {
|
|
231
307
|
elements.setup.hidden = false;
|
|
232
308
|
}
|
|
309
|
+
return attention(result, { title, summary }, messages);
|
|
233
310
|
}
|
|
234
311
|
|
|
235
312
|
function bindPreferences(elements, getConfig, saveConfig, getMessages, openDevice) {
|
|
@@ -254,13 +331,17 @@
|
|
|
254
331
|
},
|
|
255
332
|
true,
|
|
256
333
|
);
|
|
257
|
-
|
|
334
|
+
const onPreferenceChange = async (event) => {
|
|
258
335
|
const control = event.target;
|
|
259
336
|
const serial = control?.dataset?.serial;
|
|
260
337
|
const key = control?.dataset?.preference;
|
|
261
338
|
const existing = getConfig();
|
|
262
339
|
if (!existing || !serial || !key) return;
|
|
263
|
-
const defaults = { represented: true, audio: true, snapshotMode: 'Refresh' };
|
|
340
|
+
const defaults = { represented: true, audio: true, securitySystem: true, snapshotMode: 'Refresh' };
|
|
341
|
+
if (key === 'armingModes') {
|
|
342
|
+
await saveArmingMode(control, serial, existing, saveConfig, getMessages, elements);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
264
345
|
const value = key === 'snapshotMode' ? control.value : control.checked;
|
|
265
346
|
markPendingPreference(control, key, value);
|
|
266
347
|
if (key === 'snapshotMode') {
|
|
@@ -282,9 +363,16 @@
|
|
|
282
363
|
(candidate) => candidate.dataset.serial === serial,
|
|
283
364
|
);
|
|
284
365
|
tile?.classList.toggle('device-tile-disabled', !value);
|
|
366
|
+
const grid = control.closest('.preference-grid');
|
|
367
|
+
const securityOff = grid?.querySelector('[data-preference="securitySystem"]')?.checked === false;
|
|
368
|
+
grid?.querySelectorAll('[data-requires-representation]').forEach((setting) => {
|
|
369
|
+
setting.hidden = !value || (securityOff && setting.hasAttribute('data-requires-security-system'));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (key === 'securitySystem') {
|
|
285
373
|
control
|
|
286
374
|
.closest('.preference-grid')
|
|
287
|
-
?.querySelectorAll('[data-requires-
|
|
375
|
+
?.querySelectorAll('[data-requires-security-system]')
|
|
288
376
|
.forEach((setting) => {
|
|
289
377
|
setting.hidden = !value;
|
|
290
378
|
});
|
|
@@ -292,7 +380,34 @@
|
|
|
292
380
|
} catch {
|
|
293
381
|
elements.summary.textContent = getMessages().preferenceSaveFailed;
|
|
294
382
|
}
|
|
295
|
-
}
|
|
383
|
+
};
|
|
384
|
+
for (const container of elements.preferenceContainers ?? [elements.deviceSettingsControls ?? elements.groups]) {
|
|
385
|
+
container.addEventListener('change', onPreferenceChange);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Records one HomeKit state's guard mode, keeping the map sparse: a state set back to the plugin's own choice
|
|
391
|
+
* is removed rather than pinned, so a later default reaches a household that never overrode it.
|
|
392
|
+
*/
|
|
393
|
+
async function saveArmingMode(control, serial, existing, saveConfig, getMessages, elements) {
|
|
394
|
+
const slot = control.dataset.slot;
|
|
395
|
+
const value = control.value;
|
|
396
|
+
markPendingPreference(control, 'armingModes', value);
|
|
397
|
+
const entityPreferences = { ...(existing.entityPreferences ?? {}) };
|
|
398
|
+
const preference = { ...(entityPreferences[serial] ?? {}) };
|
|
399
|
+
const armingModes = { ...(preference.armingModes ?? {}) };
|
|
400
|
+
if (value === '') delete armingModes[slot];
|
|
401
|
+
else armingModes[slot] = value;
|
|
402
|
+
if (Object.keys(armingModes).length === 0) delete preference.armingModes;
|
|
403
|
+
else preference.armingModes = armingModes;
|
|
404
|
+
if (Object.keys(preference).length === 0) delete entityPreferences[serial];
|
|
405
|
+
else entityPreferences[serial] = preference;
|
|
406
|
+
try {
|
|
407
|
+
await saveConfig({ ...existing, entityPreferences });
|
|
408
|
+
} catch {
|
|
409
|
+
elements.summary.textContent = getMessages().preferenceSaveFailed;
|
|
410
|
+
}
|
|
296
411
|
}
|
|
297
412
|
|
|
298
413
|
/**
|
|
@@ -312,7 +427,9 @@
|
|
|
312
427
|
const preference = {
|
|
313
428
|
represented: preferences[device.serial]?.represented ?? true,
|
|
314
429
|
audio: preferences[device.serial]?.audio ?? true,
|
|
430
|
+
securitySystem: preferences[device.serial]?.securitySystem ?? true,
|
|
315
431
|
snapshotMode: preferences[device.serial]?.snapshotMode ?? 'Refresh',
|
|
432
|
+
armingModes: { ...DEFAULT_ARMING_MODES, ...preferences[device.serial]?.armingModes },
|
|
316
433
|
};
|
|
317
434
|
const controls = device.preferences
|
|
318
435
|
.map((key) => preferenceControl(device, key, preference, messages))
|
package/i18n/runtime/en.json
CHANGED
|
@@ -4,22 +4,28 @@
|
|
|
4
4
|
"log.action.checkPhysicalArmingMode": "Check the physical security-system mode before retrying",
|
|
5
5
|
"log.action.checkPhysicalLight": "Check the physical smart-light state before retrying",
|
|
6
6
|
"log.action.checkPhysicalLock": "Check the physical lock state before retrying",
|
|
7
|
+
"log.action.controlTimedOut": "The device did not answer in time; use the Eufy app for this control, and attach a diagnostics file to your issue if it keeps happening",
|
|
7
8
|
"log.action.checkCameraSnapshot": "Check that the camera is reachable and can produce an image, then open the camera again",
|
|
8
9
|
"log.action.waitCameraControl": "No action is needed; the control returns when the camera reports it again",
|
|
9
10
|
"log.action.checkCameraControl": "Check the camera in the Eufy app; this control is unavailable until it reports its state",
|
|
10
11
|
"log.action.retryCameraControl": "Try the control again; the camera did not confirm the change",
|
|
11
12
|
"log.action.enableCamera": "Turn the camera on in the Eufy app, then open live view again",
|
|
12
|
-
"log.action.
|
|
13
|
+
"log.action.notSupportedYet": "No action is needed; these devices are not supported in HomeKit yet",
|
|
13
14
|
"log.action.reduceConcurrentMedia": "Open live view again; if it keeps happening, close a camera already open or raise the concurrent camera media limit in the plugin settings",
|
|
14
15
|
"log.action.reauthenticate": "Authenticate again in the Homebridge Eufy dashboard, then restart Homebridge",
|
|
15
16
|
"log.action.retryArming": "Retry the security-system control manually",
|
|
16
17
|
"log.action.retryLight": "Retry the smart-light control manually",
|
|
17
18
|
"log.action.retryLiveView": "Open live view again, and check network access between Homebridge and the camera",
|
|
19
|
+
"log.action.setFfmpegPath": "Set a custom FFmpeg path in Advanced settings in the Homebridge Eufy dashboard",
|
|
20
|
+
"log.action.checkFfmpegPath": "FFmpeg could not be started; check the custom FFmpeg path in Advanced settings in the Homebridge Eufy dashboard",
|
|
21
|
+
"log.action.checkStation": "Check that the station or HomeBase this camera uses is powered and online, and network access between Homebridge and it, then open live view again",
|
|
22
|
+
"log.action.retryLiveViewNoPicture": "The camera answered but sent no picture in time. Open live view again; if it keeps happening, send a diagnostics file",
|
|
18
23
|
"log.action.retryLock": "Retry the lock control manually",
|
|
19
24
|
"log.action.retryTalkback": "Close and reopen the camera, then try speaking again",
|
|
25
|
+
"log.action.checkTalkbackMicrophone": "The Home app sent no voice. Allow the Home app to use the microphone in iPhone Settings, then tap the microphone button in the live view and speak",
|
|
20
26
|
"log.action.reviewRuntime": "Review Homebridge Eufy health in the dashboard and restart Homebridge",
|
|
21
27
|
"log.action.stopOtherOwner": "Stop the other Homebridge Eufy owner before restarting Homebridge",
|
|
22
|
-
"log.action.
|
|
28
|
+
"log.action.mapArmingMode": "Assign this mode to a HomeKit state under HomeKit guard modes in the device's settings in the Homebridge Eufy dashboard",
|
|
23
29
|
"log.action.waitBattery": "Wait for complete battery capability evidence",
|
|
24
30
|
"log.action.waitArming": "Wait for complete arming capability evidence",
|
|
25
31
|
"log.action.waitBatteryObservation": "Wait for a valid battery observation",
|
|
@@ -39,6 +45,7 @@
|
|
|
39
45
|
"log.condition.affectedOne": "1 accessory is affected.",
|
|
40
46
|
"log.condition.recovered": "Recovered: {summary}.",
|
|
41
47
|
"log.diagnosticFileFailed": "Diagnostic file logging failed; check storage permissions.",
|
|
48
|
+
"log.liveVideoEncoders": "Live video encoders, in order of use: {encoders}.",
|
|
42
49
|
"log.discardedSettings": "Some legacy settings were discarded. Review and acknowledge them in the Homebridge Eufy dashboard.",
|
|
43
50
|
"log.snapshotCacheInvalid": "An invalid retained camera image was removed",
|
|
44
51
|
"log.homekit.batteryCapabilityUnavailable": "Battery information is unavailable",
|
|
@@ -85,7 +92,7 @@
|
|
|
85
92
|
"log.notice.stateSubscriberFailed": "A runtime state consumer failed; review Homebridge Eufy health in the dashboard.",
|
|
86
93
|
"log.notice.statusPublicationFailed": "Runtime status could not be published; dashboard status may be stale.",
|
|
87
94
|
"log.notice.channelServeFailed": "Live runtime status could not be served to the plugin settings page; it will show the last published status instead.",
|
|
88
|
-
"log.notice.diagnosticsAuthorizationArmed": "
|
|
95
|
+
"log.notice.diagnosticsAuthorizationArmed": "A diagnostics capture started; finish it from the plugin settings before {finishBy}.",
|
|
89
96
|
"log.notice.standDownNotRearmed": "Homebridge Eufy stood down for an authentication and could not take the account session back; restart Homebridge to bring it back.",
|
|
90
97
|
"log.notice.standDownAccountReplaced": "Homebridge Eufy stood down for an authentication that replaced the account; restart Homebridge to use the new account.",
|
|
91
98
|
"log.runtime.authenticationRequired": "Eufy authentication is required",
|
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.41",
|
|
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,21 +44,23 @@
|
|
|
47
44
|
],
|
|
48
45
|
"dependencies": {
|
|
49
46
|
"@homebridge/plugin-ui-utils": "^2.2.6",
|
|
50
|
-
"@mega-yfue/eufy-sdk": "0.
|
|
47
|
+
"@mega-yfue/eufy-sdk": "0.6.0-beta.26",
|
|
51
48
|
"ffmpeg-for-homebridge": "2.2.2"
|
|
52
49
|
},
|
|
53
50
|
"devDependencies": {
|
|
54
|
-
"@homebridge/hap-nodejs": "2.2.
|
|
55
|
-
"@types/node": "^26.
|
|
51
|
+
"@homebridge/hap-nodejs": "2.2.3",
|
|
52
|
+
"@types/node": "^26.6.5",
|
|
56
53
|
"homebridge": "^2.4.0",
|
|
57
|
-
"prettier": "^3.9.
|
|
58
|
-
"rimraf": "^6.1.3",
|
|
54
|
+
"prettier": "^3.9.10",
|
|
59
55
|
"typescript": "^7.0.2",
|
|
60
|
-
"vitest": "^5.0.
|
|
56
|
+
"vitest": "^5.0.5"
|
|
61
57
|
},
|
|
62
58
|
"homepage": "https://homebridge-plugins.github.io/homebridge-eufy/",
|
|
63
59
|
"author": "homebridge-eufy",
|
|
64
60
|
"allowScripts": {
|
|
65
61
|
"ffmpeg-for-homebridge@2.2.2": true
|
|
62
|
+
},
|
|
63
|
+
"optionalDependencies": {
|
|
64
|
+
"node-datachannel": "0.33.5"
|
|
66
65
|
}
|
|
67
66
|
}
|
|
@@ -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,61 +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
|
-
const deviceProfiles = ['device-representation', 'control-state', 'live-media', 'hksv-recording'];
|
|
13
|
-
|
|
14
|
-
function start() {
|
|
15
|
-
return { mode: 'tiles', profile: undefined };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function select(state, profile) {
|
|
19
|
-
return { ...state, mode: deviceProfiles.includes(profile) ? 'devices' : 'frequency', profile };
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function chooseDevices(state, devices) {
|
|
23
|
-
return { ...state, mode: 'frequency', devices };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function reject() {
|
|
27
|
-
return start();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function chooseReproductionMode(state, reproductionMode) {
|
|
31
|
-
return { ...state, mode: 'match', reproductionMode };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function backFromFrequency(state) {
|
|
35
|
-
return deviceProfiles.includes(state.profile) ? { ...state, mode: 'devices' } : start();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function screen(session, startingAnother) {
|
|
39
|
-
if (session.partialExportAvailable && !startingAnother) return 'review';
|
|
40
|
-
if (startingAnother || session.status === 'inactive' || session.status === 'expired') return 'choose';
|
|
41
|
-
if (session.status === 'authorized' || session.status === 'reproducing') return 'reproduce';
|
|
42
|
-
return 'status';
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function backgroundActive(session) {
|
|
46
|
-
return session.status === 'reproducing' && session.profile === 'dashboard-ui';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
global.HomebridgeEufyDiagnosticsWizard = {
|
|
50
|
-
backFromFrequency,
|
|
51
|
-
backgroundActive,
|
|
52
|
-
chooseDevices,
|
|
53
|
-
chooseReproductionMode,
|
|
54
|
-
deviceProfiles,
|
|
55
|
-
profiles,
|
|
56
|
-
reject,
|
|
57
|
-
screen,
|
|
58
|
-
select,
|
|
59
|
-
start,
|
|
60
|
-
};
|
|
61
|
-
})(window);
|