homebridge-eufy-security 4.4.2-beta.2 → 4.4.2-beta.20

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 (147) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/FUNDING.yml +1 -0
  3. package/README.md +14 -17
  4. package/dist/accessories/AutoSyncStationAccessory.js +26 -30
  5. package/dist/accessories/AutoSyncStationAccessory.js.map +1 -1
  6. package/dist/accessories/BaseAccessory.js +33 -46
  7. package/dist/accessories/BaseAccessory.js.map +1 -1
  8. package/dist/accessories/CameraAccessory.js +92 -167
  9. package/dist/accessories/CameraAccessory.js.map +1 -1
  10. package/dist/accessories/Device.js +11 -15
  11. package/dist/accessories/Device.js.map +1 -1
  12. package/dist/accessories/EntrySensorAccessory.js +7 -12
  13. package/dist/accessories/EntrySensorAccessory.js.map +1 -1
  14. package/dist/accessories/LockAccessory.js +20 -24
  15. package/dist/accessories/LockAccessory.js.map +1 -1
  16. package/dist/accessories/MotionSensorAccessory.js +7 -12
  17. package/dist/accessories/MotionSensorAccessory.js.map +1 -1
  18. package/dist/accessories/StationAccessory.js +40 -45
  19. package/dist/accessories/StationAccessory.js.map +1 -1
  20. package/dist/config.js +1 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/controller/LocalLivestreamManager.js +85 -89
  23. package/dist/controller/LocalLivestreamManager.js.map +1 -1
  24. package/dist/controller/recordingDelegate.js +116 -108
  25. package/dist/controller/recordingDelegate.js.map +1 -1
  26. package/dist/controller/snapshotDelegate.js +379 -0
  27. package/dist/controller/snapshotDelegate.js.map +1 -0
  28. package/dist/controller/streamingDelegate.js +202 -207
  29. package/dist/controller/streamingDelegate.js.map +1 -1
  30. package/dist/index.js +9 -7
  31. package/dist/index.js.map +1 -1
  32. package/dist/interfaces.js +1 -2
  33. package/dist/interfaces.js.map +1 -1
  34. package/dist/platform.js +145 -165
  35. package/dist/platform.js.map +1 -1
  36. package/dist/settings.js +26 -19
  37. package/dist/settings.js.map +1 -1
  38. package/dist/utils/Talkback.js +9 -13
  39. package/dist/utils/Talkback.js.map +1 -1
  40. package/dist/utils/configTypes.js +12 -7
  41. package/dist/utils/configTypes.js.map +1 -1
  42. package/dist/utils/ffmpeg.js +44 -52
  43. package/dist/utils/ffmpeg.js.map +1 -1
  44. package/dist/utils/interfaces.js +1 -5
  45. package/dist/utils/interfaces.js.map +1 -1
  46. package/dist/utils/utils.js +32 -134
  47. package/dist/utils/utils.js.map +1 -1
  48. package/dist/version.js +1 -4
  49. package/dist/version.js.map +1 -1
  50. package/eslint.config.mjs +25 -47
  51. package/homebridge-eufy-security.png +0 -0
  52. package/homebridge-ui/public/app.js +221 -0
  53. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  54. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  55. package/homebridge-ui/public/assets/devices/homebasemini_large.jpg +0 -0
  56. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  57. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  58. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  59. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  60. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  61. package/homebridge-ui/public/components/device-card.js +141 -0
  62. package/homebridge-ui/public/components/guard-modes.js +88 -0
  63. package/homebridge-ui/public/components/number-input.js +121 -0
  64. package/homebridge-ui/public/components/select.js +73 -0
  65. package/homebridge-ui/public/components/toggle.js +68 -0
  66. package/homebridge-ui/public/index.html +24 -14
  67. package/homebridge-ui/public/services/api.js +129 -0
  68. package/homebridge-ui/public/services/config.js +144 -0
  69. package/homebridge-ui/public/style.css +705 -0
  70. package/homebridge-ui/public/utils/countries.js +73 -0
  71. package/homebridge-ui/public/utils/device-images.js +69 -0
  72. package/homebridge-ui/public/utils/helpers.js +47 -0
  73. package/homebridge-ui/public/views/dashboard.js +211 -0
  74. package/homebridge-ui/public/views/device-detail.js +631 -0
  75. package/homebridge-ui/public/views/login.js +625 -0
  76. package/homebridge-ui/public/views/settings.js +389 -0
  77. package/homebridge-ui/public/views/unsupported-detail.js +216 -0
  78. package/homebridge-ui/server.js +567 -450
  79. package/package.json +19 -59
  80. package/dist/accessories/AutoSyncStationAccessory.d.ts +0 -42
  81. package/dist/accessories/AutoSyncStationAccessory.d.ts.map +0 -1
  82. package/dist/accessories/BaseAccessory.d.ts +0 -56
  83. package/dist/accessories/BaseAccessory.d.ts.map +0 -1
  84. package/dist/accessories/CameraAccessory.d.ts +0 -84
  85. package/dist/accessories/CameraAccessory.d.ts.map +0 -1
  86. package/dist/accessories/Device.d.ts +0 -19
  87. package/dist/accessories/Device.d.ts.map +0 -1
  88. package/dist/accessories/EntrySensorAccessory.d.ts +0 -25
  89. package/dist/accessories/EntrySensorAccessory.d.ts.map +0 -1
  90. package/dist/accessories/LockAccessory.d.ts +0 -45
  91. package/dist/accessories/LockAccessory.d.ts.map +0 -1
  92. package/dist/accessories/MotionSensorAccessory.d.ts +0 -25
  93. package/dist/accessories/MotionSensorAccessory.d.ts.map +0 -1
  94. package/dist/accessories/StationAccessory.d.ts +0 -86
  95. package/dist/accessories/StationAccessory.d.ts.map +0 -1
  96. package/dist/config.d.ts +0 -31
  97. package/dist/config.d.ts.map +0 -1
  98. package/dist/controller/LocalLivestreamManager.d.ts +0 -31
  99. package/dist/controller/LocalLivestreamManager.d.ts.map +0 -1
  100. package/dist/controller/SnapshotManager.d.ts +0 -64
  101. package/dist/controller/SnapshotManager.d.ts.map +0 -1
  102. package/dist/controller/SnapshotManager.js +0 -434
  103. package/dist/controller/SnapshotManager.js.map +0 -1
  104. package/dist/controller/recordingDelegate.d.ts +0 -27
  105. package/dist/controller/recordingDelegate.d.ts.map +0 -1
  106. package/dist/controller/streamingDelegate.d.ts +0 -52
  107. package/dist/controller/streamingDelegate.d.ts.map +0 -1
  108. package/dist/index.d.ts +0 -7
  109. package/dist/index.d.ts.map +0 -1
  110. package/dist/interfaces.d.ts +0 -15
  111. package/dist/interfaces.d.ts.map +0 -1
  112. package/dist/platform.d.ts +0 -94
  113. package/dist/platform.d.ts.map +0 -1
  114. package/dist/settings.d.ts +0 -23
  115. package/dist/settings.d.ts.map +0 -1
  116. package/dist/utils/Talkback.d.ts +0 -21
  117. package/dist/utils/Talkback.d.ts.map +0 -1
  118. package/dist/utils/configTypes.d.ts +0 -68
  119. package/dist/utils/configTypes.d.ts.map +0 -1
  120. package/dist/utils/ffmpeg.d.ts +0 -111
  121. package/dist/utils/ffmpeg.d.ts.map +0 -1
  122. package/dist/utils/interfaces.d.ts +0 -8
  123. package/dist/utils/interfaces.d.ts.map +0 -1
  124. package/dist/utils/utils.d.ts +0 -35
  125. package/dist/utils/utils.d.ts.map +0 -1
  126. package/dist/version.d.ts +0 -2
  127. package/dist/version.d.ts.map +0 -1
  128. package/homebridge-ui/configui/app/util/types.d.ts +0 -65
  129. package/homebridge-ui/configui/app/util/types.d.ts.map +0 -1
  130. package/homebridge-ui/configui/app/util/types.js +0 -18
  131. package/homebridge-ui/configui/app/util/types.js.map +0 -1
  132. package/homebridge-ui/public/3rdpartylicenses.txt +0 -561
  133. package/homebridge-ui/public/assets/devices/homebase2_large.jpg +0 -0
  134. package/homebridge-ui/public/assets/devices/homebase3_large.jpg +0 -0
  135. package/homebridge-ui/public/assets/images/homebridge-eufy-security.png +0 -0
  136. package/homebridge-ui/public/favicon.ico +0 -0
  137. package/homebridge-ui/public/main.f6423287e0411c87.js +0 -1
  138. package/homebridge-ui/public/polyfills.efa2f09ba3e49167.js +0 -1
  139. package/homebridge-ui/public/runtime.ccde331d62c423b3.js +0 -1
  140. package/homebridge-ui/public/styles.13e635bf7a488639.css +0 -5
  141. package/homebridge-ui/server.d.ts +0 -2
  142. package/homebridge-ui/server.d.ts.map +0 -1
  143. package/homebridge-ui/server.js.map +0 -1
  144. package/homebridge-ui/version.d.ts +0 -2
  145. package/homebridge-ui/version.d.ts.map +0 -1
  146. package/homebridge-ui/version.js +0 -5
  147. package/homebridge-ui/version.js.map +0 -1
@@ -0,0 +1,389 @@
1
+ /**
2
+ * Settings View — global plugin settings with progressive disclosure.
3
+ * Simple: re-login, download logs, reset.
4
+ * Advanced: polling, livestream, guard modes, auto-sync, etc.
5
+ */
6
+ // eslint-disable-next-line no-unused-vars
7
+ const SettingsView = {
8
+
9
+ _advancedOpen: false,
10
+ _downloadInProgress: false,
11
+
12
+ async render(container) {
13
+ container.innerHTML = '';
14
+ this._advancedOpen = false;
15
+
16
+ const config = await Config.get();
17
+
18
+ // Header
19
+ const header = document.createElement('div');
20
+ header.className = 'eufy-header';
21
+
22
+ const backBtn = document.createElement('button');
23
+ backBtn.className = 'btn btn-link p-0';
24
+ backBtn.innerHTML = '← Back';
25
+ backBtn.style.textDecoration = 'none';
26
+ backBtn.addEventListener('click', () => App.navigate('dashboard'));
27
+
28
+ const titleEl = document.createElement('h4');
29
+ titleEl.textContent = 'Settings';
30
+
31
+ header.appendChild(backBtn);
32
+ header.appendChild(titleEl);
33
+ // Empty spacer for alignment
34
+ header.appendChild(document.createElement('div'));
35
+ container.appendChild(header);
36
+
37
+ // ── Quick Actions ──
38
+ const actionsSection = document.createElement('div');
39
+ actionsSection.className = 'settings-section';
40
+
41
+ const actionsTitle = document.createElement('div');
42
+ actionsTitle.className = 'detail-section__title';
43
+ actionsTitle.textContent = 'Quick Actions';
44
+ actionsSection.appendChild(actionsTitle);
45
+
46
+ const btnRow = document.createElement('div');
47
+ btnRow.className = 'settings-btn-row';
48
+
49
+ // Re-login button
50
+ const btnLogin = document.createElement('button');
51
+ btnLogin.className = 'btn btn-outline-primary btn-sm';
52
+ btnLogin.textContent = '🔄 Re-login / Re-fetch new devices';
53
+ btnLogin.addEventListener('click', () => App.navigate('login'));
54
+ btnRow.appendChild(btnLogin);
55
+
56
+ // Download logs button
57
+ const btnLogs = document.createElement('button');
58
+ btnLogs.className = 'btn btn-outline-secondary btn-sm';
59
+ btnLogs.textContent = '📋 Download Logs';
60
+ btnLogs.addEventListener('click', () => this._downloadLogs(container));
61
+ btnRow.appendChild(btnLogs);
62
+
63
+ // Bug report button
64
+ const btnBug = document.createElement('button');
65
+ btnBug.className = 'btn btn-outline-secondary btn-sm';
66
+ btnBug.textContent = '🐛 Report Issue';
67
+ btnBug.addEventListener('click', () => this._reportIssue());
68
+ btnRow.appendChild(btnBug);
69
+
70
+ // Reset plugin button
71
+ const btnReset = document.createElement('button');
72
+ btnReset.className = 'btn btn-outline-danger btn-sm';
73
+ btnReset.textContent = '🗑️ Reset Plugin';
74
+ btnReset.addEventListener('click', () => this._confirmReset(container));
75
+ btnRow.appendChild(btnReset);
76
+
77
+ actionsSection.appendChild(btnRow);
78
+
79
+ // Log download progress placeholder
80
+ const logProgress = document.createElement('div');
81
+ logProgress.id = 'log-download-progress';
82
+ actionsSection.appendChild(logProgress);
83
+
84
+ container.appendChild(actionsSection);
85
+
86
+ // ── Credentials Info ──
87
+ const credsSection = document.createElement('div');
88
+ credsSection.className = 'settings-section';
89
+
90
+ const credsTitle = document.createElement('div');
91
+ credsTitle.className = 'detail-section__title';
92
+ credsTitle.textContent = 'Account';
93
+ credsSection.appendChild(credsTitle);
94
+
95
+ const credsInfo = document.createElement('div');
96
+ credsInfo.className = 'text-muted';
97
+ credsInfo.style.fontSize = '0.85rem';
98
+ const email = config.username || 'Not configured';
99
+ const country = config.country || '—';
100
+ credsInfo.innerHTML = `
101
+ <div class="mb-1"><strong>Email:</strong> ${this._escHtml(email)}</div>
102
+ <div><strong>Country:</strong> ${country}</div>
103
+ `;
104
+ credsSection.appendChild(credsInfo);
105
+ container.appendChild(credsSection);
106
+
107
+ // ── Debugging ──
108
+ const debugSection = document.createElement('div');
109
+ debugSection.className = 'settings-section';
110
+
111
+ const debugTitle = document.createElement('div');
112
+ debugTitle.className = 'detail-section__title';
113
+ debugTitle.textContent = 'Debugging';
114
+ debugSection.appendChild(debugTitle);
115
+
116
+ Toggle.render(debugSection, {
117
+ id: 'toggle-detailed-log',
118
+ label: 'Enable Debug Logging',
119
+ help: 'Enable verbose logging for troubleshooting. When reporting issues, enable this, reproduce the problem, then use the "Download Logs" button above to capture everything.',
120
+ checked: !!config.enableDetailedLogging,
121
+ onChange: async (checked) => {
122
+ await Config.updateGlobal({ enableDetailedLogging: checked });
123
+ },
124
+ });
125
+
126
+ const debugHint = document.createElement('div');
127
+ debugHint.className = 'text-muted mt-1';
128
+ debugHint.style.fontSize = '0.8rem';
129
+ debugHint.innerHTML = '💡 After enabling, reproduce the issue, then click <strong>📋 Download Logs</strong> above to collect the debug output.<br>⚠️ Remember to disable debug logging once you\'re done — it generates a lot of data and may impact performance.';
130
+ debugSection.appendChild(debugHint);
131
+
132
+ container.appendChild(debugSection);
133
+
134
+ // ── Advanced Settings ──
135
+ const advBtn = document.createElement('button');
136
+ advBtn.className = 'advanced-toggle';
137
+ advBtn.innerHTML = `
138
+ <span class="advanced-toggle__chevron" id="settings-adv-chevron">▶</span>
139
+ Advanced Settings
140
+ `;
141
+ advBtn.addEventListener('click', () => {
142
+ this._advancedOpen = !this._advancedOpen;
143
+ const advSection = container.querySelector('#settings-advanced');
144
+ if (advSection) advSection.style.display = this._advancedOpen ? 'block' : 'none';
145
+ const chevron = container.querySelector('#settings-adv-chevron');
146
+ if (chevron) chevron.classList.toggle('advanced-toggle__chevron--open', this._advancedOpen);
147
+ });
148
+ container.appendChild(advBtn);
149
+
150
+ const advSection = document.createElement('div');
151
+ advSection.id = 'settings-advanced';
152
+ advSection.style.display = 'none';
153
+
154
+ // ── Polling & Livestream ──
155
+ const perfTitle = document.createElement('div');
156
+ perfTitle.className = 'detail-section__title';
157
+ perfTitle.textContent = 'Performance';
158
+ advSection.appendChild(perfTitle);
159
+
160
+ NumberInput.render(advSection, {
161
+ id: 'num-polling',
162
+ label: 'Polling Interval',
163
+ help: 'How often (in minutes) to poll the Eufy Cloud for updates. Higher values = less API usage.',
164
+ value: config.pollingIntervalMinutes || 10,
165
+ min: 1, max: 120, step: 1,
166
+ suffix: 'min',
167
+ onChange: async (val) => {
168
+ await Config.updateGlobal({ pollingIntervalMinutes: val });
169
+ },
170
+ });
171
+
172
+ NumberInput.render(advSection, {
173
+ id: 'num-livestream',
174
+ label: 'Max Livestream Duration',
175
+ help: 'Maximum duration (in seconds) for a single livestream session.',
176
+ value: config.CameraMaxLivestreamDuration || 30,
177
+ min: 10, max: 86400, step: 10,
178
+ suffix: 'sec',
179
+ onChange: async (val) => {
180
+ await Config.updateGlobal({ CameraMaxLivestreamDuration: val });
181
+ },
182
+ });
183
+
184
+ // ── Default Guard Modes ──
185
+ const guardTitle = document.createElement('div');
186
+ guardTitle.className = 'detail-section__title mt-3';
187
+ guardTitle.textContent = 'Default Guard Modes';
188
+ advSection.appendChild(guardTitle);
189
+
190
+ const guardHelp = document.createElement('p');
191
+ guardHelp.className = 'text-muted';
192
+ guardHelp.style.fontSize = '0.8rem';
193
+ guardHelp.textContent = 'Default HomeKit-to-Eufy guard mode mapping. Can be overridden per station.';
194
+ advSection.appendChild(guardHelp);
195
+
196
+ GuardModes.render(advSection, {
197
+ hkHome: config.hkHome ?? 1,
198
+ hkAway: config.hkAway ?? 0,
199
+ hkNight: config.hkNight ?? 1,
200
+ hkOff: config.hkOff ?? 63,
201
+ onChange: async (modes) => {
202
+ await Config.updateGlobal(modes);
203
+ },
204
+ });
205
+
206
+ // ── Misc Toggles ──
207
+ const miscTitle = document.createElement('div');
208
+ miscTitle.className = 'detail-section__title mt-3';
209
+ miscTitle.textContent = 'Miscellaneous';
210
+ advSection.appendChild(miscTitle);
211
+
212
+ Toggle.render(advSection, {
213
+ id: 'toggle-auto-sync',
214
+ label: 'Auto Sync Station',
215
+ help: 'Automatically synchronize station mode with HomeKit security system.',
216
+ checked: !!config.autoSyncStation,
217
+ onChange: async (checked) => {
218
+ await Config.updateGlobal({ autoSyncStation: checked });
219
+ },
220
+ });
221
+
222
+ Toggle.render(advSection, {
223
+ id: 'toggle-clean-cache',
224
+ label: 'Clean Cache',
225
+ help: 'Remove stale cached accessories on next restart.',
226
+ checked: !!config.cleanCache,
227
+ onChange: async (checked) => {
228
+ await Config.updateGlobal({ cleanCache: checked });
229
+ },
230
+ });
231
+
232
+ Toggle.render(advSection, {
233
+ id: 'toggle-omit-logs',
234
+ label: 'Omit Log Files',
235
+ help: 'Disable writing plugin log files to disk.',
236
+ checked: !!config.omitLogFiles,
237
+ onChange: async (checked) => {
238
+ await Config.updateGlobal({ omitLogFiles: checked });
239
+ },
240
+ });
241
+
242
+ Toggle.render(advSection, {
243
+ id: 'toggle-ignore-multi-warning',
244
+ label: 'Ignore Multiple Devices Warning',
245
+ help: 'Suppress warning when multiple plugins manage the same device.',
246
+ checked: !!config.ignoreMultipleDevicesWarning,
247
+ onChange: async (checked) => {
248
+ await Config.updateGlobal({ ignoreMultipleDevicesWarning: checked });
249
+ },
250
+ });
251
+
252
+ Toggle.render(advSection, {
253
+ id: 'toggle-pkcs1',
254
+ label: 'Embedded PKCS1 Support',
255
+ help: 'Enable embedded PKCS1 support for device communication.',
256
+ checked: !!config.enableEmbeddedPKCS1Support,
257
+ onChange: async (checked) => {
258
+ await Config.updateGlobal({ enableEmbeddedPKCS1Support: checked });
259
+ },
260
+ });
261
+
262
+ container.appendChild(advSection);
263
+
264
+
265
+ },
266
+
267
+ // ===== Report Issue =====
268
+ async _reportIssue() {
269
+ try {
270
+ homebridge.toast.info('Gathering system info...', 'Report Issue');
271
+ const info = await Api.getSystemInfo();
272
+
273
+ // Build environment section for the template
274
+ const envSection = [
275
+ `- **Plugin Version**: ${info.pluginVersion}`,
276
+ `- **Homebridge Version**: ${info.homebridgeVersion}`,
277
+ `- **Node.js Version**: ${info.nodeVersion}`,
278
+ `- **OS**: ${info.os}`,
279
+ `- **eufy-security-client**: ${info.eufyClientVersion}`,
280
+ ].join('\n');
281
+
282
+ // Build the URL with query params matching the bug report template field IDs
283
+ const params = new URLSearchParams();
284
+ params.set('template', 'bug_report.yml');
285
+ params.set('environment', envSection);
286
+
287
+ const url = 'https://github.com/homebridge-plugins/homebridge-eufy-security/issues/new?' + params.toString();
288
+ window.open(url, '_blank');
289
+ } catch (e) {
290
+ // Fallback: open without pre-fill
291
+ homebridge.toast.error('Could not gather system info. Opening blank issue form.');
292
+ window.open('https://github.com/homebridge-plugins/homebridge-eufy-security/issues/new?template=bug_report.yml', '_blank');
293
+ }
294
+ },
295
+
296
+ // ===== Log Download =====
297
+ async _downloadLogs(container) {
298
+ if (this._downloadInProgress) return;
299
+ this._downloadInProgress = true;
300
+
301
+ const progressArea = container.querySelector('#log-download-progress');
302
+ if (progressArea) {
303
+ progressArea.innerHTML = `
304
+ <div class="log-progress mt-2">
305
+ <div class="progress">
306
+ <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 10%" id="log-progress-bar"></div>
307
+ </div>
308
+ <small class="text-muted" id="log-progress-status">Preparing...</small>
309
+ </div>
310
+ `;
311
+ }
312
+
313
+ // Listen for progress events
314
+ Api.onDownloadLogsProgress((data) => {
315
+ const bar = document.querySelector('#log-progress-bar');
316
+ const status = document.querySelector('#log-progress-status');
317
+ if (bar) bar.style.width = data.progress + '%';
318
+ if (status) status.textContent = data.status;
319
+ });
320
+
321
+ try {
322
+ const buffer = await Api.downloadLogs();
323
+ // Convert to base64 data URI (blob: URLs are blocked by Homebridge CSP)
324
+ const bytes = new Uint8Array(buffer.data || buffer);
325
+ let binary = '';
326
+ for (let i = 0; i < bytes.length; i++) {
327
+ binary += String.fromCharCode(bytes[i]);
328
+ }
329
+ const base64 = btoa(binary);
330
+ const a = document.createElement('a');
331
+ a.href = 'data:application/zip;base64,' + base64;
332
+ a.download = 'eufy-security-logs.zip';
333
+ document.body.appendChild(a);
334
+ a.click();
335
+ document.body.removeChild(a);
336
+
337
+ homebridge.toast.success('Logs downloaded.');
338
+ } catch (e) {
339
+ homebridge.toast.error('Failed to download logs: ' + (e.message || e));
340
+ } finally {
341
+ this._downloadInProgress = false;
342
+ if (progressArea) progressArea.innerHTML = '';
343
+ }
344
+ },
345
+
346
+ // ===== Reset Confirmation =====
347
+ _confirmReset(container) {
348
+ const existing = container.querySelector('#reset-confirm');
349
+ if (existing) { existing.remove(); return; }
350
+
351
+ const confirm = document.createElement('div');
352
+ confirm.id = 'reset-confirm';
353
+ confirm.className = 'alert alert-danger mt-3';
354
+ confirm.innerHTML = `
355
+ <strong>Are you sure?</strong> This will delete all persistent data, stored accessories, and logs.
356
+ You will need to log in again.
357
+ <div class="mt-2">
358
+ <button class="btn btn-danger btn-sm me-2" id="btn-confirm-reset">Yes, Reset Everything</button>
359
+ <button class="btn btn-outline-secondary btn-sm" id="btn-cancel-reset">Cancel</button>
360
+ </div>
361
+ `;
362
+
363
+ confirm.querySelector('#btn-confirm-reset').addEventListener('click', async () => {
364
+ try {
365
+ await Api.resetPlugin();
366
+ // Clear config
367
+ await Config.update({
368
+ platform: 'EufySecurity',
369
+ });
370
+ await Config.save();
371
+ homebridge.toast.success('Plugin reset. Please restart Homebridge.');
372
+ App.state.stations = [];
373
+ App.navigate('login');
374
+ } catch (e) {
375
+ homebridge.toast.error('Reset failed: ' + (e.message || e));
376
+ }
377
+ });
378
+
379
+ confirm.querySelector('#btn-cancel-reset').addEventListener('click', () => {
380
+ confirm.remove();
381
+ });
382
+
383
+ container.appendChild(confirm);
384
+ },
385
+
386
+ _escHtml(str) {
387
+ return Helpers.escHtml(str);
388
+ },
389
+ };
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Unsupported Device Detail View — shows device info dump and guided CTA
3
+ * for requesting support via GitHub.
4
+ */
5
+ // eslint-disable-next-line no-unused-vars
6
+ const UnsupportedDetailView = {
7
+
8
+ _container: null,
9
+
10
+ /**
11
+ * @param {HTMLElement} container
12
+ * @param {string} id - uniqueId / serial number
13
+ */
14
+ async render(container, id) {
15
+ this._container = container;
16
+ container.innerHTML = '';
17
+
18
+ const accessory = this._findAccessory(id);
19
+
20
+ if (!accessory) {
21
+ container.innerHTML = `
22
+ <div class="text-center text-muted py-5">
23
+ <p>Device not found.</p>
24
+ <button class="btn btn-outline-secondary btn-sm" id="btn-back">Back to Dashboard</button>
25
+ </div>`;
26
+ container.querySelector('#btn-back').addEventListener('click', () => App.navigate('dashboard'));
27
+ return;
28
+ }
29
+
30
+ // Header (no image for unsupported)
31
+ this._renderHeader(container, accessory);
32
+
33
+ // Main content
34
+ const content = document.createElement('div');
35
+ this._renderDetail(content, accessory);
36
+ container.appendChild(content);
37
+ },
38
+
39
+ // ===== Header =====
40
+ _renderHeader(container, accessory) {
41
+ const header = document.createElement('div');
42
+ header.className = 'detail-header';
43
+
44
+ const backBtn = document.createElement('button');
45
+ backBtn.className = 'btn btn-link p-0';
46
+ backBtn.innerHTML = '← Back';
47
+ backBtn.style.textDecoration = 'none';
48
+ backBtn.addEventListener('click', () => App.navigate('dashboard'));
49
+
50
+ const info = document.createElement('div');
51
+ info.className = 'detail-header__info';
52
+ info.innerHTML = `
53
+ <h5>${Helpers.escHtml(accessory.displayName)}</h5>
54
+ <small>${accessory.typename || ('Type ' + accessory.type)} · ${accessory.uniqueId}</small>
55
+ `;
56
+
57
+ header.appendChild(backBtn);
58
+ header.appendChild(info);
59
+ container.appendChild(header);
60
+ },
61
+
62
+ // ===== Detail Content =====
63
+ _renderDetail(content, accessory) {
64
+ const REPO = 'homebridge-plugins/homebridge-eufy-security';
65
+ const LABEL = 'device-support';
66
+ const COMPAT_URL = 'https://bropat.github.io/eufy-security-client/#/supported_devices';
67
+
68
+ const section = document.createElement('div');
69
+ section.className = 'detail-section unsupported-detail';
70
+
71
+ // Description
72
+ const desc = document.createElement('p');
73
+ desc.className = 'text-muted';
74
+ desc.innerHTML =
75
+ '<strong>ℹ️ This device was detected but is not yet supported.</strong><br />' +
76
+ 'New device support must first be added to the eufy-security-client library. ' +
77
+ 'If your device is not on the compatibility list, please <strong>search for existing issues first</strong> before opening a new one.';
78
+ section.appendChild(desc);
79
+
80
+ // Device info dump — all available data in one box
81
+ const props = accessory.properties || {};
82
+ const deviceInfo = {
83
+ uniqueId: accessory.uniqueId,
84
+ displayName: accessory.displayName,
85
+ type: accessory.type,
86
+ typename: accessory.typename || undefined,
87
+ ...props,
88
+ };
89
+ // Remove potentially large/sensitive fields
90
+ delete deviceInfo.picture;
91
+
92
+ // CTA stepper + buttons
93
+ const stepsWrap = document.createElement('div');
94
+ stepsWrap.className = 'unsupported-detail__steps';
95
+
96
+ // Stepper frieze
97
+ const stepper = document.createElement('div');
98
+ stepper.className = 'unsupported-stepper';
99
+ const steps = [
100
+ { num: '1', label: 'Check', color: 'success' },
101
+ { num: '2', label: 'Search', color: 'primary' },
102
+ { num: '3', label: 'Create', color: 'danger' },
103
+ ];
104
+ steps.forEach((step, i) => {
105
+ const stepEl = document.createElement('div');
106
+ stepEl.className = 'unsupported-stepper__step';
107
+ stepEl.innerHTML = `<span class="unsupported-stepper__circle unsupported-stepper__circle--${step.color}">${step.num}</span><span class="unsupported-stepper__label">${step.label}</span>`;
108
+ stepper.appendChild(stepEl);
109
+ if (i < steps.length - 1) {
110
+ const line = document.createElement('div');
111
+ line.className = 'unsupported-stepper__line';
112
+ stepper.appendChild(line);
113
+ }
114
+ });
115
+ stepsWrap.appendChild(stepper);
116
+
117
+ // Buttons row
118
+ const btnGroup = document.createElement('div');
119
+ btnGroup.className = 'unsupported-detail__actions';
120
+
121
+ // 1) Check compatibility list
122
+ const compatBtn = document.createElement('a');
123
+ compatBtn.href = COMPAT_URL;
124
+ compatBtn.target = '_blank';
125
+ compatBtn.rel = 'noopener noreferrer';
126
+ compatBtn.className = 'btn btn-success';
127
+ compatBtn.textContent = 'Check Compatibility ↗';
128
+ btnGroup.appendChild(compatBtn);
129
+
130
+ // 2) Search existing issues with label
131
+ const searchQuery = encodeURIComponent(`is:issue label:${LABEL} ${accessory.type}`);
132
+ const searchBtn = document.createElement('a');
133
+ searchBtn.href = `https://github.com/${REPO}/issues?q=${searchQuery}`;
134
+ searchBtn.target = '_blank';
135
+ searchBtn.rel = 'noopener noreferrer';
136
+ searchBtn.className = 'btn btn-outline-primary';
137
+ searchBtn.textContent = 'Search Existing Issues ↗';
138
+ btnGroup.appendChild(searchBtn);
139
+
140
+ // 3) Create new issue using the device_support template
141
+ const model = props.model || accessory.type;
142
+ const issueTitle = encodeURIComponent(`[Device Support] ${model} (Type ${accessory.type})`);
143
+ const deviceDump = JSON.stringify(deviceInfo, null, 2);
144
+ const templateParams = [
145
+ `template=device_support.yml`,
146
+ `title=${issueTitle}`,
147
+ `labels=${LABEL}`,
148
+ `device_info=${encodeURIComponent(deviceDump)}`,
149
+ ].join('&');
150
+ const createBtn = document.createElement('a');
151
+ createBtn.href = `https://github.com/${REPO}/issues/new?${templateParams}`;
152
+ createBtn.target = '_blank';
153
+ createBtn.rel = 'noopener noreferrer';
154
+ createBtn.className = 'btn btn-outline-danger';
155
+ createBtn.textContent = 'Create an Issue ↗';
156
+ btnGroup.appendChild(createBtn);
157
+
158
+ stepsWrap.appendChild(btnGroup);
159
+ section.appendChild(stepsWrap);
160
+
161
+ // External links note
162
+ const extNote = document.createElement('p');
163
+ extNote.className = 'text-muted';
164
+ extNote.style.cssText = 'font-size: 0.75rem; text-align: right;';
165
+ extNote.textContent = '↗ These links open in a new browser tab on GitHub.';
166
+ section.appendChild(extNote);
167
+
168
+ const infoTitle = document.createElement('div');
169
+ infoTitle.className = 'detail-section__title';
170
+ infoTitle.textContent = 'Device Information';
171
+ section.appendChild(infoTitle);
172
+
173
+ // JSON dump with copy button overlay
174
+ const dumpWrap = document.createElement('div');
175
+ dumpWrap.className = 'unsupported-detail__dump-wrap';
176
+
177
+ const copyBtn = document.createElement('button');
178
+ copyBtn.className = 'unsupported-detail__copy-btn';
179
+ copyBtn.textContent = '📋 Copy';
180
+ copyBtn.addEventListener('click', () => {
181
+ navigator.clipboard.writeText(pre.textContent).then(() => {
182
+ copyBtn.textContent = '✓ Copied!';
183
+ setTimeout(() => { copyBtn.textContent = '📋 Copy'; }, 2000);
184
+ });
185
+ });
186
+
187
+ const pre = document.createElement('pre');
188
+ pre.className = 'unsupported-detail__dump';
189
+ pre.textContent = JSON.stringify(deviceInfo, null, 2);
190
+
191
+ dumpWrap.appendChild(copyBtn);
192
+ dumpWrap.appendChild(pre);
193
+ section.appendChild(dumpWrap);
194
+
195
+ content.appendChild(section);
196
+ },
197
+
198
+ // ===== Helpers =====
199
+
200
+ /**
201
+ * Find an accessory (device or station) by uniqueId across all stations.
202
+ * For unsupported standalone stations the device may be null, so we fall back to the station.
203
+ * @param {string} id
204
+ * @returns {object|null}
205
+ */
206
+ _findAccessory(id) {
207
+ const stations = App.state.stations || [];
208
+ for (const s of stations) {
209
+ if (s.uniqueId === id) return s;
210
+ for (const d of s.devices || []) {
211
+ if (d.uniqueId === id) return d;
212
+ }
213
+ }
214
+ return null;
215
+ },
216
+ };