@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.
- package/CHANGELOG.md +5 -0
- package/FUNDING.yml +1 -0
- package/LICENSE +176 -0
- package/README.md +67 -0
- package/config.schema.json +6 -0
- package/dist/accessories/AutoSyncStationAccessory.js +156 -0
- package/dist/accessories/AutoSyncStationAccessory.js.map +1 -0
- package/dist/accessories/BaseAccessory.js +247 -0
- package/dist/accessories/BaseAccessory.js.map +1 -0
- package/dist/accessories/CameraAccessory.js +431 -0
- package/dist/accessories/CameraAccessory.js.map +1 -0
- package/dist/accessories/Device.js +67 -0
- package/dist/accessories/Device.js.map +1 -0
- package/dist/accessories/EntrySensorAccessory.js +48 -0
- package/dist/accessories/EntrySensorAccessory.js.map +1 -0
- package/dist/accessories/LockAccessory.js +142 -0
- package/dist/accessories/LockAccessory.js.map +1 -0
- package/dist/accessories/MotionSensorAccessory.js +48 -0
- package/dist/accessories/MotionSensorAccessory.js.map +1 -0
- package/dist/accessories/SmartDropAccessory.js +145 -0
- package/dist/accessories/SmartDropAccessory.js.map +1 -0
- package/dist/accessories/StationAccessory.js +371 -0
- package/dist/accessories/StationAccessory.js.map +1 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -0
- package/dist/controller/LocalLivestreamManager.js +116 -0
- package/dist/controller/LocalLivestreamManager.js.map +1 -0
- package/dist/controller/recordingDelegate.js +208 -0
- package/dist/controller/recordingDelegate.js.map +1 -0
- package/dist/controller/snapshotDelegate.js +345 -0
- package/dist/controller/snapshotDelegate.js.map +1 -0
- package/dist/controller/streamingDelegate.js +345 -0
- package/dist/controller/streamingDelegate.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.js +2 -0
- package/dist/interfaces.js.map +1 -0
- 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/platform.js +716 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.js +38 -0
- package/dist/settings.js.map +1 -0
- package/dist/utils/Talkback.js +92 -0
- package/dist/utils/Talkback.js.map +1 -0
- package/dist/utils/accessoriesStore.js +206 -0
- package/dist/utils/accessoriesStore.js.map +1 -0
- package/dist/utils/configTypes.js +35 -0
- package/dist/utils/configTypes.js.map +1 -0
- package/dist/utils/ffmpeg.js +843 -0
- package/dist/utils/ffmpeg.js.map +1 -0
- package/dist/utils/interfaces.js +8 -0
- package/dist/utils/interfaces.js.map +1 -0
- package/dist/utils/utils.js +44 -0
- package/dist/utils/utils.js.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/eslint.config.mjs +18 -0
- package/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/public/app.js +225 -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/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/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/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.jpg +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.jpg +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/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.jpg +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/sensor_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_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/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/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/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.jpg +0 -0
- package/homebridge-ui/public/assets/devices/walllight_s120_large.jpg +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 +1 -0
- package/homebridge-ui/public/assets/icons/battery_0.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_1.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_2.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_3.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_4.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_5.svg +1 -0
- package/homebridge-ui/public/assets/icons/battery_6.svg +1 -0
- package/homebridge-ui/public/assets/icons/bolt.svg +1 -0
- package/homebridge-ui/public/assets/icons/bug-report.svg +1 -0
- package/homebridge-ui/public/assets/icons/copy.svg +1 -0
- package/homebridge-ui/public/assets/icons/delete.svg +1 -0
- package/homebridge-ui/public/assets/icons/download.svg +1 -0
- package/homebridge-ui/public/assets/icons/info.svg +1 -0
- package/homebridge-ui/public/assets/icons/inventory.svg +1 -0
- package/homebridge-ui/public/assets/icons/refresh.svg +1 -0
- package/homebridge-ui/public/assets/icons/satellite_alt.svg +1 -0
- package/homebridge-ui/public/assets/icons/settings.svg +1 -0
- package/homebridge-ui/public/assets/icons/settings_backup_restore.svg +1 -0
- package/homebridge-ui/public/assets/icons/solar_power.svg +1 -0
- package/homebridge-ui/public/assets/icons/warning.svg +1 -0
- package/homebridge-ui/public/components/device-card.js +162 -0
- package/homebridge-ui/public/components/guard-modes.js +88 -0
- package/homebridge-ui/public/components/number-input.js +121 -0
- package/homebridge-ui/public/components/select.js +73 -0
- package/homebridge-ui/public/components/toggle.js +68 -0
- package/homebridge-ui/public/index.html +27 -0
- package/homebridge-ui/public/services/api.js +214 -0
- package/homebridge-ui/public/services/config.js +144 -0
- package/homebridge-ui/public/style.css +775 -0
- package/homebridge-ui/public/utils/countries.js +73 -0
- package/homebridge-ui/public/utils/device-images.js +89 -0
- package/homebridge-ui/public/utils/helpers.js +87 -0
- package/homebridge-ui/public/views/dashboard.js +226 -0
- package/homebridge-ui/public/views/device-detail.js +610 -0
- package/homebridge-ui/public/views/diagnostics.js +296 -0
- package/homebridge-ui/public/views/login.js +636 -0
- package/homebridge-ui/public/views/settings.js +192 -0
- package/homebridge-ui/public/views/unsupported-detail.js +296 -0
- package/homebridge-ui/server.js +1327 -0
- 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/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
|
+
};
|