@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.
- package/AUTHORS +4 -0
- package/CODE_OF_CONDUCT.md +30 -0
- package/CONTRIBUTING.md +77 -0
- package/README.md +152 -49
- package/SECURITY.md +30 -0
- package/config.schema.json +92 -3
- package/dist/account/ownership.js +290 -0
- package/dist/account/ownership.js.map +1 -0
- package/dist/account/persistence.js +274 -0
- package/dist/account/persistence.js.map +1 -0
- package/dist/account/temporary-authentication.js +348 -0
- package/dist/account/temporary-authentication.js.map +1 -0
- package/dist/configuration.js +167 -0
- package/dist/configuration.js.map +1 -0
- package/dist/device/member-evidence.js +66 -0
- package/dist/device/member-evidence.js.map +1 -0
- package/dist/device/member-trust.js +114 -0
- package/dist/device/member-trust.js.map +1 -0
- package/dist/device/snapshot.js +190 -0
- package/dist/device/snapshot.js.map +1 -0
- package/dist/diagnostics.js +2218 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/homekit/adapter.js +2 -0
- package/dist/homekit/adapter.js.map +1 -0
- package/dist/homekit/adapters/battery.js +208 -0
- package/dist/homekit/adapters/battery.js.map +1 -0
- package/dist/homekit/adapters/camera-controls.js +527 -0
- package/dist/homekit/adapters/camera-controls.js.map +1 -0
- package/dist/homekit/adapters/camera-streaming.js +1515 -0
- package/dist/homekit/adapters/camera-streaming.js.map +1 -0
- package/dist/homekit/adapters/contact.js +138 -0
- package/dist/homekit/adapters/contact.js.map +1 -0
- package/dist/homekit/adapters/doorbell.js +57 -0
- package/dist/homekit/adapters/doorbell.js.map +1 -0
- package/dist/homekit/adapters/information.js +55 -0
- package/dist/homekit/adapters/information.js.map +1 -0
- package/dist/homekit/adapters/lock.js +489 -0
- package/dist/homekit/adapters/lock.js.map +1 -0
- package/dist/homekit/adapters/motion.js +100 -0
- package/dist/homekit/adapters/motion.js.map +1 -0
- package/dist/homekit/adapters/registry.js +26 -0
- package/dist/homekit/adapters/registry.js.map +1 -0
- package/dist/homekit/adapters/security-system.js +425 -0
- package/dist/homekit/adapters/security-system.js.map +1 -0
- package/dist/homekit/adapters/siren.js +136 -0
- package/dist/homekit/adapters/siren.js.map +1 -0
- package/dist/homekit/adapters/smart-light.js +662 -0
- package/dist/homekit/adapters/smart-light.js.map +1 -0
- package/dist/homekit/coverage-matrix.js +119 -0
- package/dist/homekit/coverage-matrix.js.map +1 -0
- package/dist/homekit/device-control.js +175 -0
- package/dist/homekit/device-control.js.map +1 -0
- package/dist/homekit/reconciler.js +319 -0
- package/dist/homekit/reconciler.js.map +1 -0
- package/dist/homekit/representation.js +23 -0
- package/dist/homekit/representation.js.map +1 -0
- package/dist/index.js +4 -9
- package/dist/index.js.map +1 -1
- package/dist/media/contracts.js +2 -0
- package/dist/media/contracts.js.map +1 -0
- package/dist/media/last-successful-image.js +208 -0
- package/dist/media/last-successful-image.js.map +1 -0
- package/dist/media/live-stream.js +1003 -0
- package/dist/media/live-stream.js.map +1 -0
- package/dist/media/recording.js +560 -0
- package/dist/media/recording.js.map +1 -0
- package/dist/media/session-budget.js +39 -0
- package/dist/media/session-budget.js.map +1 -0
- package/dist/media/snapshot.js +406 -0
- package/dist/media/snapshot.js.map +1 -0
- package/dist/media/station-live-sessions.js +101 -0
- package/dist/media/station-live-sessions.js.map +1 -0
- package/dist/platform.js +97 -809
- package/dist/platform.js.map +1 -1
- package/dist/runtime/channel.js +375 -0
- package/dist/runtime/channel.js.map +1 -0
- package/dist/runtime/owner.js +682 -0
- package/dist/runtime/owner.js.map +1 -0
- package/dist/runtime/sdk-client.js +211 -0
- package/dist/runtime/sdk-client.js.map +1 -0
- package/dist/runtime/tracker.js +211 -0
- package/dist/runtime/tracker.js.map +1 -0
- package/dist/settings.js +1 -43
- package/dist/settings.js.map +1 -1
- package/dist/storage.js +59 -0
- package/dist/storage.js.map +1 -0
- package/dist/ui/dashboard.js +181 -0
- package/dist/ui/dashboard.js.map +1 -0
- package/dist/ui/device-image.js +22 -0
- package/dist/ui/device-image.js.map +1 -0
- package/dist/ui/runtime-channel-client.js +305 -0
- package/dist/ui/runtime-channel-client.js.map +1 -0
- package/dist/ui/server.js +259 -0
- package/dist/ui/server.js.map +1 -0
- package/homebridge-ui/public/app.css +1902 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T1250.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2070.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2080.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2081.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2103.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2117.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2118.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2119.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T211A.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2120.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2123.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2128.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2130.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2132.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2150.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2181.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2182.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2190.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2192.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2193.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2194.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2210.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2250.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2251.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2252.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2253.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2254.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2255.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2256.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2257.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2258.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2259.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2261.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2262.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2266.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2267.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2268.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2270.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2272.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2273.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2276.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2277.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2278.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2280.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2292.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2320.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2351.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2352.webp +0 -0
- package/homebridge-ui/public/assets/devices/clean/clean-T2353.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L00.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L01.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L02.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L04.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L10.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L20.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L30.webp +0 -0
- package/homebridge-ui/public/assets/devices/life/life-T8L40.webp +0 -0
- package/homebridge-ui/public/assets/devices/mower/mower-T2801.webp +0 -0
- package/homebridge-ui/public/assets/devices/mower/mower-T280B.webp +0 -0
- package/homebridge-ui/public/assets/devices/mower/mower-T2880.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T7400.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T7401.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8001.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8002.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8010.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8021.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8023.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8025.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8030.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8110.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8111.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8112.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8113.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8114.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8122.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8123.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8124.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8130.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8131.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8134.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8140.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8141.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8150.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8151.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8152.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8153.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8160.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8161.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8162.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8170.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8171.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8172.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8173.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8200.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8200X.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8201.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8203.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8210.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8213.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8214.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8222.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8400.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8401.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8410.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8411.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8414.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8416.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8417.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8419.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8419N.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8420.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8420X.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8422.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8423.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8424.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8425.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8441.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8442.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8452.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T84A0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T84A1.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8500.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8502.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8503.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8504.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8506.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8510.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8510P.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8520.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8520P.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8530.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8531.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85D0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85L0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85P0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T85V0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8600.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T86P2.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8790.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T87B0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T87B2.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8900.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8910.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8920.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8960.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8B0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8E00.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8N00.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T8W11C.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90F0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90M0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90R0.webp +0 -0
- package/homebridge-ui/public/assets/devices/security/security-T90S0.webp +0 -0
- package/homebridge-ui/public/assets/icons/inventory.svg +1 -1
- package/homebridge-ui/public/assets/icons/troubleshoot.svg +1 -0
- package/homebridge-ui/public/assets/icons/warning.svg +1 -1
- package/homebridge-ui/public/assets/logo-dark.svg +10 -0
- package/homebridge-ui/public/assets/logo.svg +10 -0
- package/homebridge-ui/public/i18n/en.json +218 -0
- package/homebridge-ui/public/i18n/fr.json +218 -0
- package/homebridge-ui/public/index.html +377 -28
- package/homebridge-ui/public/js/app.js +1065 -0
- package/homebridge-ui/public/js/dashboard.js +385 -0
- package/homebridge-ui/public/js/legacy-settings.js +37 -0
- package/homebridge-ui/public/js/profile-wizard.js +68 -0
- package/homebridge-ui/server.js +2 -1393
- package/i18n/runtime/en.json +100 -0
- package/media/Snapshot-Unavailable.jpg +0 -0
- package/media/Snapshot-black.jpg +0 -0
- package/media/camera-disabled.jpg +0 -0
- package/media/camera-offline.jpg +0 -0
- package/package.json +29 -24
- package/.claude/CLAUDE.md +0 -175
- package/.claude/PRD.md +0 -241
- package/.claude/docs/debug-recording-lessons.md +0 -91
- package/.claude/docs/hksv-recording-fix.md +0 -160
- package/.claude/skills/architect/SKILL.md +0 -76
- package/.claude/skills/developer/SKILL.md +0 -59
- package/.claude/skills/new-device-support/SKILL.md +0 -212
- package/.claude/skills/new-device-support/check-device.mjs +0 -363
- package/.claude/skills/new-device-support/map-properties.mjs +0 -450
- package/.claude/skills/new-device-support/verify-device.mjs +0 -272
- package/.claude/skills/planner/SKILL.md +0 -100
- package/.claude/skills/qa/SKILL.md +0 -79
- package/.claude/skills/support/SKILL.md +0 -175
- package/.prettierignore +0 -7
- package/.prettierrc.json +0 -7
- package/FUNDING.yml +0 -1
- package/dist/accessories/AutoSyncStationAccessory.js +0 -157
- package/dist/accessories/AutoSyncStationAccessory.js.map +0 -1
- package/dist/accessories/BaseAccessory.js +0 -261
- package/dist/accessories/BaseAccessory.js.map +0 -1
- package/dist/accessories/CameraAccessory.js +0 -459
- package/dist/accessories/CameraAccessory.js.map +0 -1
- package/dist/accessories/Device.js +0 -65
- package/dist/accessories/Device.js.map +0 -1
- package/dist/accessories/EntrySensorAccessory.js +0 -48
- package/dist/accessories/EntrySensorAccessory.js.map +0 -1
- package/dist/accessories/GarageDoorAccessory.js +0 -160
- package/dist/accessories/GarageDoorAccessory.js.map +0 -1
- package/dist/accessories/LockAccessory.js +0 -142
- package/dist/accessories/LockAccessory.js.map +0 -1
- package/dist/accessories/MotionSensorAccessory.js +0 -48
- package/dist/accessories/MotionSensorAccessory.js.map +0 -1
- package/dist/accessories/SmartDropAccessory.js +0 -141
- package/dist/accessories/SmartDropAccessory.js.map +0 -1
- package/dist/accessories/StationAccessory.js +0 -373
- package/dist/accessories/StationAccessory.js.map +0 -1
- package/dist/config.js +0 -26
- package/dist/config.js.map +0 -1
- package/dist/controller/DebugRecordingManager.js +0 -402
- package/dist/controller/DebugRecordingManager.js.map +0 -1
- package/dist/controller/LocalLivestreamManager.js +0 -371
- package/dist/controller/LocalLivestreamManager.js.map +0 -1
- package/dist/controller/recordingDelegate.js +0 -252
- package/dist/controller/recordingDelegate.js.map +0 -1
- package/dist/controller/snapshotDelegate.js +0 -348
- package/dist/controller/snapshotDelegate.js.map +0 -1
- package/dist/controller/streamingDelegate.js +0 -389
- package/dist/controller/streamingDelegate.js.map +0 -1
- package/dist/interfaces.js +0 -2
- package/dist/interfaces.js.map +0 -1
- package/dist/media/Snapshot-Unavailable.png +0 -0
- package/dist/media/Snapshot-Unavailable.xcf +0 -0
- package/dist/media/Snapshot-black.png +0 -0
- package/dist/media/camera-disabled.png +0 -0
- package/dist/media/camera-offline.png +0 -0
- package/dist/media/media/Snapshot-Unavailable.png +0 -0
- package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
- package/dist/media/media/Snapshot-black.png +0 -0
- package/dist/media/media/camera-disabled.png +0 -0
- package/dist/media/media/camera-offline.png +0 -0
- package/dist/utils/Talkback.js +0 -162
- package/dist/utils/Talkback.js.map +0 -1
- package/dist/utils/accessoriesStore.js +0 -210
- package/dist/utils/accessoriesStore.js.map +0 -1
- package/dist/utils/configTypes.js +0 -38
- package/dist/utils/configTypes.js.map +0 -1
- package/dist/utils/ffmpeg.js +0 -1111
- package/dist/utils/ffmpeg.js.map +0 -1
- package/dist/utils/interfaces.js +0 -8
- package/dist/utils/interfaces.js.map +0 -1
- package/dist/utils/utils.js +0 -224
- package/dist/utils/utils.js.map +0 -1
- package/dist/version.js +0 -2
- package/dist/version.js.map +0 -1
- package/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/diagnosticsCrypto.js +0 -112
- package/homebridge-ui/public/app.js +0 -237
- package/homebridge-ui/public/assets/devices/4g_lte_cam_s330_large.png +0 -0
- package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
- package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell2k_large.png +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
- package/homebridge-ui/public/assets/devices/eufyCamS4_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
- package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/flood_freeze_sensor_e20_t90F0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/homebasemini_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamC220_t8419N_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
- package/homebridge-ui/public/assets/devices/indoorcams350_large.png +0 -0
- package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
- package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
- package/homebridge-ui/public/assets/devices/nvr_s4_max_T8N00_large.png +0 -0
- package/homebridge-ui/public/assets/devices/pir_sensor_e20_large.png +0 -0
- package/homebridge-ui/public/assets/devices/poe_bullet_ptz_cam_s4_T8E00_large.png +0 -0
- package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
- package/homebridge-ui/public/assets/devices/siren_sensor_e20_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85D0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85L0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85P0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_t85V0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlock_video_t8531_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/smoke_alarm_e10_t90S0_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocame42_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/unknown.png +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s100_large.png +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s120_large.png +0 -0
- package/homebridge-ui/public/assets/devices/water_freeze_sensor_t8920_large.png +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
- package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
- package/homebridge-ui/public/assets/icons/attach.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_0.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_1.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_2.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_3.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_4.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_5.svg +0 -1
- package/homebridge-ui/public/assets/icons/battery_6.svg +0 -1
- package/homebridge-ui/public/assets/icons/copy.svg +0 -1
- package/homebridge-ui/public/assets/icons/delete.svg +0 -1
- package/homebridge-ui/public/assets/icons/download.svg +0 -1
- package/homebridge-ui/public/assets/icons/info.svg +0 -1
- package/homebridge-ui/public/assets/icons/refresh.svg +0 -1
- package/homebridge-ui/public/assets/icons/satellite_alt.svg +0 -1
- package/homebridge-ui/public/assets/icons/solar_power.svg +0 -1
- package/homebridge-ui/public/components/device-card.js +0 -174
- package/homebridge-ui/public/components/guard-modes.js +0 -88
- package/homebridge-ui/public/components/number-input.js +0 -121
- package/homebridge-ui/public/components/select.js +0 -73
- package/homebridge-ui/public/components/toggle.js +0 -68
- package/homebridge-ui/public/services/api.js +0 -250
- package/homebridge-ui/public/services/config.js +0 -144
- package/homebridge-ui/public/style.css +0 -818
- package/homebridge-ui/public/utils/countries.js +0 -73
- package/homebridge-ui/public/utils/device-images.js +0 -102
- package/homebridge-ui/public/utils/helpers.js +0 -115
- package/homebridge-ui/public/views/dashboard.js +0 -226
- package/homebridge-ui/public/views/device-detail.js +0 -654
- package/homebridge-ui/public/views/diagnostics.js +0 -402
- package/homebridge-ui/public/views/login.js +0 -650
- package/homebridge-ui/public/views/recordings.js +0 -319
- package/homebridge-ui/public/views/settings.js +0 -197
- package/homebridge-ui/public/views/unsupported-detail.js +0 -296
- package/media/Snapshot-Unavailable.png +0 -0
- package/media/Snapshot-Unavailable.xcf +0 -0
- package/media/Snapshot-black.png +0 -0
- package/media/camera-disabled.png +0 -0
- package/media/camera-offline.png +0 -0
- package/scripts/decrypt-diagnostics.mjs +0 -257
package/homebridge-ui/server.js
CHANGED
|
@@ -1,1394 +1,3 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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();
|