homebridge-bedjet 0.3.1 → 0.3.3
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 +16 -7
- package/homebridge-ui/public/index.html +212 -122
- package/homebridge-ui/server.js +61 -2
- package/package.json +1 -1
- package/src/accessory.ts +16 -7
package/dist/accessory.js
CHANGED
|
@@ -105,13 +105,22 @@ class BedJetAccessory {
|
|
|
105
105
|
return 1; // HEAT / TURBO / EXTENDED_HEAT
|
|
106
106
|
})
|
|
107
107
|
.onSet((value) => {
|
|
108
|
-
|
|
108
|
+
const val = value;
|
|
109
109
|
const wasOff = this.bedjet.state.operatingMode === constants_1.OperatingMode.STANDBY
|
|
110
110
|
|| this.bedjet.state.operatingMode === constants_1.OperatingMode.WAIT;
|
|
111
|
-
const turningOn =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
const turningOn = val !== 0;
|
|
112
|
+
let mode;
|
|
113
|
+
if (val === 3 || (turningOn && wasOff)) {
|
|
114
|
+
// Siri sends Auto (3) when turning on — and any turn-on from standby —
|
|
115
|
+
// should respect the user's configured defaultMode.
|
|
116
|
+
mode = config.defaultMode
|
|
117
|
+
? DEFAULT_MODE_MAP[config.defaultMode]
|
|
118
|
+
: TARGET_TO_MODE[val] ?? constants_1.OperatingMode.HEAT;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
mode = TARGET_TO_MODE[val] ?? constants_1.OperatingMode.STANDBY;
|
|
122
|
+
}
|
|
123
|
+
this._applyModeAndDefaults(mode, wasOff && turningOn).catch(err => this.platform.log.error(`[${config.name}] setOperatingMode failed: ${err}`));
|
|
115
124
|
});
|
|
116
125
|
// FanV2 service
|
|
117
126
|
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
@@ -130,7 +139,7 @@ class BedJetAccessory {
|
|
|
130
139
|
const mode = this.config.defaultMode
|
|
131
140
|
? DEFAULT_MODE_MAP[this.config.defaultMode]
|
|
132
141
|
: constants_1.OperatingMode.HEAT;
|
|
133
|
-
this._applyModeAndDefaults(mode, true
|
|
142
|
+
this._applyModeAndDefaults(mode, true).catch(err => this.platform.log.error(`[${config.name}] turn on failed: ${err}`));
|
|
134
143
|
}
|
|
135
144
|
});
|
|
136
145
|
this.fanService.getCharacteristic(Characteristic.RotationSpeed)
|
|
@@ -165,7 +174,7 @@ class BedJetAccessory {
|
|
|
165
174
|
* (fan turn-on path); false when HomeKit supplied the mode explicitly
|
|
166
175
|
* (thermostat path).
|
|
167
176
|
*/
|
|
168
|
-
async _applyModeAndDefaults(mode, applyDefaults
|
|
177
|
+
async _applyModeAndDefaults(mode, applyDefaults) {
|
|
169
178
|
const { config } = this;
|
|
170
179
|
// Determine HomeKit target state for pending optimistic value
|
|
171
180
|
const pendingState = mode === constants_1.OperatingMode.STANDBY ? 0
|
|
@@ -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
|
@@ -130,13 +130,23 @@ export class BedJetAccessory {
|
|
|
130
130
|
return 1; // HEAT / TURBO / EXTENDED_HEAT
|
|
131
131
|
})
|
|
132
132
|
.onSet((value: CharacteristicValue) => {
|
|
133
|
-
|
|
133
|
+
const val = value as number;
|
|
134
134
|
const wasOff = this.bedjet.state.operatingMode === OperatingMode.STANDBY
|
|
135
135
|
|| this.bedjet.state.operatingMode === OperatingMode.WAIT;
|
|
136
|
-
const turningOn =
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
const turningOn = val !== 0;
|
|
137
|
+
|
|
138
|
+
let mode: OperatingMode;
|
|
139
|
+
if (val === 3 || (turningOn && wasOff)) {
|
|
140
|
+
// Siri sends Auto (3) when turning on — and any turn-on from standby —
|
|
141
|
+
// should respect the user's configured defaultMode.
|
|
142
|
+
mode = config.defaultMode
|
|
143
|
+
? DEFAULT_MODE_MAP[config.defaultMode]
|
|
144
|
+
: TARGET_TO_MODE[val] ?? OperatingMode.HEAT;
|
|
145
|
+
} else {
|
|
146
|
+
mode = TARGET_TO_MODE[val] ?? OperatingMode.STANDBY;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._applyModeAndDefaults(mode, wasOff && turningOn).catch(err =>
|
|
140
150
|
this.platform.log.error(`[${config.name}] setOperatingMode failed: ${err}`),
|
|
141
151
|
);
|
|
142
152
|
});
|
|
@@ -162,7 +172,7 @@ export class BedJetAccessory {
|
|
|
162
172
|
const mode = this.config.defaultMode
|
|
163
173
|
? DEFAULT_MODE_MAP[this.config.defaultMode]
|
|
164
174
|
: OperatingMode.HEAT;
|
|
165
|
-
this._applyModeAndDefaults(mode, true
|
|
175
|
+
this._applyModeAndDefaults(mode, true).catch(err =>
|
|
166
176
|
this.platform.log.error(`[${config.name}] turn on failed: ${err}`),
|
|
167
177
|
);
|
|
168
178
|
}
|
|
@@ -211,7 +221,6 @@ export class BedJetAccessory {
|
|
|
211
221
|
private async _applyModeAndDefaults(
|
|
212
222
|
mode: OperatingMode,
|
|
213
223
|
applyDefaults: boolean,
|
|
214
|
-
applyMode: boolean,
|
|
215
224
|
): Promise<void> {
|
|
216
225
|
const { config } = this;
|
|
217
226
|
|