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.
@@ -55,11 +55,10 @@
55
55
  .spinner {
56
56
  width: 13px;
57
57
  height: 13px;
58
- border: 2px solid rgba(255,255,255,0.15);
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-info .device-name { font-weight: 500; }
93
- .device-info .device-addr {
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: 4px;
103
+ gap: 5px;
105
104
  flex-shrink: 0;
105
+ min-width: 120px;
106
106
  }
107
107
 
108
108
  .add-btn {
109
- padding: 6px 15px;
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
- .inline-msg {
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
- .inline-msg.ok { color: #4caf50; }
130
- .inline-msg.err { color: #f28b82; }
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" onclick="startScan()">Scan for BedJets</button>
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
- const SCAN_MS = 12000;
159
- let scanning = false;
160
- const addedSet = new Set();
158
+ var SCAN_MS = 12000;
159
+ var scanning = false;
161
160
 
162
- // Safely get existing configured addresses
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
- // Run after homebridge is ready
174
- if (typeof homebridge !== 'undefined') {
175
- getExistingAddresses();
176
- } else {
177
- document.addEventListener('DOMContentLoaded', getExistingAddresses);
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
- const btn = document.getElementById('scanBtn');
185
- const status = document.getElementById('scanStatus');
186
- const pWrap = document.getElementById('progressWrap');
187
- const pFill = document.getElementById('progressFill');
188
- const results = document.getElementById('results');
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
- btn.disabled = true;
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> Scanning…';
190
+ status.innerHTML = '<div class="spinner"></div>&nbsp;Scanning…';
195
191
 
196
- let elapsed = 0;
197
- const tickMs = 200;
198
- const timer = setInterval(() => {
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
- const res = await homebridge.request('/scan');
201
+ var res = await homebridge.request('/scan');
206
202
  clearInterval(timer);
207
203
  pFill.style.width = '100%';
208
204
  status.innerHTML = '';
209
205
 
210
- await getExistingAddresses();
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(d => deviceCard(d)).join('');
210
+ results.innerHTML = devices.map(renderCard).join('');
217
211
  }
218
212
  } catch (err) {
219
213
  clearInterval(timer);
220
214
  status.innerHTML = '';
221
- const msg = (err && err.message) ? err.message : String(err);
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(() => { pWrap.style.display = 'none'; pFill.style.width = '0%'; }, 500);
225
- btn.disabled = false;
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
- function deviceCard(d) {
231
- const already = addedSet.has(d.address.toUpperCase());
232
- const id = 'btn_' + d.address.replace(/:/g, '');
233
- const mid = 'msg_' + d.address.replace(/:/g, '');
234
- return `
235
- <div class="device-card">
236
- <div class="device-info">
237
- <div class="device-name">${esc(d.name)}</div>
238
- <div class="device-addr">${esc(d.address)}</div>
239
- </div>
240
- <div class="device-right">
241
- <button id="${id}" class="add-btn${already ? ' done' : ''}"
242
- ${already ? 'disabled' : ''}
243
- onclick="addDevice(${JSON.stringify(d.name)}, ${JSON.stringify(d.address)}, '${id}', '${mid}')">
244
- ${already ? '✓ Already added' : 'Add to Config'}
245
- </button>
246
- <div id="${mid}" class="inline-msg"></div>
247
- </div>
248
- </div>`;
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
- async function addDevice(name, address, btnId, msgId) {
252
- const btn = document.getElementById(btnId);
253
- const msg = document.getElementById(msgId);
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
- // Get current config (returns array of platform configs)
262
- let configs = [];
263
- try { configs = await homebridge.getPluginConfig(); } catch (e) { configs = []; }
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
- btn.classList.add('done');
276
- msg.textContent = 'Already in config';
277
- msg.className = 'inline-msg ok';
278
- addedSet.add(address.toUpperCase());
279
- return;
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
- btn.classList.remove('done');
300
- const errMsg = (err && err.message) ? err.message : String(err);
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, '&amp;').replace(/</g, '&lt;')
309
- .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
297
+ .replace(/&/g, '&amp;')
298
+ .replace(/</g, '&lt;')
299
+ .replace(/>/g, '&gt;')
300
+ .replace(/"/g, '&quot;');
310
301
  }
311
302
  </script>
312
303
  </body>
@@ -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
- // Resolve node-ble from the plugin root (one level up from homebridge-ui/)
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', this.handleScan.bind(this));
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 = await device.getName().catch(() => null);
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();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-bedjet",
3
3
  "displayName": "BedJet",
4
- "version": "0.2.4",
4
+ "version": "0.2.6",
5
5
  "description": "Homebridge plugin for BedJet V3 via Bluetooth LE",
6
6
  "license": "MIT",
7
7
  "main": "dist/index.js",