@homebridge-plugins/homebridge-eufy-security 0.0.1

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 (185) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/FUNDING.yml +1 -0
  3. package/LICENSE +176 -0
  4. package/README.md +67 -0
  5. package/config.schema.json +6 -0
  6. package/dist/accessories/AutoSyncStationAccessory.js +156 -0
  7. package/dist/accessories/AutoSyncStationAccessory.js.map +1 -0
  8. package/dist/accessories/BaseAccessory.js +247 -0
  9. package/dist/accessories/BaseAccessory.js.map +1 -0
  10. package/dist/accessories/CameraAccessory.js +431 -0
  11. package/dist/accessories/CameraAccessory.js.map +1 -0
  12. package/dist/accessories/Device.js +67 -0
  13. package/dist/accessories/Device.js.map +1 -0
  14. package/dist/accessories/EntrySensorAccessory.js +48 -0
  15. package/dist/accessories/EntrySensorAccessory.js.map +1 -0
  16. package/dist/accessories/LockAccessory.js +142 -0
  17. package/dist/accessories/LockAccessory.js.map +1 -0
  18. package/dist/accessories/MotionSensorAccessory.js +48 -0
  19. package/dist/accessories/MotionSensorAccessory.js.map +1 -0
  20. package/dist/accessories/SmartDropAccessory.js +145 -0
  21. package/dist/accessories/SmartDropAccessory.js.map +1 -0
  22. package/dist/accessories/StationAccessory.js +371 -0
  23. package/dist/accessories/StationAccessory.js.map +1 -0
  24. package/dist/config.js +25 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/controller/LocalLivestreamManager.js +116 -0
  27. package/dist/controller/LocalLivestreamManager.js.map +1 -0
  28. package/dist/controller/recordingDelegate.js +208 -0
  29. package/dist/controller/recordingDelegate.js.map +1 -0
  30. package/dist/controller/snapshotDelegate.js +345 -0
  31. package/dist/controller/snapshotDelegate.js.map +1 -0
  32. package/dist/controller/streamingDelegate.js +345 -0
  33. package/dist/controller/streamingDelegate.js.map +1 -0
  34. package/dist/index.js +11 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces.js +2 -0
  37. package/dist/interfaces.js.map +1 -0
  38. package/dist/media/Snapshot-Unavailable.png +0 -0
  39. package/dist/media/Snapshot-Unavailable.xcf +0 -0
  40. package/dist/media/Snapshot-black.png +0 -0
  41. package/dist/media/camera-disabled.png +0 -0
  42. package/dist/media/camera-offline.png +0 -0
  43. package/dist/media/media/Snapshot-Unavailable.png +0 -0
  44. package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
  45. package/dist/media/media/Snapshot-black.png +0 -0
  46. package/dist/media/media/camera-disabled.png +0 -0
  47. package/dist/media/media/camera-offline.png +0 -0
  48. package/dist/platform.js +716 -0
  49. package/dist/platform.js.map +1 -0
  50. package/dist/settings.js +38 -0
  51. package/dist/settings.js.map +1 -0
  52. package/dist/utils/Talkback.js +92 -0
  53. package/dist/utils/Talkback.js.map +1 -0
  54. package/dist/utils/accessoriesStore.js +206 -0
  55. package/dist/utils/accessoriesStore.js.map +1 -0
  56. package/dist/utils/configTypes.js +35 -0
  57. package/dist/utils/configTypes.js.map +1 -0
  58. package/dist/utils/ffmpeg.js +843 -0
  59. package/dist/utils/ffmpeg.js.map +1 -0
  60. package/dist/utils/interfaces.js +8 -0
  61. package/dist/utils/interfaces.js.map +1 -0
  62. package/dist/utils/utils.js +44 -0
  63. package/dist/utils/utils.js.map +1 -0
  64. package/dist/version.js +2 -0
  65. package/dist/version.js.map +1 -0
  66. package/eslint.config.mjs +18 -0
  67. package/homebridge-eufy-security.png +0 -0
  68. package/homebridge-ui/public/app.js +225 -0
  69. package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
  70. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
  71. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
  72. package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
  73. package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
  74. package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
  75. package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
  76. package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
  77. package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
  78. package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
  79. package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
  80. package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
  81. package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
  82. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  83. package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
  84. package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
  85. package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
  86. package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
  87. package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
  88. package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
  89. package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.jpg +0 -0
  90. package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
  91. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  92. package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
  93. package/homebridge-ui/public/assets/devices/homebasemini_large.jpg +0 -0
  94. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  95. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  96. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  97. package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
  98. package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
  99. package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
  100. package/homebridge-ui/public/assets/devices/indoorcams350_large.jpg +0 -0
  101. package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
  102. package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
  103. package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
  104. package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
  105. package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
  106. package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
  107. package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
  108. package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
  109. package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
  110. package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
  111. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
  112. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
  113. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
  114. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
  115. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
  116. package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
  117. package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
  118. package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
  119. package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
  120. package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
  121. package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
  122. package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
  123. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  124. package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
  125. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  126. package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
  127. package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
  128. package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
  129. package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
  130. package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
  131. package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
  132. package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
  133. package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
  134. package/homebridge-ui/public/assets/devices/unknown.png +0 -0
  135. package/homebridge-ui/public/assets/devices/walllight_s100_large.jpg +0 -0
  136. package/homebridge-ui/public/assets/devices/walllight_s120_large.jpg +0 -0
  137. package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
  138. package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
  139. package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
  140. package/homebridge-ui/public/assets/icons/attach.svg +1 -0
  141. package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
  142. package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
  143. package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
  144. package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
  145. package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
  146. package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
  147. package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
  148. package/homebridge-ui/public/assets/icons/bolt.svg +1 -0
  149. package/homebridge-ui/public/assets/icons/bug-report.svg +1 -0
  150. package/homebridge-ui/public/assets/icons/copy.svg +1 -0
  151. package/homebridge-ui/public/assets/icons/delete.svg +1 -0
  152. package/homebridge-ui/public/assets/icons/download.svg +1 -0
  153. package/homebridge-ui/public/assets/icons/info.svg +1 -0
  154. package/homebridge-ui/public/assets/icons/inventory.svg +1 -0
  155. package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
  156. package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
  157. package/homebridge-ui/public/assets/icons/settings.svg +1 -0
  158. package/homebridge-ui/public/assets/icons/settings_backup_restore.svg +1 -0
  159. package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
  160. package/homebridge-ui/public/assets/icons/warning.svg +1 -0
  161. package/homebridge-ui/public/components/device-card.js +162 -0
  162. package/homebridge-ui/public/components/guard-modes.js +88 -0
  163. package/homebridge-ui/public/components/number-input.js +121 -0
  164. package/homebridge-ui/public/components/select.js +73 -0
  165. package/homebridge-ui/public/components/toggle.js +68 -0
  166. package/homebridge-ui/public/index.html +27 -0
  167. package/homebridge-ui/public/services/api.js +214 -0
  168. package/homebridge-ui/public/services/config.js +144 -0
  169. package/homebridge-ui/public/style.css +775 -0
  170. package/homebridge-ui/public/utils/countries.js +73 -0
  171. package/homebridge-ui/public/utils/device-images.js +89 -0
  172. package/homebridge-ui/public/utils/helpers.js +87 -0
  173. package/homebridge-ui/public/views/dashboard.js +226 -0
  174. package/homebridge-ui/public/views/device-detail.js +610 -0
  175. package/homebridge-ui/public/views/diagnostics.js +296 -0
  176. package/homebridge-ui/public/views/login.js +636 -0
  177. package/homebridge-ui/public/views/settings.js +192 -0
  178. package/homebridge-ui/public/views/unsupported-detail.js +296 -0
  179. package/homebridge-ui/server.js +1327 -0
  180. package/media/Snapshot-Unavailable.png +0 -0
  181. package/media/Snapshot-Unavailable.xcf +0 -0
  182. package/media/Snapshot-black.png +0 -0
  183. package/media/camera-disabled.png +0 -0
  184. package/media/camera-offline.png +0 -0
  185. package/package.json +64 -0
@@ -0,0 +1,610 @@
1
+ /**
2
+ * Device Detail View — per-device/station settings with progressive disclosure.
3
+ * Simple mode shows 3-4 toggles, Advanced expands full options.
4
+ */
5
+ // eslint-disable-next-line no-unused-vars
6
+ const DeviceDetailView = {
7
+
8
+ _container: null,
9
+ _advancedOpen: false,
10
+ _expertOpen: false,
11
+
12
+ /**
13
+ * @param {HTMLElement} container
14
+ * @param {string} type - 'device' or 'station'
15
+ * @param {string} id - uniqueId / serial number
16
+ */
17
+ async render(container, type, id) {
18
+ this._container = container;
19
+ this._advancedOpen = false;
20
+ this._expertOpen = false;
21
+ container.innerHTML = '';
22
+
23
+ const config = await Config.get();
24
+ const { station, device } = this._findAccessory(type, id);
25
+
26
+ if (!station && !device) {
27
+ container.innerHTML = `
28
+ <div class="text-center text-muted py-5">
29
+ <p>Device not found.</p>
30
+ <button class="btn btn-outline-secondary btn-sm" id="btn-back">Back to Dashboard</button>
31
+ </div>`;
32
+ container.querySelector('#btn-back').addEventListener('click', () => App.navigate('dashboard'));
33
+ return;
34
+ }
35
+
36
+ // For unsupported standalone stations routed as 'device', the device is null — use station
37
+ const accessory = (type === 'station' ? station : device) || station;
38
+ const isUnsupported = accessory && accessory.unsupported === true;
39
+
40
+ // Redirect unsupported devices to their dedicated view
41
+ if (isUnsupported) {
42
+ App.navigate('unsupported/' + accessory.uniqueId);
43
+ return;
44
+ }
45
+
46
+ // Header with back button + device image
47
+ this._renderHeader(container, accessory, type);
48
+
49
+ // Main content area
50
+ const content = document.createElement('div');
51
+
52
+ if (type === 'device' && device) {
53
+ const accessoryConfig = type === 'station'
54
+ ? Config.getStationConfig(id)
55
+ : Config.getDeviceConfig(id);
56
+ this._renderDeviceSettings(content, device, accessoryConfig || {}, config);
57
+
58
+ // For standalone devices, also show station settings (if station supports security control)
59
+ if (device.standalone && station && !station.noSecurityControl) {
60
+ const stationConfig = Config.getStationConfig(station.uniqueId) || {};
61
+ this._renderStandaloneStationSection(content, station, stationConfig, config);
62
+ }
63
+ } else if (type === 'station' && station) {
64
+ const accessoryConfig = Config.getStationConfig(id);
65
+ this._renderStationSettings(content, station, accessoryConfig || {}, config);
66
+ }
67
+
68
+ container.appendChild(content);
69
+
70
+
71
+ },
72
+
73
+ // ===== Header =====
74
+ _renderHeader(container, accessory, type) {
75
+ const header = document.createElement('div');
76
+ header.className = 'detail-header';
77
+
78
+ const backBtn = document.createElement('button');
79
+ backBtn.className = 'btn btn-link p-0';
80
+ backBtn.innerHTML = '← Back';
81
+ backBtn.style.textDecoration = 'none';
82
+ backBtn.addEventListener('click', () => App.navigate('dashboard'));
83
+
84
+ const info = document.createElement('div');
85
+ info.className = 'detail-header__info';
86
+ info.innerHTML = `
87
+ <h5>${this._escHtml(accessory.displayName)}</h5>
88
+ <small>${accessory.typename || ('Type ' + accessory.type)} · ${accessory.uniqueId}</small>
89
+ `;
90
+
91
+ header.appendChild(backBtn);
92
+
93
+ const img = document.createElement('img');
94
+ img.className = 'detail-header__image';
95
+ img.src = DeviceImages.getPath(accessory.type);
96
+ img.alt = accessory.displayName;
97
+ header.appendChild(img);
98
+
99
+ header.appendChild(info);
100
+ container.appendChild(header);
101
+ },
102
+
103
+ // ===== Device Settings (Camera/Doorbell/Sensor/Lock) =====
104
+ _renderDeviceSettings(content, device, deviceConfig, config) {
105
+ const ignoreDevices = config.ignoreDevices || [];
106
+ const isIgnored = ignoreDevices.includes(device.uniqueId);
107
+
108
+ // ── Enable in HomeKit (always visible) ──
109
+ const enableSection = document.createElement('div');
110
+ enableSection.className = 'detail-section';
111
+
112
+ const enableTitle = document.createElement('div');
113
+ enableTitle.className = 'detail-section__title';
114
+ enableTitle.textContent = 'Device Settings';
115
+ enableSection.appendChild(enableTitle);
116
+
117
+ Toggle.render(enableSection, {
118
+ id: 'toggle-enable',
119
+ label: 'Enable in HomeKit',
120
+ help: 'When disabled, this device will not appear in the Home app.',
121
+ checked: !isIgnored,
122
+ onChange: async (checked) => {
123
+ await Config.toggleIgnore(device.uniqueId, !checked, 'device');
124
+ const rest = content.querySelector('#device-rest-settings');
125
+ if (rest) rest.style.display = checked ? '' : 'none';
126
+ },
127
+ });
128
+
129
+ content.appendChild(enableSection);
130
+
131
+ // Wrapper for all settings below Enable toggle — hidden when device is ignored
132
+ const restSettings = document.createElement('div');
133
+ restSettings.id = 'device-rest-settings';
134
+ if (isIgnored) restSettings.style.display = 'none';
135
+
136
+ // ── Simple Settings ──
137
+ const simpleSection = document.createElement('div');
138
+ simpleSection.className = 'detail-section';
139
+
140
+ // Camera-specific simple settings
141
+ if (device.isCamera) {
142
+ Toggle.render(simpleSection, {
143
+ id: 'toggle-camera-enable',
144
+ label: 'Camera Feed',
145
+ help: 'Show camera feed in HomeKit. Disable to expose only sensors.',
146
+ checked: deviceConfig.enableCamera !== false,
147
+ onChange: async (checked) => {
148
+ await Config.updateDeviceConfig(device.uniqueId, { enableCamera: checked });
149
+ },
150
+ });
151
+
152
+ if (device.DeviceMotionDetection) {
153
+ Toggle.render(simpleSection, {
154
+ id: 'toggle-motion',
155
+ label: 'Motion Detection',
156
+ help: 'Expose motion detection as a sensor in HomeKit.',
157
+ checked: deviceConfig.motionButton !== false,
158
+ onChange: async (checked) => {
159
+ await Config.updateDeviceConfig(device.uniqueId, { motionButton: checked });
160
+ },
161
+ });
162
+ }
163
+ }
164
+
165
+ restSettings.appendChild(simpleSection);
166
+
167
+ // ── Advanced Settings (collapsed by default) ──
168
+ if (device.isCamera) {
169
+ this._renderAdvancedToggle(restSettings, () => {
170
+ this._advancedOpen = !this._advancedOpen;
171
+ const advSection = content.querySelector('#advanced-section');
172
+ if (advSection) advSection.style.display = this._advancedOpen ? 'block' : 'none';
173
+ const chevron = content.querySelector('.advanced-toggle__chevron');
174
+ if (chevron) chevron.classList.toggle('advanced-toggle__chevron--open', this._advancedOpen);
175
+ });
176
+
177
+ const advSection = document.createElement('div');
178
+ advSection.id = 'advanced-section';
179
+ advSection.style.display = 'none';
180
+
181
+ // ── Streaming ──
182
+ const streamTitle = document.createElement('div');
183
+ streamTitle.className = 'detail-section__title';
184
+ streamTitle.textContent = 'Streaming';
185
+ advSection.appendChild(streamTitle);
186
+
187
+ if (device.supportsRTSP) {
188
+ Toggle.render(advSection, {
189
+ id: 'toggle-rtsp',
190
+ label: 'RTSP Streaming',
191
+ help: 'Use RTSP stream instead of P2P. Requires the camera to have RTSP enabled in the Eufy app.',
192
+ checked: !!deviceConfig.rtsp,
193
+ onChange: async (checked) => {
194
+ await Config.updateDeviceConfig(device.uniqueId, { rtsp: checked });
195
+ },
196
+ });
197
+ }
198
+
199
+ if (device.supportsTalkback) {
200
+ Toggle.render(advSection, {
201
+ id: 'toggle-talkback',
202
+ label: 'Two-Way Audio',
203
+ help: 'Enable talkback / two-way audio in HomeKit.',
204
+ checked: !!deviceConfig.talkback,
205
+ onChange: async (checked) => {
206
+ await Config.updateDeviceConfig(device.uniqueId, { talkback: checked });
207
+ },
208
+ });
209
+ }
210
+
211
+ // ── HomeKit Secure Video ──
212
+ const hsvTitle = document.createElement('div');
213
+ hsvTitle.className = 'detail-section__title';
214
+ hsvTitle.textContent = 'HomeKit Secure Video';
215
+ advSection.appendChild(hsvTitle);
216
+
217
+ NumberInput.render(advSection, {
218
+ id: 'num-hsv-duration',
219
+ label: 'HSV Recording Duration',
220
+ help: 'Maximum recording duration in seconds for HSV clips.',
221
+ value: deviceConfig.hsvRecordingDuration || 90,
222
+ min: 10,
223
+ max: 300,
224
+ step: 10,
225
+ suffix: 'sec',
226
+ onChange: async (val) => {
227
+ await Config.updateDeviceConfig(device.uniqueId, { hsvRecordingDuration: val });
228
+ },
229
+ });
230
+
231
+ // ── Snapshots ──
232
+ const snapTitle = document.createElement('div');
233
+ snapTitle.className = 'detail-section__title';
234
+ snapTitle.textContent = 'Snapshots';
235
+ advSection.appendChild(snapTitle);
236
+
237
+ Select.render(advSection, {
238
+ id: 'select-snapshot-method',
239
+ label: 'Snapshot Method',
240
+ help: 'How to capture snapshots. "Auto" lets the plugin decide the best method.',
241
+ options: [
242
+ { value: '0', label: 'Auto' },
243
+ { value: '1', label: 'From Livestream' },
244
+ { value: '2', label: 'From Cloud' },
245
+ { value: '3', label: 'Preload on Event' },
246
+ ],
247
+ value: String(deviceConfig.snapshotHandlingMethod || 0),
248
+ onChange: async (val) => {
249
+ await Config.updateDeviceConfig(device.uniqueId, { snapshotHandlingMethod: parseInt(val) });
250
+ },
251
+ });
252
+
253
+ Toggle.render(advSection, {
254
+ id: 'toggle-delay-snapshot',
255
+ label: 'Delay Snapshot Capture',
256
+ help: 'Wait briefly before taking a snapshot to get a clearer image.',
257
+ checked: !!deviceConfig.delayCameraSnapshot,
258
+ onChange: async (checked) => {
259
+ await Config.updateDeviceConfig(device.uniqueId, { delayCameraSnapshot: checked });
260
+ },
261
+ });
262
+
263
+ // ── Camera Buttons ──
264
+ const btnTitle = document.createElement('div');
265
+ btnTitle.className = 'detail-section__title';
266
+ btnTitle.textContent = 'Camera Buttons';
267
+ advSection.appendChild(btnTitle);
268
+
269
+ Toggle.render(advSection, {
270
+ id: 'toggle-enable-btn',
271
+ label: 'Enable Switch',
272
+ help: 'Add a switch to enable/disable the camera from HomeKit.',
273
+ checked: !!deviceConfig.enableButton,
274
+ onChange: async (checked) => {
275
+ await Config.updateDeviceConfig(device.uniqueId, { enableButton: checked });
276
+ },
277
+ });
278
+
279
+ if (device.DeviceLight) {
280
+ Toggle.render(advSection, {
281
+ id: 'toggle-light-btn',
282
+ label: 'Light Switch',
283
+ help: 'Add a switch to control the camera\'s spotlight/floodlight.',
284
+ checked: !!deviceConfig.lightButton,
285
+ onChange: async (checked) => {
286
+ await Config.updateDeviceConfig(device.uniqueId, { lightButton: checked });
287
+ },
288
+ });
289
+ }
290
+
291
+ if (device.DeviceChimeIndoor) {
292
+ Toggle.render(advSection, {
293
+ id: 'toggle-chime-btn',
294
+ label: 'Indoor Chime Switch',
295
+ help: 'Add a switch to control the indoor chime.',
296
+ checked: !!deviceConfig.indoorChimeButton,
297
+ onChange: async (checked) => {
298
+ await Config.updateDeviceConfig(device.uniqueId, { indoorChimeButton: checked });
299
+ },
300
+ });
301
+ }
302
+
303
+ // ── Expert: Video Config ──
304
+ this._renderExpertToggle(advSection, () => {
305
+ this._expertOpen = !this._expertOpen;
306
+ const expertSection = advSection.querySelector('#expert-section');
307
+ if (expertSection) expertSection.style.display = this._expertOpen ? 'block' : 'none';
308
+ const chevron = advSection.querySelector('#expert-chevron');
309
+ if (chevron) chevron.classList.toggle('advanced-toggle__chevron--open', this._expertOpen);
310
+ });
311
+
312
+ const expertSection = document.createElement('div');
313
+ expertSection.id = 'expert-section';
314
+ expertSection.style.display = 'none';
315
+
316
+ const videoTitle = document.createElement('div');
317
+ videoTitle.className = 'detail-section__title';
318
+ videoTitle.textContent = 'Video Encoding (Expert)';
319
+ expertSection.appendChild(videoTitle);
320
+
321
+ const vc = deviceConfig.videoConfig || {};
322
+
323
+ NumberInput.render(expertSection, {
324
+ id: 'num-maxWidth',
325
+ label: 'Max Width',
326
+ help: 'Maximum video width in pixels.',
327
+ value: vc.maxWidth || 1920,
328
+ min: 320, max: 3840, step: 160,
329
+ suffix: 'px',
330
+ onChange: async (val) => {
331
+ const existing = deviceConfig.videoConfig || {};
332
+ await Config.updateDeviceConfig(device.uniqueId, { videoConfig: { ...existing, maxWidth: val } });
333
+ },
334
+ });
335
+
336
+ NumberInput.render(expertSection, {
337
+ id: 'num-maxHeight',
338
+ label: 'Max Height',
339
+ help: 'Maximum video height in pixels.',
340
+ value: vc.maxHeight || 1080,
341
+ min: 240, max: 2160, step: 120,
342
+ suffix: 'px',
343
+ onChange: async (val) => {
344
+ const existing = deviceConfig.videoConfig || {};
345
+ await Config.updateDeviceConfig(device.uniqueId, { videoConfig: { ...existing, maxHeight: val } });
346
+ },
347
+ });
348
+
349
+ NumberInput.render(expertSection, {
350
+ id: 'num-maxFPS',
351
+ label: 'Max FPS',
352
+ help: 'Maximum frames per second.',
353
+ value: vc.maxFPS || 30,
354
+ min: 10, max: 60, step: 5,
355
+ onChange: async (val) => {
356
+ const existing = deviceConfig.videoConfig || {};
357
+ await Config.updateDeviceConfig(device.uniqueId, { videoConfig: { ...existing, maxFPS: val } });
358
+ },
359
+ });
360
+
361
+ NumberInput.render(expertSection, {
362
+ id: 'num-maxBitrate',
363
+ label: 'Max Bitrate',
364
+ help: 'Maximum video bitrate in kbps.',
365
+ value: vc.maxBitrate || 1800,
366
+ min: 300, max: 8000, step: 100,
367
+ suffix: 'kbps',
368
+ onChange: async (val) => {
369
+ const existing = deviceConfig.videoConfig || {};
370
+ await Config.updateDeviceConfig(device.uniqueId, { videoConfig: { ...existing, maxBitrate: val } });
371
+ },
372
+ });
373
+
374
+ advSection.appendChild(expertSection);
375
+ restSettings.appendChild(advSection);
376
+ }
377
+
378
+ content.appendChild(restSettings);
379
+ },
380
+
381
+ // ===== Station Settings =====
382
+ _renderStationSettings(content, station, stationConfig, config) {
383
+ const ignoreStations = config.ignoreStations || [];
384
+ const isIgnored = ignoreStations.includes(station.uniqueId);
385
+
386
+ // Simple
387
+ const section = document.createElement('div');
388
+ section.className = 'detail-section';
389
+
390
+ const title = document.createElement('div');
391
+ title.className = 'detail-section__title';
392
+ title.textContent = 'Station Settings';
393
+ section.appendChild(title);
394
+
395
+ Toggle.render(section, {
396
+ id: 'toggle-station-enable',
397
+ label: 'Enable in HomeKit',
398
+ help: 'When disabled, this station\'s security panel will not appear in HomeKit.',
399
+ checked: !isIgnored,
400
+ onChange: async (checked) => {
401
+ await Config.toggleIgnore(station.uniqueId, !checked, 'station');
402
+ const rest = content.querySelector('#station-rest-settings');
403
+ if (rest) rest.style.display = checked ? '' : 'none';
404
+ },
405
+ });
406
+
407
+ content.appendChild(section);
408
+
409
+ // Wrapper for all settings below Enable toggle — hidden when station is ignored
410
+ const stationRest = document.createElement('div');
411
+ stationRest.id = 'station-rest-settings';
412
+ if (isIgnored) stationRest.style.display = 'none';
413
+
414
+ // Advanced — Guard Modes Mapping
415
+ this._renderAdvancedToggle(stationRest, () => {
416
+ this._advancedOpen = !this._advancedOpen;
417
+ const advSection = content.querySelector('#advanced-section');
418
+ if (advSection) advSection.style.display = this._advancedOpen ? 'block' : 'none';
419
+ const chevron = content.querySelector('.advanced-toggle__chevron');
420
+ if (chevron) chevron.classList.toggle('advanced-toggle__chevron--open', this._advancedOpen);
421
+ });
422
+
423
+ const advSection = document.createElement('div');
424
+ advSection.id = 'advanced-section';
425
+ advSection.style.display = 'none';
426
+
427
+ const guardTitle = document.createElement('div');
428
+ guardTitle.className = 'detail-section__title';
429
+ guardTitle.textContent = 'Guard Mode Mapping';
430
+ advSection.appendChild(guardTitle);
431
+
432
+ const guardHelp = document.createElement('p');
433
+ guardHelp.className = 'text-muted';
434
+ guardHelp.style.fontSize = '0.8rem';
435
+ guardHelp.textContent = 'Map HomeKit security modes to Eufy guard modes for this station.';
436
+ advSection.appendChild(guardHelp);
437
+
438
+ GuardModes.render(advSection, {
439
+ hkHome: stationConfig.hkHome ?? config.hkHome ?? 1,
440
+ hkAway: stationConfig.hkAway ?? config.hkAway ?? 0,
441
+ hkNight: stationConfig.hkNight ?? config.hkNight ?? 1,
442
+ hkOff: stationConfig.hkOff ?? config.hkOff ?? 63,
443
+ onChange: async (modes) => {
444
+ await Config.updateStationConfig(station.uniqueId, modes);
445
+ },
446
+ });
447
+
448
+ // Manual Alarm
449
+ const alarmTitle = document.createElement('div');
450
+ alarmTitle.className = 'detail-section__title mt-3';
451
+ alarmTitle.textContent = 'Manual Alarm';
452
+ advSection.appendChild(alarmTitle);
453
+
454
+ NumberInput.render(advSection, {
455
+ id: 'num-alarm-seconds',
456
+ label: 'Alarm Duration',
457
+ help: 'How long (in seconds) a manually triggered alarm should sound.',
458
+ value: stationConfig.manualAlarmSeconds || 30,
459
+ min: 5,
460
+ max: 300,
461
+ step: 5,
462
+ suffix: 'sec',
463
+ onChange: async (val) => {
464
+ await Config.updateStationConfig(station.uniqueId, { manualAlarmSeconds: val });
465
+ },
466
+ });
467
+
468
+ stationRest.appendChild(advSection);
469
+ content.appendChild(stationRest);
470
+ },
471
+
472
+ // ===== Standalone Station Settings (shown in device detail for standalone devices) =====
473
+ _renderStandaloneStationSection(content, station, stationConfig, config) {
474
+ const ignoreStations = config.ignoreStations || [];
475
+ const isIgnored = ignoreStations.includes(station.uniqueId);
476
+
477
+ // Divider
478
+ const divider = document.createElement('hr');
479
+ divider.className = 'my-4';
480
+ content.appendChild(divider);
481
+
482
+ // Enable toggle section
483
+ const section = document.createElement('div');
484
+ section.className = 'detail-section';
485
+
486
+ const title = document.createElement('div');
487
+ title.className = 'detail-section__title';
488
+ title.textContent = 'Security Panel';
489
+ section.appendChild(title);
490
+
491
+ const helpText = document.createElement('p');
492
+ helpText.className = 'text-muted';
493
+ helpText.style.fontSize = '0.8rem';
494
+ helpText.textContent = 'This device acts as its own station. Configure the security panel independently from the device.';
495
+ section.appendChild(helpText);
496
+
497
+ Toggle.render(section, {
498
+ id: 'toggle-station-enable',
499
+ label: 'Enable Security Panel',
500
+ help: 'When disabled, the security panel will not appear in HomeKit. The device itself remains active.',
501
+ checked: !isIgnored,
502
+ onChange: async (checked) => {
503
+ await Config.toggleIgnore(station.uniqueId, !checked, 'station');
504
+ const rest = content.querySelector('#standalone-station-rest');
505
+ if (rest) rest.style.display = checked ? '' : 'none';
506
+ },
507
+ });
508
+
509
+ content.appendChild(section);
510
+
511
+ // Rest of station settings (hidden when disabled)
512
+ const stationRest = document.createElement('div');
513
+ stationRest.id = 'standalone-station-rest';
514
+ if (isIgnored) stationRest.style.display = 'none';
515
+
516
+ // Guard Mode Mapping
517
+ const guardSection = document.createElement('div');
518
+ guardSection.className = 'detail-section';
519
+
520
+ const guardTitle = document.createElement('div');
521
+ guardTitle.className = 'detail-section__title';
522
+ guardTitle.textContent = 'Guard Mode Mapping';
523
+ guardSection.appendChild(guardTitle);
524
+
525
+ const guardHelp = document.createElement('p');
526
+ guardHelp.className = 'text-muted';
527
+ guardHelp.style.fontSize = '0.8rem';
528
+ guardHelp.textContent = 'Map HomeKit security modes to Eufy guard modes.';
529
+ guardSection.appendChild(guardHelp);
530
+
531
+ GuardModes.render(guardSection, {
532
+ hkHome: stationConfig.hkHome ?? config.hkHome ?? 1,
533
+ hkAway: stationConfig.hkAway ?? config.hkAway ?? 0,
534
+ hkNight: stationConfig.hkNight ?? config.hkNight ?? 1,
535
+ hkOff: stationConfig.hkOff ?? config.hkOff ?? 63,
536
+ onChange: async (modes) => {
537
+ await Config.updateStationConfig(station.uniqueId, modes);
538
+ },
539
+ });
540
+
541
+ // Manual Alarm
542
+ const alarmTitle = document.createElement('div');
543
+ alarmTitle.className = 'detail-section__title mt-3';
544
+ alarmTitle.textContent = 'Manual Alarm';
545
+ guardSection.appendChild(alarmTitle);
546
+
547
+ NumberInput.render(guardSection, {
548
+ id: 'num-station-alarm-seconds',
549
+ label: 'Alarm Duration',
550
+ help: 'How long (in seconds) a manually triggered alarm should sound.',
551
+ value: stationConfig.manualAlarmSeconds || 30,
552
+ min: 5,
553
+ max: 300,
554
+ step: 5,
555
+ suffix: 'sec',
556
+ onChange: async (val) => {
557
+ await Config.updateStationConfig(station.uniqueId, { manualAlarmSeconds: val });
558
+ },
559
+ });
560
+
561
+ stationRest.appendChild(guardSection);
562
+ content.appendChild(stationRest);
563
+ },
564
+
565
+ // ===== Helpers =====
566
+
567
+ _findAccessory(type, id) {
568
+ const stations = App.state.stations || [];
569
+ let foundStation = null;
570
+ let foundDevice = null;
571
+
572
+ for (const s of stations) {
573
+ if (s.uniqueId === id) foundStation = s;
574
+ for (const d of s.devices || []) {
575
+ if (d.uniqueId === id) {
576
+ foundDevice = d;
577
+ foundStation = s;
578
+ }
579
+ }
580
+ }
581
+
582
+ return { station: foundStation, device: foundDevice };
583
+ },
584
+
585
+ _renderAdvancedToggle(container, onClick) {
586
+ const btn = document.createElement('button');
587
+ btn.className = 'advanced-toggle';
588
+ btn.innerHTML = `
589
+ <span class="advanced-toggle__chevron">▶</span>
590
+ Advanced Settings
591
+ `;
592
+ btn.addEventListener('click', onClick);
593
+ container.appendChild(btn);
594
+ },
595
+
596
+ _renderExpertToggle(container, onClick) {
597
+ const btn = document.createElement('button');
598
+ btn.className = 'advanced-toggle';
599
+ btn.innerHTML = `
600
+ <span class="advanced-toggle__chevron" id="expert-chevron">▶</span>
601
+ Video Encoding (Expert)
602
+ `;
603
+ btn.addEventListener('click', onClick);
604
+ container.appendChild(btn);
605
+ },
606
+
607
+ _escHtml(str) {
608
+ return Helpers.escHtml(str);
609
+ },
610
+ };