@homebridge-plugins/homebridge-eufy-security 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/FUNDING.yml +1 -0
  3. package/LICENSE +176 -0
  4. package/README.md +67 -0
  5. package/config.schema.json +6 -0
  6. package/dist/accessories/AutoSyncStationAccessory.js +156 -0
  7. package/dist/accessories/AutoSyncStationAccessory.js.map +1 -0
  8. package/dist/accessories/BaseAccessory.js +247 -0
  9. package/dist/accessories/BaseAccessory.js.map +1 -0
  10. package/dist/accessories/CameraAccessory.js +431 -0
  11. package/dist/accessories/CameraAccessory.js.map +1 -0
  12. package/dist/accessories/Device.js +67 -0
  13. package/dist/accessories/Device.js.map +1 -0
  14. package/dist/accessories/EntrySensorAccessory.js +48 -0
  15. package/dist/accessories/EntrySensorAccessory.js.map +1 -0
  16. package/dist/accessories/LockAccessory.js +142 -0
  17. package/dist/accessories/LockAccessory.js.map +1 -0
  18. package/dist/accessories/MotionSensorAccessory.js +48 -0
  19. package/dist/accessories/MotionSensorAccessory.js.map +1 -0
  20. package/dist/accessories/SmartDropAccessory.js +145 -0
  21. package/dist/accessories/SmartDropAccessory.js.map +1 -0
  22. package/dist/accessories/StationAccessory.js +371 -0
  23. package/dist/accessories/StationAccessory.js.map +1 -0
  24. package/dist/config.js +25 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/controller/LocalLivestreamManager.js +116 -0
  27. package/dist/controller/LocalLivestreamManager.js.map +1 -0
  28. package/dist/controller/recordingDelegate.js +208 -0
  29. package/dist/controller/recordingDelegate.js.map +1 -0
  30. package/dist/controller/snapshotDelegate.js +345 -0
  31. package/dist/controller/snapshotDelegate.js.map +1 -0
  32. package/dist/controller/streamingDelegate.js +345 -0
  33. package/dist/controller/streamingDelegate.js.map +1 -0
  34. package/dist/index.js +11 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/interfaces.js +2 -0
  37. package/dist/interfaces.js.map +1 -0
  38. package/dist/media/Snapshot-Unavailable.png +0 -0
  39. package/dist/media/Snapshot-Unavailable.xcf +0 -0
  40. package/dist/media/Snapshot-black.png +0 -0
  41. package/dist/media/camera-disabled.png +0 -0
  42. package/dist/media/camera-offline.png +0 -0
  43. package/dist/media/media/Snapshot-Unavailable.png +0 -0
  44. package/dist/media/media/Snapshot-Unavailable.xcf +0 -0
  45. package/dist/media/media/Snapshot-black.png +0 -0
  46. package/dist/media/media/camera-disabled.png +0 -0
  47. package/dist/media/media/camera-offline.png +0 -0
  48. package/dist/platform.js +716 -0
  49. package/dist/platform.js.map +1 -0
  50. package/dist/settings.js +38 -0
  51. package/dist/settings.js.map +1 -0
  52. package/dist/utils/Talkback.js +92 -0
  53. package/dist/utils/Talkback.js.map +1 -0
  54. package/dist/utils/accessoriesStore.js +206 -0
  55. package/dist/utils/accessoriesStore.js.map +1 -0
  56. package/dist/utils/configTypes.js +35 -0
  57. package/dist/utils/configTypes.js.map +1 -0
  58. package/dist/utils/ffmpeg.js +843 -0
  59. package/dist/utils/ffmpeg.js.map +1 -0
  60. package/dist/utils/interfaces.js +8 -0
  61. package/dist/utils/interfaces.js.map +1 -0
  62. package/dist/utils/utils.js +44 -0
  63. package/dist/utils/utils.js.map +1 -0
  64. package/dist/version.js +2 -0
  65. package/dist/version.js.map +1 -0
  66. package/eslint.config.mjs +18 -0
  67. package/homebridge-eufy-security.png +0 -0
  68. package/homebridge-ui/public/app.js +225 -0
  69. package/homebridge-ui/public/assets/devices/4g_lte_starlight_large.jpg +0 -0
  70. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C30.png +0 -0
  71. package/homebridge-ui/public/assets/devices/BATTERY_DOORBELL_C31.png +0 -0
  72. package/homebridge-ui/public/assets/devices/batterydoorbell1080p_large.jpg +0 -0
  73. package/homebridge-ui/public/assets/devices/batterydoorbell2kdual_large.jpg +0 -0
  74. package/homebridge-ui/public/assets/devices/batterydoorbell_e340_large.png +0 -0
  75. package/homebridge-ui/public/assets/devices/eufy-security-client.png +0 -0
  76. package/homebridge-ui/public/assets/devices/eufycam2_large.png +0 -0
  77. package/homebridge-ui/public/assets/devices/eufycam2c_large.jpg +0 -0
  78. package/homebridge-ui/public/assets/devices/eufycam2cpro_large.jpg +0 -0
  79. package/homebridge-ui/public/assets/devices/eufycam2pro_large.jpg +0 -0
  80. package/homebridge-ui/public/assets/devices/eufycam3_large.jpg +0 -0
  81. package/homebridge-ui/public/assets/devices/eufycam3c_large.jpg +0 -0
  82. package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
  83. package/homebridge-ui/public/assets/devices/eufycam_large.jpg +0 -0
  84. package/homebridge-ui/public/assets/devices/eufycame330_large.jpg +0 -0
  85. package/homebridge-ui/public/assets/devices/floodlight2_large.jpg +0 -0
  86. package/homebridge-ui/public/assets/devices/floodlight2pro_large.jpg +0 -0
  87. package/homebridge-ui/public/assets/devices/floodlight_large.jpg +0 -0
  88. package/homebridge-ui/public/assets/devices/floodlightcame340_large.jpg +0 -0
  89. package/homebridge-ui/public/assets/devices/garage_camera_t8452_large.jpg +0 -0
  90. package/homebridge-ui/public/assets/devices/homebase2_large.png +0 -0
  91. package/homebridge-ui/public/assets/devices/homebase3_large.png +0 -0
  92. package/homebridge-ui/public/assets/devices/homebase_large.jpg +0 -0
  93. package/homebridge-ui/public/assets/devices/homebasemini_large.jpg +0 -0
  94. package/homebridge-ui/public/assets/devices/indoorcamC210_large.png +0 -0
  95. package/homebridge-ui/public/assets/devices/indoorcamC220_large.png +0 -0
  96. package/homebridge-ui/public/assets/devices/indoorcamE30_large.png +0 -0
  97. package/homebridge-ui/public/assets/devices/indoorcamc120_large.png +0 -0
  98. package/homebridge-ui/public/assets/devices/indoorcammini_large.jpg +0 -0
  99. package/homebridge-ui/public/assets/devices/indoorcamp24_large.png +0 -0
  100. package/homebridge-ui/public/assets/devices/indoorcams350_large.jpg +0 -0
  101. package/homebridge-ui/public/assets/devices/keypad_large.png +0 -0
  102. package/homebridge-ui/public/assets/devices/minibase_chime_T8023_large.jpg +0 -0
  103. package/homebridge-ui/public/assets/devices/motionsensor_large.png +0 -0
  104. package/homebridge-ui/public/assets/devices/sensor_large.png +0 -0
  105. package/homebridge-ui/public/assets/devices/smartdrop_t8790_large.png +0 -0
  106. package/homebridge-ui/public/assets/devices/smartlock_t8500_large.png +0 -0
  107. package/homebridge-ui/public/assets/devices/smartlock_t8500_wifibridge_large.jpg +0 -0
  108. package/homebridge-ui/public/assets/devices/smartlock_t8503_large.png +0 -0
  109. package/homebridge-ui/public/assets/devices/smartlock_t8504_large.jpg +0 -0
  110. package/homebridge-ui/public/assets/devices/smartlock_t8510P_t8520P_large.png +0 -0
  111. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8502_large.png +0 -0
  112. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8506_large.png +0 -0
  113. package/homebridge-ui/public/assets/devices/smartlock_touch_and_wifi_t8520_large.png +0 -0
  114. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_large.png +0 -0
  115. package/homebridge-ui/public/assets/devices/smartlock_touch_t8510_wifibridge_large.jpg +0 -0
  116. package/homebridge-ui/public/assets/devices/smartlock_video_t8530_large.png +0 -0
  117. package/homebridge-ui/public/assets/devices/smartlockwifibridge_t8021_large.jpg +0 -0
  118. package/homebridge-ui/public/assets/devices/smartsafe_s10_t7400_large.png +0 -0
  119. package/homebridge-ui/public/assets/devices/smartsafe_s12_t7401_large.png +0 -0
  120. package/homebridge-ui/public/assets/devices/smarttrack_card_t87B2_large.png +0 -0
  121. package/homebridge-ui/public/assets/devices/smarttrack_link_t87B0_large.png +0 -0
  122. package/homebridge-ui/public/assets/devices/solocamc210_large.jpg +0 -0
  123. package/homebridge-ui/public/assets/devices/solocamc35_large.png +0 -0
  124. package/homebridge-ui/public/assets/devices/solocame20_large.jpg +0 -0
  125. package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
  126. package/homebridge-ui/public/assets/devices/solocame40_large.jpg +0 -0
  127. package/homebridge-ui/public/assets/devices/solocaml20_large.jpg +0 -0
  128. package/homebridge-ui/public/assets/devices/solocams220_large.jpg +0 -0
  129. package/homebridge-ui/public/assets/devices/solocams340_large.png +0 -0
  130. package/homebridge-ui/public/assets/devices/solocams40_large.jpg +0 -0
  131. package/homebridge-ui/public/assets/devices/soloindoorcamc24_large.jpg +0 -0
  132. package/homebridge-ui/public/assets/devices/solooutdoorcamc22_large.png +0 -0
  133. package/homebridge-ui/public/assets/devices/solooutdoorcamc24_large.jpg +0 -0
  134. package/homebridge-ui/public/assets/devices/unknown.png +0 -0
  135. package/homebridge-ui/public/assets/devices/walllight_s100_large.jpg +0 -0
  136. package/homebridge-ui/public/assets/devices/walllight_s120_large.jpg +0 -0
  137. package/homebridge-ui/public/assets/devices/wireddoorbell1080p_large.jpg +0 -0
  138. package/homebridge-ui/public/assets/devices/wireddoorbell2k_large.png +0 -0
  139. package/homebridge-ui/public/assets/devices/wireddoorbelldual_large.jpg +0 -0
  140. package/homebridge-ui/public/assets/icons/attach.svg +1 -0
  141. package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
  142. package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
  143. package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
  144. package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
  145. package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
  146. package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
  147. package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
  148. package/homebridge-ui/public/assets/icons/bolt.svg +1 -0
  149. package/homebridge-ui/public/assets/icons/bug-report.svg +1 -0
  150. package/homebridge-ui/public/assets/icons/copy.svg +1 -0
  151. package/homebridge-ui/public/assets/icons/delete.svg +1 -0
  152. package/homebridge-ui/public/assets/icons/download.svg +1 -0
  153. package/homebridge-ui/public/assets/icons/info.svg +1 -0
  154. package/homebridge-ui/public/assets/icons/inventory.svg +1 -0
  155. package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
  156. package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
  157. package/homebridge-ui/public/assets/icons/settings.svg +1 -0
  158. package/homebridge-ui/public/assets/icons/settings_backup_restore.svg +1 -0
  159. package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
  160. package/homebridge-ui/public/assets/icons/warning.svg +1 -0
  161. package/homebridge-ui/public/components/device-card.js +162 -0
  162. package/homebridge-ui/public/components/guard-modes.js +88 -0
  163. package/homebridge-ui/public/components/number-input.js +121 -0
  164. package/homebridge-ui/public/components/select.js +73 -0
  165. package/homebridge-ui/public/components/toggle.js +68 -0
  166. package/homebridge-ui/public/index.html +27 -0
  167. package/homebridge-ui/public/services/api.js +214 -0
  168. package/homebridge-ui/public/services/config.js +144 -0
  169. package/homebridge-ui/public/style.css +775 -0
  170. package/homebridge-ui/public/utils/countries.js +73 -0
  171. package/homebridge-ui/public/utils/device-images.js +89 -0
  172. package/homebridge-ui/public/utils/helpers.js +87 -0
  173. package/homebridge-ui/public/views/dashboard.js +226 -0
  174. package/homebridge-ui/public/views/device-detail.js +610 -0
  175. package/homebridge-ui/public/views/diagnostics.js +296 -0
  176. package/homebridge-ui/public/views/login.js +636 -0
  177. package/homebridge-ui/public/views/settings.js +192 -0
  178. package/homebridge-ui/public/views/unsupported-detail.js +296 -0
  179. package/homebridge-ui/server.js +1327 -0
  180. package/media/Snapshot-Unavailable.png +0 -0
  181. package/media/Snapshot-Unavailable.xcf +0 -0
  182. package/media/Snapshot-black.png +0 -0
  183. package/media/camera-disabled.png +0 -0
  184. package/media/camera-offline.png +0 -0
  185. package/package.json +64 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Settings View — global plugin settings with progressive disclosure.
3
+ * Account info and advanced configuration options.
4
+ */
5
+ // eslint-disable-next-line no-unused-vars
6
+ const SettingsView = {
7
+
8
+ _advancedOpen: false,
9
+
10
+ async render(container) {
11
+ container.innerHTML = '';
12
+ this._advancedOpen = false;
13
+
14
+ const config = await Config.get();
15
+
16
+ // Header
17
+ const header = document.createElement('div');
18
+ header.className = 'eufy-header';
19
+
20
+ const backBtn = document.createElement('button');
21
+ backBtn.className = 'btn btn-link p-0';
22
+ backBtn.innerHTML = '← Back';
23
+ backBtn.style.textDecoration = 'none';
24
+ backBtn.addEventListener('click', () => App.navigate('dashboard'));
25
+
26
+ const titleEl = document.createElement('h4');
27
+ titleEl.textContent = 'Settings';
28
+
29
+ header.appendChild(backBtn);
30
+ header.appendChild(titleEl);
31
+ // Empty spacer for alignment
32
+ header.appendChild(document.createElement('div'));
33
+ container.appendChild(header);
34
+
35
+ // ── Credentials Info ──
36
+ const credsSection = document.createElement('div');
37
+ credsSection.className = 'settings-section';
38
+
39
+ const credsTitle = document.createElement('div');
40
+ credsTitle.className = 'detail-section__title';
41
+ credsTitle.textContent = 'Account';
42
+ credsSection.appendChild(credsTitle);
43
+
44
+ const credsInfo = document.createElement('div');
45
+ credsInfo.className = 'text-muted';
46
+ credsInfo.style.fontSize = '0.85rem';
47
+ const email = config.username || 'Not configured';
48
+ const country = config.country || '—';
49
+ credsInfo.innerHTML = `
50
+ <div class="mb-1"><strong>Email:</strong> ${this._escHtml(email)}</div>
51
+ <div><strong>Country:</strong> ${country}</div>
52
+ `;
53
+ credsSection.appendChild(credsInfo);
54
+ container.appendChild(credsSection);
55
+
56
+ // ── Advanced Settings ──
57
+ const advBtn = document.createElement('button');
58
+ advBtn.className = 'advanced-toggle';
59
+ advBtn.innerHTML = `
60
+ <span class="advanced-toggle__chevron" id="settings-adv-chevron">▶</span>
61
+ Advanced Settings
62
+ `;
63
+ advBtn.addEventListener('click', () => {
64
+ this._advancedOpen = !this._advancedOpen;
65
+ const advSection = container.querySelector('#settings-advanced');
66
+ if (advSection) advSection.style.display = this._advancedOpen ? 'block' : 'none';
67
+ const chevron = container.querySelector('#settings-adv-chevron');
68
+ if (chevron) chevron.classList.toggle('advanced-toggle__chevron--open', this._advancedOpen);
69
+ });
70
+ container.appendChild(advBtn);
71
+
72
+ const advSection = document.createElement('div');
73
+ advSection.id = 'settings-advanced';
74
+ advSection.style.display = 'none';
75
+
76
+ // ── Polling & Livestream ──
77
+ const perfTitle = document.createElement('div');
78
+ perfTitle.className = 'detail-section__title';
79
+ perfTitle.textContent = 'Performance';
80
+ advSection.appendChild(perfTitle);
81
+
82
+ NumberInput.render(advSection, {
83
+ id: 'num-polling',
84
+ label: 'Polling Interval',
85
+ help: 'How often (in minutes) to poll the Eufy Cloud for updates. Higher values = less API usage.',
86
+ value: config.pollingIntervalMinutes || 10,
87
+ min: 1, max: 120, step: 1,
88
+ suffix: 'min',
89
+ onChange: async (val) => {
90
+ await Config.updateGlobal({ pollingIntervalMinutes: val });
91
+ },
92
+ });
93
+
94
+ NumberInput.render(advSection, {
95
+ id: 'num-livestream',
96
+ label: 'Max Livestream Duration',
97
+ help: 'Maximum duration (in seconds) for a single livestream session.',
98
+ value: config.CameraMaxLivestreamDuration || 30,
99
+ min: 10, max: 86400, step: 10,
100
+ suffix: 'sec',
101
+ onChange: async (val) => {
102
+ await Config.updateGlobal({ CameraMaxLivestreamDuration: val });
103
+ },
104
+ });
105
+
106
+ // ── Default Guard Modes ──
107
+ const guardTitle = document.createElement('div');
108
+ guardTitle.className = 'detail-section__title mt-3';
109
+ guardTitle.textContent = 'Default Guard Modes';
110
+ advSection.appendChild(guardTitle);
111
+
112
+ const guardHelp = document.createElement('p');
113
+ guardHelp.className = 'text-muted';
114
+ guardHelp.style.fontSize = '0.8rem';
115
+ guardHelp.textContent = 'Default HomeKit-to-Eufy guard mode mapping. Can be overridden per station.';
116
+ advSection.appendChild(guardHelp);
117
+
118
+ GuardModes.render(advSection, {
119
+ hkHome: config.hkHome ?? 1,
120
+ hkAway: config.hkAway ?? 0,
121
+ hkNight: config.hkNight ?? 1,
122
+ hkOff: config.hkOff ?? 63,
123
+ onChange: async (modes) => {
124
+ await Config.updateGlobal(modes);
125
+ },
126
+ });
127
+
128
+ // ── Misc Toggles ──
129
+ const miscTitle = document.createElement('div');
130
+ miscTitle.className = 'detail-section__title mt-3';
131
+ miscTitle.textContent = 'Miscellaneous';
132
+ advSection.appendChild(miscTitle);
133
+
134
+ Toggle.render(advSection, {
135
+ id: 'toggle-auto-sync',
136
+ label: 'Auto Sync Station',
137
+ help: 'Automatically synchronize station mode with HomeKit security system.',
138
+ checked: !!config.autoSyncStation,
139
+ onChange: async (checked) => {
140
+ await Config.updateGlobal({ autoSyncStation: checked });
141
+ },
142
+ });
143
+
144
+ Toggle.render(advSection, {
145
+ id: 'toggle-clean-cache',
146
+ label: 'Clean Cache',
147
+ help: 'Remove stale cached accessories on next restart.',
148
+ checked: !!config.cleanCache,
149
+ onChange: async (checked) => {
150
+ await Config.updateGlobal({ cleanCache: checked });
151
+ },
152
+ });
153
+
154
+ Toggle.render(advSection, {
155
+ id: 'toggle-omit-logs',
156
+ label: 'Omit Log Files',
157
+ help: 'Disable writing plugin log files to disk.',
158
+ checked: !!config.omitLogFiles,
159
+ onChange: async (checked) => {
160
+ await Config.updateGlobal({ omitLogFiles: checked });
161
+ },
162
+ });
163
+
164
+ Toggle.render(advSection, {
165
+ id: 'toggle-ignore-multi-warning',
166
+ label: 'Ignore Multiple Devices Warning',
167
+ help: 'Suppress warning when multiple plugins manage the same device.',
168
+ checked: !!config.ignoreMultipleDevicesWarning,
169
+ onChange: async (checked) => {
170
+ await Config.updateGlobal({ ignoreMultipleDevicesWarning: checked });
171
+ },
172
+ });
173
+
174
+ Toggle.render(advSection, {
175
+ id: 'toggle-pkcs1',
176
+ label: 'Embedded PKCS1 Support',
177
+ help: 'Enable embedded PKCS1 support for device communication.',
178
+ checked: !!config.enableEmbeddedPKCS1Support,
179
+ onChange: async (checked) => {
180
+ await Config.updateGlobal({ enableEmbeddedPKCS1Support: checked });
181
+ },
182
+ });
183
+
184
+ container.appendChild(advSection);
185
+
186
+
187
+ },
188
+
189
+ _escHtml(str) {
190
+ return Helpers.escHtml(str);
191
+ },
192
+ };
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Unsupported Device Detail View — shows device info dump and guided CTA
3
+ * for requesting support via GitHub.
4
+ */
5
+ // eslint-disable-next-line no-unused-vars
6
+ const UnsupportedDetailView = {
7
+
8
+ _container: null,
9
+
10
+ /**
11
+ * @param {HTMLElement} container
12
+ * @param {string} id - uniqueId / serial number
13
+ */
14
+ async render(container, id) {
15
+ this._container = container;
16
+ container.innerHTML = '';
17
+
18
+ const accessory = this._findAccessory(id);
19
+
20
+ if (!accessory) {
21
+ container.innerHTML = `
22
+ <div class="text-center text-muted py-5">
23
+ <p>Device not found.</p>
24
+ <button class="btn btn-outline-secondary btn-sm" id="btn-back">Back to Dashboard</button>
25
+ </div>`;
26
+ container.querySelector('#btn-back').addEventListener('click', () => App.navigate('dashboard'));
27
+ return;
28
+ }
29
+
30
+ // Header (no image for unsupported)
31
+ this._renderHeader(container, accessory);
32
+
33
+ // Main content
34
+ const content = document.createElement('div');
35
+ await this._renderDetail(content, accessory);
36
+ container.appendChild(content);
37
+ },
38
+
39
+ // ===== Header =====
40
+ _renderHeader(container, accessory) {
41
+ const header = document.createElement('div');
42
+ header.className = 'detail-header';
43
+
44
+ const backBtn = document.createElement('button');
45
+ backBtn.className = 'btn btn-link p-0';
46
+ backBtn.innerHTML = '← Back';
47
+ backBtn.style.textDecoration = 'none';
48
+ backBtn.addEventListener('click', () => App.navigate('dashboard'));
49
+
50
+ const info = document.createElement('div');
51
+ info.className = 'detail-header__info';
52
+ info.innerHTML = `
53
+ <h5>${Helpers.escHtml(accessory.displayName)}</h5>
54
+ <small>${accessory.typename || ('Type ' + accessory.type)} · ${accessory.uniqueId}</small>
55
+ `;
56
+
57
+ header.appendChild(backBtn);
58
+ header.appendChild(info);
59
+ container.appendChild(header);
60
+ },
61
+
62
+ // ===== Detail Content =====
63
+ async _renderDetail(content, accessory) {
64
+ const REPO = 'homebridge-plugins/homebridge-eufy-security';
65
+ const LABEL = 'device-support';
66
+ const COMPAT_URL = 'https://bropat.github.io/eufy-security-client/#/supported_devices';
67
+
68
+ const section = document.createElement('div');
69
+ section.className = 'detail-section unsupported-detail';
70
+
71
+ // Description
72
+ const desc = document.createElement('p');
73
+ desc.className = 'text-muted';
74
+ desc.innerHTML =
75
+ '<strong>' + Helpers.iconHtml('info.svg') + ' This device was detected but is not yet supported.</strong><br />' +
76
+ 'New device support must first be added to the eufy-security-client library. ' +
77
+ 'If your device is not on the compatibility list, please <strong>search for existing issues first</strong> before opening a new one.';
78
+ section.appendChild(desc);
79
+
80
+ // Device info dump — all available data in one box
81
+ const props = accessory.properties || {};
82
+
83
+ // Load raw intel from unsupported.json (served via /unsupportedDevices endpoint)
84
+ let unsupportedIntel = null;
85
+ try {
86
+ const unsupportedData = await Api.loadUnsupportedDevices();
87
+ const entries = unsupportedData.devices || [];
88
+ unsupportedIntel = entries.find(e => e.uniqueId === accessory.uniqueId) || null;
89
+ } catch (e) {
90
+ // ignore — will fall back to basic properties
91
+ }
92
+
93
+ const deviceInfo = {
94
+ uniqueId: accessory.uniqueId,
95
+ displayName: accessory.displayName,
96
+ type: accessory.type,
97
+ typename: accessory.typename || undefined,
98
+ ...props,
99
+ ...(unsupportedIntel ? {
100
+ rawDevice: unsupportedIntel.rawDevice,
101
+ rawProperties: unsupportedIntel.rawProperties,
102
+ } : {}),
103
+ };
104
+ // Remove potentially large/sensitive fields
105
+ delete deviceInfo.picture;
106
+
107
+ // CTA stepper + buttons
108
+ const stepsWrap = document.createElement('div');
109
+ stepsWrap.className = 'unsupported-detail__steps';
110
+
111
+ // Stepper frieze
112
+ const stepper = document.createElement('div');
113
+ stepper.className = 'unsupported-stepper';
114
+ const steps = [
115
+ { num: '1', label: 'Check', color: 'success' },
116
+ { num: '2', label: 'Search', color: 'primary' },
117
+ { num: '3', label: 'Copy', color: 'warning' },
118
+ { num: '4', label: 'Create', color: 'danger' },
119
+ ];
120
+ steps.forEach((step, i) => {
121
+ const stepEl = document.createElement('div');
122
+ stepEl.className = 'unsupported-stepper__step';
123
+ stepEl.innerHTML = `<span class="unsupported-stepper__circle unsupported-stepper__circle--${step.color}">${step.num}</span><span class="unsupported-stepper__label">${step.label}</span>`;
124
+ stepper.appendChild(stepEl);
125
+ if (i < steps.length - 1) {
126
+ const line = document.createElement('div');
127
+ line.className = 'unsupported-stepper__line';
128
+ stepper.appendChild(line);
129
+ }
130
+ });
131
+ stepsWrap.appendChild(stepper);
132
+
133
+ // Buttons row
134
+ const btnGroup = document.createElement('div');
135
+ btnGroup.className = 'unsupported-detail__actions';
136
+
137
+ // 1) Check compatibility list
138
+ const compatBtn = document.createElement('a');
139
+ compatBtn.href = COMPAT_URL;
140
+ compatBtn.target = '_blank';
141
+ compatBtn.rel = 'noopener noreferrer';
142
+ compatBtn.className = 'btn btn-success';
143
+ compatBtn.textContent = 'Check Compatibility ↗';
144
+ btnGroup.appendChild(compatBtn);
145
+
146
+ // 2) Search existing issues with label
147
+ const searchQuery = encodeURIComponent(`is:issue label:${LABEL} ${accessory.type}`);
148
+ const searchBtn = document.createElement('a');
149
+ searchBtn.href = `https://github.com/${REPO}/issues?q=${searchQuery}`;
150
+ searchBtn.target = '_blank';
151
+ searchBtn.rel = 'noopener noreferrer';
152
+ searchBtn.className = 'btn btn-outline-primary';
153
+ searchBtn.textContent = 'Search Existing Issues ↗';
154
+ btnGroup.appendChild(searchBtn);
155
+
156
+ // 3) Copy device JSON to clipboard
157
+ const deviceDump = JSON.stringify(deviceInfo, null, 2);
158
+ const copyJsonBtn = document.createElement('button');
159
+ copyJsonBtn.className = 'btn btn-outline-warning';
160
+ copyJsonBtn.textContent = 'Copy Device Info';
161
+ copyJsonBtn.addEventListener('click', () => {
162
+ this._copyToClipboard(deviceDump).then(() => {
163
+ copyJsonBtn.textContent = '✓ Copied!';
164
+ setTimeout(() => { copyJsonBtn.textContent = 'Copy Device Info'; }, 2000);
165
+ });
166
+ });
167
+ btnGroup.appendChild(copyJsonBtn);
168
+
169
+ // 4) Create new issue (without embedding JSON — user pastes it)
170
+ const model = props.model || accessory.type;
171
+ const issueTitle = encodeURIComponent(`[Device Support] ${model} (Type ${accessory.type})`);
172
+ const templateParams = [
173
+ `template=device_support.yml`,
174
+ `title=${issueTitle}`,
175
+ `labels=${LABEL}`,
176
+ ].join('&');
177
+ const createBtn = document.createElement('a');
178
+ createBtn.href = `https://github.com/${REPO}/issues/new?${templateParams}`;
179
+ createBtn.target = '_blank';
180
+ createBtn.rel = 'noopener noreferrer';
181
+ createBtn.className = 'btn btn-outline-danger';
182
+ createBtn.textContent = 'Create an Issue ↗';
183
+ btnGroup.appendChild(createBtn);
184
+
185
+ stepsWrap.appendChild(btnGroup);
186
+
187
+ // Paste reminder note
188
+ const pasteNote = document.createElement('p');
189
+ pasteNote.className = 'unsupported-detail__paste-note';
190
+ pasteNote.innerHTML = Helpers.iconHtml('info.svg', 14) +
191
+ ' Copy the <strong>Device Information</strong> below first, then paste it into the issue form on GitHub.';
192
+ stepsWrap.appendChild(pasteNote);
193
+
194
+ section.appendChild(stepsWrap);
195
+
196
+ // External links note
197
+ const extNote = document.createElement('p');
198
+ extNote.className = 'text-muted';
199
+ extNote.style.cssText = 'font-size: 0.75rem; text-align: right;';
200
+ extNote.textContent = '↗ These links open in a new browser tab on GitHub.';
201
+ section.appendChild(extNote);
202
+
203
+ const infoTitle = document.createElement('div');
204
+ infoTitle.className = 'detail-section__title';
205
+ infoTitle.textContent = 'Device Information';
206
+ section.appendChild(infoTitle);
207
+
208
+ const infoNote = document.createElement('p');
209
+ infoNote.className = 'text-muted';
210
+ infoNote.style.cssText = 'font-size: 0.75rem;';
211
+ infoNote.textContent = 'Diagnostic information below includes raw device properties from the client library.';
212
+ section.appendChild(infoNote);
213
+
214
+ // JSON dump with copy button overlay
215
+ const dumpWrap = document.createElement('div');
216
+ dumpWrap.className = 'unsupported-detail__dump-wrap';
217
+
218
+ const copyBtn = document.createElement('button');
219
+ copyBtn.className = 'unsupported-detail__copy-btn';
220
+
221
+ const _setCopyLabel = (text) => {
222
+ copyBtn.innerHTML = '';
223
+ copyBtn.appendChild(Helpers.icon('copy.svg'));
224
+ copyBtn.append(' ' + text);
225
+ };
226
+ _setCopyLabel('Copy');
227
+
228
+ copyBtn.addEventListener('click', () => {
229
+ this._copyToClipboard(pre.textContent).then(() => {
230
+ copyBtn.textContent = '✓ Copied!';
231
+ setTimeout(() => { _setCopyLabel('Copy'); }, 2000);
232
+ });
233
+ });
234
+
235
+ const pre = document.createElement('pre');
236
+ pre.className = 'unsupported-detail__dump';
237
+ pre.textContent = JSON.stringify(deviceInfo, null, 2);
238
+
239
+ dumpWrap.appendChild(copyBtn);
240
+ dumpWrap.appendChild(pre);
241
+ section.appendChild(dumpWrap);
242
+
243
+ content.appendChild(section);
244
+ },
245
+
246
+ // ===== Helpers =====
247
+
248
+ /**
249
+ * Copy text to clipboard with fallback for non-secure contexts (e.g. Homebridge iframe).
250
+ * @param {string} text
251
+ * @returns {Promise<void>}
252
+ */
253
+ _copyToClipboard(text) {
254
+ if (navigator.clipboard && navigator.clipboard.writeText) {
255
+ return navigator.clipboard.writeText(text).catch(() => this._copyFallback(text));
256
+ }
257
+ return this._copyFallback(text);
258
+ },
259
+
260
+ _copyFallback(text) {
261
+ return new Promise((resolve, reject) => {
262
+ const textarea = document.createElement('textarea');
263
+ textarea.value = text;
264
+ textarea.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0';
265
+ document.body.appendChild(textarea);
266
+ textarea.select();
267
+ try {
268
+ document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));
269
+ } catch (err) {
270
+ reject(err);
271
+ } finally {
272
+ document.body.removeChild(textarea);
273
+ }
274
+ });
275
+ },
276
+
277
+ /**
278
+ * Find an accessory (device or station) by uniqueId across all stations.
279
+ * For standalone devices the station and device share the same uniqueId —
280
+ * prefer the device because it carries richer data.
281
+ * @param {string} id
282
+ * @returns {object|null}
283
+ */
284
+ _findAccessory(id) {
285
+ const stations = App.state.stations || [];
286
+ for (const s of stations) {
287
+ // Check devices first — standalone devices have the same uniqueId as their station
288
+ // but carry richer data
289
+ for (const d of s.devices || []) {
290
+ if (d.uniqueId === id) return d;
291
+ }
292
+ if (s.uniqueId === id) return s;
293
+ }
294
+ return null;
295
+ },
296
+ };