homebridge-bedjet 0.3.0 → 0.3.2
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/dist/accessory.js +2 -2
- package/homebridge-ui/public/index.html +212 -122
- package/homebridge-ui/server.js +61 -2
- package/package.json +1 -1
- package/src/accessory.ts +2 -2
package/dist/accessory.js
CHANGED
|
@@ -76,7 +76,7 @@ class BedJetAccessory {
|
|
|
76
76
|
.setCharacteristic(Characteristic.SerialNumber, config.address);
|
|
77
77
|
// Thermostat service
|
|
78
78
|
this.thermostatService = this.accessory.getService(Service.Thermostat)
|
|
79
|
-
?? this.accessory.addService(Service.Thermostat,
|
|
79
|
+
?? this.accessory.addService(Service.Thermostat, 'BedJet', 'thermostat');
|
|
80
80
|
this.thermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits)
|
|
81
81
|
.onGet(() => Characteristic.TemperatureDisplayUnits.CELSIUS);
|
|
82
82
|
this.thermostatService.getCharacteristic(Characteristic.CurrentTemperature)
|
|
@@ -115,7 +115,7 @@ class BedJetAccessory {
|
|
|
115
115
|
});
|
|
116
116
|
// FanV2 service
|
|
117
117
|
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
118
|
-
?? this.accessory.addService(Service.Fanv2,
|
|
118
|
+
?? this.accessory.addService(Service.Fanv2, 'BedJet Fan', 'fan');
|
|
119
119
|
this.fanService.getCharacteristic(Characteristic.Active)
|
|
120
120
|
.onGet(() => this.bedjet.state.operatingMode !== constants_1.OperatingMode.STANDBY
|
|
121
121
|
? Characteristic.Active.ACTIVE
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>BedJet
|
|
6
|
+
<title>BedJet</title>
|
|
7
7
|
<style>
|
|
8
8
|
body {
|
|
9
9
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
@@ -20,9 +20,17 @@
|
|
|
20
20
|
text-transform: uppercase;
|
|
21
21
|
letter-spacing: 0.06em;
|
|
22
22
|
opacity: 0.5;
|
|
23
|
-
margin: 0 0
|
|
23
|
+
margin: 0 0 10px;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
hr.divider {
|
|
27
|
+
border: none;
|
|
28
|
+
border-top: 1px solid rgba(128,128,128,0.2);
|
|
29
|
+
margin: 20px 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ── Scan row ────────────────────────────────────────────────────────── */
|
|
33
|
+
|
|
26
34
|
.scan-row {
|
|
27
35
|
display: flex;
|
|
28
36
|
align-items: center;
|
|
@@ -77,7 +85,7 @@
|
|
|
77
85
|
transition: width 0.3s linear;
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
/* ── Device card
|
|
88
|
+
/* ── Device card (shared by both sections) ───────────────────────────── */
|
|
81
89
|
|
|
82
90
|
.device-card {
|
|
83
91
|
border: 1px solid rgba(128,128,128,0.25);
|
|
@@ -86,13 +94,7 @@
|
|
|
86
94
|
margin-bottom: 10px;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
|
-
.card-header {
|
|
90
|
-
display: flex;
|
|
91
|
-
align-items: center;
|
|
92
|
-
justify-content: space-between;
|
|
93
|
-
margin-bottom: 10px;
|
|
94
|
-
}
|
|
95
|
-
|
|
97
|
+
.card-header { margin-bottom: 10px; }
|
|
96
98
|
.device-name { font-weight: 500; }
|
|
97
99
|
.device-addr {
|
|
98
100
|
font-size: 11px;
|
|
@@ -101,7 +103,7 @@
|
|
|
101
103
|
margin-top: 2px;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
/* ── Defaults
|
|
106
|
+
/* ── Defaults fields ─────────────────────────────────────────────────── */
|
|
105
107
|
|
|
106
108
|
.defaults-row {
|
|
107
109
|
display: flex;
|
|
@@ -145,7 +147,7 @@
|
|
|
145
147
|
border-color: #e25c00;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
/* ──
|
|
150
|
+
/* ── Card footer ─────────────────────────────────────────────────────── */
|
|
149
151
|
|
|
150
152
|
.card-footer {
|
|
151
153
|
display: flex;
|
|
@@ -161,7 +163,7 @@
|
|
|
161
163
|
.card-msg.ok { color: #4caf50; }
|
|
162
164
|
.card-msg.err { color: #f28b82; }
|
|
163
165
|
|
|
164
|
-
.
|
|
166
|
+
.primary-btn {
|
|
165
167
|
padding: 6px 16px;
|
|
166
168
|
border: none;
|
|
167
169
|
border-radius: 5px;
|
|
@@ -174,8 +176,8 @@
|
|
|
174
176
|
white-space: nowrap;
|
|
175
177
|
flex-shrink: 0;
|
|
176
178
|
}
|
|
177
|
-
.
|
|
178
|
-
.
|
|
179
|
+
.primary-btn:hover:not(:disabled) { opacity: 0.85; }
|
|
180
|
+
.primary-btn:disabled { opacity: 0.55; cursor: default; }
|
|
179
181
|
|
|
180
182
|
/* ── Notices ─────────────────────────────────────────────────────────── */
|
|
181
183
|
|
|
@@ -191,18 +193,22 @@
|
|
|
191
193
|
</head>
|
|
192
194
|
<body>
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
<!-- ── Section 1: Configured devices ────────────────────────────────────── -->
|
|
197
|
+
<h6>Configured Devices</h6>
|
|
198
|
+
<div id="configuredDevices"><div class="notice" style="opacity:0.4">Loading…</div></div>
|
|
199
|
+
|
|
200
|
+
<hr class="divider" />
|
|
195
201
|
|
|
202
|
+
<!-- ── Section 2: Bluetooth discovery ───────────────────────────────────── -->
|
|
203
|
+
<h6>Add New Device</h6>
|
|
196
204
|
<div class="scan-row">
|
|
197
205
|
<button id="scanBtn">Scan for BedJets</button>
|
|
198
206
|
<div id="scanStatus"></div>
|
|
199
207
|
</div>
|
|
200
|
-
|
|
201
208
|
<div class="progress-wrap" id="progressWrap">
|
|
202
209
|
<div class="progress-fill" id="progressFill"></div>
|
|
203
210
|
</div>
|
|
204
|
-
|
|
205
|
-
<div id="results"></div>
|
|
211
|
+
<div id="scanResults"></div>
|
|
206
212
|
|
|
207
213
|
<script>
|
|
208
214
|
var SCAN_MS = 12000;
|
|
@@ -216,114 +222,128 @@
|
|
|
216
222
|
{ value: 'dry', label: 'Dry' },
|
|
217
223
|
];
|
|
218
224
|
|
|
219
|
-
// ──
|
|
225
|
+
// ── On load: show configured devices ─────────────────────────────────────
|
|
220
226
|
|
|
221
|
-
|
|
227
|
+
loadConfiguredDevices();
|
|
228
|
+
|
|
229
|
+
async function loadConfiguredDevices() {
|
|
230
|
+
var container = document.getElementById('configuredDevices');
|
|
231
|
+
try {
|
|
232
|
+
var res = await homebridge.request('/get-devices');
|
|
233
|
+
var devices = (res && Array.isArray(res.devices)) ? res.devices : [];
|
|
234
|
+
if (devices.length === 0) {
|
|
235
|
+
container.innerHTML = '<div class="notice" style="opacity:0.5">No devices configured yet. Use the scanner below to add one.</div>';
|
|
236
|
+
} else {
|
|
237
|
+
container.innerHTML = '';
|
|
238
|
+
devices.forEach(function (d) {
|
|
239
|
+
container.appendChild(buildEditCard(d));
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
container.innerHTML = '<div class="notice error">Could not load config: ' + esc(err && err.message ? err.message : String(err)) + '</div>';
|
|
244
|
+
}
|
|
245
|
+
}
|
|
222
246
|
|
|
223
|
-
// Event delegation
|
|
224
|
-
|
|
247
|
+
// ── Event delegation ──────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
document.getElementById('configuredDevices').addEventListener('click', function (e) {
|
|
225
250
|
var btn = e.target;
|
|
226
|
-
if (!btn.classList.contains('
|
|
251
|
+
if (!btn.classList.contains('primary-btn') || btn.disabled) return;
|
|
227
252
|
var card = btn.closest('.device-card');
|
|
228
253
|
if (!card) return;
|
|
254
|
+
saveDefaults(btn, card);
|
|
255
|
+
});
|
|
229
256
|
|
|
257
|
+
document.getElementById('scanResults').addEventListener('click', function (e) {
|
|
258
|
+
var btn = e.target;
|
|
259
|
+
if (!btn.classList.contains('primary-btn') || btn.disabled) return;
|
|
260
|
+
var card = btn.closest('.device-card');
|
|
261
|
+
if (!card) return;
|
|
230
262
|
var name = card.getAttribute('data-name');
|
|
231
263
|
var address = card.getAttribute('data-address');
|
|
232
|
-
|
|
233
|
-
// Read defaults from the card's inputs
|
|
234
|
-
var modeEl = card.querySelector('.field-mode');
|
|
235
|
-
var tempEl = card.querySelector('.field-temp');
|
|
236
|
-
var fanEl = card.querySelector('.field-fan');
|
|
237
|
-
|
|
238
|
-
var defaultMode = modeEl ? modeEl.value : 'heat';
|
|
239
|
-
var defaultTemperature = tempEl && tempEl.value !== '' ? parseFloat(tempEl.value) : null;
|
|
240
|
-
var defaultFanSpeed = fanEl && fanEl.value !== '' ? parseInt(fanEl.value, 10) : null;
|
|
241
|
-
|
|
242
|
-
addDevice(btn, card, name, address, defaultMode, defaultTemperature, defaultFanSpeed);
|
|
264
|
+
addDevice(btn, card, name, address);
|
|
243
265
|
});
|
|
244
266
|
|
|
245
|
-
|
|
267
|
+
document.getElementById('scanBtn').addEventListener('click', startScan);
|
|
246
268
|
|
|
247
|
-
|
|
248
|
-
if (scanning) return;
|
|
249
|
-
scanning = true;
|
|
269
|
+
// ── Build card for an already-configured device (edit mode) ──────────────
|
|
250
270
|
|
|
251
|
-
|
|
252
|
-
var
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
var results = document.getElementById('results');
|
|
271
|
+
function buildEditCard(d) {
|
|
272
|
+
var card = document.createElement('div');
|
|
273
|
+
card.className = 'device-card';
|
|
274
|
+
card.setAttribute('data-address', d.address);
|
|
256
275
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
276
|
+
var header = document.createElement('div');
|
|
277
|
+
header.className = 'card-header';
|
|
278
|
+
var nameEl = document.createElement('div');
|
|
279
|
+
nameEl.className = 'device-name';
|
|
280
|
+
nameEl.textContent = d.name;
|
|
281
|
+
var addrEl = document.createElement('div');
|
|
282
|
+
addrEl.className = 'device-addr';
|
|
283
|
+
addrEl.textContent = d.address;
|
|
284
|
+
header.appendChild(nameEl);
|
|
285
|
+
header.appendChild(addrEl);
|
|
286
|
+
card.appendChild(header);
|
|
262
287
|
|
|
263
|
-
|
|
264
|
-
var tickMs = 200;
|
|
265
|
-
var timer = setInterval(function () {
|
|
266
|
-
elapsed += tickMs;
|
|
267
|
-
pFill.style.width = Math.min(100, (elapsed / SCAN_MS) * 100) + '%';
|
|
268
|
-
if (elapsed >= SCAN_MS) clearInterval(timer);
|
|
269
|
-
}, tickMs);
|
|
288
|
+
card.appendChild(buildDefaultsRow(d.defaultMode, d.defaultTemperature, d.defaultFanSpeed));
|
|
270
289
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
290
|
+
var footer = document.createElement('div');
|
|
291
|
+
footer.className = 'card-footer';
|
|
292
|
+
var msg = document.createElement('div');
|
|
293
|
+
msg.className = 'card-msg';
|
|
294
|
+
var btn = document.createElement('button');
|
|
295
|
+
btn.className = 'primary-btn';
|
|
296
|
+
btn.textContent = 'Save';
|
|
297
|
+
footer.appendChild(msg);
|
|
298
|
+
footer.appendChild(btn);
|
|
299
|
+
card.appendChild(footer);
|
|
276
300
|
|
|
277
|
-
|
|
278
|
-
if (devices.length === 0) {
|
|
279
|
-
results.innerHTML = '<div class="notice">No BedJet devices found nearby. Make sure your BedJet is powered on and not already connected to another app, then try again.</div>';
|
|
280
|
-
} else {
|
|
281
|
-
// Build cards via DOM (no inline JS strings → no quote escaping issues)
|
|
282
|
-
results.innerHTML = '';
|
|
283
|
-
devices.forEach(function (d) {
|
|
284
|
-
results.appendChild(buildCard(d));
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
} catch (err) {
|
|
288
|
-
clearInterval(timer);
|
|
289
|
-
status.innerHTML = '';
|
|
290
|
-
results.innerHTML = '<div class="notice error">Scan failed: ' + esc(err && err.message ? err.message : String(err)) + '</div>';
|
|
291
|
-
} finally {
|
|
292
|
-
setTimeout(function () { pWrap.style.display = 'none'; pFill.style.width = '0%'; }, 500);
|
|
293
|
-
scanBtn.disabled = false;
|
|
294
|
-
scanning = false;
|
|
295
|
-
}
|
|
301
|
+
return card;
|
|
296
302
|
}
|
|
297
303
|
|
|
298
|
-
// ──
|
|
304
|
+
// ── Build card for a newly discovered device (add mode) ───────────────────
|
|
299
305
|
|
|
300
|
-
function
|
|
306
|
+
function buildDiscoverCard(d) {
|
|
301
307
|
var card = document.createElement('div');
|
|
302
308
|
card.className = 'device-card';
|
|
303
309
|
card.setAttribute('data-name', d.name);
|
|
304
310
|
card.setAttribute('data-address', d.address);
|
|
305
311
|
|
|
306
|
-
// — Header: name + address —
|
|
307
312
|
var header = document.createElement('div');
|
|
308
313
|
header.className = 'card-header';
|
|
309
|
-
|
|
310
|
-
var info = document.createElement('div');
|
|
311
314
|
var nameEl = document.createElement('div');
|
|
312
315
|
nameEl.className = 'device-name';
|
|
313
316
|
nameEl.textContent = d.name;
|
|
314
317
|
var addrEl = document.createElement('div');
|
|
315
318
|
addrEl.className = 'device-addr';
|
|
316
319
|
addrEl.textContent = d.address;
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
header.appendChild(info);
|
|
320
|
+
header.appendChild(nameEl);
|
|
321
|
+
header.appendChild(addrEl);
|
|
320
322
|
card.appendChild(header);
|
|
321
323
|
|
|
322
|
-
|
|
324
|
+
card.appendChild(buildDefaultsRow(null, null, null));
|
|
325
|
+
|
|
326
|
+
var footer = document.createElement('div');
|
|
327
|
+
footer.className = 'card-footer';
|
|
328
|
+
var msg = document.createElement('div');
|
|
329
|
+
msg.className = 'card-msg';
|
|
330
|
+
var btn = document.createElement('button');
|
|
331
|
+
btn.className = 'primary-btn';
|
|
332
|
+
btn.textContent = 'Add to Config';
|
|
333
|
+
footer.appendChild(msg);
|
|
334
|
+
footer.appendChild(btn);
|
|
335
|
+
card.appendChild(footer);
|
|
336
|
+
|
|
337
|
+
return card;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── Shared defaults row builder ───────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
function buildDefaultsRow(currentMode, currentTemp, currentFan) {
|
|
323
343
|
var row = document.createElement('div');
|
|
324
344
|
row.className = 'defaults-row';
|
|
325
345
|
|
|
326
|
-
// Mode
|
|
346
|
+
// Mode
|
|
327
347
|
var modeField = document.createElement('div');
|
|
328
348
|
modeField.className = 'field';
|
|
329
349
|
modeField.style.flex = '2';
|
|
@@ -335,14 +355,14 @@
|
|
|
335
355
|
var opt = document.createElement('option');
|
|
336
356
|
opt.value = m.value;
|
|
337
357
|
opt.textContent = m.label;
|
|
338
|
-
if (m.value === 'heat') opt.selected = true;
|
|
358
|
+
if (m.value === (currentMode || 'heat')) opt.selected = true;
|
|
339
359
|
modeSelect.appendChild(opt);
|
|
340
360
|
});
|
|
341
361
|
modeField.appendChild(modeLabel);
|
|
342
362
|
modeField.appendChild(modeSelect);
|
|
343
363
|
row.appendChild(modeField);
|
|
344
364
|
|
|
345
|
-
// Temperature
|
|
365
|
+
// Temperature
|
|
346
366
|
var tempField = document.createElement('div');
|
|
347
367
|
tempField.className = 'field';
|
|
348
368
|
var tempLabel = document.createElement('label');
|
|
@@ -354,11 +374,12 @@
|
|
|
354
374
|
tempInput.max = '43';
|
|
355
375
|
tempInput.step = '0.5';
|
|
356
376
|
tempInput.placeholder = 'Optional';
|
|
377
|
+
if (currentTemp != null) tempInput.value = currentTemp;
|
|
357
378
|
tempField.appendChild(tempLabel);
|
|
358
379
|
tempField.appendChild(tempInput);
|
|
359
380
|
row.appendChild(tempField);
|
|
360
381
|
|
|
361
|
-
// Fan speed
|
|
382
|
+
// Fan speed
|
|
362
383
|
var fanField = document.createElement('div');
|
|
363
384
|
fanField.className = 'field';
|
|
364
385
|
var fanLabel = document.createElement('label');
|
|
@@ -370,46 +391,66 @@
|
|
|
370
391
|
fanInput.max = '100';
|
|
371
392
|
fanInput.step = '5';
|
|
372
393
|
fanInput.placeholder = 'Optional';
|
|
394
|
+
if (currentFan != null) fanInput.value = currentFan;
|
|
373
395
|
fanField.appendChild(fanLabel);
|
|
374
396
|
fanField.appendChild(fanInput);
|
|
375
397
|
row.appendChild(fanField);
|
|
376
398
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
// — Footer: message + button —
|
|
380
|
-
var footer = document.createElement('div');
|
|
381
|
-
footer.className = 'card-footer';
|
|
399
|
+
return row;
|
|
400
|
+
}
|
|
382
401
|
|
|
383
|
-
|
|
384
|
-
msg.className = 'card-msg';
|
|
402
|
+
// ── Save defaults for existing device ─────────────────────────────────────
|
|
385
403
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
404
|
+
async function saveDefaults(btn, card) {
|
|
405
|
+
var msg = card.querySelector('.card-msg');
|
|
406
|
+
var address = card.getAttribute('data-address');
|
|
407
|
+
var defaultMode = card.querySelector('.field-mode').value;
|
|
408
|
+
var tempVal = card.querySelector('.field-temp').value;
|
|
409
|
+
var fanVal = card.querySelector('.field-fan').value;
|
|
410
|
+
var defaultTemp = tempVal !== '' ? parseFloat(tempVal) : null;
|
|
411
|
+
var defaultFan = fanVal !== '' ? parseInt(fanVal, 10) : null;
|
|
389
412
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
413
|
+
btn.disabled = true;
|
|
414
|
+
btn.textContent = 'Saving…';
|
|
415
|
+
if (msg) { msg.textContent = ''; msg.className = 'card-msg'; }
|
|
393
416
|
|
|
394
|
-
|
|
417
|
+
try {
|
|
418
|
+
await homebridge.request('/update-device', {
|
|
419
|
+
address: address,
|
|
420
|
+
defaultMode: defaultMode,
|
|
421
|
+
defaultTemperature: defaultTemp,
|
|
422
|
+
defaultFanSpeed: defaultFan,
|
|
423
|
+
});
|
|
424
|
+
btn.disabled = false;
|
|
425
|
+
btn.textContent = 'Save';
|
|
426
|
+
if (msg) { msg.textContent = '✓ Saved'; msg.className = 'card-msg ok'; }
|
|
427
|
+
setTimeout(function () { if (msg) msg.textContent = ''; }, 3000);
|
|
428
|
+
} catch (err) {
|
|
429
|
+
btn.disabled = false;
|
|
430
|
+
btn.textContent = 'Save';
|
|
431
|
+
if (msg) { msg.textContent = 'Error: ' + esc(err && err.message ? err.message : String(err)); msg.className = 'card-msg err'; }
|
|
432
|
+
}
|
|
395
433
|
}
|
|
396
434
|
|
|
397
|
-
// ── Add device
|
|
435
|
+
// ── Add newly discovered device ───────────────────────────────────────────
|
|
398
436
|
|
|
399
|
-
async function addDevice(btn, card, name, address
|
|
400
|
-
var msg
|
|
437
|
+
async function addDevice(btn, card, name, address) {
|
|
438
|
+
var msg = card.querySelector('.card-msg');
|
|
439
|
+
var defaultMode = card.querySelector('.field-mode').value;
|
|
440
|
+
var tempVal = card.querySelector('.field-temp').value;
|
|
441
|
+
var fanVal = card.querySelector('.field-fan').value;
|
|
442
|
+
var defaultTemp = tempVal !== '' ? parseFloat(tempVal) : null;
|
|
443
|
+
var defaultFan = fanVal !== '' ? parseInt(fanVal, 10) : null;
|
|
401
444
|
|
|
402
445
|
btn.disabled = true;
|
|
403
446
|
btn.textContent = 'Saving…';
|
|
404
|
-
if (msg) { msg.textContent = ''; msg.className = 'card-msg'; }
|
|
405
|
-
|
|
406
|
-
// Lock inputs while saving
|
|
407
447
|
card.querySelectorAll('select, input').forEach(function (el) { el.disabled = true; });
|
|
448
|
+
if (msg) { msg.textContent = ''; msg.className = 'card-msg'; }
|
|
408
449
|
|
|
409
450
|
try {
|
|
410
451
|
var payload = { name: name, address: address, defaultMode: defaultMode };
|
|
411
|
-
if (
|
|
412
|
-
if (
|
|
452
|
+
if (defaultTemp !== null) payload.defaultTemperature = defaultTemp;
|
|
453
|
+
if (defaultFan !== null) payload.defaultFanSpeed = defaultFan;
|
|
413
454
|
|
|
414
455
|
var result = await homebridge.request('/add-device', payload);
|
|
415
456
|
|
|
@@ -420,23 +461,72 @@
|
|
|
420
461
|
btn.textContent = '✓ Added!';
|
|
421
462
|
btn.style.background = '#145c30';
|
|
422
463
|
if (msg) { msg.textContent = 'Saved — restart Homebridge.'; msg.className = 'card-msg ok'; }
|
|
464
|
+
// Refresh the configured devices section
|
|
465
|
+
loadConfiguredDevices();
|
|
423
466
|
}
|
|
424
467
|
} catch (err) {
|
|
425
|
-
// Re-enable on failure so the user can retry
|
|
426
468
|
btn.disabled = false;
|
|
427
469
|
btn.textContent = 'Add to Config';
|
|
428
470
|
card.querySelectorAll('select, input').forEach(function (el) { el.disabled = false; });
|
|
429
|
-
|
|
430
|
-
|
|
471
|
+
if (msg) { msg.textContent = 'Error: ' + esc(err && err.message ? err.message : String(err)); msg.className = 'card-msg err'; }
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
async function startScan() {
|
|
478
|
+
if (scanning) return;
|
|
479
|
+
scanning = true;
|
|
480
|
+
|
|
481
|
+
var scanBtn = document.getElementById('scanBtn');
|
|
482
|
+
var status = document.getElementById('scanStatus');
|
|
483
|
+
var pWrap = document.getElementById('progressWrap');
|
|
484
|
+
var pFill = document.getElementById('progressFill');
|
|
485
|
+
var results = document.getElementById('scanResults');
|
|
486
|
+
|
|
487
|
+
scanBtn.disabled = true;
|
|
488
|
+
results.innerHTML = '';
|
|
489
|
+
pFill.style.width = '0%';
|
|
490
|
+
pWrap.style.display = 'block';
|
|
491
|
+
status.innerHTML = '<div class="spinner"></div> Scanning…';
|
|
492
|
+
|
|
493
|
+
var elapsed = 0;
|
|
494
|
+
var timer = setInterval(function () {
|
|
495
|
+
elapsed += 200;
|
|
496
|
+
pFill.style.width = Math.min(100, (elapsed / SCAN_MS) * 100) + '%';
|
|
497
|
+
if (elapsed >= SCAN_MS) clearInterval(timer);
|
|
498
|
+
}, 200);
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
var res = await homebridge.request('/scan');
|
|
502
|
+
clearInterval(timer);
|
|
503
|
+
pFill.style.width = '100%';
|
|
504
|
+
status.innerHTML = '';
|
|
505
|
+
|
|
506
|
+
var devices = (res && Array.isArray(res.devices)) ? res.devices : [];
|
|
507
|
+
if (devices.length === 0) {
|
|
508
|
+
results.innerHTML = '<div class="notice">No BedJet devices found nearby. Make sure your BedJet is powered on and not already connected to another app, then try again.</div>';
|
|
509
|
+
} else {
|
|
510
|
+
results.innerHTML = '';
|
|
511
|
+
devices.forEach(function (d) {
|
|
512
|
+
results.appendChild(buildDiscoverCard(d));
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
} catch (err) {
|
|
516
|
+
clearInterval(timer);
|
|
517
|
+
status.innerHTML = '';
|
|
518
|
+
results.innerHTML = '<div class="notice error">Scan failed: ' + esc(err && err.message ? err.message : String(err)) + '</div>';
|
|
519
|
+
} finally {
|
|
520
|
+
setTimeout(function () { pWrap.style.display = 'none'; pFill.style.width = '0%'; }, 500);
|
|
521
|
+
scanBtn.disabled = false;
|
|
522
|
+
scanning = false;
|
|
431
523
|
}
|
|
432
524
|
}
|
|
433
525
|
|
|
434
526
|
function esc(s) {
|
|
435
527
|
return String(s)
|
|
436
|
-
.replace(/&/g, '&')
|
|
437
|
-
.replace(
|
|
438
|
-
.replace(/>/g, '>')
|
|
439
|
-
.replace(/"/g, '"');
|
|
528
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
529
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
440
530
|
}
|
|
441
531
|
</script>
|
|
442
532
|
</body>
|
package/homebridge-ui/server.js
CHANGED
|
@@ -28,8 +28,10 @@ function getConfigPath() {
|
|
|
28
28
|
class BedJetUiServer extends HomebridgePluginUiServer {
|
|
29
29
|
constructor() {
|
|
30
30
|
super();
|
|
31
|
-
this.onRequest('/scan',
|
|
32
|
-
this.onRequest('/add-device',
|
|
31
|
+
this.onRequest('/scan', this.handleScan.bind(this));
|
|
32
|
+
this.onRequest('/add-device', this.handleAddDevice.bind(this));
|
|
33
|
+
this.onRequest('/get-devices', this.handleGetDevices.bind(this));
|
|
34
|
+
this.onRequest('/update-device', this.handleUpdateDevice.bind(this));
|
|
33
35
|
this.ready();
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -124,6 +126,63 @@ class BedJetUiServer extends HomebridgePluginUiServer {
|
|
|
124
126
|
|
|
125
127
|
return { status: 'ok' };
|
|
126
128
|
}
|
|
129
|
+
|
|
130
|
+
// ── Read configured devices ───────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
async handleGetDevices() {
|
|
133
|
+
const configPath = getConfigPath();
|
|
134
|
+
try {
|
|
135
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
136
|
+
const platform = (config.platforms || []).find(p => p.platform === 'BedJetPlatform');
|
|
137
|
+
return { devices: (platform && Array.isArray(platform.devices)) ? platform.devices : [] };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
throw new Error(`Cannot read config: ${err.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Update defaults for an existing device ────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async handleUpdateDevice(body) {
|
|
146
|
+
const { address, defaultMode, defaultTemperature, defaultFanSpeed } = body || {};
|
|
147
|
+
if (!address) throw new Error('Missing address');
|
|
148
|
+
|
|
149
|
+
const configPath = getConfigPath();
|
|
150
|
+
let config;
|
|
151
|
+
try {
|
|
152
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
throw new Error(`Cannot read config: ${err.message}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const platform = (config.platforms || []).find(p => p.platform === 'BedJetPlatform');
|
|
158
|
+
if (!platform) throw new Error('BedJetPlatform not found in config');
|
|
159
|
+
|
|
160
|
+
const device = (platform.devices || []).find(
|
|
161
|
+
d => d.address && d.address.toUpperCase() === address.toUpperCase(),
|
|
162
|
+
);
|
|
163
|
+
if (!device) throw new Error('Device not found in config');
|
|
164
|
+
|
|
165
|
+
if (defaultMode) device.defaultMode = defaultMode;
|
|
166
|
+
else delete device.defaultMode;
|
|
167
|
+
if (defaultTemperature !== undefined && defaultTemperature !== null && defaultTemperature !== '') {
|
|
168
|
+
device.defaultTemperature = Number(defaultTemperature);
|
|
169
|
+
} else {
|
|
170
|
+
delete device.defaultTemperature;
|
|
171
|
+
}
|
|
172
|
+
if (defaultFanSpeed !== undefined && defaultFanSpeed !== null && defaultFanSpeed !== '') {
|
|
173
|
+
device.defaultFanSpeed = Number(defaultFanSpeed);
|
|
174
|
+
} else {
|
|
175
|
+
delete device.defaultFanSpeed;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'utf8');
|
|
180
|
+
} catch (err) {
|
|
181
|
+
throw new Error(`Cannot write config: ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { status: 'ok' };
|
|
185
|
+
}
|
|
127
186
|
}
|
|
128
187
|
|
|
129
188
|
new BedJetUiServer();
|
package/package.json
CHANGED
package/src/accessory.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class BedJetAccessory {
|
|
|
96
96
|
|
|
97
97
|
// Thermostat service
|
|
98
98
|
this.thermostatService = this.accessory.getService(Service.Thermostat)
|
|
99
|
-
?? this.accessory.addService(Service.Thermostat,
|
|
99
|
+
?? this.accessory.addService(Service.Thermostat, 'BedJet', 'thermostat');
|
|
100
100
|
|
|
101
101
|
this.thermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits)
|
|
102
102
|
.onGet(() => Characteristic.TemperatureDisplayUnits.CELSIUS);
|
|
@@ -143,7 +143,7 @@ export class BedJetAccessory {
|
|
|
143
143
|
|
|
144
144
|
// FanV2 service
|
|
145
145
|
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
146
|
-
?? this.accessory.addService(Service.Fanv2,
|
|
146
|
+
?? this.accessory.addService(Service.Fanv2, 'BedJet Fan', 'fan');
|
|
147
147
|
|
|
148
148
|
this.fanService.getCharacteristic(Characteristic.Active)
|
|
149
149
|
.onGet(() =>
|