homebridge-bedjet 0.2.8 → 0.3.0
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/README.md +2 -0
- package/dist/accessory.js +17 -2
- package/homebridge-ui/public/index.html +196 -57
- package/homebridge-ui/server.js +7 -2
- package/package.json +1 -1
- package/src/accessory.ts +18 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# homebridge-bedjet
|
|
2
2
|
|
|
3
|
+
DISCLAIMER - This project is entirely vibecoded. I don't have development experience.
|
|
4
|
+
|
|
3
5
|
Homebridge plugin for the **BedJet V3** climate comfort system via Bluetooth LE.
|
|
4
6
|
|
|
5
7
|
Exposes a **Thermostat** and a **Fan** accessory in HomeKit so you can control temperature and fan speed from the Home app, Siri, or automations — without needing the BedJet cloud or the BedJet mobile app running.
|
package/dist/accessory.js
CHANGED
|
@@ -10,6 +10,20 @@ const DEFAULT_MODE_MAP = {
|
|
|
10
10
|
cool: constants_1.OperatingMode.COOL,
|
|
11
11
|
dry: constants_1.OperatingMode.DRY,
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* HomeKit only allows alphanumeric, space, and apostrophe characters in names,
|
|
15
|
+
* starting and ending with alphanumeric. Underscores and other punctuation
|
|
16
|
+
* cause HAP warnings and may prevent the accessory from pairing.
|
|
17
|
+
*/
|
|
18
|
+
function sanitizeName(name) {
|
|
19
|
+
return name
|
|
20
|
+
.replace(/_/g, ' ') // underscores → spaces
|
|
21
|
+
.replace(/[^a-zA-Z0-9 ']/g, ' ') // anything else invalid → space
|
|
22
|
+
.replace(/\s+/g, ' ') // collapse multiple spaces
|
|
23
|
+
.trim()
|
|
24
|
+
.replace(/^[^a-zA-Z0-9]+/, '') // must start with alphanumeric
|
|
25
|
+
.replace(/[^a-zA-Z0-9]+$/, '') || 'BedJet'; // must end with alphanumeric
|
|
26
|
+
}
|
|
13
27
|
// OperatingMode → CurrentHeatingCoolingState value
|
|
14
28
|
const CURRENT_STATE_MAP = {
|
|
15
29
|
[constants_1.OperatingMode.STANDBY]: 0, // OFF
|
|
@@ -52,6 +66,7 @@ class BedJetAccessory {
|
|
|
52
66
|
this.pendingMode = null;
|
|
53
67
|
this.pendingModeTimer = null;
|
|
54
68
|
const { Service, Characteristic } = platform.api.hap;
|
|
69
|
+
const safeName = sanitizeName(config.name);
|
|
55
70
|
// AccessoryInformation
|
|
56
71
|
const infoService = this.accessory.getService(Service.AccessoryInformation)
|
|
57
72
|
?? this.accessory.addService(Service.AccessoryInformation);
|
|
@@ -61,7 +76,7 @@ class BedJetAccessory {
|
|
|
61
76
|
.setCharacteristic(Characteristic.SerialNumber, config.address);
|
|
62
77
|
// Thermostat service
|
|
63
78
|
this.thermostatService = this.accessory.getService(Service.Thermostat)
|
|
64
|
-
?? this.accessory.addService(Service.Thermostat,
|
|
79
|
+
?? this.accessory.addService(Service.Thermostat, safeName, 'thermostat');
|
|
65
80
|
this.thermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits)
|
|
66
81
|
.onGet(() => Characteristic.TemperatureDisplayUnits.CELSIUS);
|
|
67
82
|
this.thermostatService.getCharacteristic(Characteristic.CurrentTemperature)
|
|
@@ -100,7 +115,7 @@ class BedJetAccessory {
|
|
|
100
115
|
});
|
|
101
116
|
// FanV2 service
|
|
102
117
|
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
103
|
-
?? this.accessory.addService(Service.Fanv2, `${
|
|
118
|
+
?? this.accessory.addService(Service.Fanv2, `${safeName} Fan`, 'fan');
|
|
104
119
|
this.fanService.getCharacteristic(Characteristic.Active)
|
|
105
120
|
.onGet(() => this.bedjet.state.operatingMode !== constants_1.OperatingMode.STANDBY
|
|
106
121
|
? Characteristic.Active.ACTIVE
|
|
@@ -77,15 +77,20 @@
|
|
|
77
77
|
transition: width 0.3s linear;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/* ── Device card ─────────────────────────────────────────────────────── */
|
|
81
|
+
|
|
80
82
|
.device-card {
|
|
81
83
|
border: 1px solid rgba(128,128,128,0.25);
|
|
82
84
|
border-radius: 8px;
|
|
83
|
-
padding:
|
|
84
|
-
margin-bottom:
|
|
85
|
+
padding: 12px 13px;
|
|
86
|
+
margin-bottom: 10px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.card-header {
|
|
85
90
|
display: flex;
|
|
86
91
|
align-items: center;
|
|
87
92
|
justify-content: space-between;
|
|
88
|
-
|
|
93
|
+
margin-bottom: 10px;
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
.device-name { font-weight: 500; }
|
|
@@ -96,18 +101,68 @@
|
|
|
96
101
|
margin-top: 2px;
|
|
97
102
|
}
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
/* ── Defaults row ────────────────────────────────────────────────────── */
|
|
105
|
+
|
|
106
|
+
.defaults-row {
|
|
100
107
|
display: flex;
|
|
101
|
-
|
|
108
|
+
gap: 8px;
|
|
102
109
|
align-items: flex-end;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
min-width: 120px;
|
|
110
|
+
flex-wrap: wrap;
|
|
111
|
+
margin-bottom: 10px;
|
|
106
112
|
}
|
|
107
113
|
|
|
108
|
-
.
|
|
114
|
+
.field {
|
|
115
|
+
display: flex;
|
|
116
|
+
flex-direction: column;
|
|
117
|
+
gap: 3px;
|
|
118
|
+
flex: 1;
|
|
119
|
+
min-width: 80px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.field label {
|
|
123
|
+
font-size: 11px;
|
|
124
|
+
opacity: 0.55;
|
|
125
|
+
font-weight: 500;
|
|
126
|
+
text-transform: uppercase;
|
|
127
|
+
letter-spacing: 0.04em;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.field select,
|
|
131
|
+
.field input[type="number"] {
|
|
132
|
+
padding: 5px 7px;
|
|
133
|
+
border-radius: 5px;
|
|
134
|
+
border: 1px solid rgba(128,128,128,0.35);
|
|
135
|
+
background: rgba(128,128,128,0.08);
|
|
136
|
+
color: inherit;
|
|
137
|
+
font-size: 13px;
|
|
109
138
|
width: 100%;
|
|
110
|
-
|
|
139
|
+
box-sizing: border-box;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.field select:focus,
|
|
143
|
+
.field input[type="number"]:focus {
|
|
144
|
+
outline: none;
|
|
145
|
+
border-color: #e25c00;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ── Footer row (button + message) ──────────────────────────────────── */
|
|
149
|
+
|
|
150
|
+
.card-footer {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.card-msg {
|
|
158
|
+
font-size: 12px;
|
|
159
|
+
flex: 1;
|
|
160
|
+
}
|
|
161
|
+
.card-msg.ok { color: #4caf50; }
|
|
162
|
+
.card-msg.err { color: #f28b82; }
|
|
163
|
+
|
|
164
|
+
.add-btn {
|
|
165
|
+
padding: 6px 16px;
|
|
111
166
|
border: none;
|
|
112
167
|
border-radius: 5px;
|
|
113
168
|
font-size: 13px;
|
|
@@ -116,18 +171,13 @@
|
|
|
116
171
|
background: #1a7a40;
|
|
117
172
|
color: #fff;
|
|
118
173
|
transition: opacity 0.15s;
|
|
174
|
+
white-space: nowrap;
|
|
175
|
+
flex-shrink: 0;
|
|
119
176
|
}
|
|
120
177
|
.add-btn:hover:not(:disabled) { opacity: 0.85; }
|
|
121
178
|
.add-btn:disabled { opacity: 0.55; cursor: default; }
|
|
122
179
|
|
|
123
|
-
|
|
124
|
-
font-size: 11px;
|
|
125
|
-
text-align: right;
|
|
126
|
-
min-height: 14px;
|
|
127
|
-
width: 100%;
|
|
128
|
-
}
|
|
129
|
-
.card-msg.ok { color: #4caf50; }
|
|
130
|
-
.card-msg.err { color: #f28b82; }
|
|
180
|
+
/* ── Notices ─────────────────────────────────────────────────────────── */
|
|
131
181
|
|
|
132
182
|
.notice {
|
|
133
183
|
padding: 10px 13px;
|
|
@@ -158,17 +208,38 @@
|
|
|
158
208
|
var SCAN_MS = 12000;
|
|
159
209
|
var scanning = false;
|
|
160
210
|
|
|
161
|
-
|
|
211
|
+
var MODES = [
|
|
212
|
+
{ value: 'heat', label: 'Heat' },
|
|
213
|
+
{ value: 'turbo', label: 'Turbo' },
|
|
214
|
+
{ value: 'extendedHeat', label: 'Extended Heat' },
|
|
215
|
+
{ value: 'cool', label: 'Cool (Fan Only)' },
|
|
216
|
+
{ value: 'dry', label: 'Dry' },
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
// ── Scan button ───────────────────────────────────────────────────────────
|
|
162
220
|
|
|
163
221
|
document.getElementById('scanBtn').addEventListener('click', startScan);
|
|
164
222
|
|
|
165
|
-
// Event delegation for
|
|
223
|
+
// Event delegation for Add buttons
|
|
166
224
|
document.getElementById('results').addEventListener('click', function (e) {
|
|
167
225
|
var btn = e.target;
|
|
168
226
|
if (!btn.classList.contains('add-btn') || btn.disabled) return;
|
|
169
|
-
var
|
|
170
|
-
|
|
171
|
-
|
|
227
|
+
var card = btn.closest('.device-card');
|
|
228
|
+
if (!card) return;
|
|
229
|
+
|
|
230
|
+
var name = card.getAttribute('data-name');
|
|
231
|
+
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);
|
|
172
243
|
});
|
|
173
244
|
|
|
174
245
|
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
@@ -177,11 +248,11 @@
|
|
|
177
248
|
if (scanning) return;
|
|
178
249
|
scanning = true;
|
|
179
250
|
|
|
180
|
-
var scanBtn
|
|
181
|
-
var status
|
|
182
|
-
var pWrap
|
|
183
|
-
var pFill
|
|
184
|
-
var results
|
|
251
|
+
var scanBtn = document.getElementById('scanBtn');
|
|
252
|
+
var status = document.getElementById('scanStatus');
|
|
253
|
+
var pWrap = document.getElementById('progressWrap');
|
|
254
|
+
var pFill = document.getElementById('progressFill');
|
|
255
|
+
var results = document.getElementById('results');
|
|
185
256
|
|
|
186
257
|
scanBtn.disabled = true;
|
|
187
258
|
results.innerHTML = '';
|
|
@@ -205,9 +276,13 @@
|
|
|
205
276
|
|
|
206
277
|
var devices = (res && Array.isArray(res.devices)) ? res.devices : [];
|
|
207
278
|
if (devices.length === 0) {
|
|
208
|
-
results.innerHTML = '<div class="notice">No BedJet devices found nearby. Make sure your BedJet is powered on and not connected to another app, then try again.</div>';
|
|
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>';
|
|
209
280
|
} else {
|
|
210
|
-
|
|
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
|
+
});
|
|
211
286
|
}
|
|
212
287
|
} catch (err) {
|
|
213
288
|
clearInterval(timer);
|
|
@@ -220,61 +295,123 @@
|
|
|
220
295
|
}
|
|
221
296
|
}
|
|
222
297
|
|
|
223
|
-
// ── Card
|
|
224
|
-
// Device name/address stored in data-* attributes — no inline JS strings
|
|
298
|
+
// ── Card builder (pure DOM, no innerHTML with user data) ──────────────────
|
|
225
299
|
|
|
226
|
-
function
|
|
300
|
+
function buildCard(d) {
|
|
227
301
|
var card = document.createElement('div');
|
|
228
302
|
card.className = 'device-card';
|
|
303
|
+
card.setAttribute('data-name', d.name);
|
|
304
|
+
card.setAttribute('data-address', d.address);
|
|
229
305
|
|
|
230
|
-
|
|
306
|
+
// — Header: name + address —
|
|
307
|
+
var header = document.createElement('div');
|
|
308
|
+
header.className = 'card-header';
|
|
231
309
|
|
|
310
|
+
var info = document.createElement('div');
|
|
232
311
|
var nameEl = document.createElement('div');
|
|
233
312
|
nameEl.className = 'device-name';
|
|
234
313
|
nameEl.textContent = d.name;
|
|
235
|
-
|
|
236
314
|
var addrEl = document.createElement('div');
|
|
237
315
|
addrEl.className = 'device-addr';
|
|
238
316
|
addrEl.textContent = d.address;
|
|
239
|
-
|
|
240
317
|
info.appendChild(nameEl);
|
|
241
318
|
info.appendChild(addrEl);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
319
|
+
header.appendChild(info);
|
|
320
|
+
card.appendChild(header);
|
|
321
|
+
|
|
322
|
+
// — Defaults row —
|
|
323
|
+
var row = document.createElement('div');
|
|
324
|
+
row.className = 'defaults-row';
|
|
325
|
+
|
|
326
|
+
// Mode select
|
|
327
|
+
var modeField = document.createElement('div');
|
|
328
|
+
modeField.className = 'field';
|
|
329
|
+
modeField.style.flex = '2';
|
|
330
|
+
var modeLabel = document.createElement('label');
|
|
331
|
+
modeLabel.textContent = 'Default Mode';
|
|
332
|
+
var modeSelect = document.createElement('select');
|
|
333
|
+
modeSelect.className = 'field-mode';
|
|
334
|
+
MODES.forEach(function (m) {
|
|
335
|
+
var opt = document.createElement('option');
|
|
336
|
+
opt.value = m.value;
|
|
337
|
+
opt.textContent = m.label;
|
|
338
|
+
if (m.value === 'heat') opt.selected = true;
|
|
339
|
+
modeSelect.appendChild(opt);
|
|
340
|
+
});
|
|
341
|
+
modeField.appendChild(modeLabel);
|
|
342
|
+
modeField.appendChild(modeSelect);
|
|
343
|
+
row.appendChild(modeField);
|
|
344
|
+
|
|
345
|
+
// Temperature input
|
|
346
|
+
var tempField = document.createElement('div');
|
|
347
|
+
tempField.className = 'field';
|
|
348
|
+
var tempLabel = document.createElement('label');
|
|
349
|
+
tempLabel.textContent = 'Temp °C';
|
|
350
|
+
var tempInput = document.createElement('input');
|
|
351
|
+
tempInput.type = 'number';
|
|
352
|
+
tempInput.className = 'field-temp';
|
|
353
|
+
tempInput.min = '19';
|
|
354
|
+
tempInput.max = '43';
|
|
355
|
+
tempInput.step = '0.5';
|
|
356
|
+
tempInput.placeholder = 'Optional';
|
|
357
|
+
tempField.appendChild(tempLabel);
|
|
358
|
+
tempField.appendChild(tempInput);
|
|
359
|
+
row.appendChild(tempField);
|
|
360
|
+
|
|
361
|
+
// Fan speed input
|
|
362
|
+
var fanField = document.createElement('div');
|
|
363
|
+
fanField.className = 'field';
|
|
364
|
+
var fanLabel = document.createElement('label');
|
|
365
|
+
fanLabel.textContent = 'Fan %';
|
|
366
|
+
var fanInput = document.createElement('input');
|
|
367
|
+
fanInput.type = 'number';
|
|
368
|
+
fanInput.className = 'field-fan';
|
|
369
|
+
fanInput.min = '5';
|
|
370
|
+
fanInput.max = '100';
|
|
371
|
+
fanInput.step = '5';
|
|
372
|
+
fanInput.placeholder = 'Optional';
|
|
373
|
+
fanField.appendChild(fanLabel);
|
|
374
|
+
fanField.appendChild(fanInput);
|
|
375
|
+
row.appendChild(fanField);
|
|
376
|
+
|
|
377
|
+
card.appendChild(row);
|
|
378
|
+
|
|
379
|
+
// — Footer: message + button —
|
|
380
|
+
var footer = document.createElement('div');
|
|
381
|
+
footer.className = 'card-footer';
|
|
382
|
+
|
|
383
|
+
var msg = document.createElement('div');
|
|
384
|
+
msg.className = 'card-msg';
|
|
245
385
|
|
|
246
386
|
var btn = document.createElement('button');
|
|
247
387
|
btn.className = 'add-btn';
|
|
248
388
|
btn.textContent = 'Add to Config';
|
|
249
|
-
btn.setAttribute('data-name', d.name);
|
|
250
|
-
btn.setAttribute('data-address', d.address);
|
|
251
|
-
|
|
252
|
-
var msgEl = document.createElement('div');
|
|
253
|
-
msgEl.className = 'card-msg';
|
|
254
389
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
card.appendChild(
|
|
258
|
-
card.appendChild(right);
|
|
390
|
+
footer.appendChild(msg);
|
|
391
|
+
footer.appendChild(btn);
|
|
392
|
+
card.appendChild(footer);
|
|
259
393
|
|
|
260
|
-
|
|
261
|
-
btn._msgEl = msgEl;
|
|
262
|
-
|
|
263
|
-
return card.outerHTML;
|
|
394
|
+
return card;
|
|
264
395
|
}
|
|
265
396
|
|
|
266
|
-
// ── Add device
|
|
397
|
+
// ── Add device ────────────────────────────────────────────────────────────
|
|
267
398
|
|
|
268
|
-
async function addDevice(btn, name, address) {
|
|
269
|
-
|
|
270
|
-
var msg = btn.parentElement ? btn.parentElement.querySelector('.card-msg') : null;
|
|
399
|
+
async function addDevice(btn, card, name, address, defaultMode, defaultTemperature, defaultFanSpeed) {
|
|
400
|
+
var msg = card.querySelector('.card-msg');
|
|
271
401
|
|
|
272
402
|
btn.disabled = true;
|
|
273
403
|
btn.textContent = 'Saving…';
|
|
274
404
|
if (msg) { msg.textContent = ''; msg.className = 'card-msg'; }
|
|
275
405
|
|
|
406
|
+
// Lock inputs while saving
|
|
407
|
+
card.querySelectorAll('select, input').forEach(function (el) { el.disabled = true; });
|
|
408
|
+
|
|
276
409
|
try {
|
|
277
|
-
var
|
|
410
|
+
var payload = { name: name, address: address, defaultMode: defaultMode };
|
|
411
|
+
if (defaultTemperature !== null) payload.defaultTemperature = defaultTemperature;
|
|
412
|
+
if (defaultFanSpeed !== null) payload.defaultFanSpeed = defaultFanSpeed;
|
|
413
|
+
|
|
414
|
+
var result = await homebridge.request('/add-device', payload);
|
|
278
415
|
|
|
279
416
|
if (result && result.status === 'already_exists') {
|
|
280
417
|
btn.textContent = '✓ Already added';
|
|
@@ -285,8 +422,10 @@
|
|
|
285
422
|
if (msg) { msg.textContent = 'Saved — restart Homebridge.'; msg.className = 'card-msg ok'; }
|
|
286
423
|
}
|
|
287
424
|
} catch (err) {
|
|
425
|
+
// Re-enable on failure so the user can retry
|
|
288
426
|
btn.disabled = false;
|
|
289
427
|
btn.textContent = 'Add to Config';
|
|
428
|
+
card.querySelectorAll('select, input').forEach(function (el) { el.disabled = false; });
|
|
290
429
|
var errMsg = err && err.message ? err.message : String(err);
|
|
291
430
|
if (msg) { msg.textContent = 'Error: ' + errMsg; msg.className = 'card-msg err'; }
|
|
292
431
|
}
|
package/homebridge-ui/server.js
CHANGED
|
@@ -73,7 +73,7 @@ class BedJetUiServer extends HomebridgePluginUiServer {
|
|
|
73
73
|
// ── Config write ─────────────────────────────────────────────────────────────
|
|
74
74
|
|
|
75
75
|
async handleAddDevice(body) {
|
|
76
|
-
const { name, address } = body || {};
|
|
76
|
+
const { name, address, defaultMode, defaultTemperature, defaultFanSpeed } = body || {};
|
|
77
77
|
|
|
78
78
|
if (!name || !address) {
|
|
79
79
|
throw new Error('Missing name or address');
|
|
@@ -109,7 +109,12 @@ class BedJetUiServer extends HomebridgePluginUiServer {
|
|
|
109
109
|
return { status: 'already_exists' };
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
const entry = { name, address, scanTimeout: 30 };
|
|
113
|
+
if (defaultMode) entry.defaultMode = defaultMode;
|
|
114
|
+
if (defaultTemperature !== undefined && defaultTemperature !== null) entry.defaultTemperature = Number(defaultTemperature);
|
|
115
|
+
if (defaultFanSpeed !== undefined && defaultFanSpeed !== null) entry.defaultFanSpeed = Number(defaultFanSpeed);
|
|
116
|
+
|
|
117
|
+
platform.devices.push(entry);
|
|
113
118
|
|
|
114
119
|
try {
|
|
115
120
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'utf8');
|
package/package.json
CHANGED
package/src/accessory.ts
CHANGED
|
@@ -12,6 +12,21 @@ const DEFAULT_MODE_MAP: Record<DefaultMode, OperatingMode> = {
|
|
|
12
12
|
dry: OperatingMode.DRY,
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* HomeKit only allows alphanumeric, space, and apostrophe characters in names,
|
|
17
|
+
* starting and ending with alphanumeric. Underscores and other punctuation
|
|
18
|
+
* cause HAP warnings and may prevent the accessory from pairing.
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeName(name: string): string {
|
|
21
|
+
return name
|
|
22
|
+
.replace(/_/g, ' ') // underscores → spaces
|
|
23
|
+
.replace(/[^a-zA-Z0-9 ']/g, ' ') // anything else invalid → space
|
|
24
|
+
.replace(/\s+/g, ' ') // collapse multiple spaces
|
|
25
|
+
.trim()
|
|
26
|
+
.replace(/^[^a-zA-Z0-9]+/, '') // must start with alphanumeric
|
|
27
|
+
.replace(/[^a-zA-Z0-9]+$/, '') || 'BedJet'; // must end with alphanumeric
|
|
28
|
+
}
|
|
29
|
+
|
|
15
30
|
// OperatingMode → CurrentHeatingCoolingState value
|
|
16
31
|
const CURRENT_STATE_MAP: Record<OperatingMode, number> = {
|
|
17
32
|
[OperatingMode.STANDBY]: 0, // OFF
|
|
@@ -69,6 +84,7 @@ export class BedJetAccessory {
|
|
|
69
84
|
private readonly config: BedJetConfig,
|
|
70
85
|
) {
|
|
71
86
|
const { Service, Characteristic } = platform.api.hap;
|
|
87
|
+
const safeName = sanitizeName(config.name);
|
|
72
88
|
|
|
73
89
|
// AccessoryInformation
|
|
74
90
|
const infoService = this.accessory.getService(Service.AccessoryInformation)
|
|
@@ -80,7 +96,7 @@ export class BedJetAccessory {
|
|
|
80
96
|
|
|
81
97
|
// Thermostat service
|
|
82
98
|
this.thermostatService = this.accessory.getService(Service.Thermostat)
|
|
83
|
-
?? this.accessory.addService(Service.Thermostat,
|
|
99
|
+
?? this.accessory.addService(Service.Thermostat, safeName, 'thermostat');
|
|
84
100
|
|
|
85
101
|
this.thermostatService.getCharacteristic(Characteristic.TemperatureDisplayUnits)
|
|
86
102
|
.onGet(() => Characteristic.TemperatureDisplayUnits.CELSIUS);
|
|
@@ -127,7 +143,7 @@ export class BedJetAccessory {
|
|
|
127
143
|
|
|
128
144
|
// FanV2 service
|
|
129
145
|
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
130
|
-
?? this.accessory.addService(Service.Fanv2, `${
|
|
146
|
+
?? this.accessory.addService(Service.Fanv2, `${safeName} Fan`, 'fan');
|
|
131
147
|
|
|
132
148
|
this.fanService.getCharacteristic(Characteristic.Active)
|
|
133
149
|
.onGet(() =>
|