@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 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-80q-33 0-56.5-23.5T120-160v-451q-18-11-29-28.5T80-680v-120q0-33 23.5-56.5T160-880h640q33 0 56.5 23.5T880-800v120q0 23-11 40.5T840-611v451q0 33-23.5 56.5T760-80H200Zm0-520v440h560v-440H200Zm-40-80h640v-120H160v120Zm200 280h240v-80H360v80Zm120 20Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M560-32v-80q117 0 198.5-81.5T840-392h80q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T560-32Zm0-160v-80q50 0 85-35t35-85h80q0 83-58.5 141.5T560-192ZM222-57q-15 0-30-6t-27-17L23-222q-11-12-17-27t-6-30q0-16 6-30.5T23-335l127-127q23-23 57-23.5t57 22.5l50 50 28-28-50-50q-23-23-23-56t23-56l57-57q23-23 56.5-23t56.5 23l50 50 28-28-50-50q-23-23-23-56.5t23-56.5l127-127q12-12 27-18t30-6q15 0 29.5 6t26.5 18l142 142q12 11 17.5 25.5T895-730q0 15-5.5 30T872-673L745-546q-23 23-56.5 23T632-546l-50-50-28 28 50 50q23 23 22.5 56.5T603-405l-56 56q-23 23-56.5 23T434-349l-50-50-28 28 50 50q23 23 22.5 57T405-207L278-80q-11 11-25.5 17T222-57Zm0-79 42-42-142-142-42 42 142 142Zm85-85 42-42-142-142-42 42 142 142Zm184-184 56-56-142-142-56 56 142 142Zm198-198 42-42-142-142-42 42 142 142Zm85-85 42-42-142-142-42 42 142 142ZM448-504Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-400q-33 0-56.5-23.5T400-480q0-33 23.5-56.5T480-560q33 0 56.5 23.5T560-480q0 33-23.5 56.5T480-400Zm0 280q-139 0-241-91.5T122-440h82q14 104 92.5 172T480-200q117 0 198.5-81.5T760-480q0-117-81.5-198.5T480-760q-69 0-129 32t-101 88h110v80H120v-240h80v94q51-64 124.5-99T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 75-28.5 140.5t-77 114q-48.5 48.5-114 77T480-120Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m80-80 80-400h640l80 400H80Zm40-720v-80h120v80H120Zm58 640h262v-80H194l-16 80Zm67-427-57-56 85-85 57 56-85 85Zm-35 267h230v-80H226l-16 80Zm128.5-418.5Q280-797 280-880h80q0 50 35 85t85 35q50 0 85-35t35-85h80q0 83-58.5 141.5T480-680q-83 0-141.5-58.5ZM480-880Zm-40 360v-120h80v120h-80Zm80 360h262l-16-80H520v80Zm0-160h230l-16-80H520v80Zm195-267-84-85 56-56 85 84-57 57Zm5-213v-80h120v80H720Z"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm330.5-51.5Q520-263 520-280t-11.5-28.5Q497-320 480-320t-28.5 11.5Q440-297 440-280t11.5 28.5Q463-240 480-240t28.5-11.5ZM440-360h80v-200h-80v200Zm40-100Z"/></svg>
@@ -0,0 +1,162 @@
1
+ /**
2
+ * DeviceCard component — renders a single device/station card for the dashboard grid.
3
+ *
4
+ * Usage:
5
+ * DeviceCard.render(container, {
6
+ * device: { uniqueId, displayName, type, typename, ... },
7
+ * isStation: false,
8
+ * enabled: true,
9
+ * onClick: (device) => { ... },
10
+ * onToggle: (device, enabled) => { ... }
11
+ * });
12
+ */
13
+ // eslint-disable-next-line no-unused-vars
14
+ const DeviceCard = {
15
+
16
+ /**
17
+ * @param {HTMLElement} container
18
+ * @param {object} opts
19
+ * @param {object} opts.device - L_Device or L_Station data
20
+ * @param {boolean} [opts.isStation]
21
+ * @param {boolean} opts.enabled - whether device is enabled (not ignored)
22
+ * @param {function} opts.onClick - callback(device)
23
+ * @param {function} opts.onToggle - callback(device, enabled)
24
+ * @returns {HTMLElement}
25
+ */
26
+ render(container, opts) {
27
+ const d = opts.device;
28
+ const isUnsupported = d.unsupported === true;
29
+ const isIgnored = d.ignored === true;
30
+
31
+ const col = document.createElement('div');
32
+ col.className = 'col-6 col-md-4 col-lg-3 mb-3';
33
+
34
+ const card = document.createElement('div');
35
+ card.className = 'device-card';
36
+ if (isUnsupported) card.classList.add('device-card--unsupported');
37
+ if (isIgnored) card.classList.add('device-card--ignored');
38
+
39
+ // Image (skip for unsupported)
40
+ if (!isUnsupported) {
41
+ const imgWrap = document.createElement('div');
42
+ imgWrap.className = 'device-card__image-wrap';
43
+ const img = document.createElement('img');
44
+ img.src = DeviceImages.getPath(d.type);
45
+ img.alt = d.displayName;
46
+ img.loading = 'lazy';
47
+ imgWrap.appendChild(img);
48
+ card.appendChild(imgWrap);
49
+ }
50
+
51
+ // Body
52
+ const body = document.createElement('div');
53
+ body.className = 'device-card__body';
54
+
55
+ const name = document.createElement('div');
56
+ name.className = 'device-card__name';
57
+ name.textContent = d.displayName;
58
+ name.title = d.displayName;
59
+
60
+ // Meta row: left side = battery/charging info, right side = toggle
61
+ const metaRow = document.createElement('div');
62
+ metaRow.className = 'device-card__meta-row';
63
+
64
+ const meta = document.createElement('div');
65
+ meta.className = 'device-card__meta';
66
+
67
+ // Build meta line with DOM nodes so we can mix text and SVG icons
68
+ const metaFragments = [];
69
+ if (isUnsupported) {
70
+ metaFragments.push('Type ' + d.type);
71
+ }
72
+
73
+ // Power info (pre-computed by server)
74
+ const pw = d.power || {};
75
+ if (pw.battery !== undefined) {
76
+ metaFragments.push({ icon: Helpers.batteryIcon(pw.battery), text: pw.battery + '%' });
77
+ } else if (pw.batteryLow === true) {
78
+ metaFragments.push({ icon: Helpers.batteryIcon(0), text: 'Low' });
79
+ } else if (pw.batteryLow === false) {
80
+ metaFragments.push({ icon: Helpers.batteryIcon(100), text: 'OK' });
81
+ }
82
+ if (pw.icon && pw.label) {
83
+ metaFragments.push({ icon: pw.icon, text: pw.label });
84
+ }
85
+ metaFragments.forEach((frag, i) => {
86
+ if (i > 0) meta.append(' · ');
87
+ if (typeof frag === 'string') {
88
+ meta.append(frag);
89
+ } else {
90
+ meta.appendChild(Helpers.icon(frag.icon, 14, frag.text));
91
+ meta.append(' ' + frag.text);
92
+ }
93
+ });
94
+
95
+ metaRow.appendChild(meta);
96
+
97
+ // Toggle — inline with meta, only for non-unsupported devices
98
+ if (!isUnsupported) {
99
+ const switchWrap = document.createElement('div');
100
+ switchWrap.className = 'form-check form-switch mb-0';
101
+ switchWrap.addEventListener('click', (e) => e.stopPropagation());
102
+
103
+ const toggle = document.createElement('input');
104
+ toggle.type = 'checkbox';
105
+ toggle.className = 'form-check-input';
106
+ toggle.checked = opts.enabled;
107
+ toggle.role = 'switch';
108
+ toggle.title = opts.enabled ? 'Enabled in HomeKit' : 'Disabled in HomeKit';
109
+
110
+ toggle.addEventListener('change', (e) => {
111
+ e.stopPropagation();
112
+ if (opts.onToggle) opts.onToggle(d, toggle.checked);
113
+ });
114
+
115
+ switchWrap.appendChild(toggle);
116
+ metaRow.appendChild(switchWrap);
117
+ }
118
+
119
+ body.appendChild(name);
120
+ body.appendChild(metaRow);
121
+
122
+ // Footer with badges (unsupported / disabled)
123
+ const footer = document.createElement('div');
124
+ footer.className = 'device-card__footer';
125
+
126
+ const badgeArea = document.createElement('div');
127
+ let hasBadge = false;
128
+
129
+ if (isUnsupported) {
130
+ const badge = document.createElement('span');
131
+ badge.className = 'badge badge-unsupported';
132
+ badge.textContent = 'Not Supported';
133
+ badgeArea.appendChild(badge);
134
+
135
+ const hint = document.createElement('div');
136
+ hint.className = 'device-card__hint';
137
+ hint.textContent = 'Click to help us add support';
138
+ badgeArea.appendChild(hint);
139
+ hasBadge = true;
140
+ } else if (isIgnored) {
141
+ const badge = document.createElement('span');
142
+ badge.className = 'badge bg-secondary';
143
+ badge.textContent = 'Disabled';
144
+ badgeArea.appendChild(badge);
145
+ hasBadge = true;
146
+ }
147
+
148
+ footer.appendChild(badgeArea);
149
+
150
+ card.appendChild(body);
151
+ if (hasBadge) card.appendChild(footer);
152
+
153
+ // Click handler — navigate to detail
154
+ card.addEventListener('click', () => {
155
+ if (opts.onClick) opts.onClick(d);
156
+ });
157
+
158
+ col.appendChild(card);
159
+ if (container) container.appendChild(col);
160
+ return col;
161
+ },
162
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Guard Modes mapping component.
3
+ * Maps HomeKit modes (Home, Away, Night, Off) to Eufy guard mode numbers.
4
+ *
5
+ * Usage:
6
+ * GuardModes.render(container, {
7
+ * hkHome: 1, hkAway: 0, hkNight: 1, hkOff: 63,
8
+ * onChange: (modes) => { ... } // { hkHome, hkAway, hkNight, hkOff }
9
+ * });
10
+ */
11
+ // eslint-disable-next-line no-unused-vars
12
+ const GuardModes = {
13
+ EUFY_MODES: [
14
+ { value: '0', label: 'Away' },
15
+ { value: '1', label: 'Home' },
16
+ { value: '2', label: 'Schedule' },
17
+ { value: '3', label: 'Custom 1' },
18
+ { value: '4', label: 'Custom 2' },
19
+ { value: '5', label: 'Custom 3' },
20
+ { value: '6', label: 'Off' },
21
+ { value: '47', label: 'Geofencing' },
22
+ { value: '63', label: 'Disarmed' },
23
+ ],
24
+
25
+ /**
26
+ * @param {HTMLElement} container
27
+ * @param {object} opts
28
+ * @param {number} [opts.hkHome]
29
+ * @param {number} [opts.hkAway]
30
+ * @param {number} [opts.hkNight]
31
+ * @param {number} [opts.hkOff]
32
+ * @param {boolean} [opts.disabled]
33
+ * @param {function} opts.onChange - callback({ hkHome, hkAway, hkNight, hkOff })
34
+ * @returns {HTMLElement}
35
+ */
36
+ render(container, opts) {
37
+ const wrap = document.createElement('div');
38
+ wrap.className = 'guard-mode-grid';
39
+
40
+ const modes = {
41
+ hkHome: { label: 'HomeKit Home', value: opts.hkHome ?? 1 },
42
+ hkAway: { label: 'HomeKit Away', value: opts.hkAway ?? 0 },
43
+ hkNight: { label: 'HomeKit Night', value: opts.hkNight ?? 1 },
44
+ hkOff: { label: 'HomeKit Off', value: opts.hkOff ?? 63 },
45
+ };
46
+
47
+ const currentValues = {
48
+ hkHome: modes.hkHome.value,
49
+ hkAway: modes.hkAway.value,
50
+ hkNight: modes.hkNight.value,
51
+ hkOff: modes.hkOff.value,
52
+ };
53
+
54
+ Object.entries(modes).forEach(([key, mode]) => {
55
+ const group = document.createElement('div');
56
+ group.className = 'form-group';
57
+
58
+ const label = document.createElement('label');
59
+ label.textContent = mode.label;
60
+ label.setAttribute('for', 'guard-' + key);
61
+
62
+ const select = document.createElement('select');
63
+ select.className = 'form-select form-select-sm';
64
+ select.id = 'guard-' + key;
65
+ select.disabled = !!opts.disabled;
66
+
67
+ this.EUFY_MODES.forEach((em) => {
68
+ const option = document.createElement('option');
69
+ option.value = em.value;
70
+ option.textContent = em.label;
71
+ if (String(em.value) === String(mode.value)) option.selected = true;
72
+ select.appendChild(option);
73
+ });
74
+
75
+ select.addEventListener('change', () => {
76
+ currentValues[key] = parseInt(select.value);
77
+ if (opts.onChange) opts.onChange({ ...currentValues });
78
+ });
79
+
80
+ group.appendChild(label);
81
+ group.appendChild(select);
82
+ wrap.appendChild(group);
83
+ });
84
+
85
+ if (container) container.appendChild(wrap);
86
+ return wrap;
87
+ },
88
+ };
@@ -0,0 +1,121 @@
1
+ /**
2
+ * NumberInput component — renders a number input with +/- buttons, label, and optional help tooltip.
3
+ *
4
+ * Usage:
5
+ * NumberInput.render(container, {
6
+ * id: 'polling-interval',
7
+ * label: 'Polling Interval',
8
+ * help: 'Minutes between cloud polls',
9
+ * value: 10,
10
+ * min: 1,
11
+ * max: 120,
12
+ * step: 1,
13
+ * suffix: 'min',
14
+ * onChange: (value) => { ... }
15
+ * });
16
+ */
17
+ // eslint-disable-next-line no-unused-vars
18
+ const NumberInput = {
19
+ /**
20
+ * @param {HTMLElement} container
21
+ * @param {object} opts
22
+ * @param {string} opts.id
23
+ * @param {string} opts.label
24
+ * @param {string} [opts.help]
25
+ * @param {number} opts.value
26
+ * @param {number} [opts.min]
27
+ * @param {number} [opts.max]
28
+ * @param {number} [opts.step]
29
+ * @param {string} [opts.suffix] - text after the input (e.g., 'min', 'sec')
30
+ * @param {boolean} [opts.disabled]
31
+ * @param {function} opts.onChange - callback(value: number)
32
+ * @returns {HTMLElement}
33
+ */
34
+ render(container, opts) {
35
+ const row = document.createElement('div');
36
+ row.className = 'eufy-toggle';
37
+
38
+ const labelWrap = document.createElement('div');
39
+ labelWrap.className = 'eufy-toggle__label';
40
+
41
+ const labelEl = document.createElement('label');
42
+ labelEl.setAttribute('for', opts.id);
43
+ labelEl.textContent = opts.label;
44
+ labelWrap.appendChild(labelEl);
45
+
46
+ if (opts.help) {
47
+ const helpEl = document.createElement('span');
48
+ helpEl.className = 'eufy-toggle__help';
49
+ helpEl.textContent = '?';
50
+ helpEl.title = opts.help;
51
+ labelWrap.appendChild(helpEl);
52
+ }
53
+
54
+ const inputGroup = document.createElement('div');
55
+ inputGroup.className = 'number-input-group';
56
+
57
+ const step = opts.step || 1;
58
+ const min = opts.min !== undefined ? opts.min : 0;
59
+ const max = opts.max !== undefined ? opts.max : 99999;
60
+
61
+ const btnMinus = document.createElement('button');
62
+ btnMinus.type = 'button';
63
+ btnMinus.className = 'btn btn-outline-secondary btn-sm';
64
+ btnMinus.textContent = '−';
65
+
66
+ const input = document.createElement('input');
67
+ input.type = 'number';
68
+ input.className = 'form-control form-control-sm';
69
+ input.id = opts.id;
70
+ input.value = opts.value;
71
+ input.min = min;
72
+ input.max = max;
73
+ input.step = step;
74
+ input.disabled = !!opts.disabled;
75
+
76
+ const btnPlus = document.createElement('button');
77
+ btnPlus.type = 'button';
78
+ btnPlus.className = 'btn btn-outline-secondary btn-sm';
79
+ btnPlus.textContent = '+';
80
+
81
+ const fireChange = () => {
82
+ let val = parseFloat(input.value);
83
+ if (isNaN(val)) val = min;
84
+ val = Math.max(min, Math.min(max, val));
85
+ input.value = val;
86
+ if (opts.onChange) opts.onChange(val);
87
+ };
88
+
89
+ btnMinus.addEventListener('click', () => {
90
+ let val = parseFloat(input.value) - step;
91
+ input.value = Math.max(min, val);
92
+ fireChange();
93
+ });
94
+
95
+ btnPlus.addEventListener('click', () => {
96
+ let val = parseFloat(input.value) + step;
97
+ input.value = Math.min(max, val);
98
+ fireChange();
99
+ });
100
+
101
+ input.addEventListener('change', fireChange);
102
+
103
+ inputGroup.appendChild(btnMinus);
104
+ inputGroup.appendChild(input);
105
+ inputGroup.appendChild(btnPlus);
106
+
107
+ if (opts.suffix) {
108
+ const suffixEl = document.createElement('span');
109
+ suffixEl.className = 'text-muted ms-1';
110
+ suffixEl.style.fontSize = '0.8rem';
111
+ suffixEl.textContent = opts.suffix;
112
+ inputGroup.appendChild(suffixEl);
113
+ }
114
+
115
+ row.appendChild(labelWrap);
116
+ row.appendChild(inputGroup);
117
+
118
+ if (container) container.appendChild(row);
119
+ return row;
120
+ },
121
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Select component — renders a dropdown with label and optional help tooltip.
3
+ *
4
+ * Usage:
5
+ * Select.render(container, {
6
+ * id: 'snapshot-method',
7
+ * label: 'Snapshot Method',
8
+ * help: 'How snapshots are captured',
9
+ * options: [{ value: '0', label: 'Auto' }, { value: '1', label: 'From Stream' }],
10
+ * value: '0',
11
+ * onChange: (value) => { ... }
12
+ * });
13
+ */
14
+ // eslint-disable-next-line no-unused-vars
15
+ const Select = {
16
+ /**
17
+ * @param {HTMLElement} container
18
+ * @param {object} opts
19
+ * @param {string} opts.id
20
+ * @param {string} opts.label
21
+ * @param {string} [opts.help]
22
+ * @param {Array<{value: string, label: string}>} opts.options
23
+ * @param {string} opts.value - initial selected value
24
+ * @param {boolean} [opts.disabled]
25
+ * @param {function} opts.onChange - callback(value: string)
26
+ * @returns {HTMLElement}
27
+ */
28
+ render(container, opts) {
29
+ const row = document.createElement('div');
30
+ row.className = 'eufy-toggle';
31
+
32
+ const labelWrap = document.createElement('div');
33
+ labelWrap.className = 'eufy-toggle__label';
34
+
35
+ const labelEl = document.createElement('label');
36
+ labelEl.setAttribute('for', opts.id);
37
+ labelEl.textContent = opts.label;
38
+ labelWrap.appendChild(labelEl);
39
+
40
+ if (opts.help) {
41
+ const helpEl = document.createElement('span');
42
+ helpEl.className = 'eufy-toggle__help';
43
+ helpEl.textContent = '?';
44
+ helpEl.title = opts.help;
45
+ labelWrap.appendChild(helpEl);
46
+ }
47
+
48
+ const selectEl = document.createElement('select');
49
+ selectEl.className = 'form-select form-select-sm';
50
+ selectEl.id = opts.id;
51
+ selectEl.style.width = 'auto';
52
+ selectEl.style.maxWidth = '200px';
53
+ selectEl.disabled = !!opts.disabled;
54
+
55
+ (opts.options || []).forEach((opt) => {
56
+ const option = document.createElement('option');
57
+ option.value = opt.value;
58
+ option.textContent = opt.label;
59
+ if (String(opt.value) === String(opts.value)) option.selected = true;
60
+ selectEl.appendChild(option);
61
+ });
62
+
63
+ selectEl.addEventListener('change', () => {
64
+ if (opts.onChange) opts.onChange(selectEl.value);
65
+ });
66
+
67
+ row.appendChild(labelWrap);
68
+ row.appendChild(selectEl);
69
+
70
+ if (container) container.appendChild(row);
71
+ return row;
72
+ },
73
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Toggle component — renders an on/off switch with label and optional help tooltip.
3
+ *
4
+ * Usage:
5
+ * Toggle.render(container, {
6
+ * id: 'enable-camera',
7
+ * label: 'Enable Camera',
8
+ * help: 'Show this camera in HomeKit',
9
+ * checked: true,
10
+ * onChange: (checked) => { ... }
11
+ * });
12
+ */
13
+ // eslint-disable-next-line no-unused-vars
14
+ const Toggle = {
15
+ /**
16
+ * @param {HTMLElement} container - parent element to append into
17
+ * @param {object} opts
18
+ * @param {string} opts.id - unique id for the input
19
+ * @param {string} opts.label - display label
20
+ * @param {string} [opts.help] - tooltip text
21
+ * @param {boolean} opts.checked - initial state
22
+ * @param {boolean} [opts.disabled] - disabled state
23
+ * @param {function} opts.onChange - callback(checked: boolean)
24
+ * @returns {HTMLElement}
25
+ */
26
+ render(container, opts) {
27
+ const row = document.createElement('div');
28
+ row.className = 'eufy-toggle';
29
+
30
+ const labelWrap = document.createElement('div');
31
+ labelWrap.className = 'eufy-toggle__label';
32
+
33
+ const labelEl = document.createElement('label');
34
+ labelEl.setAttribute('for', opts.id);
35
+ labelEl.textContent = opts.label;
36
+ labelWrap.appendChild(labelEl);
37
+
38
+ if (opts.help) {
39
+ const helpEl = document.createElement('span');
40
+ helpEl.className = 'eufy-toggle__help';
41
+ helpEl.textContent = '?';
42
+ helpEl.title = opts.help;
43
+ labelWrap.appendChild(helpEl);
44
+ }
45
+
46
+ const switchWrap = document.createElement('div');
47
+ switchWrap.className = 'form-check form-switch mb-0';
48
+
49
+ const input = document.createElement('input');
50
+ input.type = 'checkbox';
51
+ input.className = 'form-check-input';
52
+ input.id = opts.id;
53
+ input.checked = !!opts.checked;
54
+ input.disabled = !!opts.disabled;
55
+ input.role = 'switch';
56
+
57
+ input.addEventListener('change', () => {
58
+ if (opts.onChange) opts.onChange(input.checked);
59
+ });
60
+
61
+ switchWrap.appendChild(input);
62
+ row.appendChild(labelWrap);
63
+ row.appendChild(switchWrap);
64
+
65
+ if (container) container.appendChild(row);
66
+ return row;
67
+ },
68
+ };
@@ -0,0 +1,27 @@
1
+ <link rel="stylesheet" href="style.css">
2
+
3
+ <div id="app">
4
+ <div class="d-flex justify-content-center align-items-center" style="min-height: 200px;">
5
+ <div class="spinner-border text-primary" role="status">
6
+ <span class="visually-hidden">Loading...</span>
7
+ </div>
8
+ </div>
9
+ </div>
10
+
11
+ <script src="utils/countries.js"></script>
12
+ <script src="utils/device-images.js"></script>
13
+ <script src="utils/helpers.js"></script>
14
+ <script src="services/api.js"></script>
15
+ <script src="services/config.js"></script>
16
+ <script src="components/toggle.js"></script>
17
+ <script src="components/select.js"></script>
18
+ <script src="components/number-input.js"></script>
19
+ <script src="components/guard-modes.js"></script>
20
+ <script src="components/device-card.js"></script>
21
+ <script src="views/login.js"></script>
22
+ <script src="views/dashboard.js"></script>
23
+ <script src="views/device-detail.js"></script>
24
+ <script src="views/unsupported-detail.js"></script>
25
+ <script src="views/settings.js"></script>
26
+ <script src="views/diagnostics.js"></script>
27
+ <script src="app.js"></script>