homebridge-eufy-security 4.4.2-beta.2 → 4.4.2-beta.20
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 +1 -1
- package/FUNDING.yml +1 -0
- package/README.md +14 -17
- package/dist/accessories/AutoSyncStationAccessory.js +26 -30
- package/dist/accessories/AutoSyncStationAccessory.js.map +1 -1
- package/dist/accessories/BaseAccessory.js +33 -46
- package/dist/accessories/BaseAccessory.js.map +1 -1
- package/dist/accessories/CameraAccessory.js +92 -167
- package/dist/accessories/CameraAccessory.js.map +1 -1
- package/dist/accessories/Device.js +11 -15
- package/dist/accessories/Device.js.map +1 -1
- package/dist/accessories/EntrySensorAccessory.js +7 -12
- package/dist/accessories/EntrySensorAccessory.js.map +1 -1
- package/dist/accessories/LockAccessory.js +20 -24
- package/dist/accessories/LockAccessory.js.map +1 -1
- package/dist/accessories/MotionSensorAccessory.js +7 -12
- package/dist/accessories/MotionSensorAccessory.js.map +1 -1
- package/dist/accessories/StationAccessory.js +40 -45
- package/dist/accessories/StationAccessory.js.map +1 -1
- package/dist/config.js +1 -4
- package/dist/config.js.map +1 -1
- package/dist/controller/LocalLivestreamManager.js +85 -89
- package/dist/controller/LocalLivestreamManager.js.map +1 -1
- package/dist/controller/recordingDelegate.js +116 -108
- package/dist/controller/recordingDelegate.js.map +1 -1
- package/dist/controller/snapshotDelegate.js +379 -0
- package/dist/controller/snapshotDelegate.js.map +1 -0
- package/dist/controller/streamingDelegate.js +202 -207
- package/dist/controller/streamingDelegate.js.map +1 -1
- package/dist/index.js +9 -7
- package/dist/index.js.map +1 -1
- package/dist/interfaces.js +1 -2
- package/dist/interfaces.js.map +1 -1
- package/dist/platform.js +145 -165
- package/dist/platform.js.map +1 -1
- package/dist/settings.js +26 -19
- package/dist/settings.js.map +1 -1
- package/dist/utils/Talkback.js +9 -13
- package/dist/utils/Talkback.js.map +1 -1
- package/dist/utils/configTypes.js +12 -7
- package/dist/utils/configTypes.js.map +1 -1
- package/dist/utils/ffmpeg.js +44 -52
- package/dist/utils/ffmpeg.js.map +1 -1
- package/dist/utils/interfaces.js +1 -5
- package/dist/utils/interfaces.js.map +1 -1
- package/dist/utils/utils.js +32 -134
- package/dist/utils/utils.js.map +1 -1
- package/dist/version.js +1 -4
- package/dist/version.js.map +1 -1
- package/eslint.config.mjs +25 -47
- package/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/public/app.js +221 -0
- package/homebridge-ui/public/assets/devices/eufycam3pro_large.png +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.png +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/solocamc35_large.png +0 -0
- package/homebridge-ui/public/assets/devices/solocame30_large.png +0 -0
- package/homebridge-ui/public/components/device-card.js +141 -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 +24 -14
- package/homebridge-ui/public/services/api.js +129 -0
- package/homebridge-ui/public/services/config.js +144 -0
- package/homebridge-ui/public/style.css +705 -0
- package/homebridge-ui/public/utils/countries.js +73 -0
- package/homebridge-ui/public/utils/device-images.js +69 -0
- package/homebridge-ui/public/utils/helpers.js +47 -0
- package/homebridge-ui/public/views/dashboard.js +211 -0
- package/homebridge-ui/public/views/device-detail.js +631 -0
- package/homebridge-ui/public/views/login.js +625 -0
- package/homebridge-ui/public/views/settings.js +389 -0
- package/homebridge-ui/public/views/unsupported-detail.js +216 -0
- package/homebridge-ui/server.js +567 -450
- package/package.json +19 -59
- package/dist/accessories/AutoSyncStationAccessory.d.ts +0 -42
- package/dist/accessories/AutoSyncStationAccessory.d.ts.map +0 -1
- package/dist/accessories/BaseAccessory.d.ts +0 -56
- package/dist/accessories/BaseAccessory.d.ts.map +0 -1
- package/dist/accessories/CameraAccessory.d.ts +0 -84
- package/dist/accessories/CameraAccessory.d.ts.map +0 -1
- package/dist/accessories/Device.d.ts +0 -19
- package/dist/accessories/Device.d.ts.map +0 -1
- package/dist/accessories/EntrySensorAccessory.d.ts +0 -25
- package/dist/accessories/EntrySensorAccessory.d.ts.map +0 -1
- package/dist/accessories/LockAccessory.d.ts +0 -45
- package/dist/accessories/LockAccessory.d.ts.map +0 -1
- package/dist/accessories/MotionSensorAccessory.d.ts +0 -25
- package/dist/accessories/MotionSensorAccessory.d.ts.map +0 -1
- package/dist/accessories/StationAccessory.d.ts +0 -86
- package/dist/accessories/StationAccessory.d.ts.map +0 -1
- package/dist/config.d.ts +0 -31
- package/dist/config.d.ts.map +0 -1
- package/dist/controller/LocalLivestreamManager.d.ts +0 -31
- package/dist/controller/LocalLivestreamManager.d.ts.map +0 -1
- package/dist/controller/SnapshotManager.d.ts +0 -64
- package/dist/controller/SnapshotManager.d.ts.map +0 -1
- package/dist/controller/SnapshotManager.js +0 -434
- package/dist/controller/SnapshotManager.js.map +0 -1
- package/dist/controller/recordingDelegate.d.ts +0 -27
- package/dist/controller/recordingDelegate.d.ts.map +0 -1
- package/dist/controller/streamingDelegate.d.ts +0 -52
- package/dist/controller/streamingDelegate.d.ts.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/interfaces.d.ts +0 -15
- package/dist/interfaces.d.ts.map +0 -1
- package/dist/platform.d.ts +0 -94
- package/dist/platform.d.ts.map +0 -1
- package/dist/settings.d.ts +0 -23
- package/dist/settings.d.ts.map +0 -1
- package/dist/utils/Talkback.d.ts +0 -21
- package/dist/utils/Talkback.d.ts.map +0 -1
- package/dist/utils/configTypes.d.ts +0 -68
- package/dist/utils/configTypes.d.ts.map +0 -1
- package/dist/utils/ffmpeg.d.ts +0 -111
- package/dist/utils/ffmpeg.d.ts.map +0 -1
- package/dist/utils/interfaces.d.ts +0 -8
- package/dist/utils/interfaces.d.ts.map +0 -1
- package/dist/utils/utils.d.ts +0 -35
- package/dist/utils/utils.d.ts.map +0 -1
- package/dist/version.d.ts +0 -2
- package/dist/version.d.ts.map +0 -1
- package/homebridge-ui/configui/app/util/types.d.ts +0 -65
- package/homebridge-ui/configui/app/util/types.d.ts.map +0 -1
- package/homebridge-ui/configui/app/util/types.js +0 -18
- package/homebridge-ui/configui/app/util/types.js.map +0 -1
- package/homebridge-ui/public/3rdpartylicenses.txt +0 -561
- package/homebridge-ui/public/assets/devices/homebase2_large.jpg +0 -0
- package/homebridge-ui/public/assets/devices/homebase3_large.jpg +0 -0
- package/homebridge-ui/public/assets/images/homebridge-eufy-security.png +0 -0
- package/homebridge-ui/public/favicon.ico +0 -0
- package/homebridge-ui/public/main.f6423287e0411c87.js +0 -1
- package/homebridge-ui/public/polyfills.efa2f09ba3e49167.js +0 -1
- package/homebridge-ui/public/runtime.ccde331d62c423b3.js +0 -1
- package/homebridge-ui/public/styles.13e635bf7a488639.css +0 -5
- package/homebridge-ui/server.d.ts +0 -2
- package/homebridge-ui/server.d.ts.map +0 -1
- package/homebridge-ui/server.js.map +0 -1
- package/homebridge-ui/version.d.ts +0 -2
- package/homebridge-ui/version.d.ts.map +0 -1
- package/homebridge-ui/version.js +0 -5
- package/homebridge-ui/version.js.map +0 -1
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App — main controller, hash-based SPA router, and global state.
|
|
3
|
+
* Entry point initialized on homebridge 'ready' event.
|
|
4
|
+
*/
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
const App = {
|
|
7
|
+
/** Global state shared across views */
|
|
8
|
+
state: {
|
|
9
|
+
stations: [],
|
|
10
|
+
cacheDate: null, // ISO string of when accessories were last fetched
|
|
11
|
+
initialized: false,
|
|
12
|
+
nodeVersionWarning: null, // { nodeVersion, affected, acknowledged }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
/** Root container element */
|
|
16
|
+
_root: null,
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initialize the app. Called once on homebridge 'ready' event.
|
|
20
|
+
*/
|
|
21
|
+
async init() {
|
|
22
|
+
this._root = document.getElementById('app');
|
|
23
|
+
|
|
24
|
+
// Let Homebridge's native Save button handle saving to disk.
|
|
25
|
+
// We call updatePluginConfig() on every change to keep config in sync.
|
|
26
|
+
|
|
27
|
+
// Listen for admin account error globally
|
|
28
|
+
Api.onAdminAccountUsed(() => {
|
|
29
|
+
this._showAdminError();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Listen for cache warnings (stale data, version mismatch)
|
|
33
|
+
Api.onCacheWarning((data) => {
|
|
34
|
+
switch (data.reason) {
|
|
35
|
+
case 'stale':
|
|
36
|
+
homebridge.toast.warning(
|
|
37
|
+
`Your cached devices are ${data.ageDays} days old. Please re-login to refresh.`,
|
|
38
|
+
'Stale Cache',
|
|
39
|
+
);
|
|
40
|
+
break;
|
|
41
|
+
case 'versionWarn':
|
|
42
|
+
homebridge.toast.warning(
|
|
43
|
+
`Plugin updated (${data.storedVersion} → ${data.currentVersion}). Consider re-logging in to refresh devices.`,
|
|
44
|
+
'Version Updated',
|
|
45
|
+
);
|
|
46
|
+
break;
|
|
47
|
+
case 'versionForce':
|
|
48
|
+
homebridge.toast.error(
|
|
49
|
+
`Plugin version changed significantly (${data.storedVersion} → ${data.currentVersion}). Please re-login.`,
|
|
50
|
+
'Re-login Required',
|
|
51
|
+
);
|
|
52
|
+
this.navigate('login');
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Listen for hash changes
|
|
58
|
+
window.addEventListener('hashchange', () => this._onRoute());
|
|
59
|
+
|
|
60
|
+
// Determine initial route
|
|
61
|
+
try {
|
|
62
|
+
// Load config without pushing it back to Homebridge
|
|
63
|
+
const config = await Config.load();
|
|
64
|
+
if (!config.platform) {
|
|
65
|
+
// Initialize default config only if truly empty
|
|
66
|
+
await Config.update({ platform: 'EufySecurity', name: 'EufySecurity' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stored = await Api.loadStoredAccessories();
|
|
70
|
+
const stations = stored.stations || stored;
|
|
71
|
+
const cacheDate = stored.storedAt || null;
|
|
72
|
+
if (stations && stations.length > 0) {
|
|
73
|
+
this.state.stations = stations;
|
|
74
|
+
this.state.cacheDate = cacheDate;
|
|
75
|
+
// Check Node.js version for returning users (skipping login)
|
|
76
|
+
await this.checkNodeVersion();
|
|
77
|
+
if (!window.location.hash || window.location.hash === '#/') {
|
|
78
|
+
this.navigate('dashboard');
|
|
79
|
+
} else {
|
|
80
|
+
this._onRoute();
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
this.navigate('login');
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// No stored accessories — go to login
|
|
87
|
+
this.navigate('login');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.state.initialized = true;
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Navigate to a route.
|
|
95
|
+
* @param {string} route - e.g. 'login', 'dashboard', 'detail/device/SERIAL', 'settings'
|
|
96
|
+
*/
|
|
97
|
+
navigate(route) {
|
|
98
|
+
window.location.hash = '#/' + route;
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle route changes.
|
|
103
|
+
*/
|
|
104
|
+
_onRoute() {
|
|
105
|
+
const hash = window.location.hash.replace('#/', '') || '';
|
|
106
|
+
const parts = hash.split('/');
|
|
107
|
+
const route = parts[0];
|
|
108
|
+
|
|
109
|
+
// Clear the root container
|
|
110
|
+
this._root.innerHTML = '';
|
|
111
|
+
|
|
112
|
+
switch (route) {
|
|
113
|
+
case 'login':
|
|
114
|
+
LoginView.render(this._root);
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'dashboard':
|
|
118
|
+
DashboardView.render(this._root);
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case 'detail': {
|
|
122
|
+
const type = parts[1]; // 'device' or 'station'
|
|
123
|
+
const id = parts[2]; // uniqueId
|
|
124
|
+
if (type && id) {
|
|
125
|
+
DeviceDetailView.render(this._root, type, id);
|
|
126
|
+
} else {
|
|
127
|
+
this.navigate('dashboard');
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
case 'unsupported': {
|
|
133
|
+
const uid = parts[1];
|
|
134
|
+
if (uid) {
|
|
135
|
+
UnsupportedDetailView.render(this._root, uid);
|
|
136
|
+
} else {
|
|
137
|
+
this.navigate('dashboard');
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
case 'settings':
|
|
143
|
+
SettingsView.render(this._root);
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
default:
|
|
147
|
+
// Unknown route — try dashboard if we have stations, otherwise login
|
|
148
|
+
if (this.state.stations.length > 0) {
|
|
149
|
+
this.navigate('dashboard');
|
|
150
|
+
} else {
|
|
151
|
+
this.navigate('login');
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Show a blocking error when admin account is detected.
|
|
159
|
+
*/
|
|
160
|
+
_showAdminError() {
|
|
161
|
+
this._root.innerHTML = `
|
|
162
|
+
<div class="alert alert-danger alert-admin mt-4" role="alert">
|
|
163
|
+
<h5 class="alert-heading">⚠️ Admin Account Detected</h5>
|
|
164
|
+
<p>
|
|
165
|
+
You are not using a <strong>dedicated guest admin account</strong>.
|
|
166
|
+
Using your primary Eufy account can cause conflicts with the Eufy app.
|
|
167
|
+
</p>
|
|
168
|
+
<hr>
|
|
169
|
+
<p class="mb-0">
|
|
170
|
+
Please create a dedicated admin account for this plugin.
|
|
171
|
+
<a href="https://github.com/homebridge-plugins/homebridge-eufy-security/wiki/Create-a-dedicated-admin-account-for-Homebridge-Eufy-Security-Plugin"
|
|
172
|
+
target="_blank" class="alert-link">
|
|
173
|
+
Follow this guide
|
|
174
|
+
</a>
|
|
175
|
+
</p>
|
|
176
|
+
</div>
|
|
177
|
+
`;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if a Node.js version string is affected by the RSA_PKCS1_PADDING removal.
|
|
182
|
+
* Affected: 18.19.1+, 20.11.1+, 21.6.2+, 22.x, 23.x, and 24.x < 24.5.0.
|
|
183
|
+
* Safe: <= 18.19.0, <= 20.11.0, <= 21.6.1, and >= 24.5.0.
|
|
184
|
+
* @param {string} versionStr - e.g. 'v20.18.1'
|
|
185
|
+
* @returns {boolean}
|
|
186
|
+
*/
|
|
187
|
+
isNodeVersionAffected(versionStr) {
|
|
188
|
+
const parts = versionStr.replace('v', '').split('.').map(Number);
|
|
189
|
+
const [major, minor, patch] = parts;
|
|
190
|
+
|
|
191
|
+
if (major === 18 && (minor > 19 || (minor === 19 && patch >= 1))) return true;
|
|
192
|
+
if (major === 20 && (minor > 11 || (minor === 11 && patch >= 1))) return true;
|
|
193
|
+
if (major === 21 && (minor > 6 || (minor === 6 && patch >= 2))) return true;
|
|
194
|
+
if (major === 22 || major === 23) return true;
|
|
195
|
+
if (major === 24 && (minor < 5)) return true;
|
|
196
|
+
|
|
197
|
+
return false;
|
|
198
|
+
},
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Fetch the Node.js version from the server and update state.
|
|
202
|
+
*/
|
|
203
|
+
async checkNodeVersion() {
|
|
204
|
+
try {
|
|
205
|
+
const info = await Api.getSystemInfo();
|
|
206
|
+
const affected = this.isNodeVersionAffected(info.nodeVersion);
|
|
207
|
+
this.state.nodeVersionWarning = {
|
|
208
|
+
nodeVersion: info.nodeVersion,
|
|
209
|
+
affected,
|
|
210
|
+
acknowledged: false,
|
|
211
|
+
};
|
|
212
|
+
} catch (e) {
|
|
213
|
+
// Silently ignore — non-critical check
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// ===== Bootstrap the app when Homebridge UI is ready =====
|
|
219
|
+
homebridge.addEventListener('ready', () => {
|
|
220
|
+
App.init();
|
|
221
|
+
});
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeviceCard component — renders a single device/station card for the dashboard grid.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* DeviceCard.render(container, {
|
|
6
|
+
* device: { uniqueId, displayName, type, typename, ... },
|
|
7
|
+
* isStation: false,
|
|
8
|
+
* enabled: true,
|
|
9
|
+
* onClick: (device) => { ... },
|
|
10
|
+
* onToggle: (device, enabled) => { ... }
|
|
11
|
+
* });
|
|
12
|
+
*/
|
|
13
|
+
// eslint-disable-next-line no-unused-vars
|
|
14
|
+
const DeviceCard = {
|
|
15
|
+
|
|
16
|
+
CHARGING_LABELS: {
|
|
17
|
+
1: 'Charging',
|
|
18
|
+
2: 'Unplugged',
|
|
19
|
+
3: 'Plugged In',
|
|
20
|
+
4: 'Solar',
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {HTMLElement} container
|
|
25
|
+
* @param {object} opts
|
|
26
|
+
* @param {object} opts.device - L_Device or L_Station data
|
|
27
|
+
* @param {boolean} [opts.isStation]
|
|
28
|
+
* @param {boolean} opts.enabled - whether device is enabled (not ignored)
|
|
29
|
+
* @param {function} opts.onClick - callback(device)
|
|
30
|
+
* @param {function} opts.onToggle - callback(device, enabled)
|
|
31
|
+
* @returns {HTMLElement}
|
|
32
|
+
*/
|
|
33
|
+
render(container, opts) {
|
|
34
|
+
const d = opts.device;
|
|
35
|
+
const isUnsupported = d.unsupported === true;
|
|
36
|
+
const isIgnored = d.ignored === true;
|
|
37
|
+
|
|
38
|
+
const col = document.createElement('div');
|
|
39
|
+
col.className = 'col-6 col-md-4 col-lg-3 mb-3';
|
|
40
|
+
|
|
41
|
+
const card = document.createElement('div');
|
|
42
|
+
card.className = 'device-card';
|
|
43
|
+
if (isUnsupported) card.classList.add('device-card--unsupported');
|
|
44
|
+
if (isIgnored) card.classList.add('device-card--ignored');
|
|
45
|
+
|
|
46
|
+
// Image (skip for unsupported)
|
|
47
|
+
if (!isUnsupported) {
|
|
48
|
+
const imgWrap = document.createElement('div');
|
|
49
|
+
imgWrap.className = 'device-card__image-wrap';
|
|
50
|
+
const img = document.createElement('img');
|
|
51
|
+
img.src = DeviceImages.getPath(d.type);
|
|
52
|
+
img.alt = d.displayName;
|
|
53
|
+
img.loading = 'lazy';
|
|
54
|
+
imgWrap.appendChild(img);
|
|
55
|
+
card.appendChild(imgWrap);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Body
|
|
59
|
+
const body = document.createElement('div');
|
|
60
|
+
body.className = 'device-card__body';
|
|
61
|
+
|
|
62
|
+
const name = document.createElement('div');
|
|
63
|
+
name.className = 'device-card__name';
|
|
64
|
+
name.textContent = d.displayName;
|
|
65
|
+
name.title = d.displayName;
|
|
66
|
+
|
|
67
|
+
const meta = document.createElement('div');
|
|
68
|
+
meta.className = 'device-card__meta';
|
|
69
|
+
|
|
70
|
+
const metaParts = [];
|
|
71
|
+
if (isUnsupported) {
|
|
72
|
+
metaParts.push('Type ' + d.type);
|
|
73
|
+
} else if (d.typename) {
|
|
74
|
+
metaParts.push(d.typename);
|
|
75
|
+
}
|
|
76
|
+
if (d.hasBattery && d.properties && d.properties.battery !== undefined) {
|
|
77
|
+
metaParts.push('🔋 ' + d.properties.battery + '%');
|
|
78
|
+
}
|
|
79
|
+
if (d.chargingStatus && this.CHARGING_LABELS[d.chargingStatus]) {
|
|
80
|
+
metaParts.push('⚡ ' + this.CHARGING_LABELS[d.chargingStatus]);
|
|
81
|
+
}
|
|
82
|
+
meta.textContent = metaParts.join(' · ');
|
|
83
|
+
|
|
84
|
+
body.appendChild(name);
|
|
85
|
+
body.appendChild(meta);
|
|
86
|
+
|
|
87
|
+
// Footer with badges + toggle
|
|
88
|
+
const footer = document.createElement('div');
|
|
89
|
+
footer.className = 'device-card__footer';
|
|
90
|
+
|
|
91
|
+
const badgeArea = document.createElement('div');
|
|
92
|
+
|
|
93
|
+
if (isUnsupported) {
|
|
94
|
+
const badge = document.createElement('span');
|
|
95
|
+
badge.className = 'badge badge-unsupported';
|
|
96
|
+
badge.textContent = 'Not Supported';
|
|
97
|
+
badgeArea.appendChild(badge);
|
|
98
|
+
} else if (isIgnored) {
|
|
99
|
+
const badge = document.createElement('span');
|
|
100
|
+
badge.className = 'badge bg-secondary';
|
|
101
|
+
badge.textContent = 'Disabled';
|
|
102
|
+
badgeArea.appendChild(badge);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
footer.appendChild(badgeArea);
|
|
106
|
+
|
|
107
|
+
// Toggle — only for non-unsupported devices
|
|
108
|
+
if (!isUnsupported) {
|
|
109
|
+
const switchWrap = document.createElement('div');
|
|
110
|
+
switchWrap.className = 'form-check form-switch mb-0';
|
|
111
|
+
switchWrap.addEventListener('click', (e) => e.stopPropagation());
|
|
112
|
+
|
|
113
|
+
const toggle = document.createElement('input');
|
|
114
|
+
toggle.type = 'checkbox';
|
|
115
|
+
toggle.className = 'form-check-input';
|
|
116
|
+
toggle.checked = opts.enabled;
|
|
117
|
+
toggle.role = 'switch';
|
|
118
|
+
toggle.title = opts.enabled ? 'Enabled in HomeKit' : 'Disabled in HomeKit';
|
|
119
|
+
|
|
120
|
+
toggle.addEventListener('change', (e) => {
|
|
121
|
+
e.stopPropagation();
|
|
122
|
+
if (opts.onToggle) opts.onToggle(d, toggle.checked);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
switchWrap.appendChild(toggle);
|
|
126
|
+
footer.appendChild(switchWrap);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
card.appendChild(body);
|
|
130
|
+
card.appendChild(footer);
|
|
131
|
+
|
|
132
|
+
// Click handler — navigate to detail
|
|
133
|
+
card.addEventListener('click', () => {
|
|
134
|
+
if (opts.onClick) opts.onClick(d);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
col.appendChild(card);
|
|
138
|
+
if (container) container.appendChild(col);
|
|
139
|
+
return col;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guard Modes mapping component.
|
|
3
|
+
* Maps HomeKit modes (Home, Away, Night, Off) to Eufy guard mode numbers.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* GuardModes.render(container, {
|
|
7
|
+
* hkHome: 1, hkAway: 0, hkNight: 1, hkOff: 63,
|
|
8
|
+
* onChange: (modes) => { ... } // { hkHome, hkAway, hkNight, hkOff }
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
// eslint-disable-next-line no-unused-vars
|
|
12
|
+
const GuardModes = {
|
|
13
|
+
EUFY_MODES: [
|
|
14
|
+
{ value: '0', label: 'Away' },
|
|
15
|
+
{ value: '1', label: 'Home' },
|
|
16
|
+
{ value: '2', label: 'Schedule' },
|
|
17
|
+
{ value: '3', label: 'Custom 1' },
|
|
18
|
+
{ value: '4', label: 'Custom 2' },
|
|
19
|
+
{ value: '5', label: 'Custom 3' },
|
|
20
|
+
{ value: '6', label: 'Off' },
|
|
21
|
+
{ value: '47', label: 'Geofencing' },
|
|
22
|
+
{ value: '63', label: 'Disarmed' },
|
|
23
|
+
],
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {HTMLElement} container
|
|
27
|
+
* @param {object} opts
|
|
28
|
+
* @param {number} [opts.hkHome]
|
|
29
|
+
* @param {number} [opts.hkAway]
|
|
30
|
+
* @param {number} [opts.hkNight]
|
|
31
|
+
* @param {number} [opts.hkOff]
|
|
32
|
+
* @param {boolean} [opts.disabled]
|
|
33
|
+
* @param {function} opts.onChange - callback({ hkHome, hkAway, hkNight, hkOff })
|
|
34
|
+
* @returns {HTMLElement}
|
|
35
|
+
*/
|
|
36
|
+
render(container, opts) {
|
|
37
|
+
const wrap = document.createElement('div');
|
|
38
|
+
wrap.className = 'guard-mode-grid';
|
|
39
|
+
|
|
40
|
+
const modes = {
|
|
41
|
+
hkHome: { label: 'HomeKit Home', value: opts.hkHome ?? 1 },
|
|
42
|
+
hkAway: { label: 'HomeKit Away', value: opts.hkAway ?? 0 },
|
|
43
|
+
hkNight: { label: 'HomeKit Night', value: opts.hkNight ?? 1 },
|
|
44
|
+
hkOff: { label: 'HomeKit Off', value: opts.hkOff ?? 63 },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const currentValues = {
|
|
48
|
+
hkHome: modes.hkHome.value,
|
|
49
|
+
hkAway: modes.hkAway.value,
|
|
50
|
+
hkNight: modes.hkNight.value,
|
|
51
|
+
hkOff: modes.hkOff.value,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
Object.entries(modes).forEach(([key, mode]) => {
|
|
55
|
+
const group = document.createElement('div');
|
|
56
|
+
group.className = 'form-group';
|
|
57
|
+
|
|
58
|
+
const label = document.createElement('label');
|
|
59
|
+
label.textContent = mode.label;
|
|
60
|
+
label.setAttribute('for', 'guard-' + key);
|
|
61
|
+
|
|
62
|
+
const select = document.createElement('select');
|
|
63
|
+
select.className = 'form-select form-select-sm';
|
|
64
|
+
select.id = 'guard-' + key;
|
|
65
|
+
select.disabled = !!opts.disabled;
|
|
66
|
+
|
|
67
|
+
this.EUFY_MODES.forEach((em) => {
|
|
68
|
+
const option = document.createElement('option');
|
|
69
|
+
option.value = em.value;
|
|
70
|
+
option.textContent = em.label;
|
|
71
|
+
if (String(em.value) === String(mode.value)) option.selected = true;
|
|
72
|
+
select.appendChild(option);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
select.addEventListener('change', () => {
|
|
76
|
+
currentValues[key] = parseInt(select.value);
|
|
77
|
+
if (opts.onChange) opts.onChange({ ...currentValues });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
group.appendChild(label);
|
|
81
|
+
group.appendChild(select);
|
|
82
|
+
wrap.appendChild(group);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (container) container.appendChild(wrap);
|
|
86
|
+
return wrap;
|
|
87
|
+
},
|
|
88
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NumberInput component — renders a number input with +/- buttons, label, and optional help tooltip.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* NumberInput.render(container, {
|
|
6
|
+
* id: 'polling-interval',
|
|
7
|
+
* label: 'Polling Interval',
|
|
8
|
+
* help: 'Minutes between cloud polls',
|
|
9
|
+
* value: 10,
|
|
10
|
+
* min: 1,
|
|
11
|
+
* max: 120,
|
|
12
|
+
* step: 1,
|
|
13
|
+
* suffix: 'min',
|
|
14
|
+
* onChange: (value) => { ... }
|
|
15
|
+
* });
|
|
16
|
+
*/
|
|
17
|
+
// eslint-disable-next-line no-unused-vars
|
|
18
|
+
const NumberInput = {
|
|
19
|
+
/**
|
|
20
|
+
* @param {HTMLElement} container
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.id
|
|
23
|
+
* @param {string} opts.label
|
|
24
|
+
* @param {string} [opts.help]
|
|
25
|
+
* @param {number} opts.value
|
|
26
|
+
* @param {number} [opts.min]
|
|
27
|
+
* @param {number} [opts.max]
|
|
28
|
+
* @param {number} [opts.step]
|
|
29
|
+
* @param {string} [opts.suffix] - text after the input (e.g., 'min', 'sec')
|
|
30
|
+
* @param {boolean} [opts.disabled]
|
|
31
|
+
* @param {function} opts.onChange - callback(value: number)
|
|
32
|
+
* @returns {HTMLElement}
|
|
33
|
+
*/
|
|
34
|
+
render(container, opts) {
|
|
35
|
+
const row = document.createElement('div');
|
|
36
|
+
row.className = 'eufy-toggle';
|
|
37
|
+
|
|
38
|
+
const labelWrap = document.createElement('div');
|
|
39
|
+
labelWrap.className = 'eufy-toggle__label';
|
|
40
|
+
|
|
41
|
+
const labelEl = document.createElement('label');
|
|
42
|
+
labelEl.setAttribute('for', opts.id);
|
|
43
|
+
labelEl.textContent = opts.label;
|
|
44
|
+
labelWrap.appendChild(labelEl);
|
|
45
|
+
|
|
46
|
+
if (opts.help) {
|
|
47
|
+
const helpEl = document.createElement('span');
|
|
48
|
+
helpEl.className = 'eufy-toggle__help';
|
|
49
|
+
helpEl.textContent = '?';
|
|
50
|
+
helpEl.title = opts.help;
|
|
51
|
+
labelWrap.appendChild(helpEl);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const inputGroup = document.createElement('div');
|
|
55
|
+
inputGroup.className = 'number-input-group';
|
|
56
|
+
|
|
57
|
+
const step = opts.step || 1;
|
|
58
|
+
const min = opts.min !== undefined ? opts.min : 0;
|
|
59
|
+
const max = opts.max !== undefined ? opts.max : 99999;
|
|
60
|
+
|
|
61
|
+
const btnMinus = document.createElement('button');
|
|
62
|
+
btnMinus.type = 'button';
|
|
63
|
+
btnMinus.className = 'btn btn-outline-secondary btn-sm';
|
|
64
|
+
btnMinus.textContent = '−';
|
|
65
|
+
|
|
66
|
+
const input = document.createElement('input');
|
|
67
|
+
input.type = 'number';
|
|
68
|
+
input.className = 'form-control form-control-sm';
|
|
69
|
+
input.id = opts.id;
|
|
70
|
+
input.value = opts.value;
|
|
71
|
+
input.min = min;
|
|
72
|
+
input.max = max;
|
|
73
|
+
input.step = step;
|
|
74
|
+
input.disabled = !!opts.disabled;
|
|
75
|
+
|
|
76
|
+
const btnPlus = document.createElement('button');
|
|
77
|
+
btnPlus.type = 'button';
|
|
78
|
+
btnPlus.className = 'btn btn-outline-secondary btn-sm';
|
|
79
|
+
btnPlus.textContent = '+';
|
|
80
|
+
|
|
81
|
+
const fireChange = () => {
|
|
82
|
+
let val = parseFloat(input.value);
|
|
83
|
+
if (isNaN(val)) val = min;
|
|
84
|
+
val = Math.max(min, Math.min(max, val));
|
|
85
|
+
input.value = val;
|
|
86
|
+
if (opts.onChange) opts.onChange(val);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
btnMinus.addEventListener('click', () => {
|
|
90
|
+
let val = parseFloat(input.value) - step;
|
|
91
|
+
input.value = Math.max(min, val);
|
|
92
|
+
fireChange();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
btnPlus.addEventListener('click', () => {
|
|
96
|
+
let val = parseFloat(input.value) + step;
|
|
97
|
+
input.value = Math.min(max, val);
|
|
98
|
+
fireChange();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
input.addEventListener('change', fireChange);
|
|
102
|
+
|
|
103
|
+
inputGroup.appendChild(btnMinus);
|
|
104
|
+
inputGroup.appendChild(input);
|
|
105
|
+
inputGroup.appendChild(btnPlus);
|
|
106
|
+
|
|
107
|
+
if (opts.suffix) {
|
|
108
|
+
const suffixEl = document.createElement('span');
|
|
109
|
+
suffixEl.className = 'text-muted ms-1';
|
|
110
|
+
suffixEl.style.fontSize = '0.8rem';
|
|
111
|
+
suffixEl.textContent = opts.suffix;
|
|
112
|
+
inputGroup.appendChild(suffixEl);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
row.appendChild(labelWrap);
|
|
116
|
+
row.appendChild(inputGroup);
|
|
117
|
+
|
|
118
|
+
if (container) container.appendChild(row);
|
|
119
|
+
return row;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select component — renders a dropdown with label and optional help tooltip.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* Select.render(container, {
|
|
6
|
+
* id: 'snapshot-method',
|
|
7
|
+
* label: 'Snapshot Method',
|
|
8
|
+
* help: 'How snapshots are captured',
|
|
9
|
+
* options: [{ value: '0', label: 'Auto' }, { value: '1', label: 'From Stream' }],
|
|
10
|
+
* value: '0',
|
|
11
|
+
* onChange: (value) => { ... }
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
// eslint-disable-next-line no-unused-vars
|
|
15
|
+
const Select = {
|
|
16
|
+
/**
|
|
17
|
+
* @param {HTMLElement} container
|
|
18
|
+
* @param {object} opts
|
|
19
|
+
* @param {string} opts.id
|
|
20
|
+
* @param {string} opts.label
|
|
21
|
+
* @param {string} [opts.help]
|
|
22
|
+
* @param {Array<{value: string, label: string}>} opts.options
|
|
23
|
+
* @param {string} opts.value - initial selected value
|
|
24
|
+
* @param {boolean} [opts.disabled]
|
|
25
|
+
* @param {function} opts.onChange - callback(value: string)
|
|
26
|
+
* @returns {HTMLElement}
|
|
27
|
+
*/
|
|
28
|
+
render(container, opts) {
|
|
29
|
+
const row = document.createElement('div');
|
|
30
|
+
row.className = 'eufy-toggle';
|
|
31
|
+
|
|
32
|
+
const labelWrap = document.createElement('div');
|
|
33
|
+
labelWrap.className = 'eufy-toggle__label';
|
|
34
|
+
|
|
35
|
+
const labelEl = document.createElement('label');
|
|
36
|
+
labelEl.setAttribute('for', opts.id);
|
|
37
|
+
labelEl.textContent = opts.label;
|
|
38
|
+
labelWrap.appendChild(labelEl);
|
|
39
|
+
|
|
40
|
+
if (opts.help) {
|
|
41
|
+
const helpEl = document.createElement('span');
|
|
42
|
+
helpEl.className = 'eufy-toggle__help';
|
|
43
|
+
helpEl.textContent = '?';
|
|
44
|
+
helpEl.title = opts.help;
|
|
45
|
+
labelWrap.appendChild(helpEl);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const selectEl = document.createElement('select');
|
|
49
|
+
selectEl.className = 'form-select form-select-sm';
|
|
50
|
+
selectEl.id = opts.id;
|
|
51
|
+
selectEl.style.width = 'auto';
|
|
52
|
+
selectEl.style.maxWidth = '200px';
|
|
53
|
+
selectEl.disabled = !!opts.disabled;
|
|
54
|
+
|
|
55
|
+
(opts.options || []).forEach((opt) => {
|
|
56
|
+
const option = document.createElement('option');
|
|
57
|
+
option.value = opt.value;
|
|
58
|
+
option.textContent = opt.label;
|
|
59
|
+
if (String(opt.value) === String(opts.value)) option.selected = true;
|
|
60
|
+
selectEl.appendChild(option);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
selectEl.addEventListener('change', () => {
|
|
64
|
+
if (opts.onChange) opts.onChange(selectEl.value);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
row.appendChild(labelWrap);
|
|
68
|
+
row.appendChild(selectEl);
|
|
69
|
+
|
|
70
|
+
if (container) container.appendChild(row);
|
|
71
|
+
return row;
|
|
72
|
+
},
|
|
73
|
+
};
|