homebridge-bedjet 0.2.0 → 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.
- package/config.schema.json +1 -0
- package/dist/accessory.d.ts +7 -0
- package/dist/accessory.js +33 -7
- package/dist/accessory.js.map +1 -1
- package/homebridge-ui/public/index.html +307 -0
- package/homebridge-ui/server.js +67 -0
- package/package.json +2 -1
- package/src/accessory.ts +46 -12
package/config.schema.json
CHANGED
package/dist/accessory.d.ts
CHANGED
|
@@ -10,6 +10,13 @@ export declare class BedJetAccessory {
|
|
|
10
10
|
private readonly bedjet;
|
|
11
11
|
private tempDebounce;
|
|
12
12
|
private fanDebounce;
|
|
13
|
+
private pendingTemp;
|
|
14
|
+
private pendingTempTimer;
|
|
15
|
+
private pendingFanSpeed;
|
|
16
|
+
private pendingFanSpeedTimer;
|
|
17
|
+
private pendingMode;
|
|
18
|
+
private pendingModeTimer;
|
|
19
|
+
private setPending;
|
|
13
20
|
constructor(platform: BedJetPlatform, accessory: PlatformAccessory, config: BedJetConfig);
|
|
14
21
|
private _syncHomeKit;
|
|
15
22
|
}
|
package/dist/accessory.js
CHANGED
|
@@ -21,6 +21,15 @@ const TARGET_TO_MODE = {
|
|
|
21
21
|
3: constants_1.OperatingMode.HEAT, // AUTO — fall back to heat
|
|
22
22
|
};
|
|
23
23
|
class BedJetAccessory {
|
|
24
|
+
setPending(value, store, timer, ms = 3000) {
|
|
25
|
+
if (this[timer]) {
|
|
26
|
+
clearTimeout(this[timer]);
|
|
27
|
+
}
|
|
28
|
+
this[store] = value;
|
|
29
|
+
this[timer] = setTimeout(() => {
|
|
30
|
+
this[store] = null;
|
|
31
|
+
}, ms);
|
|
32
|
+
}
|
|
24
33
|
constructor(platform, accessory, config) {
|
|
25
34
|
this.platform = platform;
|
|
26
35
|
this.accessory = accessory;
|
|
@@ -28,6 +37,13 @@ class BedJetAccessory {
|
|
|
28
37
|
// Debounce handles for setter commands
|
|
29
38
|
this.tempDebounce = null;
|
|
30
39
|
this.fanDebounce = null;
|
|
40
|
+
// Optimistic values — held after a set to suppress stale BLE notification bounces
|
|
41
|
+
this.pendingTemp = null;
|
|
42
|
+
this.pendingTempTimer = null;
|
|
43
|
+
this.pendingFanSpeed = null;
|
|
44
|
+
this.pendingFanSpeedTimer = null;
|
|
45
|
+
this.pendingMode = null;
|
|
46
|
+
this.pendingModeTimer = null;
|
|
31
47
|
const { Service, Characteristic } = platform.api.hap;
|
|
32
48
|
// AccessoryInformation
|
|
33
49
|
const infoService = this.accessory.getService(Service.AccessoryInformation)
|
|
@@ -45,8 +61,9 @@ class BedJetAccessory {
|
|
|
45
61
|
.onGet(() => this.bedjet.state.currentTemperature);
|
|
46
62
|
this.thermostatService.getCharacteristic(Characteristic.TargetTemperature)
|
|
47
63
|
.setProps({ minValue: 19, maxValue: 43, minStep: 0.5 })
|
|
48
|
-
.onGet(() => this.bedjet.state.targetTemperature)
|
|
64
|
+
.onGet(() => this.pendingTemp ?? this.bedjet.state.targetTemperature)
|
|
49
65
|
.onSet((value) => {
|
|
66
|
+
this.setPending(value, 'pendingTemp', 'pendingTempTimer');
|
|
50
67
|
if (this.tempDebounce) {
|
|
51
68
|
clearTimeout(this.tempDebounce);
|
|
52
69
|
}
|
|
@@ -66,6 +83,7 @@ class BedJetAccessory {
|
|
|
66
83
|
return 1; // HEAT / TURBO / EXTENDED_HEAT
|
|
67
84
|
})
|
|
68
85
|
.onSet((value) => {
|
|
86
|
+
this.setPending(value, 'pendingMode', 'pendingModeTimer');
|
|
69
87
|
const mode = TARGET_TO_MODE[value] ?? constants_1.OperatingMode.STANDBY;
|
|
70
88
|
this.bedjet.setOperatingMode(mode).catch(err => this.platform.log.error(`[${config.name}] setOperatingMode failed: ${err}`));
|
|
71
89
|
});
|
|
@@ -77,6 +95,9 @@ class BedJetAccessory {
|
|
|
77
95
|
? Characteristic.Active.ACTIVE
|
|
78
96
|
: Characteristic.Active.INACTIVE)
|
|
79
97
|
.onSet((value) => {
|
|
98
|
+
// pendingMode: 0=OFF, 1=HEAT (active), used to suppress stale BLE bounce
|
|
99
|
+
const pendingModeValue = value === Characteristic.Active.INACTIVE ? 0 : 1;
|
|
100
|
+
this.setPending(pendingModeValue, 'pendingMode', 'pendingModeTimer');
|
|
80
101
|
if (value === Characteristic.Active.INACTIVE) {
|
|
81
102
|
this.bedjet.setOperatingMode(constants_1.OperatingMode.STANDBY).catch(err => this.platform.log.error(`[${config.name}] setOperatingMode(STANDBY) failed: ${err}`));
|
|
82
103
|
}
|
|
@@ -89,8 +110,9 @@ class BedJetAccessory {
|
|
|
89
110
|
});
|
|
90
111
|
this.fanService.getCharacteristic(Characteristic.RotationSpeed)
|
|
91
112
|
.setProps({ minValue: 5, maxValue: 100, minStep: 5 })
|
|
92
|
-
.onGet(() => this.bedjet.state.fanSpeed)
|
|
113
|
+
.onGet(() => this.pendingFanSpeed ?? this.bedjet.state.fanSpeed)
|
|
93
114
|
.onSet((value) => {
|
|
115
|
+
this.setPending(value, 'pendingFanSpeed', 'pendingFanSpeedTimer');
|
|
94
116
|
if (this.fanDebounce) {
|
|
95
117
|
clearTimeout(this.fanDebounce);
|
|
96
118
|
}
|
|
@@ -122,18 +144,22 @@ class BedJetAccessory {
|
|
|
122
144
|
.getCharacteristic(Characteristic.TargetTemperature)
|
|
123
145
|
.setProps({ minValue: minTemp, maxValue: maxTemp });
|
|
124
146
|
this.thermostatService.updateCharacteristic(Characteristic.CurrentTemperature, clamp(state.currentTemperature, -270, 100));
|
|
125
|
-
|
|
126
|
-
this.
|
|
127
|
-
const
|
|
147
|
+
// Use pending (optimistic) values if set — suppresses stale BLE notification bounces
|
|
148
|
+
const targetTemp = this.pendingTemp ?? clamp(state.targetTemperature, minTemp, maxTemp);
|
|
149
|
+
const fanSpeed = this.pendingFanSpeed ?? clamp(state.fanSpeed, 5, 100);
|
|
150
|
+
const derivedTargetState = state.operatingMode === constants_1.OperatingMode.STANDBY || state.operatingMode === constants_1.OperatingMode.WAIT
|
|
128
151
|
? 0
|
|
129
152
|
: state.operatingMode === constants_1.OperatingMode.COOL || state.operatingMode === constants_1.OperatingMode.DRY
|
|
130
153
|
? 2
|
|
131
154
|
: 1;
|
|
155
|
+
const targetState = this.pendingMode ?? derivedTargetState;
|
|
156
|
+
this.thermostatService.updateCharacteristic(Characteristic.TargetTemperature, targetTemp);
|
|
157
|
+
this.thermostatService.updateCharacteristic(Characteristic.CurrentHeatingCoolingState, CURRENT_STATE_MAP[state.operatingMode] ?? 0);
|
|
132
158
|
this.thermostatService.updateCharacteristic(Characteristic.TargetHeatingCoolingState, targetState);
|
|
133
|
-
this.fanService.updateCharacteristic(Characteristic.Active,
|
|
159
|
+
this.fanService.updateCharacteristic(Characteristic.Active, targetState !== 0
|
|
134
160
|
? Characteristic.Active.ACTIVE
|
|
135
161
|
: Characteristic.Active.INACTIVE);
|
|
136
|
-
this.fanService.updateCharacteristic(Characteristic.RotationSpeed,
|
|
162
|
+
this.fanService.updateCharacteristic(Characteristic.RotationSpeed, fanSpeed);
|
|
137
163
|
}
|
|
138
164
|
}
|
|
139
165
|
exports.BedJetAccessory = BedJetAccessory;
|
package/dist/accessory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":";;;AACA,4CAAyC;AACzC,kDAAmD;AAInD,mDAAmD;AACnD,MAAM,iBAAiB,GAAkC;IACvD,CAAC,yBAAa,CAAC,OAAO,CAAC,EAAQ,CAAC,EAAE,MAAM;IACxC,CAAC,yBAAa,CAAC,IAAI,CAAC,EAAW,CAAC,EAAE,OAAO;IACzC,CAAC,yBAAa,CAAC,KAAK,CAAC,EAAU,CAAC,EAAE,OAAO;IACzC,CAAC,yBAAa,CAAC,aAAa,CAAC,EAAE,CAAC,EAAE,OAAO;IACzC,CAAC,yBAAa,CAAC,IAAI,CAAC,EAAW,CAAC,EAAE,OAAO;IACzC,CAAC,yBAAa,CAAC,GAAG,CAAC,EAAY,CAAC,EAAE,OAAO;IACzC,CAAC,yBAAa,CAAC,IAAI,CAAC,EAAW,CAAC,EAAE,MAAM;CACzC,CAAC;AAEF,kDAAkD;AAClD,MAAM,cAAc,GAAkC;IACpD,CAAC,EAAE,yBAAa,CAAC,OAAO;IACxB,CAAC,EAAE,yBAAa,CAAC,IAAI;IACrB,CAAC,EAAE,yBAAa,CAAC,IAAI;IACrB,CAAC,EAAE,yBAAa,CAAC,IAAI,EAAE,2BAA2B;CACnD,CAAC;AAEF,MAAa,eAAe;
|
|
1
|
+
{"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"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"}
|
|
@@ -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, '&')
|
|
300
|
+
.replace(/</g, '<')
|
|
301
|
+
.replace(/>/g, '>')
|
|
302
|
+
.replace(/"/g, '"');
|
|
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.
|
|
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": {
|
package/src/accessory.ts
CHANGED
|
@@ -32,6 +32,29 @@ export class BedJetAccessory {
|
|
|
32
32
|
private tempDebounce: NodeJS.Timeout | null = null;
|
|
33
33
|
private fanDebounce: NodeJS.Timeout | null = null;
|
|
34
34
|
|
|
35
|
+
// Optimistic values — held after a set to suppress stale BLE notification bounces
|
|
36
|
+
private pendingTemp: number | null = null;
|
|
37
|
+
private pendingTempTimer: NodeJS.Timeout | null = null;
|
|
38
|
+
private pendingFanSpeed: number | null = null;
|
|
39
|
+
private pendingFanSpeedTimer: NodeJS.Timeout | null = null;
|
|
40
|
+
private pendingMode: number | null = null;
|
|
41
|
+
private pendingModeTimer: NodeJS.Timeout | null = null;
|
|
42
|
+
|
|
43
|
+
private setPending<T>(
|
|
44
|
+
value: T,
|
|
45
|
+
store: 'pendingTemp' | 'pendingFanSpeed' | 'pendingMode',
|
|
46
|
+
timer: 'pendingTempTimer' | 'pendingFanSpeedTimer' | 'pendingModeTimer',
|
|
47
|
+
ms = 3000,
|
|
48
|
+
): void {
|
|
49
|
+
if (this[timer]) {
|
|
50
|
+
clearTimeout(this[timer] as NodeJS.Timeout);
|
|
51
|
+
}
|
|
52
|
+
(this as unknown as Record<string, unknown>)[store] = value;
|
|
53
|
+
(this as unknown as Record<string, unknown>)[timer] = setTimeout(() => {
|
|
54
|
+
(this as unknown as Record<string, unknown>)[store] = null;
|
|
55
|
+
}, ms);
|
|
56
|
+
}
|
|
57
|
+
|
|
35
58
|
constructor(
|
|
36
59
|
private readonly platform: BedJetPlatform,
|
|
37
60
|
private readonly accessory: PlatformAccessory,
|
|
@@ -59,8 +82,9 @@ export class BedJetAccessory {
|
|
|
59
82
|
|
|
60
83
|
this.thermostatService.getCharacteristic(Characteristic.TargetTemperature)
|
|
61
84
|
.setProps({ minValue: 19, maxValue: 43, minStep: 0.5 })
|
|
62
|
-
.onGet(() => this.bedjet.state.targetTemperature)
|
|
85
|
+
.onGet(() => this.pendingTemp ?? this.bedjet.state.targetTemperature)
|
|
63
86
|
.onSet((value: CharacteristicValue) => {
|
|
87
|
+
this.setPending(value as number, 'pendingTemp', 'pendingTempTimer');
|
|
64
88
|
if (this.tempDebounce) {
|
|
65
89
|
clearTimeout(this.tempDebounce);
|
|
66
90
|
}
|
|
@@ -82,6 +106,7 @@ export class BedJetAccessory {
|
|
|
82
106
|
return 1; // HEAT / TURBO / EXTENDED_HEAT
|
|
83
107
|
})
|
|
84
108
|
.onSet((value: CharacteristicValue) => {
|
|
109
|
+
this.setPending(value as number, 'pendingMode', 'pendingModeTimer');
|
|
85
110
|
const mode = TARGET_TO_MODE[value as number] ?? OperatingMode.STANDBY;
|
|
86
111
|
this.bedjet.setOperatingMode(mode).catch(err =>
|
|
87
112
|
this.platform.log.error(`[${config.name}] setOperatingMode failed: ${err}`),
|
|
@@ -99,6 +124,9 @@ export class BedJetAccessory {
|
|
|
99
124
|
: Characteristic.Active.INACTIVE,
|
|
100
125
|
)
|
|
101
126
|
.onSet((value: CharacteristicValue) => {
|
|
127
|
+
// pendingMode: 0=OFF, 1=HEAT (active), used to suppress stale BLE bounce
|
|
128
|
+
const pendingModeValue = value === Characteristic.Active.INACTIVE ? 0 : 1;
|
|
129
|
+
this.setPending(pendingModeValue, 'pendingMode', 'pendingModeTimer');
|
|
102
130
|
if (value === Characteristic.Active.INACTIVE) {
|
|
103
131
|
this.bedjet.setOperatingMode(OperatingMode.STANDBY).catch(err =>
|
|
104
132
|
this.platform.log.error(`[${config.name}] setOperatingMode(STANDBY) failed: ${err}`),
|
|
@@ -115,8 +143,9 @@ export class BedJetAccessory {
|
|
|
115
143
|
|
|
116
144
|
this.fanService.getCharacteristic(Characteristic.RotationSpeed)
|
|
117
145
|
.setProps({ minValue: 5, maxValue: 100, minStep: 5 })
|
|
118
|
-
.onGet(() => this.bedjet.state.fanSpeed)
|
|
146
|
+
.onGet(() => this.pendingFanSpeed ?? this.bedjet.state.fanSpeed)
|
|
119
147
|
.onSet((value: CharacteristicValue) => {
|
|
148
|
+
this.setPending(value as number, 'pendingFanSpeed', 'pendingFanSpeedTimer');
|
|
120
149
|
if (this.fanDebounce) {
|
|
121
150
|
clearTimeout(this.fanDebounce);
|
|
122
151
|
}
|
|
@@ -165,9 +194,21 @@ export class BedJetAccessory {
|
|
|
165
194
|
clamp(state.currentTemperature, -270, 100),
|
|
166
195
|
);
|
|
167
196
|
|
|
197
|
+
// Use pending (optimistic) values if set — suppresses stale BLE notification bounces
|
|
198
|
+
const targetTemp = this.pendingTemp ?? clamp(state.targetTemperature, minTemp, maxTemp);
|
|
199
|
+
const fanSpeed = this.pendingFanSpeed ?? clamp(state.fanSpeed, 5, 100);
|
|
200
|
+
|
|
201
|
+
const derivedTargetState =
|
|
202
|
+
state.operatingMode === OperatingMode.STANDBY || state.operatingMode === OperatingMode.WAIT
|
|
203
|
+
? 0
|
|
204
|
+
: state.operatingMode === OperatingMode.COOL || state.operatingMode === OperatingMode.DRY
|
|
205
|
+
? 2
|
|
206
|
+
: 1;
|
|
207
|
+
const targetState = this.pendingMode ?? derivedTargetState;
|
|
208
|
+
|
|
168
209
|
this.thermostatService.updateCharacteristic(
|
|
169
210
|
Characteristic.TargetTemperature,
|
|
170
|
-
|
|
211
|
+
targetTemp,
|
|
171
212
|
);
|
|
172
213
|
|
|
173
214
|
this.thermostatService.updateCharacteristic(
|
|
@@ -175,13 +216,6 @@ export class BedJetAccessory {
|
|
|
175
216
|
CURRENT_STATE_MAP[state.operatingMode] ?? 0,
|
|
176
217
|
);
|
|
177
218
|
|
|
178
|
-
const targetState =
|
|
179
|
-
state.operatingMode === OperatingMode.STANDBY || state.operatingMode === OperatingMode.WAIT
|
|
180
|
-
? 0
|
|
181
|
-
: state.operatingMode === OperatingMode.COOL || state.operatingMode === OperatingMode.DRY
|
|
182
|
-
? 2
|
|
183
|
-
: 1;
|
|
184
|
-
|
|
185
219
|
this.thermostatService.updateCharacteristic(
|
|
186
220
|
Characteristic.TargetHeatingCoolingState,
|
|
187
221
|
targetState,
|
|
@@ -189,14 +223,14 @@ export class BedJetAccessory {
|
|
|
189
223
|
|
|
190
224
|
this.fanService.updateCharacteristic(
|
|
191
225
|
Characteristic.Active,
|
|
192
|
-
|
|
226
|
+
targetState !== 0
|
|
193
227
|
? Characteristic.Active.ACTIVE
|
|
194
228
|
: Characteristic.Active.INACTIVE,
|
|
195
229
|
);
|
|
196
230
|
|
|
197
231
|
this.fanService.updateCharacteristic(
|
|
198
232
|
Characteristic.RotationSpeed,
|
|
199
|
-
|
|
233
|
+
fanSpeed,
|
|
200
234
|
);
|
|
201
235
|
}
|
|
202
236
|
}
|