homebridge-bedjet 0.2.4 → 0.2.6
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/homebridge-ui/public/index.html +106 -115
- package/homebridge-ui/server.js +76 -19
- package/package.json +1 -1
|
@@ -55,11 +55,10 @@
|
|
|
55
55
|
.spinner {
|
|
56
56
|
width: 13px;
|
|
57
57
|
height: 13px;
|
|
58
|
-
border: 2px solid rgba(
|
|
58
|
+
border: 2px solid rgba(128,128,128,0.3);
|
|
59
59
|
border-top-color: #e25c00;
|
|
60
60
|
border-radius: 50%;
|
|
61
61
|
animation: spin 0.7s linear infinite;
|
|
62
|
-
flex-shrink: 0;
|
|
63
62
|
}
|
|
64
63
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
65
64
|
|
|
@@ -89,8 +88,8 @@
|
|
|
89
88
|
gap: 10px;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
.device-
|
|
93
|
-
.device-
|
|
91
|
+
.device-name { font-weight: 500; }
|
|
92
|
+
.device-addr {
|
|
94
93
|
font-size: 11px;
|
|
95
94
|
opacity: 0.55;
|
|
96
95
|
font-family: 'SF Mono', 'Consolas', monospace;
|
|
@@ -101,12 +100,14 @@
|
|
|
101
100
|
display: flex;
|
|
102
101
|
flex-direction: column;
|
|
103
102
|
align-items: flex-end;
|
|
104
|
-
gap:
|
|
103
|
+
gap: 5px;
|
|
105
104
|
flex-shrink: 0;
|
|
105
|
+
min-width: 120px;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
.add-btn {
|
|
109
|
-
|
|
109
|
+
width: 100%;
|
|
110
|
+
padding: 6px 12px;
|
|
110
111
|
border: none;
|
|
111
112
|
border-radius: 5px;
|
|
112
113
|
font-size: 13px;
|
|
@@ -115,19 +116,18 @@
|
|
|
115
116
|
background: #1a7a40;
|
|
116
117
|
color: #fff;
|
|
117
118
|
transition: opacity 0.15s;
|
|
118
|
-
white-space: nowrap;
|
|
119
119
|
}
|
|
120
120
|
.add-btn:hover:not(:disabled) { opacity: 0.85; }
|
|
121
121
|
.add-btn:disabled { opacity: 0.55; cursor: default; }
|
|
122
|
-
.add-btn.done { background: #145c30; }
|
|
123
122
|
|
|
124
|
-
.
|
|
123
|
+
.card-msg {
|
|
125
124
|
font-size: 11px;
|
|
126
|
-
max-width: 140px;
|
|
127
125
|
text-align: right;
|
|
126
|
+
min-height: 14px;
|
|
127
|
+
width: 100%;
|
|
128
128
|
}
|
|
129
|
-
.
|
|
130
|
-
.
|
|
129
|
+
.card-msg.ok { color: #4caf50; }
|
|
130
|
+
.card-msg.err { color: #f28b82; }
|
|
131
131
|
|
|
132
132
|
.notice {
|
|
133
133
|
padding: 10px 13px;
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
<h6>Bluetooth Discovery</h6>
|
|
145
145
|
|
|
146
146
|
<div class="scan-row">
|
|
147
|
-
<button id="scanBtn"
|
|
147
|
+
<button id="scanBtn">Scan for BedJets</button>
|
|
148
148
|
<div id="scanStatus"></div>
|
|
149
149
|
</div>
|
|
150
150
|
|
|
@@ -155,158 +155,149 @@
|
|
|
155
155
|
<div id="results"></div>
|
|
156
156
|
|
|
157
157
|
<script>
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const addedSet = new Set();
|
|
158
|
+
var SCAN_MS = 12000;
|
|
159
|
+
var scanning = false;
|
|
161
160
|
|
|
162
|
-
//
|
|
163
|
-
async function getExistingAddresses() {
|
|
164
|
-
try {
|
|
165
|
-
const configs = await homebridge.getPluginConfig();
|
|
166
|
-
const cfg = configs && configs[0];
|
|
167
|
-
if (cfg && Array.isArray(cfg.devices)) {
|
|
168
|
-
cfg.devices.forEach(d => { if (d.address) addedSet.add(d.address.toUpperCase()); });
|
|
169
|
-
}
|
|
170
|
-
} catch (e) { /* ignore */ }
|
|
171
|
-
}
|
|
161
|
+
// ── Wire up buttons via addEventListener, never inline onclick ────────────
|
|
172
162
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
document.getElementById('scanBtn').addEventListener('click', startScan);
|
|
164
|
+
|
|
165
|
+
// Event delegation for dynamically created Add buttons
|
|
166
|
+
document.getElementById('results').addEventListener('click', function (e) {
|
|
167
|
+
var btn = e.target;
|
|
168
|
+
if (!btn.classList.contains('add-btn') || btn.disabled) return;
|
|
169
|
+
var name = btn.getAttribute('data-name');
|
|
170
|
+
var address = btn.getAttribute('data-address');
|
|
171
|
+
addDevice(btn, name, address);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ── Scan ──────────────────────────────────────────────────────────────────
|
|
179
175
|
|
|
180
176
|
async function startScan() {
|
|
181
177
|
if (scanning) return;
|
|
182
178
|
scanning = true;
|
|
183
179
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
180
|
+
var scanBtn = document.getElementById('scanBtn');
|
|
181
|
+
var status = document.getElementById('scanStatus');
|
|
182
|
+
var pWrap = document.getElementById('progressWrap');
|
|
183
|
+
var pFill = document.getElementById('progressFill');
|
|
184
|
+
var results = document.getElementById('results');
|
|
189
185
|
|
|
190
|
-
|
|
186
|
+
scanBtn.disabled = true;
|
|
191
187
|
results.innerHTML = '';
|
|
192
188
|
pFill.style.width = '0%';
|
|
193
189
|
pWrap.style.display = 'block';
|
|
194
|
-
status.innerHTML = '<div class="spinner"></div
|
|
190
|
+
status.innerHTML = '<div class="spinner"></div> Scanning…';
|
|
195
191
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
192
|
+
var elapsed = 0;
|
|
193
|
+
var tickMs = 200;
|
|
194
|
+
var timer = setInterval(function () {
|
|
199
195
|
elapsed += tickMs;
|
|
200
196
|
pFill.style.width = Math.min(100, (elapsed / SCAN_MS) * 100) + '%';
|
|
201
197
|
if (elapsed >= SCAN_MS) clearInterval(timer);
|
|
202
198
|
}, tickMs);
|
|
203
199
|
|
|
204
200
|
try {
|
|
205
|
-
|
|
201
|
+
var res = await homebridge.request('/scan');
|
|
206
202
|
clearInterval(timer);
|
|
207
203
|
pFill.style.width = '100%';
|
|
208
204
|
status.innerHTML = '';
|
|
209
205
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const devices = (res && res.devices) ? res.devices : [];
|
|
206
|
+
var devices = (res && Array.isArray(res.devices)) ? res.devices : [];
|
|
213
207
|
if (devices.length === 0) {
|
|
214
|
-
results.innerHTML = '<div class="notice">No BedJet devices found. Make sure your BedJet is powered on and not connected to another app.</div>';
|
|
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>';
|
|
215
209
|
} else {
|
|
216
|
-
results.innerHTML = devices.map(
|
|
210
|
+
results.innerHTML = devices.map(renderCard).join('');
|
|
217
211
|
}
|
|
218
212
|
} catch (err) {
|
|
219
213
|
clearInterval(timer);
|
|
220
214
|
status.innerHTML = '';
|
|
221
|
-
|
|
222
|
-
results.innerHTML = `<div class="notice error">Scan failed: ${esc(msg)}</div>`;
|
|
215
|
+
results.innerHTML = '<div class="notice error">Scan failed: ' + esc(err && err.message ? err.message : String(err)) + '</div>';
|
|
223
216
|
} finally {
|
|
224
|
-
setTimeout(()
|
|
225
|
-
|
|
217
|
+
setTimeout(function () { pWrap.style.display = 'none'; pFill.style.width = '0%'; }, 500);
|
|
218
|
+
scanBtn.disabled = false;
|
|
226
219
|
scanning = false;
|
|
227
220
|
}
|
|
228
221
|
}
|
|
229
222
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
223
|
+
// ── Card ──────────────────────────────────────────────────────────────────
|
|
224
|
+
// Device name/address stored in data-* attributes — no inline JS strings
|
|
225
|
+
|
|
226
|
+
function renderCard(d) {
|
|
227
|
+
var card = document.createElement('div');
|
|
228
|
+
card.className = 'device-card';
|
|
229
|
+
|
|
230
|
+
var info = document.createElement('div');
|
|
231
|
+
|
|
232
|
+
var nameEl = document.createElement('div');
|
|
233
|
+
nameEl.className = 'device-name';
|
|
234
|
+
nameEl.textContent = d.name;
|
|
235
|
+
|
|
236
|
+
var addrEl = document.createElement('div');
|
|
237
|
+
addrEl.className = 'device-addr';
|
|
238
|
+
addrEl.textContent = d.address;
|
|
239
|
+
|
|
240
|
+
info.appendChild(nameEl);
|
|
241
|
+
info.appendChild(addrEl);
|
|
242
|
+
|
|
243
|
+
var right = document.createElement('div');
|
|
244
|
+
right.className = 'device-right';
|
|
245
|
+
|
|
246
|
+
var btn = document.createElement('button');
|
|
247
|
+
btn.className = 'add-btn';
|
|
248
|
+
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
|
+
|
|
255
|
+
right.appendChild(btn);
|
|
256
|
+
right.appendChild(msgEl);
|
|
257
|
+
card.appendChild(info);
|
|
258
|
+
card.appendChild(right);
|
|
259
|
+
|
|
260
|
+
// Store msgEl reference on btn for easy access in handler
|
|
261
|
+
btn._msgEl = msgEl;
|
|
262
|
+
|
|
263
|
+
return card.outerHTML;
|
|
249
264
|
}
|
|
250
265
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
266
|
+
// ── Add device ─────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
async function addDevice(btn, name, address) {
|
|
269
|
+
// Find the sibling message element
|
|
270
|
+
var msg = btn.parentElement ? btn.parentElement.querySelector('.card-msg') : null;
|
|
254
271
|
|
|
255
272
|
btn.disabled = true;
|
|
256
273
|
btn.textContent = 'Saving…';
|
|
257
|
-
msg.textContent = '';
|
|
258
|
-
msg.className = 'inline-msg';
|
|
274
|
+
if (msg) { msg.textContent = ''; msg.className = 'card-msg'; }
|
|
259
275
|
|
|
260
276
|
try {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
let cfg = (configs && configs.length > 0) ? Object.assign({}, configs[0]) : {};
|
|
266
|
-
if (!cfg.platform) cfg.platform = 'BedJetPlatform';
|
|
267
|
-
if (!Array.isArray(cfg.devices)) cfg.devices = [];
|
|
268
|
-
|
|
269
|
-
// Deduplicate
|
|
270
|
-
const already = cfg.devices.some(
|
|
271
|
-
d => d.address && d.address.toUpperCase() === address.toUpperCase()
|
|
272
|
-
);
|
|
273
|
-
if (already) {
|
|
277
|
+
var result = await homebridge.request('/add-device', { name: name, address: address });
|
|
278
|
+
|
|
279
|
+
if (result && result.status === 'already_exists') {
|
|
274
280
|
btn.textContent = '✓ Already added';
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
if (msg) { msg.textContent = 'Already in config.'; msg.className = 'card-msg ok'; }
|
|
282
|
+
} else {
|
|
283
|
+
btn.textContent = '✓ Added!';
|
|
284
|
+
btn.style.background = '#145c30';
|
|
285
|
+
if (msg) { msg.textContent = 'Saved — restart Homebridge.'; msg.className = 'card-msg ok'; }
|
|
280
286
|
}
|
|
281
|
-
|
|
282
|
-
cfg.devices.push({ name: name, address: address, scanTimeout: 30 });
|
|
283
|
-
|
|
284
|
-
await homebridge.updatePluginConfig([cfg]);
|
|
285
|
-
await homebridge.savePluginConfig();
|
|
286
|
-
|
|
287
|
-
addedSet.add(address.toUpperCase());
|
|
288
|
-
btn.textContent = '✓ Added';
|
|
289
|
-
btn.classList.add('done');
|
|
290
|
-
msg.textContent = 'Saved! Restart Homebridge.';
|
|
291
|
-
msg.className = 'inline-msg ok';
|
|
292
|
-
|
|
293
|
-
// Try toast as a bonus (might not be available in all versions)
|
|
294
|
-
try { homebridge.toast.success(name + ' added. Restart Homebridge to activate.', 'Saved'); } catch (e) { /* ignore */ }
|
|
295
|
-
|
|
296
287
|
} catch (err) {
|
|
297
288
|
btn.disabled = false;
|
|
298
289
|
btn.textContent = 'Add to Config';
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
msg.textContent = 'Error: ' + errMsg;
|
|
302
|
-
msg.className = 'inline-msg err';
|
|
290
|
+
var errMsg = err && err.message ? err.message : String(err);
|
|
291
|
+
if (msg) { msg.textContent = 'Error: ' + errMsg; msg.className = 'card-msg err'; }
|
|
303
292
|
}
|
|
304
293
|
}
|
|
305
294
|
|
|
306
295
|
function esc(s) {
|
|
307
296
|
return String(s)
|
|
308
|
-
.replace(/&/g, '&')
|
|
309
|
-
.replace(
|
|
297
|
+
.replace(/&/g, '&')
|
|
298
|
+
.replace(/</g, '<')
|
|
299
|
+
.replace(/>/g, '>')
|
|
300
|
+
.replace(/"/g, '"');
|
|
310
301
|
}
|
|
311
302
|
</script>
|
|
312
303
|
</body>
|
package/homebridge-ui/server.js
CHANGED
|
@@ -2,20 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
5
7
|
|
|
6
|
-
//
|
|
8
|
+
// node-ble lives one level up in the plugin's own node_modules
|
|
7
9
|
const NodeBle = require(path.join(__dirname, '..', 'node_modules', 'node-ble'));
|
|
8
10
|
const { createBluetooth } = NodeBle;
|
|
9
11
|
|
|
10
12
|
const SCAN_DURATION_MS = 12000;
|
|
11
13
|
|
|
14
|
+
// homebridge-config-ui-x sets UIX_CONFIG_PATH when spawning this child process
|
|
15
|
+
function getConfigPath() {
|
|
16
|
+
if (process.env.UIX_CONFIG_PATH) return process.env.UIX_CONFIG_PATH;
|
|
17
|
+
// Common fallback locations
|
|
18
|
+
const candidates = [
|
|
19
|
+
path.join(os.homedir(), '.homebridge', 'config.json'),
|
|
20
|
+
'/var/lib/homebridge/config.json',
|
|
21
|
+
];
|
|
22
|
+
for (const c of candidates) {
|
|
23
|
+
if (fs.existsSync(c)) return c;
|
|
24
|
+
}
|
|
25
|
+
return path.join(os.homedir(), '.homebridge', 'config.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
class BedJetUiServer extends HomebridgePluginUiServer {
|
|
13
29
|
constructor() {
|
|
14
30
|
super();
|
|
15
|
-
this.onRequest('/scan',
|
|
31
|
+
this.onRequest('/scan', this.handleScan.bind(this));
|
|
32
|
+
this.onRequest('/add-device', this.handleAddDevice.bind(this));
|
|
16
33
|
this.ready();
|
|
17
34
|
}
|
|
18
35
|
|
|
36
|
+
// ── BLE scan ────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
19
38
|
async handleScan() {
|
|
20
39
|
let destroy = null;
|
|
21
40
|
const found = [];
|
|
@@ -25,43 +44,81 @@ class BedJetUiServer extends HomebridgePluginUiServer {
|
|
|
25
44
|
destroy = ble.destroy;
|
|
26
45
|
|
|
27
46
|
const adapter = await ble.bluetooth.defaultAdapter();
|
|
28
|
-
|
|
29
47
|
const wasDiscovering = await adapter.isDiscovering();
|
|
30
|
-
if (!wasDiscovering)
|
|
31
|
-
await adapter.startDiscovery();
|
|
32
|
-
}
|
|
48
|
+
if (!wasDiscovering) await adapter.startDiscovery();
|
|
33
49
|
|
|
34
|
-
// Scan for SCAN_DURATION_MS
|
|
35
50
|
await new Promise(resolve => setTimeout(resolve, SCAN_DURATION_MS));
|
|
36
51
|
|
|
37
|
-
if (!wasDiscovering) {
|
|
38
|
-
await adapter.stopDiscovery().catch(() => {});
|
|
39
|
-
}
|
|
52
|
+
if (!wasDiscovering) await adapter.stopDiscovery().catch(() => {});
|
|
40
53
|
|
|
41
|
-
// Query all devices BlueZ discovered
|
|
42
54
|
const addresses = await adapter.devices();
|
|
43
|
-
|
|
44
55
|
for (const address of addresses) {
|
|
45
56
|
try {
|
|
46
57
|
const device = await adapter.getDevice(address);
|
|
47
|
-
const name
|
|
58
|
+
const name = await device.getName().catch(() => null);
|
|
48
59
|
if (name && name.toUpperCase().includes('BEDJET')) {
|
|
49
60
|
found.push({ name, address });
|
|
50
61
|
}
|
|
51
|
-
} catch {
|
|
52
|
-
// ignore individual device errors
|
|
53
|
-
}
|
|
62
|
+
} catch { /* skip device */ }
|
|
54
63
|
}
|
|
55
64
|
} catch (err) {
|
|
56
65
|
throw new Error(`Bluetooth scan failed: ${err.message || err}`);
|
|
57
66
|
} finally {
|
|
58
|
-
if (destroy) {
|
|
59
|
-
try { destroy(); } catch { /* ignore */ }
|
|
60
|
-
}
|
|
67
|
+
if (destroy) { try { destroy(); } catch { /* ignore */ } }
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
return { devices: found };
|
|
64
71
|
}
|
|
72
|
+
|
|
73
|
+
// ── Config write ─────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
async handleAddDevice(body) {
|
|
76
|
+
const { name, address } = body || {};
|
|
77
|
+
|
|
78
|
+
if (!name || !address) {
|
|
79
|
+
throw new Error('Missing name or address');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const configPath = getConfigPath();
|
|
83
|
+
|
|
84
|
+
let raw;
|
|
85
|
+
try {
|
|
86
|
+
raw = fs.readFileSync(configPath, 'utf8');
|
|
87
|
+
} catch (err) {
|
|
88
|
+
throw new Error(`Cannot read config file (${configPath}): ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let config;
|
|
92
|
+
try {
|
|
93
|
+
config = JSON.parse(raw);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
throw new Error(`Config file is not valid JSON: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!Array.isArray(config.platforms)) config.platforms = [];
|
|
99
|
+
|
|
100
|
+
let platform = config.platforms.find(p => p.platform === 'BedJetPlatform');
|
|
101
|
+
if (!platform) {
|
|
102
|
+
platform = { platform: 'BedJetPlatform', name: 'BedJetPlatform', devices: [] };
|
|
103
|
+
config.platforms.push(platform);
|
|
104
|
+
}
|
|
105
|
+
if (!Array.isArray(platform.devices)) platform.devices = [];
|
|
106
|
+
|
|
107
|
+
const addrUp = address.toUpperCase();
|
|
108
|
+
if (platform.devices.some(d => d.address && d.address.toUpperCase() === addrUp)) {
|
|
109
|
+
return { status: 'already_exists' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
platform.devices.push({ name, address, scanTimeout: 30 });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 4), 'utf8');
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw new Error(`Cannot write config file: ${err.message}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { status: 'ok' };
|
|
121
|
+
}
|
|
65
122
|
}
|
|
66
123
|
|
|
67
124
|
new BedJetUiServer();
|