homebridge-bedjet 0.2.1 → 0.2.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.
@@ -2,6 +2,7 @@
2
2
  "pluginAlias": "BedJetPlatform",
3
3
  "pluginType": "platform",
4
4
  "singular": false,
5
+ "customUi": true,
5
6
  "schema": {
6
7
  "type": "object",
7
8
  "properties": {
@@ -0,0 +1,307 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>BedJet Discovery</title>
7
+ <style>
8
+ :root {
9
+ --card-bg: #1e1e1e;
10
+ --card-border: #3a3a3a;
11
+ --text: #e0e0e0;
12
+ --text-muted: #888;
13
+ --btn-bg: #c25700;
14
+ --btn-hover: #d96200;
15
+ --btn-text: #fff;
16
+ --add-bg: #1a6b3a;
17
+ --add-hover: #1f8047;
18
+ --done-bg: #2a4a2a;
19
+ --done-text: #6fcf97;
20
+ --error-bg: #4a1a1a;
21
+ --error-text: #f28b82;
22
+ --row-border: #2e2e2e;
23
+ --spinner-color: #c25700;
24
+ --address-color: #aaa;
25
+ }
26
+
27
+ body {
28
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
+ margin: 0;
30
+ padding: 16px;
31
+ background: transparent;
32
+ color: var(--text);
33
+ font-size: 14px;
34
+ }
35
+
36
+ h6 {
37
+ font-size: 13px;
38
+ font-weight: 600;
39
+ text-transform: uppercase;
40
+ letter-spacing: 0.05em;
41
+ color: var(--text-muted);
42
+ margin: 0 0 12px;
43
+ }
44
+
45
+ .scan-row {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 12px;
49
+ margin-bottom: 16px;
50
+ }
51
+
52
+ button {
53
+ padding: 8px 18px;
54
+ border: none;
55
+ border-radius: 6px;
56
+ font-size: 14px;
57
+ font-weight: 500;
58
+ cursor: pointer;
59
+ transition: background 0.15s;
60
+ }
61
+
62
+ #scanBtn {
63
+ background: var(--btn-bg);
64
+ color: var(--btn-text);
65
+ }
66
+ #scanBtn:hover:not(:disabled) { background: var(--btn-hover); }
67
+ #scanBtn:disabled { opacity: 0.5; cursor: not-allowed; }
68
+
69
+ #status {
70
+ font-size: 13px;
71
+ color: var(--text-muted);
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 8px;
75
+ }
76
+
77
+ .spinner {
78
+ width: 14px;
79
+ height: 14px;
80
+ border: 2px solid transparent;
81
+ border-top-color: var(--spinner-color);
82
+ border-radius: 50%;
83
+ animation: spin 0.7s linear infinite;
84
+ flex-shrink: 0;
85
+ }
86
+ @keyframes spin { to { transform: rotate(360deg); } }
87
+
88
+ .progress-bar-wrap {
89
+ height: 3px;
90
+ background: var(--card-border);
91
+ border-radius: 2px;
92
+ overflow: hidden;
93
+ margin-bottom: 14px;
94
+ }
95
+ .progress-bar-fill {
96
+ height: 100%;
97
+ background: var(--btn-bg);
98
+ width: 0%;
99
+ transition: width 0.4s linear;
100
+ border-radius: 2px;
101
+ }
102
+
103
+ #results {
104
+ margin-top: 4px;
105
+ }
106
+
107
+ .device-card {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: space-between;
111
+ padding: 10px 12px;
112
+ border-radius: 6px;
113
+ border: 1px solid var(--card-border);
114
+ background: var(--card-bg);
115
+ margin-bottom: 8px;
116
+ }
117
+
118
+ .device-info {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 2px;
122
+ }
123
+
124
+ .device-name {
125
+ font-weight: 500;
126
+ }
127
+
128
+ .device-address {
129
+ font-size: 12px;
130
+ color: var(--address-color);
131
+ font-family: 'SF Mono', 'Consolas', monospace;
132
+ }
133
+
134
+ .add-btn {
135
+ background: var(--add-bg);
136
+ color: #fff;
137
+ padding: 6px 14px;
138
+ font-size: 13px;
139
+ border-radius: 5px;
140
+ flex-shrink: 0;
141
+ }
142
+ .add-btn:hover:not(:disabled) { background: var(--add-hover); }
143
+ .add-btn:disabled {
144
+ background: var(--done-bg);
145
+ color: var(--done-text);
146
+ cursor: default;
147
+ }
148
+
149
+ .empty-state, .error-state {
150
+ padding: 12px 14px;
151
+ border-radius: 6px;
152
+ font-size: 13px;
153
+ }
154
+
155
+ .empty-state {
156
+ background: var(--card-bg);
157
+ border: 1px solid var(--card-border);
158
+ color: var(--text-muted);
159
+ }
160
+
161
+ .error-state {
162
+ background: var(--error-bg);
163
+ border: 1px solid #6b2020;
164
+ color: var(--error-text);
165
+ }
166
+ </style>
167
+ </head>
168
+ <body>
169
+
170
+ <h6>Bluetooth Discovery</h6>
171
+
172
+ <div class="scan-row">
173
+ <button id="scanBtn" onclick="startScan()">Scan for BedJets</button>
174
+ <div id="status"></div>
175
+ </div>
176
+
177
+ <div class="progress-bar-wrap" id="progressWrap" style="display:none">
178
+ <div class="progress-bar-fill" id="progressFill"></div>
179
+ </div>
180
+
181
+ <div id="results"></div>
182
+
183
+ <script>
184
+ const SCAN_DURATION_MS = 12000;
185
+ let scanning = false;
186
+ let existingAddresses = new Set();
187
+
188
+ // Load existing configured addresses so we can mark them
189
+ async function loadExisting() {
190
+ try {
191
+ const configs = await homebridge.getPluginConfig();
192
+ const cfg = configs[0] || {};
193
+ (cfg.devices || []).forEach(d => {
194
+ if (d.address) existingAddresses.add(d.address.toUpperCase());
195
+ });
196
+ } catch {}
197
+ }
198
+
199
+ loadExisting();
200
+
201
+ async function startScan() {
202
+ if (scanning) return;
203
+ scanning = true;
204
+
205
+ const btn = document.getElementById('scanBtn');
206
+ const status = document.getElementById('status');
207
+ const progressWrap = document.getElementById('progressWrap');
208
+ const progressFill = document.getElementById('progressFill');
209
+ const results = document.getElementById('results');
210
+
211
+ btn.disabled = true;
212
+ results.innerHTML = '';
213
+ progressFill.style.width = '0%';
214
+ progressWrap.style.display = 'block';
215
+ status.innerHTML = '<div class="spinner"></div> Scanning…';
216
+
217
+ // Animate progress bar over scan duration
218
+ let elapsed = 0;
219
+ const tick = 200;
220
+ const timer = setInterval(() => {
221
+ elapsed += tick;
222
+ const pct = Math.min(100, (elapsed / SCAN_DURATION_MS) * 100);
223
+ progressFill.style.width = pct + '%';
224
+ if (elapsed >= SCAN_DURATION_MS) clearInterval(timer);
225
+ }, tick);
226
+
227
+ try {
228
+ const res = await homebridge.request('/scan');
229
+ clearInterval(timer);
230
+ progressFill.style.width = '100%';
231
+ status.textContent = '';
232
+
233
+ if (!res.devices || res.devices.length === 0) {
234
+ results.innerHTML = '<div class="empty-state">No BedJet devices found nearby. Make sure your BedJet is powered on and not already connected to another device.</div>';
235
+ } else {
236
+ // Reload existing in case user added something during scan
237
+ await loadExisting();
238
+ results.innerHTML = res.devices.map(d => buildCard(d)).join('');
239
+ }
240
+ } catch (err) {
241
+ clearInterval(timer);
242
+ status.textContent = '';
243
+ const msg = (err && err.message) ? err.message : String(err);
244
+ results.innerHTML = `<div class="error-state">⚠ ${msg}</div>`;
245
+ } finally {
246
+ setTimeout(() => { progressWrap.style.display = 'none'; progressFill.style.width = '0%'; }, 600);
247
+ btn.disabled = false;
248
+ scanning = false;
249
+ }
250
+ }
251
+
252
+ function buildCard(device) {
253
+ const alreadyAdded = existingAddresses.has(device.address.toUpperCase());
254
+ return `
255
+ <div class="device-card">
256
+ <div class="device-info">
257
+ <span class="device-name">${esc(device.name)}</span>
258
+ <span class="device-address">${esc(device.address)}</span>
259
+ </div>
260
+ <button
261
+ class="add-btn"
262
+ ${alreadyAdded ? 'disabled' : ''}
263
+ onclick="addDevice(${JSON.stringify(device.name)}, ${JSON.stringify(device.address)}, this)"
264
+ >${alreadyAdded ? '✓ Added' : 'Add to Config'}</button>
265
+ </div>`;
266
+ }
267
+
268
+ async function addDevice(name, address, btn) {
269
+ btn.disabled = true;
270
+ btn.textContent = 'Adding…';
271
+
272
+ try {
273
+ const configs = await homebridge.getPluginConfig();
274
+ const cfg = configs[0] || { platform: 'BedJetPlatform' };
275
+ if (!cfg.devices) cfg.devices = [];
276
+
277
+ // Avoid duplicates
278
+ if (cfg.devices.some(d => d.address && d.address.toUpperCase() === address.toUpperCase())) {
279
+ btn.textContent = '✓ Added';
280
+ return;
281
+ }
282
+
283
+ cfg.devices.push({ name, address, scanTimeout: 30 });
284
+ await homebridge.updatePluginConfig([cfg]);
285
+ await homebridge.savePluginConfig();
286
+
287
+ existingAddresses.add(address.toUpperCase());
288
+ btn.textContent = '✓ Added';
289
+ homebridge.toast.success(`${name} added to config. Restart Homebridge to activate.`, 'Device Added');
290
+ } catch (err) {
291
+ btn.disabled = false;
292
+ btn.textContent = 'Add to Config';
293
+ homebridge.toast.error('Failed to save config: ' + ((err && err.message) || err), 'Error');
294
+ }
295
+ }
296
+
297
+ function esc(str) {
298
+ return String(str)
299
+ .replace(/&/g, '&amp;')
300
+ .replace(/</g, '&lt;')
301
+ .replace(/>/g, '&gt;')
302
+ .replace(/"/g, '&quot;');
303
+ }
304
+ </script>
305
+
306
+ </body>
307
+ </html>
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils');
4
+ const path = require('path');
5
+
6
+ // Resolve node-ble from the plugin root (one level up from homebridge-ui/)
7
+ const NodeBle = require(path.join(__dirname, '..', 'node_modules', 'node-ble'));
8
+ const { createBluetooth } = NodeBle;
9
+
10
+ const SCAN_DURATION_MS = 12000;
11
+
12
+ class BedJetUiServer extends HomebridgePluginUiServer {
13
+ constructor() {
14
+ super();
15
+ this.onRequest('/scan', this.handleScan.bind(this));
16
+ this.ready();
17
+ }
18
+
19
+ async handleScan() {
20
+ let destroy = null;
21
+ const found = [];
22
+
23
+ try {
24
+ const ble = createBluetooth();
25
+ destroy = ble.destroy;
26
+
27
+ const adapter = await ble.bluetooth.defaultAdapter();
28
+
29
+ const wasDiscovering = await adapter.isDiscovering();
30
+ if (!wasDiscovering) {
31
+ await adapter.startDiscovery();
32
+ }
33
+
34
+ // Scan for SCAN_DURATION_MS
35
+ await new Promise(resolve => setTimeout(resolve, SCAN_DURATION_MS));
36
+
37
+ if (!wasDiscovering) {
38
+ await adapter.stopDiscovery().catch(() => {});
39
+ }
40
+
41
+ // Query all devices BlueZ discovered
42
+ const addresses = await adapter.devices();
43
+
44
+ for (const address of addresses) {
45
+ try {
46
+ const device = await adapter.getDevice(address);
47
+ const name = await device.getName().catch(() => null);
48
+ if (name && name.toUpperCase().includes('BEDJET')) {
49
+ found.push({ name, address });
50
+ }
51
+ } catch {
52
+ // ignore individual device errors
53
+ }
54
+ }
55
+ } catch (err) {
56
+ throw new Error(`Bluetooth scan failed: ${err.message || err}`);
57
+ } finally {
58
+ if (destroy) {
59
+ try { destroy(); } catch { /* ignore */ }
60
+ }
61
+ }
62
+
63
+ return { devices: found };
64
+ }
65
+ }
66
+
67
+ 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.1",
4
+ "version": "0.2.2",
5
5
  "description": "Homebridge plugin for BedJet V3 via Bluetooth LE",
6
6
  "license": "MIT",
7
7
  "main": "dist/index.js",
@@ -16,6 +16,7 @@
16
16
  "node": ">=18.0.0"
17
17
  },
18
18
  "dependencies": {
19
+ "@homebridge/plugin-ui-utils": "^1.0.0",
19
20
  "node-ble": "^1.10.1"
20
21
  },
21
22
  "devDependencies": {