@homebridge-plugins/homebridge-eufy-security 4.7.2 → 5.0.0-beta.0

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 (471) hide show
  1. package/AUTHORS +4 -0
  2. package/CODE_OF_CONDUCT.md +30 -0
  3. package/CONTRIBUTING.md +77 -0
  4. package/README.md +152 -49
  5. package/SECURITY.md +30 -0
  6. package/config.schema.json +92 -3
  7. package/dist/account/ownership.js +290 -0
  8. package/dist/account/ownership.js.map +1 -0
  9. package/dist/account/persistence.js +274 -0
  10. package/dist/account/persistence.js.map +1 -0
  11. package/dist/account/temporary-authentication.js +348 -0
  12. package/dist/account/temporary-authentication.js.map +1 -0
  13. package/dist/configuration.js +167 -0
  14. package/dist/configuration.js.map +1 -0
  15. package/dist/device/member-evidence.js +66 -0
  16. package/dist/device/member-evidence.js.map +1 -0
  17. package/dist/device/member-trust.js +114 -0
  18. package/dist/device/member-trust.js.map +1 -0
  19. package/dist/device/snapshot.js +190 -0
  20. package/dist/device/snapshot.js.map +1 -0
  21. package/dist/diagnostics.js +2218 -0
  22. package/dist/diagnostics.js.map +1 -0
  23. package/dist/homekit/adapter.js +2 -0
  24. package/dist/homekit/adapter.js.map +1 -0
  25. package/dist/homekit/adapters/battery.js +208 -0
  26. package/dist/homekit/adapters/battery.js.map +1 -0
  27. package/dist/homekit/adapters/camera-controls.js +527 -0
  28. package/dist/homekit/adapters/camera-controls.js.map +1 -0
  29. package/dist/homekit/adapters/camera-streaming.js +1515 -0
  30. package/dist/homekit/adapters/camera-streaming.js.map +1 -0
  31. package/dist/homekit/adapters/contact.js +138 -0
  32. package/dist/homekit/adapters/contact.js.map +1 -0
  33. package/dist/homekit/adapters/doorbell.js +57 -0
  34. package/dist/homekit/adapters/doorbell.js.map +1 -0
  35. package/dist/homekit/adapters/information.js +55 -0
  36. package/dist/homekit/adapters/information.js.map +1 -0
  37. package/dist/homekit/adapters/lock.js +489 -0
  38. package/dist/homekit/adapters/lock.js.map +1 -0
  39. package/dist/homekit/adapters/motion.js +100 -0
  40. package/dist/homekit/adapters/motion.js.map +1 -0
  41. package/dist/homekit/adapters/registry.js +26 -0
  42. package/dist/homekit/adapters/registry.js.map +1 -0
  43. package/dist/homekit/adapters/security-system.js +425 -0
  44. package/dist/homekit/adapters/security-system.js.map +1 -0
  45. package/dist/homekit/adapters/siren.js +136 -0
  46. package/dist/homekit/adapters/siren.js.map +1 -0
  47. package/dist/homekit/adapters/smart-light.js +662 -0
  48. package/dist/homekit/adapters/smart-light.js.map +1 -0
  49. package/dist/homekit/coverage-matrix.js +119 -0
  50. package/dist/homekit/coverage-matrix.js.map +1 -0
  51. package/dist/homekit/device-control.js +175 -0
  52. package/dist/homekit/device-control.js.map +1 -0
  53. package/dist/homekit/reconciler.js +319 -0
  54. package/dist/homekit/reconciler.js.map +1 -0
  55. package/dist/homekit/representation.js +23 -0
  56. package/dist/homekit/representation.js.map +1 -0
  57. package/dist/index.js +4 -9
  58. package/dist/index.js.map +1 -1
  59. package/dist/media/contracts.js +2 -0
  60. package/dist/media/contracts.js.map +1 -0
  61. package/dist/media/last-successful-image.js +208 -0
  62. package/dist/media/last-successful-image.js.map +1 -0
  63. package/dist/media/live-stream.js +1003 -0
  64. package/dist/media/live-stream.js.map +1 -0
  65. package/dist/media/recording.js +560 -0
  66. package/dist/media/recording.js.map +1 -0
  67. package/dist/media/session-budget.js +39 -0
  68. package/dist/media/session-budget.js.map +1 -0
  69. package/dist/media/snapshot.js +406 -0
  70. package/dist/media/snapshot.js.map +1 -0
  71. package/dist/media/station-live-sessions.js +101 -0
  72. package/dist/media/station-live-sessions.js.map +1 -0
  73. package/dist/platform.js +97 -809
  74. package/dist/platform.js.map +1 -1
  75. package/dist/runtime/channel.js +375 -0
  76. package/dist/runtime/channel.js.map +1 -0
  77. package/dist/runtime/owner.js +682 -0
  78. package/dist/runtime/owner.js.map +1 -0
  79. package/dist/runtime/sdk-client.js +211 -0
  80. package/dist/runtime/sdk-client.js.map +1 -0
  81. package/dist/runtime/tracker.js +211 -0
  82. package/dist/runtime/tracker.js.map +1 -0
  83. package/dist/settings.js +1 -43
  84. package/dist/settings.js.map +1 -1
  85. package/dist/storage.js +59 -0
  86. package/dist/storage.js.map +1 -0
  87. package/dist/ui/dashboard.js +181 -0
  88. package/dist/ui/dashboard.js.map +1 -0
  89. package/dist/ui/device-image.js +22 -0
  90. package/dist/ui/device-image.js.map +1 -0
  91. package/dist/ui/runtime-channel-client.js +305 -0
  92. package/dist/ui/runtime-channel-client.js.map +1 -0
  93. package/dist/ui/server.js +259 -0
  94. package/dist/ui/server.js.map +1 -0
  95. package/homebridge-ui/public/app.css +1902 -0
  96. package/homebridge-ui/public/assets/devices/clean/clean-T1250.webp +0 -0
  97. package/homebridge-ui/public/assets/devices/clean/clean-T2070.webp +0 -0
  98. package/homebridge-ui/public/assets/devices/clean/clean-T2080.webp +0 -0
  99. package/homebridge-ui/public/assets/devices/clean/clean-T2081.webp +0 -0
  100. package/homebridge-ui/public/assets/devices/clean/clean-T2103.webp +0 -0
  101. package/homebridge-ui/public/assets/devices/clean/clean-T2117.webp +0 -0
  102. package/homebridge-ui/public/assets/devices/clean/clean-T2118.webp +0 -0
  103. package/homebridge-ui/public/assets/devices/clean/clean-T2119.webp +0 -0
  104. package/homebridge-ui/public/assets/devices/clean/clean-T211A.webp +0 -0
  105. package/homebridge-ui/public/assets/devices/clean/clean-T2120.webp +0 -0
  106. package/homebridge-ui/public/assets/devices/clean/clean-T2123.webp +0 -0
  107. package/homebridge-ui/public/assets/devices/clean/clean-T2128.webp +0 -0
  108. package/homebridge-ui/public/assets/devices/clean/clean-T2130.webp +0 -0
  109. package/homebridge-ui/public/assets/devices/clean/clean-T2132.webp +0 -0
  110. package/homebridge-ui/public/assets/devices/clean/clean-T2150.webp +0 -0
  111. package/homebridge-ui/public/assets/devices/clean/clean-T2181.webp +0 -0
  112. package/homebridge-ui/public/assets/devices/clean/clean-T2182.webp +0 -0
  113. package/homebridge-ui/public/assets/devices/clean/clean-T2190.webp +0 -0
  114. package/homebridge-ui/public/assets/devices/clean/clean-T2192.webp +0 -0
  115. package/homebridge-ui/public/assets/devices/clean/clean-T2193.webp +0 -0
  116. package/homebridge-ui/public/assets/devices/clean/clean-T2194.webp +0 -0
  117. package/homebridge-ui/public/assets/devices/clean/clean-T2210.webp +0 -0
  118. package/homebridge-ui/public/assets/devices/clean/clean-T2250.webp +0 -0
  119. package/homebridge-ui/public/assets/devices/clean/clean-T2251.webp +0 -0
  120. package/homebridge-ui/public/assets/devices/clean/clean-T2252.webp +0 -0
  121. package/homebridge-ui/public/assets/devices/clean/clean-T2253.webp +0 -0
  122. package/homebridge-ui/public/assets/devices/clean/clean-T2254.webp +0 -0
  123. package/homebridge-ui/public/assets/devices/clean/clean-T2255.webp +0 -0
  124. package/homebridge-ui/public/assets/devices/clean/clean-T2256.webp +0 -0
  125. package/homebridge-ui/public/assets/devices/clean/clean-T2257.webp +0 -0
  126. package/homebridge-ui/public/assets/devices/clean/clean-T2258.webp +0 -0
  127. package/homebridge-ui/public/assets/devices/clean/clean-T2259.webp +0 -0
  128. package/homebridge-ui/public/assets/devices/clean/clean-T2261.webp +0 -0
  129. package/homebridge-ui/public/assets/devices/clean/clean-T2262.webp +0 -0
  130. package/homebridge-ui/public/assets/devices/clean/clean-T2266.webp +0 -0
  131. package/homebridge-ui/public/assets/devices/clean/clean-T2267.webp +0 -0
  132. package/homebridge-ui/public/assets/devices/clean/clean-T2268.webp +0 -0
  133. package/homebridge-ui/public/assets/devices/clean/clean-T2270.webp +0 -0
  134. package/homebridge-ui/public/assets/devices/clean/clean-T2272.webp +0 -0
  135. package/homebridge-ui/public/assets/devices/clean/clean-T2273.webp +0 -0
  136. package/homebridge-ui/public/assets/devices/clean/clean-T2276.webp +0 -0
  137. package/homebridge-ui/public/assets/devices/clean/clean-T2277.webp +0 -0
  138. package/homebridge-ui/public/assets/devices/clean/clean-T2278.webp +0 -0
  139. package/homebridge-ui/public/assets/devices/clean/clean-T2280.webp +0 -0
  140. package/homebridge-ui/public/assets/devices/clean/clean-T2292.webp +0 -0
  141. package/homebridge-ui/public/assets/devices/clean/clean-T2320.webp +0 -0
  142. package/homebridge-ui/public/assets/devices/clean/clean-T2351.webp +0 -0
  143. package/homebridge-ui/public/assets/devices/clean/clean-T2352.webp +0 -0
  144. package/homebridge-ui/public/assets/devices/clean/clean-T2353.webp +0 -0
  145. package/homebridge-ui/public/assets/devices/life/life-T8L00.webp +0 -0
  146. package/homebridge-ui/public/assets/devices/life/life-T8L01.webp +0 -0
  147. package/homebridge-ui/public/assets/devices/life/life-T8L02.webp +0 -0
  148. package/homebridge-ui/public/assets/devices/life/life-T8L04.webp +0 -0
  149. package/homebridge-ui/public/assets/devices/life/life-T8L10.webp +0 -0
  150. package/homebridge-ui/public/assets/devices/life/life-T8L20.webp +0 -0
  151. package/homebridge-ui/public/assets/devices/life/life-T8L30.webp +0 -0
  152. package/homebridge-ui/public/assets/devices/life/life-T8L40.webp +0 -0
  153. package/homebridge-ui/public/assets/devices/mower/mower-T2801.webp +0 -0
  154. package/homebridge-ui/public/assets/devices/mower/mower-T280B.webp +0 -0
  155. package/homebridge-ui/public/assets/devices/mower/mower-T2880.webp +0 -0
  156. package/homebridge-ui/public/assets/devices/security/security-T7400.webp +0 -0
  157. package/homebridge-ui/public/assets/devices/security/security-T7401.webp +0 -0
  158. package/homebridge-ui/public/assets/devices/security/security-T8001.webp +0 -0
  159. package/homebridge-ui/public/assets/devices/security/security-T8002.webp +0 -0
  160. package/homebridge-ui/public/assets/devices/security/security-T8010.webp +0 -0
  161. package/homebridge-ui/public/assets/devices/security/security-T8021.webp +0 -0
  162. package/homebridge-ui/public/assets/devices/security/security-T8023.webp +0 -0
  163. package/homebridge-ui/public/assets/devices/security/security-T8025.webp +0 -0
  164. package/homebridge-ui/public/assets/devices/security/security-T8030.webp +0 -0
  165. package/homebridge-ui/public/assets/devices/security/security-T8110.webp +0 -0
  166. package/homebridge-ui/public/assets/devices/security/security-T8111.webp +0 -0
  167. package/homebridge-ui/public/assets/devices/security/security-T8112.webp +0 -0
  168. package/homebridge-ui/public/assets/devices/security/security-T8113.webp +0 -0
  169. package/homebridge-ui/public/assets/devices/security/security-T8114.webp +0 -0
  170. package/homebridge-ui/public/assets/devices/security/security-T8122.webp +0 -0
  171. package/homebridge-ui/public/assets/devices/security/security-T8123.webp +0 -0
  172. package/homebridge-ui/public/assets/devices/security/security-T8124.webp +0 -0
  173. package/homebridge-ui/public/assets/devices/security/security-T8130.webp +0 -0
  174. package/homebridge-ui/public/assets/devices/security/security-T8131.webp +0 -0
  175. package/homebridge-ui/public/assets/devices/security/security-T8134.webp +0 -0
  176. package/homebridge-ui/public/assets/devices/security/security-T8140.webp +0 -0
  177. package/homebridge-ui/public/assets/devices/security/security-T8141.webp +0 -0
  178. package/homebridge-ui/public/assets/devices/security/security-T8150.webp +0 -0
  179. package/homebridge-ui/public/assets/devices/security/security-T8151.webp +0 -0
  180. package/homebridge-ui/public/assets/devices/security/security-T8152.webp +0 -0
  181. package/homebridge-ui/public/assets/devices/security/security-T8153.webp +0 -0
  182. package/homebridge-ui/public/assets/devices/security/security-T8160.webp +0 -0
  183. package/homebridge-ui/public/assets/devices/security/security-T8161.webp +0 -0
  184. package/homebridge-ui/public/assets/devices/security/security-T8162.webp +0 -0
  185. package/homebridge-ui/public/assets/devices/security/security-T8170.webp +0 -0
  186. package/homebridge-ui/public/assets/devices/security/security-T8171.webp +0 -0
  187. package/homebridge-ui/public/assets/devices/security/security-T8172.webp +0 -0
  188. package/homebridge-ui/public/assets/devices/security/security-T8173.webp +0 -0
  189. package/homebridge-ui/public/assets/devices/security/security-T8200.webp +0 -0
  190. package/homebridge-ui/public/assets/devices/security/security-T8200X.webp +0 -0
  191. package/homebridge-ui/public/assets/devices/security/security-T8201.webp +0 -0
  192. package/homebridge-ui/public/assets/devices/security/security-T8203.webp +0 -0
  193. package/homebridge-ui/public/assets/devices/security/security-T8210.webp +0 -0
  194. package/homebridge-ui/public/assets/devices/security/security-T8213.webp +0 -0
  195. package/homebridge-ui/public/assets/devices/security/security-T8214.webp +0 -0
  196. package/homebridge-ui/public/assets/devices/security/security-T8222.webp +0 -0
  197. package/homebridge-ui/public/assets/devices/security/security-T8400.webp +0 -0
  198. package/homebridge-ui/public/assets/devices/security/security-T8401.webp +0 -0
  199. package/homebridge-ui/public/assets/devices/security/security-T8410.webp +0 -0
  200. package/homebridge-ui/public/assets/devices/security/security-T8411.webp +0 -0
  201. package/homebridge-ui/public/assets/devices/security/security-T8414.webp +0 -0
  202. package/homebridge-ui/public/assets/devices/security/security-T8416.webp +0 -0
  203. package/homebridge-ui/public/assets/devices/security/security-T8417.webp +0 -0
  204. package/homebridge-ui/public/assets/devices/security/security-T8419.webp +0 -0
  205. package/homebridge-ui/public/assets/devices/security/security-T8419N.webp +0 -0
  206. package/homebridge-ui/public/assets/devices/security/security-T8420.webp +0 -0
  207. package/homebridge-ui/public/assets/devices/security/security-T8420X.webp +0 -0
  208. package/homebridge-ui/public/assets/devices/security/security-T8422.webp +0 -0
  209. package/homebridge-ui/public/assets/devices/security/security-T8423.webp +0 -0
  210. package/homebridge-ui/public/assets/devices/security/security-T8424.webp +0 -0
  211. package/homebridge-ui/public/assets/devices/security/security-T8425.webp +0 -0
  212. package/homebridge-ui/public/assets/devices/security/security-T8441.webp +0 -0
  213. package/homebridge-ui/public/assets/devices/security/security-T8442.webp +0 -0
  214. package/homebridge-ui/public/assets/devices/security/security-T8452.webp +0 -0
  215. package/homebridge-ui/public/assets/devices/security/security-T84A0.webp +0 -0
  216. package/homebridge-ui/public/assets/devices/security/security-T84A1.webp +0 -0
  217. package/homebridge-ui/public/assets/devices/security/security-T8500.webp +0 -0
  218. package/homebridge-ui/public/assets/devices/security/security-T8502.webp +0 -0
  219. package/homebridge-ui/public/assets/devices/security/security-T8503.webp +0 -0
  220. package/homebridge-ui/public/assets/devices/security/security-T8504.webp +0 -0
  221. package/homebridge-ui/public/assets/devices/security/security-T8506.webp +0 -0
  222. package/homebridge-ui/public/assets/devices/security/security-T8510.webp +0 -0
  223. package/homebridge-ui/public/assets/devices/security/security-T8510P.webp +0 -0
  224. package/homebridge-ui/public/assets/devices/security/security-T8520.webp +0 -0
  225. package/homebridge-ui/public/assets/devices/security/security-T8520P.webp +0 -0
  226. package/homebridge-ui/public/assets/devices/security/security-T8530.webp +0 -0
  227. package/homebridge-ui/public/assets/devices/security/security-T8531.webp +0 -0
  228. package/homebridge-ui/public/assets/devices/security/security-T85D0.webp +0 -0
  229. package/homebridge-ui/public/assets/devices/security/security-T85L0.webp +0 -0
  230. package/homebridge-ui/public/assets/devices/security/security-T85P0.webp +0 -0
  231. package/homebridge-ui/public/assets/devices/security/security-T85V0.webp +0 -0
  232. package/homebridge-ui/public/assets/devices/security/security-T8600.webp +0 -0
  233. package/homebridge-ui/public/assets/devices/security/security-T86P2.webp +0 -0
  234. package/homebridge-ui/public/assets/devices/security/security-T8790.webp +0 -0
  235. package/homebridge-ui/public/assets/devices/security/security-T87B0.webp +0 -0
  236. package/homebridge-ui/public/assets/devices/security/security-T87B2.webp +0 -0
  237. package/homebridge-ui/public/assets/devices/security/security-T8900.webp +0 -0
  238. package/homebridge-ui/public/assets/devices/security/security-T8910.webp +0 -0
  239. package/homebridge-ui/public/assets/devices/security/security-T8920.webp +0 -0
  240. package/homebridge-ui/public/assets/devices/security/security-T8960.webp +0 -0
  241. package/homebridge-ui/public/assets/devices/security/security-T8B0.webp +0 -0
  242. package/homebridge-ui/public/assets/devices/security/security-T8E00.webp +0 -0
  243. package/homebridge-ui/public/assets/devices/security/security-T8N00.webp +0 -0
  244. package/homebridge-ui/public/assets/devices/security/security-T8W11C.webp +0 -0
  245. package/homebridge-ui/public/assets/devices/security/security-T90F0.webp +0 -0
  246. package/homebridge-ui/public/assets/devices/security/security-T90M0.webp +0 -0
  247. package/homebridge-ui/public/assets/devices/security/security-T90R0.webp +0 -0
  248. package/homebridge-ui/public/assets/devices/security/security-T90S0.webp +0 -0
  249. package/homebridge-ui/public/assets/icons/inventory.svg +1 -1
  250. package/homebridge-ui/public/assets/icons/troubleshoot.svg +1 -0
  251. package/homebridge-ui/public/assets/icons/warning.svg +1 -1
  252. package/homebridge-ui/public/assets/logo-dark.svg +10 -0
  253. package/homebridge-ui/public/assets/logo.svg +10 -0
  254. package/homebridge-ui/public/i18n/en.json +218 -0
  255. package/homebridge-ui/public/i18n/fr.json +218 -0
  256. package/homebridge-ui/public/index.html +377 -28
  257. package/homebridge-ui/public/js/app.js +1065 -0
  258. package/homebridge-ui/public/js/dashboard.js +385 -0
  259. package/homebridge-ui/public/js/legacy-settings.js +37 -0
  260. package/homebridge-ui/public/js/profile-wizard.js +68 -0
  261. package/homebridge-ui/server.js +2 -1393
  262. package/i18n/runtime/en.json +100 -0
  263. package/media/Snapshot-Unavailable.jpg +0 -0
  264. package/media/Snapshot-black.jpg +0 -0
  265. package/media/camera-disabled.jpg +0 -0
  266. package/media/camera-offline.jpg +0 -0
  267. package/package.json +29 -24
  268. package/.claude/CLAUDE.md +0 -175
  269. package/.claude/PRD.md +0 -241
  270. package/.claude/docs/debug-recording-lessons.md +0 -91
  271. package/.claude/docs/hksv-recording-fix.md +0 -160
  272. package/.claude/skills/architect/SKILL.md +0 -76
  273. package/.claude/skills/developer/SKILL.md +0 -59
  274. package/.claude/skills/new-device-support/SKILL.md +0 -212
  275. package/.claude/skills/new-device-support/check-device.mjs +0 -363
  276. package/.claude/skills/new-device-support/map-properties.mjs +0 -450
  277. package/.claude/skills/new-device-support/verify-device.mjs +0 -272
  278. package/.claude/skills/planner/SKILL.md +0 -100
  279. package/.claude/skills/qa/SKILL.md +0 -79
  280. package/.claude/skills/support/SKILL.md +0 -175
  281. package/.prettierignore +0 -7
  282. package/.prettierrc.json +0 -7
  283. package/FUNDING.yml +0 -1
  284. package/dist/accessories/AutoSyncStationAccessory.js +0 -157
  285. package/dist/accessories/AutoSyncStationAccessory.js.map +0 -1
  286. package/dist/accessories/BaseAccessory.js +0 -261
  287. package/dist/accessories/BaseAccessory.js.map +0 -1
  288. package/dist/accessories/CameraAccessory.js +0 -459
  289. package/dist/accessories/CameraAccessory.js.map +0 -1
  290. package/dist/accessories/Device.js +0 -65
  291. package/dist/accessories/Device.js.map +0 -1
  292. package/dist/accessories/EntrySensorAccessory.js +0 -48
  293. package/dist/accessories/EntrySensorAccessory.js.map +0 -1
  294. package/dist/accessories/GarageDoorAccessory.js +0 -160
  295. package/dist/accessories/GarageDoorAccessory.js.map +0 -1
  296. package/dist/accessories/LockAccessory.js +0 -142
  297. package/dist/accessories/LockAccessory.js.map +0 -1
  298. package/dist/accessories/MotionSensorAccessory.js +0 -48
  299. package/dist/accessories/MotionSensorAccessory.js.map +0 -1
  300. package/dist/accessories/SmartDropAccessory.js +0 -141
  301. package/dist/accessories/SmartDropAccessory.js.map +0 -1
  302. package/dist/accessories/StationAccessory.js +0 -373
  303. package/dist/accessories/StationAccessory.js.map +0 -1
  304. package/dist/config.js +0 -26
  305. package/dist/config.js.map +0 -1
  306. package/dist/controller/DebugRecordingManager.js +0 -402
  307. package/dist/controller/DebugRecordingManager.js.map +0 -1
  308. package/dist/controller/LocalLivestreamManager.js +0 -371
  309. package/dist/controller/LocalLivestreamManager.js.map +0 -1
  310. package/dist/controller/recordingDelegate.js +0 -252
  311. package/dist/controller/recordingDelegate.js.map +0 -1
  312. package/dist/controller/snapshotDelegate.js +0 -348
  313. package/dist/controller/snapshotDelegate.js.map +0 -1
  314. package/dist/controller/streamingDelegate.js +0 -389
  315. package/dist/controller/streamingDelegate.js.map +0 -1
  316. package/dist/interfaces.js +0 -2
  317. package/dist/interfaces.js.map +0 -1
  318. package/dist/media/Snapshot-Unavailable.png +0 -0
  319. package/dist/media/Snapshot-Unavailable.xcf +0 -0
  320. package/dist/media/Snapshot-black.png +0 -0
  321. package/dist/media/camera-disabled.png +0 -0
  322. package/dist/media/camera-offline.png +0 -0
  323. package/dist/media/media/Snapshot-Unavailable.png +0 -0
  324. package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
  325. package/dist/media/media/Snapshot-black.png +0 -0
  326. package/dist/media/media/camera-disabled.png +0 -0
  327. package/dist/media/media/camera-offline.png +0 -0
  328. package/dist/utils/Talkback.js +0 -162
  329. package/dist/utils/Talkback.js.map +0 -1
  330. package/dist/utils/accessoriesStore.js +0 -210
  331. package/dist/utils/accessoriesStore.js.map +0 -1
  332. package/dist/utils/configTypes.js +0 -38
  333. package/dist/utils/configTypes.js.map +0 -1
  334. package/dist/utils/ffmpeg.js +0 -1111
  335. package/dist/utils/ffmpeg.js.map +0 -1
  336. package/dist/utils/interfaces.js +0 -8
  337. package/dist/utils/interfaces.js.map +0 -1
  338. package/dist/utils/utils.js +0 -224
  339. package/dist/utils/utils.js.map +0 -1
  340. package/dist/version.js +0 -2
  341. package/dist/version.js.map +0 -1
  342. package/homebridge-eufy-security.png +0 -0
  343. package/homebridge-ui/diagnosticsCrypto.js +0 -112
  344. package/homebridge-ui/public/app.js +0 -237
  345. package/homebridge-ui/public/assets/devices/4g_lte_cam_s330_large.png +0 -0
  346. package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
  347. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
  348. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
  349. package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
  350. package/homebridge-ui/public/assets/devices/batterydoorbell2k_large.png +0 -0
  351. package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
  352. package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
  353. package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
  354. package/homebridge-ui/public/assets/devices/eufyCamS4_large.png +0 -0
  355. package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
  356. package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
  357. package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
  358. package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
  359. package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
  360. package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
  361. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  362. package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
  363. package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
  364. package/homebridge-ui/public/assets/devices/flood_freeze_sensor_e20_t90F0_large.png +0 -0
  365. package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
  366. package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
  367. package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
  368. package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
  369. package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.png +0 -0
  370. package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
  371. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  372. package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
  373. package/homebridge-ui/public/assets/devices/homebasemini_large.png +0 -0
  374. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  375. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  376. package/homebridge-ui/public/assets/devices/indoorcamC220_t8419N_large.png +0 -0
  377. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  378. package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
  379. package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
  380. package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
  381. package/homebridge-ui/public/assets/devices/indoorcams350_large.png +0 -0
  382. package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
  383. package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
  384. package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
  385. package/homebridge-ui/public/assets/devices/nvr_s4_max_T8N00_large.png +0 -0
  386. package/homebridge-ui/public/assets/devices/pir_sensor_e20_large.png +0 -0
  387. package/homebridge-ui/public/assets/devices/poe_bullet_ptz_cam_s4_T8E00_large.png +0 -0
  388. package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
  389. package/homebridge-ui/public/assets/devices/siren_sensor_e20_large.png +0 -0
  390. package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
  391. package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
  392. package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
  393. package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
  394. package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
  395. package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
  396. package/homebridge-ui/public/assets/devices/smartlock_t85D0_large.png +0 -0
  397. package/homebridge-ui/public/assets/devices/smartlock_t85L0_large.png +0 -0
  398. package/homebridge-ui/public/assets/devices/smartlock_t85P0_large.png +0 -0
  399. package/homebridge-ui/public/assets/devices/smartlock_t85V0_large.png +0 -0
  400. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
  401. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
  402. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
  403. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
  404. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
  405. package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
  406. package/homebridge-ui/public/assets/devices/smartlock_video_t8531_large.png +0 -0
  407. package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
  408. package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
  409. package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
  410. package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
  411. package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
  412. package/homebridge-ui/public/assets/devices/smoke_alarm_e10_t90S0_large.png +0 -0
  413. package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
  414. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  415. package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
  416. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  417. package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
  418. package/homebridge-ui/public/assets/devices/solocame42_large.png +0 -0
  419. package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
  420. package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
  421. package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
  422. package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
  423. package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
  424. package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
  425. package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
  426. package/homebridge-ui/public/assets/devices/unknown.png +0 -0
  427. package/homebridge-ui/public/assets/devices/walllight_s100_large.png +0 -0
  428. package/homebridge-ui/public/assets/devices/walllight_s120_large.png +0 -0
  429. package/homebridge-ui/public/assets/devices/water_freeze_sensor_t8920_large.png +0 -0
  430. package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
  431. package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
  432. package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
  433. package/homebridge-ui/public/assets/icons/attach.svg +0 -1
  434. package/homebridge-ui/public/assets/icons/battery_0.svg +0 -1
  435. package/homebridge-ui/public/assets/icons/battery_1.svg +0 -1
  436. package/homebridge-ui/public/assets/icons/battery_2.svg +0 -1
  437. package/homebridge-ui/public/assets/icons/battery_3.svg +0 -1
  438. package/homebridge-ui/public/assets/icons/battery_4.svg +0 -1
  439. package/homebridge-ui/public/assets/icons/battery_5.svg +0 -1
  440. package/homebridge-ui/public/assets/icons/battery_6.svg +0 -1
  441. package/homebridge-ui/public/assets/icons/copy.svg +0 -1
  442. package/homebridge-ui/public/assets/icons/delete.svg +0 -1
  443. package/homebridge-ui/public/assets/icons/download.svg +0 -1
  444. package/homebridge-ui/public/assets/icons/info.svg +0 -1
  445. package/homebridge-ui/public/assets/icons/refresh.svg +0 -1
  446. package/homebridge-ui/public/assets/icons/satellite_alt.svg +0 -1
  447. package/homebridge-ui/public/assets/icons/solar_power.svg +0 -1
  448. package/homebridge-ui/public/components/device-card.js +0 -174
  449. package/homebridge-ui/public/components/guard-modes.js +0 -88
  450. package/homebridge-ui/public/components/number-input.js +0 -121
  451. package/homebridge-ui/public/components/select.js +0 -73
  452. package/homebridge-ui/public/components/toggle.js +0 -68
  453. package/homebridge-ui/public/services/api.js +0 -250
  454. package/homebridge-ui/public/services/config.js +0 -144
  455. package/homebridge-ui/public/style.css +0 -818
  456. package/homebridge-ui/public/utils/countries.js +0 -73
  457. package/homebridge-ui/public/utils/device-images.js +0 -102
  458. package/homebridge-ui/public/utils/helpers.js +0 -115
  459. package/homebridge-ui/public/views/dashboard.js +0 -226
  460. package/homebridge-ui/public/views/device-detail.js +0 -654
  461. package/homebridge-ui/public/views/diagnostics.js +0 -402
  462. package/homebridge-ui/public/views/login.js +0 -650
  463. package/homebridge-ui/public/views/recordings.js +0 -319
  464. package/homebridge-ui/public/views/settings.js +0 -197
  465. package/homebridge-ui/public/views/unsupported-detail.js +0 -296
  466. package/media/Snapshot-Unavailable.png +0 -0
  467. package/media/Snapshot-Unavailable.xcf +0 -0
  468. package/media/Snapshot-black.png +0 -0
  469. package/media/camera-disabled.png +0 -0
  470. package/media/camera-offline.png +0 -0
  471. package/scripts/decrypt-diagnostics.mjs +0 -257
@@ -1,1394 +1,3 @@
1
- import { EufySecurity, libVersion, Device, PropertyName, CommandName, DeviceType, UserType } from 'eufy-security-client';
2
- import * as fs from 'fs';
3
- import { Logger as TsLogger } from 'tslog';
4
- import { createStream } from 'rotating-file-stream';
5
- import { create as tarCreate } from 'tar';
6
- import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
7
- import path from 'path';
8
- import { fileURLToPath } from 'url';
9
- import { createRequire } from 'module';
10
- import os from 'os';
11
- import { setTimeout as delay } from 'node:timers/promises';
12
- import { encryptDiagnostics } from './diagnosticsCrypto.js';
1
+ import { EufyAuthenticationUiServer } from '../dist/ui/server.js';
13
2
 
14
- const require = createRequire(import.meta.url);
15
- const { version: LIB_VERSION } = require('../package.json');
16
-
17
- /** Max time (ms) to wait for the client to populate raw data on unsupported items. */
18
- const UNSUPPORTED_INTEL_WAIT_MS = 2 * 60 * 1000; // 2 minutes
19
-
20
- class UiServer extends HomebridgePluginUiServer {
21
-
22
- stations = [];
23
- eufyClient = null;
24
- log;
25
- tsLog;
26
- storagePath;
27
- storedAccessoriesPath;
28
- unsupportedPath;
29
-
30
- adminAccountUsed = false;
31
-
32
- // Batch processing for stations and devices
33
- pendingStations = [];
34
- pendingDevices = [];
35
-
36
- // Timer handles — declared here for visibility; cleared via scoped helpers below
37
- /** @type {ReturnType<typeof setTimeout>|null} */ _loginTimeout = null;
38
- /** @type {ReturnType<typeof setTimeout>|null} */ processingTimeout = null;
39
- /** @type {ReturnType<typeof setTimeout>|null} */ _closeTimeout = null;
40
- /** @type {ReturnType<typeof setInterval>|null} */ _debounceTickInterval = null;
41
- /** @type {ReturnType<typeof setTimeout>|null} */ _discoveryInactivityTimeout = null;
42
- /** @type {ReturnType<typeof setInterval>|null} */ _discoveryInactivityTickInterval = null;
43
-
44
- /** Set to true when the user clicks "Skip" in the UI to abort the unsupported intel wait. */
45
- _skipIntelWait = false;
46
-
47
- /** Current discovery phase — exposed via /discoveryState for UI catch-up. */
48
- _discoveryPhase = 'idle';
49
-
50
- /** Timestamp (ms) of the last diagnostics download — used for rate-limiting. */
51
- _lastDiagnosticsTime = 0;
52
-
53
- /** Seconds to wait after the last station/device event before processing. */
54
- static DISCOVERY_DEBOUNCE_SEC = 10;
55
-
56
- /** Seconds to wait after auth before giving up on device discovery. */
57
- static DISCOVERY_INACTIVITY_SEC = 30;
58
-
59
- /** Minimum cooldown between diagnostics downloads (ms). */
60
- static DIAGNOSTICS_COOLDOWN_MS = 60_000;
61
-
62
- config = {
63
- username: '',
64
- password: '',
65
- language: 'en',
66
- country: 'US',
67
- trustedDeviceName: 'My Phone',
68
- persistentDir: '',
69
- p2pConnectionSetup: 0,
70
- pollingIntervalMinutes: 1,
71
- eventDurationSeconds: 10,
72
- acceptInvitations: true,
73
- logging: {
74
- level: 1, // LogLevel.Debug — enables eufy-security-client internal logging to configui-lib.log
75
- },
76
- };
77
-
78
- constructor() {
79
- super();
80
-
81
- this.storagePath = this.homebridgeStoragePath + '/eufysecurity';
82
- this.storedAccessoriesPath = this.storagePath + '/accessories.json';
83
- this.unsupportedPath = this.storagePath + '/unsupported.json';
84
- this.config.persistentDir = this.storagePath;
85
-
86
- this.initLogger();
87
- this.initTransportStreams();
88
- this.initEventListeners();
89
- this.ready();
90
- }
91
-
92
- /** Build a unified power descriptor (source, icon, label, battery) from a properties object. */
93
- _computePower(props) {
94
- const power = { source: null, icon: null, label: null };
95
-
96
- // Battery level
97
- if (props.battery !== undefined) {
98
- power.battery = props.battery;
99
- } else if (props.batteryLow !== undefined) {
100
- // Simple sensors only expose batteryLow boolean
101
- power.batteryLow = props.batteryLow;
102
- }
103
-
104
- // Charging status (bitmask)
105
- if (props.chargingStatus !== undefined) {
106
- const cs = props.chargingStatus;
107
- const isSolar = ((cs >> 2) & 1) === 1;
108
- const isPlugSolar = ((cs >> 3) & 1) === 1;
109
- const isUsb = (cs & 1) === 1;
110
-
111
- if (isSolar || isPlugSolar) {
112
- power.source = 'solar';
113
- power.icon = 'solar_power.svg';
114
- power.label = 'Solar Charging';
115
- return power;
116
- }
117
- if (isUsb) {
118
- power.source = 'plugged';
119
- power.icon = 'bolt.svg';
120
- power.label = 'Charging';
121
- return power;
122
- }
123
- }
124
-
125
- // PowerSource property (cameras with battery/solar panel)
126
- // 0 = BATTERY, 1 = SOLAR_PANEL
127
- if (props.powerSource === 1) {
128
- power.source = 'solar';
129
- power.icon = 'solar_power.svg';
130
- power.label = 'Solar';
131
- } else if (props.powerSource === 0) {
132
- power.source = 'battery';
133
- } else if (power.battery === undefined && power.batteryLow === undefined) {
134
- // No battery info at all — AC powered (indoor cameras, stations)
135
- power.source = 'plugged';
136
- power.icon = 'bolt.svg';
137
- power.label = 'Plugged In';
138
- } else {
139
- // Has battery/batteryLow but no powerSource — simple battery device (sensors)
140
- power.source = 'battery';
141
- }
142
-
143
- return power;
144
- }
145
-
146
- initLogger() {
147
- const logOptions = {
148
- name: `[UI-${LIB_VERSION}]`,
149
- prettyLogTemplate: '[{{mm}}/{{dd}}/{{yyyy}}, {{hh}}:{{MM}}:{{ss}}]\t{{name}}\t{{logLevelName}}\t',
150
- prettyErrorTemplate: '\n{{errorName}} {{errorMessage}}\nerror stack:\n{{errorStack}}',
151
- prettyErrorStackTemplate: ' • {{fileName}}\t{{method}}\n\t{{fileNameWithLine}}',
152
- prettyErrorParentNamesSeparator: '',
153
- prettyErrorLoggerNameDelimiter: '\t',
154
- stylePrettyLogs: true,
155
- minLevel: 2,
156
- prettyLogTimeZone: 'local',
157
- prettyLogStyles: {
158
- logLevelName: {
159
- '*': ['bold', 'black', 'bgWhiteBright', 'dim'],
160
- SILLY: ['bold', 'white'],
161
- TRACE: ['bold', 'whiteBright'],
162
- DEBUG: ['bold', 'green'],
163
- INFO: ['bold', 'blue'],
164
- WARN: ['bold', 'yellow'],
165
- ERROR: ['bold', 'red'],
166
- FATAL: ['bold', 'redBright'],
167
- },
168
- dateIsoStr: 'gray',
169
- filePathWithLine: 'white',
170
- name: 'green',
171
- nameWithDelimiterPrefix: ['white', 'bold'],
172
- nameWithDelimiterSuffix: ['white', 'bold'],
173
- errorName: ['bold', 'bgRedBright', 'whiteBright'],
174
- fileName: ['yellow'],
175
- },
176
- };
177
- this.log = new TsLogger(logOptions);
178
- this.tsLog = new TsLogger({ ...logOptions, type: 'hidden', minLevel: 2 });
179
- }
180
-
181
- initTransportStreams() {
182
- fs.mkdirSync(this.storagePath, { recursive: true });
183
-
184
- const logStreams = [
185
- { name: 'configui-server.log', logger: this.log },
186
- { name: 'configui-lib.log', logger: this.tsLog },
187
- ];
188
-
189
- for (const { name, logger } of logStreams) {
190
- const logStream = createStream(name, { path: this.storagePath, interval: '1d', rotate: 3, maxSize: '200M', compress: 'gzip' });
191
-
192
- logger.attachTransport((logObj) => {
193
- const meta = logObj['_meta'];
194
- const logName = meta.name;
195
- const level = meta.logLevelName;
196
- const date = meta.date.toISOString();
197
-
198
- let message = '';
199
- for (let i = 0; i <= 5; i++) {
200
- if (logObj[i]) {
201
- message += ' ' + (typeof logObj[i] === 'string' ? logObj[i] : JSON.stringify(logObj[i]));
202
- }
203
- }
204
-
205
- logStream.write(date + '\t' + logName + '\t' + level + '\t' + message + '\n');
206
- });
207
- }
208
-
209
- this.log.debug('Using bropats eufy-security-client library in version ' + libVersion);
210
- }
211
-
212
- initEventListeners() {
213
- this.onRequest('/login', this.login.bind(this));
214
- this.onRequest('/checkCache', this.checkCache.bind(this));
215
- this.onRequest('/storedAccessories', this.loadStoredAccessories.bind(this));
216
- this.onRequest('/reset', this.resetPlugin.bind(this));
217
- this.onRequest('/downloadDiagnostics', this.downloadDiagnostics.bind(this));
218
- this.onRequest('/cleanStorage', this.cleanStorage.bind(this));
219
- this.onRequest('/systemInfo', this.getSystemInfo.bind(this));
220
- this.onRequest('/skipIntelWait', this.skipIntelWait.bind(this));
221
- this.onRequest('/discoveryState', this.getDiscoveryState.bind(this));
222
- this.onRequest('/unsupportedDevices', this.loadUnsupportedDevices.bind(this));
223
- this.onRequest('/debugRecordings', this.listDebugRecordings.bind(this));
224
- this.onRequest('/downloadDebugRecording', this.downloadDebugRecording.bind(this));
225
- this.onRequest('/deleteDebugRecording', this.deleteDebugRecording.bind(this));
226
- this.onRequest('/deleteAllDebugRecordings', this.deleteAllDebugRecordings.bind(this));
227
- }
228
-
229
- skipIntelWait() {
230
- this._skipIntelWait = true;
231
- this.log.info('User requested to skip unsupported intel wait');
232
- return { ok: true };
233
- }
234
-
235
- /** Lazily load and cache valid ISO 3166-1 alpha-2 country codes from countries.js. */
236
- _getValidCountryCodes() {
237
- if (!this._validCountryCodes) {
238
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
239
- const source = fs.readFileSync(path.join(__dirname, 'public/utils/countries.js'), 'utf-8');
240
- this._validCountryCodes = new Set(source.match(/\b[A-Z]{2}(?=\s*:)/g));
241
- }
242
- return this._validCountryCodes;
243
- }
244
-
245
- getDiscoveryState() {
246
- return {
247
- phase: this._discoveryPhase,
248
- progress: this._discoveryPhase === 'queuing' ? 30 : this._discoveryPhase === 'processing' ? 50 : 0,
249
- stations: this.pendingStations.length,
250
- devices: this.pendingDevices.length,
251
- message: this.pendingStations.length > 0 || this.pendingDevices.length > 0
252
- ? `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`
253
- : '',
254
- };
255
- }
256
-
257
- /** Emit a discovery progress event to the UI. */
258
- _emitProgress(phase, progress, message) {
259
- this.pushEvent('discoveryProgress', {
260
- phase,
261
- progress,
262
- stations: this.pendingStations.length,
263
- devices: this.pendingDevices.length,
264
- message,
265
- });
266
- }
267
-
268
- async deleteFileIfExists(filePath) {
269
- try {
270
- await fs.promises.unlink(filePath);
271
- } catch (error) {
272
- if (error.code !== 'ENOENT') {
273
- throw error;
274
- }
275
- }
276
- }
277
-
278
- async resetPersistentData() {
279
- return this.deleteFileIfExists(this.storagePath + '/persistent.json');
280
- }
281
-
282
- async resetAccessoryData() {
283
- return this.deleteFileIfExists(this.storedAccessoriesPath);
284
- }
285
-
286
- async checkCache() {
287
- const persistentFile = this.storagePath + '/persistent.json';
288
- try {
289
- if (fs.existsSync(persistentFile)) {
290
- const data = JSON.parse(await fs.promises.readFile(persistentFile, 'utf-8'));
291
- // Basic validity check: ensure it has some expected content
292
- if (data && Object.keys(data).length > 0) {
293
- this.log.debug('Persistent cache file found and valid.');
294
- return { valid: true };
295
- }
296
- }
297
- } catch (error) {
298
- this.log.warn('Error checking persistent cache: ' + error);
299
- }
300
- return { valid: false };
301
- }
302
-
303
- async login(options) {
304
- this._clearLoginTimeout();
305
-
306
- // Block login if the plugin is already running (accessories updated within 90s)
307
- if (!this.eufyClient) {
308
- try {
309
- if (fs.existsSync(this.storedAccessoriesPath)) {
310
- const data = JSON.parse(fs.readFileSync(this.storedAccessoriesPath, 'utf-8'));
311
- if (data?.storedAt) {
312
- const ageMs = Date.now() - new Date(data.storedAt).getTime();
313
- if (ageMs < 90_000) {
314
- this.log.warn('Plugin heartbeat is fresh — blocking UI login to avoid duplicate eufy client. Please stop and wait 90sec before trying again!');
315
- this.pushEvent('authError', {
316
- message: 'The plugin is currently running. Please stop it before logging in from the UI. Please stop and wait 90sec before trying again!',
317
- });
318
- return { success: false, pluginRunning: true };
319
- }
320
- }
321
- }
322
- } catch (error) {
323
- this.log.debug('Heartbeat check failed (non-blocking): ' + error);
324
- }
325
- }
326
-
327
- try {
328
- if (options && options.username && options.password && !options.reconnect) {
329
- this.log.info('deleting persistent.json and accessories due to new login');
330
- // Tear down any existing client so the new device name and credentials take effect
331
- if (this.eufyClient) {
332
- this.log.debug('Tearing down previous eufy client before fresh login');
333
- this.eufyClient.removeAllListeners();
334
- this.eufyClient.close();
335
- this.eufyClient = null;
336
- }
337
- await this.resetAccessoryData();
338
- await this.resetPersistentData();
339
- } else if (options && options.reconnect) {
340
- this.log.info('Reconnecting using persistent cache (skipping data reset)');
341
- }
342
- } catch (error) {
343
- this.log.error('Could not delete persistent.json due to error: ' + error);
344
- }
345
-
346
- if (!this.eufyClient && options && options.username && options.password && options.country) {
347
- this._clearAllTimers();
348
- this.stations = [];
349
- this.pendingStations = [];
350
- this.pendingDevices = [];
351
- this._discoveryPhase = 'authenticating';
352
- this.log.debug('init eufyClient');
353
-
354
- // Validate country code
355
- const country = typeof options.country === 'string' ? options.country.trim().toUpperCase() : '';
356
- if (!this._getValidCountryCodes().has(country)) {
357
- const raw = typeof options.country === 'object' ? JSON.stringify(options.country) : String(options.country);
358
- this.log.warn(`Invalid country code received: ${raw} — falling back to login.`);
359
- this.pushEvent('authError', { message: `Invalid country code "${raw}". Please select a valid country and try again.` });
360
- this._discoveryPhase = 'idle';
361
- return { success: false };
362
- }
363
-
364
- this.config.username = options.username;
365
- this.config.password = options.password;
366
- this.config.country = country;
367
- this.config.trustedDeviceName = (typeof options.deviceName === 'string' && options.deviceName.trim()) || this.config.trustedDeviceName;
368
- try {
369
- this.eufyClient = await EufySecurity.initialize(this.config, this.tsLog);
370
- this.eufyClient?.on('station added', this._onStationDiscovered.bind(this));
371
- this.eufyClient?.on('device added', this._onDeviceDiscovered.bind(this));
372
- this.eufyClient?.on('push connect', () => this.log.debug('Push Connected!'));
373
- this.eufyClient?.on('push close', () => this.log.debug('Push Closed!'));
374
- this.eufyClient?.on('connect', () => this.log.debug('Connected!'));
375
- this.eufyClient?.on('close', () => this.log.debug('Closed!'));
376
- } catch (error) {
377
- this.log.error('EufySecurity init failed:', error);
378
- this.pushEvent('authError', { message: 'Initialization failed. Check the logs for details.' });
379
- this._discoveryPhase = 'idle';
380
- return { success: false };
381
- }
382
- }
383
-
384
- // Timeout — fire authError event after 25s if nothing else resolved
385
- this._loginTimeout = setTimeout(() => {
386
- this.pushEvent('authError', { message: 'Authentication timed out. Please try again.' });
387
- }, 25 * 1000);
388
-
389
- if (options && options.username && options.password && options.country) {
390
- this.log.debug('login with credentials');
391
- try {
392
- this._registerOneTimeAuthHandlers();
393
- this.eufyClient?.connect()
394
- .then(() => this.log.debug('connected?: ' + this.eufyClient?.isConnected()))
395
- .catch((error) => this.log.error(error));
396
- } catch (error) {
397
- this.log.error('Login error:', error);
398
- this._clearLoginTimeout();
399
- this.pushEvent('authError', { message: 'Login failed. Check the logs for details.' });
400
- }
401
- } else if (options && options.verifyCode) {
402
- this.log.debug('login with TFA code');
403
- this._emitProgress('authenticating', 10, 'Verifying TFA code...');
404
- try {
405
- this._registerOneTimeAuthHandlers();
406
- this.eufyClient?.connect({ verifyCode: options.verifyCode, force: false })
407
- .then(() => this.log.debug('TFA connect resolved, connected?: ' + this.eufyClient?.isConnected()))
408
- .catch((error) => {
409
- this.log.error('TFA connect error:', error);
410
- this._clearLoginTimeout();
411
- this.pushEvent('authError', { message: 'TFA verification failed. Check the logs for details.' });
412
- });
413
- } catch (error) {
414
- this.log.error('TFA verification error:', error);
415
- this._clearLoginTimeout();
416
- this.pushEvent('authError', { message: 'TFA verification failed. Check the logs for details.' });
417
- }
418
- } else if (options && options.captcha) {
419
- this.log.debug('login with captcha');
420
- this._emitProgress('authenticating', 10, 'Verifying captcha...');
421
- try {
422
- this._registerOneTimeAuthHandlers();
423
- this.eufyClient?.connect({ captcha: { captchaCode: options.captcha.captchaCode, captchaId: options.captcha.captchaId }, force: false })
424
- .then(() => this.log.debug('Captcha connect resolved, connected?: ' + this.eufyClient?.isConnected()))
425
- .catch((error) => {
426
- this.log.error('Captcha connect error:', error);
427
- this._clearLoginTimeout();
428
- this.pushEvent('authError', { message: 'Captcha verification failed. Check the logs for details.' });
429
- });
430
- } catch (error) {
431
- this.log.error('Captcha verification error:', error);
432
- this._clearLoginTimeout();
433
- this.pushEvent('authError', { message: 'Captcha verification failed. Check the logs for details.' });
434
- }
435
- } else {
436
- this._clearLoginTimeout();
437
- this.pushEvent('authError', { message: 'Unsupported login method.' });
438
- }
439
-
440
- // Resolve immediately — all outcomes are delivered via push events
441
- return { pending: true };
442
- }
443
-
444
- /** Register once-only auth event handlers (TFA, captcha, connect) on the eufy client. */
445
- _registerOneTimeAuthHandlers() {
446
- this.eufyClient?.once('tfa request', () => {
447
- this._clearLoginTimeout();
448
- this.pushEvent('tfaRequest', {});
449
- });
450
- this.eufyClient?.once('captcha request', (id, captcha) => {
451
- this._clearLoginTimeout();
452
- this.pushEvent('captchaRequest', { id, captcha });
453
- });
454
- this.eufyClient?.once('connect', () => {
455
- this._clearLoginTimeout();
456
- if (this.adminAccountUsed) {
457
- return;
458
- }
459
- this.pushEvent('authSuccess', {});
460
- this._emitProgress('authenticating', 15, 'Authenticated — waiting for devices...');
461
- this._startDiscoveryInactivityTimer();
462
- });
463
- }
464
-
465
- /** Start a timer that gives up on device discovery after DISCOVERY_INACTIVITY_SEC seconds. */
466
- _startDiscoveryInactivityTimer() {
467
- if (this.pendingStations.length > 0 || this.pendingDevices.length > 0) {
468
- this.log.debug('Devices already discovered before connect event — skipping inactivity timeout');
469
- return;
470
- }
471
- this._clearDiscoveryInactivityTimer();
472
- const totalSec = UiServer.DISCOVERY_INACTIVITY_SEC;
473
- const start = Date.now();
474
-
475
- this._discoveryInactivityTickInterval = setInterval(() => {
476
- const elapsed = Math.floor((Date.now() - start) / 1000);
477
- const remaining = Math.max(0, totalSec - elapsed);
478
- const pct = Math.min(95, 15 + Math.floor((elapsed / totalSec) * 80));
479
- this._emitProgress('waitingForDevices', pct, `Authenticated — waiting for devices... ${remaining}s`);
480
- }, 1000);
481
-
482
- this._discoveryInactivityTimeout = setTimeout(() => {
483
- clearInterval(this._discoveryInactivityTickInterval);
484
- this._discoveryInactivityTickInterval = null;
485
- this.log.warn(
486
- `No stations or devices discovered within ${totalSec}s after authentication. ` +
487
- 'The account may have no devices or the guest invitation has not been accepted yet.',
488
- );
489
- this._discoveryPhase = 'done';
490
- this.stations = [];
491
- try {
492
- this.storeAccessories();
493
- } catch (error) {
494
- this.log.error('Error storing empty accessories:', error);
495
- }
496
- this._emitProgress('done', 100, 'No devices found.');
497
- this.pushEvent('addAccessory', { stations: [], noDevices: true });
498
- this.eufyClient?.removeAllListeners();
499
- this.eufyClient?.close();
500
- }, totalSec * 1000);
501
- }
502
-
503
- /** Return true if the station's account is the main admin (not a guest admin). */
504
- _isMainAdminAccount(station) {
505
- const rawStation = station.getRawStation();
506
- return rawStation.member.member_type !== UserType.ADMIN;
507
- }
508
-
509
- /** Abort login: tear down client, cancel timers, notify UI, and reset plugin storage. */
510
- _abortNonGuestAdminLogin() {
511
- this.adminAccountUsed = true;
512
- this._clearAllTimers();
513
- this.eufyClient?.removeAllListeners();
514
- this.eufyClient?.close();
515
- this.pushEvent('AdminAccountUsed', true);
516
- this.resetPlugin();
517
- this.log.error(`
518
- #########################
519
- ######### ERROR #########
520
- #########################
521
- You're not using a guest admin account with this plugin! You must use a guest admin account!
522
- Please look here for more details:
523
- https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin
524
- #########################
525
- `);
526
- }
527
-
528
- /** Clear the login timeout. */
529
- _clearLoginTimeout() {
530
- clearTimeout(this._loginTimeout);
531
- this._loginTimeout = null;
532
- }
533
-
534
- /** Clear debounce-related timers (processing, close, tick interval). */
535
- _clearDebounceTimers() {
536
- clearTimeout(this.processingTimeout);
537
- this.processingTimeout = null;
538
- clearTimeout(this._closeTimeout);
539
- this._closeTimeout = null;
540
- clearInterval(this._debounceTickInterval);
541
- this._debounceTickInterval = null;
542
- }
543
-
544
- /** Clear the post-auth discovery inactivity timer. */
545
- _clearDiscoveryInactivityTimer() {
546
- clearInterval(this._discoveryInactivityTickInterval);
547
- this._discoveryInactivityTickInterval = null;
548
- clearTimeout(this._discoveryInactivityTimeout);
549
- this._discoveryInactivityTimeout = null;
550
- }
551
-
552
- /** Clear all pending timers. */
553
- _clearAllTimers() {
554
- this._clearLoginTimeout();
555
- this._clearDebounceTimers();
556
- this._clearDiscoveryInactivityTimer();
557
- }
558
-
559
- /** Parse a semver string (e.g. '4.4.2-beta.18') into [major, minor, patch]. */
560
- _parseSemver(ver) {
561
- return (ver || '0.0.0').replace(/-.*$/, '').split('.').map(Number);
562
- }
563
-
564
- async loadStoredAccessories() {
565
- try {
566
- if (!fs.existsSync(this.storedAccessoriesPath)) {
567
- this.log.debug('Stored accessories file does not exist.');
568
- return [];
569
- }
570
-
571
- const storedData = await fs.promises.readFile(this.storedAccessoriesPath, { encoding: 'utf-8' });
572
- const { version: storedVersion, storedAt, stations: storedAccessories } = JSON.parse(storedData);
573
-
574
- // --- Cache age check (30 days) ---
575
- if (storedAt) {
576
- const ageMs = Date.now() - new Date(storedAt).getTime();
577
- const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
578
- if (ageDays >= 30) {
579
- this.pushEvent('cacheWarning', { reason: 'stale', ageDays });
580
- this.log.warn(`Stored accessories are ${ageDays} days old. User should re-login to refresh.`);
581
- }
582
- }
583
-
584
- // --- Version branch check ---
585
- if (storedVersion && storedVersion !== LIB_VERSION) {
586
- const [curMajor, curMinor] = this._parseSemver(LIB_VERSION);
587
- const [stoMajor, stoMinor] = this._parseSemver(storedVersion);
588
-
589
- if (curMajor !== stoMajor || curMinor !== stoMinor) {
590
- // Different minor (or major) branch → force re-login
591
- this.pushEvent('cacheWarning', {
592
- reason: 'versionForce',
593
- currentVersion: LIB_VERSION,
594
- storedVersion,
595
- });
596
- this.log.warn(`Stored version (${storedVersion}) is on a different branch than current (${LIB_VERSION}). Forcing re-login.`);
597
- return { stations: [], storedAt: null }; // Return empty to force login flow
598
- } else {
599
- // Same minor branch, different patch → soft warning
600
- this.pushEvent('cacheWarning', {
601
- reason: 'versionWarn',
602
- currentVersion: LIB_VERSION,
603
- storedVersion,
604
- });
605
- this.log.warn(`Stored version (${storedVersion}) differs from current (${LIB_VERSION}) but same branch. Consider re-login.`);
606
- }
607
- }
608
-
609
- return { stations: storedAccessories, storedAt: storedAt || null };
610
- } catch (error) {
611
- this.log.error('Could not get stored accessories. Most likely no stored accessories yet: ' + error);
612
- return { stations: [], storedAt: null };
613
- }
614
- }
615
-
616
- async _onStationDiscovered(station) {
617
- if (this.adminAccountUsed) {
618
- return;
619
- }
620
-
621
- if (this._isMainAdminAccount(station)) {
622
- this._abortNonGuestAdminLogin();
623
- return;
624
- }
625
-
626
- this._clearDiscoveryInactivityTimer();
627
- this.pendingStations.push(station);
628
- this.log.debug(`${station.getName()}: Station queued for processing`);
629
- this._discoveryPhase = 'queuing';
630
- this._emitProgress('queuing', 30, `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`);
631
- this._restartDiscoveryDebounce();
632
- }
633
-
634
- async _onDeviceDiscovered(device) {
635
- if (this.adminAccountUsed) {
636
- this.pushEvent('AdminAccountUsed', true);
637
- return;
638
- }
639
-
640
- const deviceType = device.getDeviceType();
641
- if (Device.isKeyPad(deviceType)) {
642
- this.log.warn(`${device.getName()}: The keypad is ignored as it serves no purpose in this plugin. You can ignore this message.`);
643
- return;
644
- }
645
-
646
- this._clearDiscoveryInactivityTimer();
647
- this.pendingDevices.push(device);
648
- this.log.debug(`${device.getName()}: Device queued for processing`);
649
- this._discoveryPhase = 'queuing';
650
- this._emitProgress('queuing', 30, `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s)...`);
651
- this._restartDiscoveryDebounce();
652
- }
653
-
654
- /** Restart the debounce timer — processing fires after DISCOVERY_DEBOUNCE_SEC of silence. */
655
- _restartDiscoveryDebounce() {
656
- this._clearDebounceTimers();
657
- this._clearDiscoveryInactivityTimer();
658
- const delaySec = UiServer.DISCOVERY_DEBOUNCE_SEC;
659
- this.log.debug(
660
- `Discovery debounce reset — will process in ${delaySec}s if no more devices arrive ` +
661
- `(${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s) queued)`,
662
- );
663
-
664
- // Tick progress from 30 → 95 during the debounce wait
665
- const debounceStart = Date.now();
666
- this._debounceTickInterval = setInterval(() => {
667
- const elapsed = (Date.now() - debounceStart) / 1000;
668
- const pct = Math.min(95, 30 + Math.floor((elapsed / delaySec) * 65));
669
- const remaining = Math.max(0, Math.ceil(delaySec - elapsed));
670
- this._emitProgress('queuing', pct, `Discovered ${this.pendingStations.length} station(s), ${this.pendingDevices.length} device(s) — waiting for more... ${remaining}s`);
671
- }, 1000);
672
-
673
- this.processingTimeout = setTimeout(() => {
674
- clearInterval(this._debounceTickInterval);
675
- this._debounceTickInterval = null;
676
- this._processPendingAccessories().catch(error => this.log.error('Error processing pending accessories:', error));
677
- }, delaySec * 1000);
678
- // Close connection after processing + potential 2-min unsupported intel wait
679
- const closeAfterSec = delaySec + (UNSUPPORTED_INTEL_WAIT_MS / 1000) + 15;
680
- this._closeTimeout = setTimeout(() => {
681
- this.eufyClient?.removeAllListeners();
682
- this.eufyClient?.close();
683
- }, closeAfterSec * 1000);
684
- }
685
-
686
- /** Process all queued stations and devices after the debounce window closes. */
687
- async _processPendingAccessories() {
688
- this.log.debug(`Processing ${this.pendingStations.length} stations and ${this.pendingDevices.length} devices`);
689
-
690
- this._discoveryPhase = 'processing';
691
- this._emitProgress('processing', 95, `Processing ${this.pendingStations.length} station(s) and ${this.pendingDevices.length} device(s)...`);
692
-
693
- if (this.pendingStations.length === 0 || this.pendingDevices.length === 0) {
694
- this.log.warn(
695
- `Discovery finished with ${this.pendingStations.length} station(s) and ${this.pendingDevices.length} device(s). ` +
696
- 'If this is unexpected, please verify your Eufy account has devices and the credentials used are for a guest admin account.',
697
- );
698
- }
699
-
700
- // Collect unsupported items (exclude hub/base station types that aren't in DeviceProperties)
701
- const unsupportedItems = [];
702
-
703
- for (const station of this.pendingStations) {
704
- try {
705
- const st = station.getDeviceType();
706
- if (!Device.isStation(st) && !Device.isSupported(st)) unsupportedItems.push(station);
707
- } catch (e) { /* ignore */ }
708
- }
709
- for (const device of this.pendingDevices) {
710
- try {
711
- if (!Device.isSupported(device.getDeviceType())) unsupportedItems.push(device);
712
- } catch (e) { /* ignore */ }
713
- }
714
-
715
- // Wait for raw data on unsupported items (user can skip via /skipIntelWait)
716
- if (unsupportedItems.length > 0) {
717
- const names = unsupportedItems.map(i => `${i.getName()} (type ${i.getDeviceType()})`).join(', ');
718
- this._skipIntelWait = false;
719
-
720
- this.pushEvent('discoveryWarning', {
721
- unsupportedCount: unsupportedItems.length,
722
- unsupportedNames: names,
723
- waitSeconds: UNSUPPORTED_INTEL_WAIT_MS / 1000,
724
- message: `${unsupportedItems.length} unsupported device(s) detected: ${names}`,
725
- });
726
-
727
- this.log.info(`Unsupported intel: waiting up to ${UNSUPPORTED_INTEL_WAIT_MS / 1000}s for raw data (user can skip)`);
728
-
729
- // Poll every second, ticking progress 50 → 95
730
- const pollMs = 1000;
731
- let waited = 0;
732
- while (waited < UNSUPPORTED_INTEL_WAIT_MS && !this._skipIntelWait) {
733
- await delay(pollMs);
734
- waited += pollMs;
735
- const pct = Math.min(95, 50 + Math.floor((waited / UNSUPPORTED_INTEL_WAIT_MS) * 45));
736
- const remaining = Math.max(0, Math.ceil((UNSUPPORTED_INTEL_WAIT_MS - waited) / 1000));
737
- this._emitProgress('unsupportedWait', pct, `Collecting data for ${unsupportedItems.length} unsupported device(s)... ${remaining}s`);
738
- }
739
-
740
- if (this._skipIntelWait) {
741
- this.log.info(`Unsupported intel wait skipped by user after ${waited / 1000}s`);
742
- } else {
743
- this.log.info(`Unsupported intel wait completed (${waited / 1000}s)`);
744
- }
745
- }
746
-
747
- this._emitProgress('buildingStations', 96, 'Building station list...');
748
-
749
- // Process queued stations
750
- for (const station of this.pendingStations) {
751
- const stationType = station.getDeviceType();
752
- const stationSerial = station.getSerial();
753
-
754
- const s = {
755
- uniqueId: stationSerial,
756
- displayName: station.getName(),
757
- type: stationType,
758
- typename: DeviceType[stationType],
759
- disabled: false,
760
- devices: [],
761
- properties: station.getProperties(),
762
- unsupported: false,
763
- };
764
-
765
- try {
766
- delete s.properties.picture;
767
- } catch (error) {
768
- // ignore
769
- }
770
-
771
- s.ignored = (this.config['ignoreStations'] ?? []).includes(s.uniqueId);
772
-
773
- // Pre-compute power info for the UI
774
- s.power = this._computePower(s.properties);
775
-
776
- if (!Device.isStation(stationType)) {
777
- // Not a hub/base station — the station IS a standalone device (station.type == device.type)
778
-
779
- if (!Device.isSupported(stationType)) {
780
- // Device type not recognized by eufy-security-client — truly unsupported
781
- s.unsupported = true;
782
-
783
- this.log.warn(`Station "${station.getName()}" (type ${stationType}) is not supported by eufy-security-client`);
784
-
785
- // Immediately add the unsupported station and skip further processing
786
- this.stations.push(s);
787
- continue;
788
- } else {
789
- // Check if the matching device was emitted by the client
790
- const hasMatchingDevice = this.pendingDevices.some(d => d.getSerial() === stationSerial);
791
-
792
- if (hasMatchingDevice) {
793
- s.standalone = true;
794
- s.disabled = true; // No separate station card; settings accessible via device card
795
-
796
- // Standalone Locks, Doorbells and SmartDrops don't have Security Control
797
- if (Device.isLock(s.type) || Device.isDoorbell(s.type) || Device.isSmartDrop(s.type)) {
798
- s.noSecurityControl = true;
799
- }
800
- } else {
801
- // Station exists but no device counterpart was emitted — unsupported
802
- s.unsupported = true;
803
- this.log.warn(`Station "${station.getName()}" (${DeviceType[stationType]}) has no matching device and will be marked as unsupported`);
804
-
805
- // Short-circuit processing for unsupported station
806
- this.stations.push(s);
807
- continue;
808
- }
809
- }
810
- }
811
-
812
- this.stations.push(s);
813
- }
814
-
815
- this._emitProgress('buildingDevices', 98, 'Building device list...');
816
-
817
- // Process queued devices and attach them to stations
818
- for (const device of this.pendingDevices) {
819
- const devType = device.getDeviceType();
820
-
821
- const d = {
822
- uniqueId: device.getSerial(),
823
- displayName: device.getName(),
824
- type: devType,
825
- typename: DeviceType[devType],
826
- standalone: device.getSerial() === device.getStationSerial(),
827
- hasBattery: device.hasBattery(),
828
- isCamera: device.isCamera() || Device.isLockWifiVideo(devType),
829
- isDoorbell: device.isDoorbell(),
830
- isKeypad: device.isKeyPad(),
831
- isMotionSensor: Device.isMotionSensor(devType),
832
- isEntrySensor: Device.isEntrySensor(devType),
833
- isLock: Device.isLock(devType),
834
- isSmartDrop: Device.isSmartDrop(devType),
835
- supportsRTSP: device.hasPropertyValue(PropertyName.DeviceRTSPStream),
836
- supportsTalkback: device.hasCommand(CommandName.DeviceStartTalkback),
837
- DeviceEnabled: device.hasProperty(PropertyName.DeviceEnabled),
838
- DeviceMotionDetection: device.hasProperty(PropertyName.DeviceMotionDetection),
839
- DeviceLight: device.hasProperty(PropertyName.DeviceLight),
840
- DeviceChimeIndoor: device.hasProperty(PropertyName.DeviceChimeIndoor),
841
- disabled: false,
842
- properties: device.getProperties(),
843
- unsupported: false,
844
- };
845
-
846
- // Mark device as unsupported if eufy-security-client doesn't recognize this device type
847
- if (!Device.isSupported(devType)) {
848
- d.unsupported = true;
849
- }
850
-
851
- // Pre-compute power info for the UI
852
- d.power = this._computePower(d.properties);
853
-
854
- try {
855
- delete d.properties.picture;
856
- } catch (error) {
857
- this.log.error(error);
858
- }
859
-
860
- d.ignored = (this.config['ignoreDevices'] ?? []).includes(d.uniqueId);
861
-
862
- const stationUniqueId = device.getStationSerial();
863
- const stationIndex = this.stations.findIndex(station => station.uniqueId === stationUniqueId);
864
-
865
- if (stationIndex !== -1) {
866
- // If parent station is unsupported, propagate flag to device
867
- if (this.stations[stationIndex].unsupported) {
868
- d.unsupported = true;
869
- }
870
-
871
- if (!this.stations[stationIndex].devices) {
872
- this.stations[stationIndex].devices = [];
873
- }
874
- this.stations[stationIndex].devices.push(d);
875
- } else {
876
- this.log.error('Station not found for device:', d.displayName);
877
- }
878
- }
879
-
880
- // Write unsupported.json with raw device intel for triage (before clearing pending queues)
881
- try {
882
- this.storeUnsupportedDevices(this.pendingStations, this.pendingDevices);
883
- } catch (error) {
884
- this.log.error('Error storing unsupported devices:', error);
885
- }
886
-
887
- // Clear pending queues
888
- this.pendingStations = [];
889
- this.pendingDevices = [];
890
-
891
- // Always send the final list to the UI, even if empty
892
- try {
893
- this.storeAccessories();
894
- } catch (error) {
895
- this.log.error('Error storing accessories:', error);
896
- }
897
-
898
- this._emitProgress('done', 100, 'Discovery complete!');
899
-
900
- this.pushEvent('addAccessory', { stations: this.stations, extendedDiscovery: unsupportedItems.length > 0 });
901
- }
902
-
903
- /** Persist discovered stations/devices to accessories.json. */
904
- storeAccessories() {
905
- fs.mkdirSync(this.storagePath, { recursive: true });
906
- const dataToStore = { version: LIB_VERSION, storedAt: new Date().toISOString(), stations: this.stations };
907
- fs.writeFileSync(this.storedAccessoriesPath, JSON.stringify(dataToStore));
908
- }
909
-
910
- // ── Sensitive-field redaction ──────────────────────────────────────────────
911
-
912
- /** Keys whose string values are partially masked before persisting to unsupported.json. */
913
- static SENSITIVE_KEYS = new Map([
914
- // Serial numbers — keep model prefix (e.g. T8170)
915
- ['station_sn', [5, 0]],
916
- ['device_sn', [5, 0]],
917
-
918
- // Network / connectivity
919
- ['wifi_ssid', [3, 0]],
920
- ['wifi_mac', [4, 0]],
921
- ['ip_addr', [3, 0]],
922
- ['local_ip', [3, 0]],
923
-
924
- // Hardware identifiers
925
- ['cpuid', [4, 0]],
926
-
927
- // P2P identifiers & key-exchange material
928
- ['p2p_did', [7, 0]],
929
- ['push_did', [7, 0]],
930
- ['ndt_did', [7, 0]],
931
- ['query_server_did', [7, 0]],
932
- ['p2p_conn', [4, 0]],
933
- ['app_conn', [4, 0]],
934
- ['p2p_license', [2, 0]],
935
- ['push_license', [2, 0]],
936
- ['ndt_license', [2, 0]],
937
- ['wakeup_key', [4, 0]],
938
- ['dsk_key', [4, 0]],
939
- ['setup_code', [2, 0]],
940
- ['setup_id', [2, 0]],
941
-
942
- // User / account identifiers
943
- ['account_id', [4, 0]],
944
- ['admin_user_id', [4, 0]],
945
- ['member_user_id', [4, 0]],
946
- ['action_user_id', [4, 0]],
947
- ['short_user_id', [4, 0]],
948
- ['email', [3, 0]],
949
- ['action_user_email', [3, 0]],
950
- ['member_nick', [3, 0]],
951
- ['nick_name', [3, 0]],
952
- ['action_user_name', [3, 0]],
953
- ['avatar', [0, 0]],
954
- ['member_avatar', [0, 0]],
955
-
956
- // House / location
957
- ['house_id', [4, 0]],
958
-
959
- // Misc identifiers
960
- ['volume', [6, 0]],
961
- ]);
962
-
963
- /**
964
- * Partially mask a string value, keeping the first `keepStart` and last
965
- * `keepEnd` characters visible. Returns the original if it is empty or
966
- * too short to meaningfully mask.
967
- */
968
- static _partialMask(value, keepStart, keepEnd) {
969
- if (typeof value !== 'string' || !value) return value;
970
- const minLen = keepStart + keepEnd + 1;
971
- if (value.length <= minLen) return value.length <= 2 ? '***' : value[0] + '***';
972
- return value.slice(0, keepStart) + '***' + (keepEnd > 0 ? value.slice(-keepEnd) : '');
973
- }
974
-
975
- /**
976
- * Deep-walk an object and partially redact every value whose key appears
977
- * in SENSITIVE_KEYS. Returns a new object — the original is not mutated.
978
- */
979
- static _redactSensitiveFields(obj) {
980
- if (Array.isArray(obj)) {
981
- return obj.map(item => UiServer._redactSensitiveFields(item));
982
- }
983
- if (obj !== null && typeof obj === 'object') {
984
- const out = {};
985
- for (const [key, val] of Object.entries(obj)) {
986
- const rule = UiServer.SENSITIVE_KEYS.get(key);
987
- if (rule && typeof val === 'string') {
988
- out[key] = UiServer._partialMask(val, rule[0], rule[1]);
989
- } else if (typeof val === 'object' && val !== null) {
990
- out[key] = UiServer._redactSensitiveFields(val);
991
- } else {
992
- out[key] = val;
993
- }
994
- }
995
- return out;
996
- }
997
- return obj;
998
- }
999
-
1000
- // ── Unsupported device storage ──────────────────────────────────────────
1001
-
1002
- /** Collect raw intel for unsupported devices/stations and write to unsupported.json. */
1003
- storeUnsupportedDevices(pendingStations, pendingDevices) {
1004
- const unsupportedEntries = [];
1005
-
1006
- // Collect unsupported standalone stations
1007
- for (const station of pendingStations) {
1008
- const stationType = station.getDeviceType();
1009
- if (!Device.isStation(stationType) && !Device.isSupported(stationType)) {
1010
- unsupportedEntries.push(this._buildUnsupportedEntry(station, 'station'));
1011
- }
1012
- }
1013
-
1014
- // Collect unsupported devices
1015
- for (const device of pendingDevices) {
1016
- if (!Device.isSupported(device.getDeviceType())) {
1017
- unsupportedEntries.push(this._buildUnsupportedEntry(device, 'device'));
1018
- }
1019
- }
1020
-
1021
- fs.mkdirSync(this.storagePath, { recursive: true });
1022
-
1023
- const dataToStore = { version: LIB_VERSION, storedAt: new Date().toISOString(), devices: unsupportedEntries };
1024
- fs.writeFileSync(this.unsupportedPath, JSON.stringify(dataToStore));
1025
- this.log.debug(`Persisted ${unsupportedEntries.length} unsupported device(s) to unsupported.json`);
1026
- }
1027
-
1028
- /** Build a triage-ready intel object for an unsupported device or station. */
1029
- _buildUnsupportedEntry(item, kind) {
1030
- const isStation = kind === 'station';
1031
- const rawData = isStation
1032
- ? (item.getRawStation ? item.getRawStation() : {})
1033
- : (item.getRawDevice ? item.getRawDevice() : {});
1034
- const rawProps = item.getRawProperties ? item.getRawProperties() : {};
1035
-
1036
- const wifiSsid = rawData.wifi_ssid || undefined;
1037
- const localIp = rawData.ip_addr || rawData.local_ip || undefined;
1038
-
1039
- return {
1040
- uniqueId: item.getSerial(),
1041
- displayName: item.getName(),
1042
- type: item.getDeviceType(),
1043
- typename: DeviceType[item.getDeviceType()] || undefined,
1044
- stationSerialNumber: isStation ? item.getSerial() : item.getStationSerial(),
1045
- model: rawData[isStation ? 'station_model' : 'device_model'],
1046
- hardwareVersion: rawData.main_hw_version,
1047
- softwareVersion: rawData.main_sw_version,
1048
- wifiSsid: wifiSsid ? UiServer._partialMask(wifiSsid, 3, 0) : undefined,
1049
- localIp: localIp ? UiServer._partialMask(localIp, 3, 0) : undefined,
1050
- rawDevice: UiServer._redactSensitiveFields(rawData),
1051
- rawProperties: rawProps,
1052
- };
1053
- }
1054
-
1055
- /** Load unsupported device intel from disk. */
1056
- async loadUnsupportedDevices() {
1057
- try {
1058
- if (!fs.existsSync(this.unsupportedPath)) {
1059
- return { devices: [] };
1060
- }
1061
- const data = JSON.parse(await fs.promises.readFile(this.unsupportedPath, 'utf-8'));
1062
- return { devices: data.devices || [] };
1063
- } catch (error) {
1064
- this.log.error('Could not load unsupported devices: ' + error);
1065
- return { devices: [] };
1066
- }
1067
- }
1068
-
1069
- async resetPlugin() {
1070
- try {
1071
- fs.rmSync(this.storagePath, { recursive: true, force: true });
1072
- return { result: 1 };
1073
- } catch (error) {
1074
- this.log.error('Could not reset plugin: ' + error);
1075
- return { result: 0 };
1076
- }
1077
- }
1078
-
1079
- async getLogFiles() {
1080
- const files = await fs.promises.readdir(this.storagePath);
1081
-
1082
- const logFiles = files.filter(file => {
1083
- return file.endsWith('.log') || file.endsWith('.log.gz');
1084
- });
1085
-
1086
- const nonEmptyLogFiles = await Promise.all(logFiles.map(async file => {
1087
- const filePath = path.join(this.storagePath, file);
1088
- const stats = await fs.promises.stat(filePath);
1089
- if (stats.size > 0) {
1090
- return file;
1091
- }
1092
- return null;
1093
- }));
1094
-
1095
- return nonEmptyLogFiles.filter(file => file !== null);
1096
- }
1097
-
1098
- async downloadDiagnostics(options = {}) {
1099
- const skipEncryption = !!options.skipEncryption;
1100
-
1101
- // Rate-limit: one download per DIAGNOSTICS_COOLDOWN_MS
1102
- const now = Date.now();
1103
- const elapsed = now - this._lastDiagnosticsTime;
1104
- if (elapsed < UiServer.DIAGNOSTICS_COOLDOWN_MS) {
1105
- const waitSec = Math.ceil((UiServer.DIAGNOSTICS_COOLDOWN_MS - elapsed) / 1000);
1106
- throw new Error(`Please wait ${waitSec}s before downloading diagnostics again.`);
1107
- }
1108
- this._lastDiagnosticsTime = now;
1109
-
1110
- this.pushEvent('diagnosticsProgress', { progress: 10, status: 'Collecting log files' });
1111
- const finalLogFiles = await this.getLogFiles();
1112
-
1113
- this.pushEvent('diagnosticsProgress', { progress: 30, status: 'Collecting diagnostic files' });
1114
- const filesToArchive = [...finalLogFiles];
1115
-
1116
- // Include accessories.json for diagnostics
1117
- if (fs.existsSync(this.storedAccessoriesPath)) {
1118
- filesToArchive.push(path.basename(this.storedAccessoriesPath));
1119
- }
1120
-
1121
- // Include unsupported.json for diagnostics
1122
- if (fs.existsSync(this.unsupportedPath)) {
1123
- filesToArchive.push(path.basename(this.unsupportedPath));
1124
- }
1125
-
1126
- this.pushEvent('diagnosticsProgress', { progress: 40, status: 'Checking archive content' });
1127
- if (filesToArchive.length === 0) {
1128
- throw new Error('No diagnostic files were found');
1129
- }
1130
-
1131
- try {
1132
- const now = new Date();
1133
- const timestamp = now.toISOString().replace(/[:T]/g, '-').replace(/\..+/, '');
1134
- const archiveName = `diagnostics-${timestamp}.tar.gz`;
1135
-
1136
- this.pushEvent('diagnosticsProgress', { progress: 45, status: `Compressing ${filesToArchive.length} files` });
1137
-
1138
- // Snapshot files to a temp directory before archiving.
1139
- // Log files are actively written to — reading them directly with tar
1140
- // causes "did not encounter expected EOF" when file size changes mid-read.
1141
-
1142
- const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'eufy-diag-'));
1143
- let fileBuffer;
1144
- try {
1145
- await Promise.all(filesToArchive.map((file) =>
1146
- fs.promises.copyFile(
1147
- path.join(this.storagePath, file),
1148
- path.join(tmpDir, file),
1149
- ),
1150
- ));
1151
-
1152
- const tarStream = tarCreate({ gzip: true, cwd: tmpDir }, filesToArchive);
1153
- const chunks = [];
1154
- for await (const chunk of tarStream) {
1155
- chunks.push(chunk);
1156
- }
1157
- fileBuffer = Buffer.concat(chunks);
1158
- } finally {
1159
- fs.promises.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
1160
- }
1161
-
1162
- if (skipEncryption) {
1163
- this.pushEvent('diagnosticsProgress', { progress: 90, status: 'Returning plain archive' });
1164
- return { buffer: fileBuffer, filename: archiveName };
1165
- }
1166
-
1167
- this.pushEvent('diagnosticsProgress', { progress: 80, status: 'Encrypting archive' });
1168
- const encryptedBuffer = encryptDiagnostics(fileBuffer);
1169
-
1170
- this.pushEvent('diagnosticsProgress', { progress: 90, status: 'Returning encrypted archive' });
1171
- return { buffer: encryptedBuffer, filename: archiveName };
1172
- } catch (error) {
1173
- this.log.error('Error while generating diagnostics archive: ' + error);
1174
- throw error;
1175
- }
1176
- }
1177
-
1178
- async cleanStorage() {
1179
- const preserved = new Set([
1180
- 'accessories.json',
1181
- 'persistent.json',
1182
- 'eufy-security.log',
1183
- 'eufy-lib.log',
1184
- 'ffmpeg.log',
1185
- 'configui-server.log',
1186
- 'configui-lib.log',
1187
- ]);
1188
- const entries = await fs.promises.readdir(this.storagePath, { withFileTypes: true });
1189
- let deleted = 0;
1190
- for (const entry of entries) {
1191
- if (!entry.isFile() || preserved.has(entry.name)) continue;
1192
- const filePath = path.join(this.storagePath, entry.name);
1193
- try {
1194
- await fs.promises.unlink(filePath);
1195
- deleted++;
1196
- this.log.debug(`Deleted: ${entry.name}`);
1197
- } catch (error) {
1198
- this.log.warn(`Failed to delete ${entry.name}: ${error}`);
1199
- }
1200
- }
1201
- this.log.info(`Cleaned ${deleted} file(s)`);
1202
- return { deleted };
1203
- }
1204
-
1205
- async getSystemInfo() {
1206
-
1207
- let homebridgeVersion = 'unknown';
1208
- try {
1209
- const hbPkg = require('homebridge/package.json');
1210
- homebridgeVersion = hbPkg.version;
1211
- } catch {
1212
- // Homebridge package not resolvable from here
1213
- }
1214
-
1215
- let deviceSummary = [];
1216
- try {
1217
- if (fs.existsSync(this.storedAccessoriesPath)) {
1218
- const storedData = JSON.parse(fs.readFileSync(this.storedAccessoriesPath, 'utf-8'));
1219
- if (storedData.stations) {
1220
- deviceSummary = storedData.stations.map(s => ({
1221
- name: s.displayName,
1222
- type: s.typename,
1223
- devices: (s.devices || []).map(d => ({
1224
- name: d.displayName,
1225
- type: d.typename,
1226
- })),
1227
- }));
1228
- }
1229
- }
1230
- } catch {
1231
- // ignore
1232
- }
1233
-
1234
- this.log.debug('System info requested by UI');
1235
-
1236
- return {
1237
- pluginVersion: LIB_VERSION,
1238
- eufyClientVersion: libVersion,
1239
- homebridgeVersion,
1240
- nodeVersion: process.version,
1241
- os: `${os.type()} ${os.release()} (${os.arch()})`,
1242
- devices: deviceSummary,
1243
- };
1244
- }
1245
-
1246
- /** Resolve and validate a recording filename, returning the absolute path. */
1247
- _resolveRecordingPath(filename) {
1248
- if (!filename || typeof filename !== 'string') {
1249
- throw new Error('Missing or invalid filename parameter.');
1250
- }
1251
- const recordingsDir = path.join(this.storagePath, 'recordings');
1252
- const sanitized = path.basename(filename);
1253
- if (!sanitized.endsWith('.mp4')) {
1254
- throw new Error('Invalid filename.');
1255
- }
1256
- const resolved = path.resolve(path.join(recordingsDir, sanitized));
1257
- if (!resolved.startsWith(path.resolve(recordingsDir))) {
1258
- throw new Error('Invalid filename.');
1259
- }
1260
- return resolved;
1261
- }
1262
-
1263
- /**
1264
- * List all debug recording files available for download.
1265
- */
1266
- async listDebugRecordings() {
1267
- const recordingsDir = path.join(this.storagePath, 'recordings');
1268
- try {
1269
- if (!fs.existsSync(recordingsDir)) {
1270
- return { recordings: [] };
1271
- }
1272
- const files = fs.readdirSync(recordingsDir)
1273
- .filter(f => f.endsWith('.mp4'))
1274
- .map(filename => {
1275
- const filePath = path.join(recordingsDir, filename);
1276
- try {
1277
- const stats = fs.statSync(filePath);
1278
- // Parse filename: <serial>_<timestamp>_<type>.mp4
1279
- // Types: hksv, livestream (current), raw, processed (legacy)
1280
- const match = filename.match(/^([A-Za-z0-9_-]+?)_(\d{4}-\d{2}-\d{2}T[\d-]+Z?)(?:_(hksv|livestream|raw|processed))?\.mp4$/);
1281
- const rawType = match && match[3] ? match[3] : 'processed';
1282
- // Normalize legacy 'raw' → 'livestream'
1283
- const type = rawType === 'raw' ? 'livestream' : rawType;
1284
- return {
1285
- filename,
1286
- serial: match ? match[1] : 'unknown',
1287
- timestamp: match ? match[2] : '',
1288
- type,
1289
- sizeBytes: stats.size,
1290
- sizeMB: (stats.size / (1024 * 1024)).toFixed(1),
1291
- createdAt: stats.mtimeMs,
1292
- createdAtISO: new Date(stats.mtimeMs).toISOString(),
1293
- };
1294
- } catch {
1295
- return null;
1296
- }
1297
- })
1298
- .filter(f => f !== null)
1299
- .sort((a, b) => b.createdAt - a.createdAt);
1300
- return { recordings: files };
1301
- } catch (error) {
1302
- this.log.error('Error listing debug recordings:', error);
1303
- throw new Error('Failed to list debug recordings. Check the logs for details.');
1304
- }
1305
- }
1306
-
1307
- /**
1308
- * Download a debug recording file in chunks to avoid memory spikes.
1309
- * Request: { filename: string, offset?: number, chunkSize?: number }
1310
- * Returns: { data: Buffer (base64), totalSize: number, offset: number, chunkSize: number, done: boolean }
1311
- */
1312
- async downloadDebugRecording(options = {}) {
1313
- const { filename, offset: rawOffset = 0, chunkSize: rawChunkSize = 256 * 1024 } = options;
1314
- const resolved = this._resolveRecordingPath(filename);
1315
-
1316
- // Coerce and clamp parameters
1317
- const numOffset = Number(rawOffset) || 0;
1318
- if (numOffset < 0) throw new Error('Invalid offset.');
1319
- const MAX_CHUNK = 2 * 1024 * 1024; // 2 MB
1320
- const numChunkSize = Math.min(Number(rawChunkSize) || 256 * 1024, MAX_CHUNK);
1321
-
1322
- if (!fs.existsSync(resolved)) {
1323
- throw new Error('Recording file not found.');
1324
- }
1325
-
1326
- const stats = fs.statSync(resolved);
1327
- const totalSize = stats.size;
1328
- const readSize = Math.min(numChunkSize, totalSize - numOffset);
1329
-
1330
- if (numOffset >= totalSize || readSize <= 0) {
1331
- return { data: null, totalSize, offset: numOffset, chunkSize: 0, done: true };
1332
- }
1333
-
1334
- const fh = await fs.promises.open(resolved, 'r');
1335
- const buf = Buffer.alloc(readSize);
1336
- try {
1337
- await fh.read(buf, 0, readSize, numOffset);
1338
- } finally {
1339
- await fh.close();
1340
- }
1341
-
1342
- const newOffset = numOffset + readSize;
1343
-
1344
- return {
1345
- data: buf,
1346
- totalSize,
1347
- offset: newOffset,
1348
- chunkSize: readSize,
1349
- done: newOffset >= totalSize,
1350
- filename: path.basename(resolved),
1351
- };
1352
- }
1353
-
1354
- /**
1355
- * Delete a specific debug recording file.
1356
- */
1357
- async deleteDebugRecording(options = {}) {
1358
- const resolved = this._resolveRecordingPath(options.filename);
1359
-
1360
- if (!fs.existsSync(resolved)) {
1361
- throw new Error('Recording file not found.');
1362
- }
1363
-
1364
- fs.unlinkSync(resolved);
1365
- const sanitized = path.basename(resolved);
1366
- this.log.info(`Deleted debug recording: ${sanitized}`);
1367
- return { deleted: true, filename: sanitized };
1368
- }
1369
-
1370
- /**
1371
- * Delete all debug recording files.
1372
- */
1373
- async deleteAllDebugRecordings() {
1374
- const recordingsDir = path.join(this.storagePath, 'recordings');
1375
- if (!fs.existsSync(recordingsDir)) {
1376
- return { deleted: 0 };
1377
- }
1378
-
1379
- const files = fs.readdirSync(recordingsDir).filter(f => f.endsWith('.mp4'));
1380
- let deleted = 0;
1381
- for (const file of files) {
1382
- try {
1383
- fs.unlinkSync(path.join(recordingsDir, file));
1384
- deleted++;
1385
- } catch {
1386
- // ignore individual failures
1387
- }
1388
- }
1389
- this.log.info(`Deleted ${deleted} debug recording(s)`);
1390
- return { deleted };
1391
- }
1392
- }
1393
-
1394
- (() => new UiServer())();
3
+ new EufyAuthenticationUiServer();