homebridge-bedjet 0.2.7 → 0.2.9
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/config.schema.json +29 -0
- package/dist/accessory.d.ts +8 -0
- package/dist/accessory.js +45 -9
- package/dist/bedjet/types.d.ts +4 -0
- package/homebridge-ui/public/index.html +196 -57
- package/homebridge-ui/server.js +7 -2
- package/package.json +1 -1
- package/src/accessory.ts +58 -12
- package/src/bedjet/types.ts +7 -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/config.schema.json
CHANGED
|
@@ -31,6 +31,35 @@
|
|
|
31
31
|
"type": "integer",
|
|
32
32
|
"default": 30,
|
|
33
33
|
"description": "How long to scan for the device before giving up (default: 30)"
|
|
34
|
+
},
|
|
35
|
+
"defaultMode": {
|
|
36
|
+
"title": "Default Mode (when turned on)",
|
|
37
|
+
"type": "string",
|
|
38
|
+
"default": "heat",
|
|
39
|
+
"oneOf": [
|
|
40
|
+
{ "title": "Heat", "enum": ["heat"] },
|
|
41
|
+
{ "title": "Turbo", "enum": ["turbo"] },
|
|
42
|
+
{ "title": "Extended Heat", "enum": ["extendedHeat"] },
|
|
43
|
+
{ "title": "Cool (Fan Only)", "enum": ["cool"] },
|
|
44
|
+
{ "title": "Dry", "enum": ["dry"] }
|
|
45
|
+
],
|
|
46
|
+
"description": "Mode to activate when the BedJet is turned on from HomeKit"
|
|
47
|
+
},
|
|
48
|
+
"defaultTemperature": {
|
|
49
|
+
"title": "Default Temperature °C (when turned on)",
|
|
50
|
+
"type": "number",
|
|
51
|
+
"minimum": 19,
|
|
52
|
+
"maximum": 43,
|
|
53
|
+
"multipleOf": 0.5,
|
|
54
|
+
"description": "Target temperature to set when turned on. Range: 19–43°C (66–109°F). Leave blank to keep the BedJet's last-used temperature."
|
|
55
|
+
},
|
|
56
|
+
"defaultFanSpeed": {
|
|
57
|
+
"title": "Default Fan Speed % (when turned on)",
|
|
58
|
+
"type": "integer",
|
|
59
|
+
"minimum": 5,
|
|
60
|
+
"maximum": 100,
|
|
61
|
+
"multipleOf": 5,
|
|
62
|
+
"description": "Fan speed to set when turned on (5–100% in 5% steps). Leave blank to keep the BedJet's last-used speed."
|
|
34
63
|
}
|
|
35
64
|
}
|
|
36
65
|
}
|
package/dist/accessory.d.ts
CHANGED
|
@@ -18,5 +18,13 @@ export declare class BedJetAccessory {
|
|
|
18
18
|
private pendingModeTimer;
|
|
19
19
|
private setPending;
|
|
20
20
|
constructor(platform: BedJetPlatform, accessory: PlatformAccessory, config: BedJetConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Set the operating mode, then optionally apply default temperature and fan
|
|
23
|
+
* speed from config. applyDefaults=true when turning on from off.
|
|
24
|
+
* applyMode=true when the mode itself should come from defaultMode config
|
|
25
|
+
* (fan turn-on path); false when HomeKit supplied the mode explicitly
|
|
26
|
+
* (thermostat path).
|
|
27
|
+
*/
|
|
28
|
+
private _applyModeAndDefaults;
|
|
21
29
|
private _syncHomeKit;
|
|
22
30
|
}
|
package/dist/accessory.js
CHANGED
|
@@ -3,6 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.BedJetAccessory = void 0;
|
|
4
4
|
const BedJet_1 = require("./bedjet/BedJet");
|
|
5
5
|
const constants_1 = require("./bedjet/constants");
|
|
6
|
+
const DEFAULT_MODE_MAP = {
|
|
7
|
+
heat: constants_1.OperatingMode.HEAT,
|
|
8
|
+
turbo: constants_1.OperatingMode.TURBO,
|
|
9
|
+
extendedHeat: constants_1.OperatingMode.EXTENDED_HEAT,
|
|
10
|
+
cool: constants_1.OperatingMode.COOL,
|
|
11
|
+
dry: constants_1.OperatingMode.DRY,
|
|
12
|
+
};
|
|
6
13
|
// OperatingMode → CurrentHeatingCoolingState value
|
|
7
14
|
const CURRENT_STATE_MAP = {
|
|
8
15
|
[constants_1.OperatingMode.STANDBY]: 0, // OFF
|
|
@@ -84,8 +91,12 @@ class BedJetAccessory {
|
|
|
84
91
|
})
|
|
85
92
|
.onSet((value) => {
|
|
86
93
|
this.setPending(value, 'pendingMode', 'pendingModeTimer');
|
|
94
|
+
const wasOff = this.bedjet.state.operatingMode === constants_1.OperatingMode.STANDBY
|
|
95
|
+
|| this.bedjet.state.operatingMode === constants_1.OperatingMode.WAIT;
|
|
96
|
+
const turningOn = value !== 0;
|
|
87
97
|
const mode = TARGET_TO_MODE[value] ?? constants_1.OperatingMode.STANDBY;
|
|
88
|
-
|
|
98
|
+
// Apply the requested mode, then defaults (temp/fan) if turning on from off
|
|
99
|
+
this._applyModeAndDefaults(mode, wasOff && turningOn, false).catch(err => this.platform.log.error(`[${config.name}] setOperatingMode failed: ${err}`));
|
|
89
100
|
});
|
|
90
101
|
// FanV2 service
|
|
91
102
|
this.fanService = this.accessory.getService(Service.Fanv2)
|
|
@@ -95,17 +106,16 @@ class BedJetAccessory {
|
|
|
95
106
|
? Characteristic.Active.ACTIVE
|
|
96
107
|
: Characteristic.Active.INACTIVE)
|
|
97
108
|
.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');
|
|
101
109
|
if (value === Characteristic.Active.INACTIVE) {
|
|
110
|
+
this.setPending(0, 'pendingMode', 'pendingModeTimer');
|
|
102
111
|
this.bedjet.setOperatingMode(constants_1.OperatingMode.STANDBY).catch(err => this.platform.log.error(`[${config.name}] setOperatingMode(STANDBY) failed: ${err}`));
|
|
103
112
|
}
|
|
104
|
-
else {
|
|
105
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
else if (this.bedjet.state.operatingMode === constants_1.OperatingMode.STANDBY) {
|
|
114
|
+
// Turning on — use configured default mode and apply all defaults
|
|
115
|
+
const mode = this.config.defaultMode
|
|
116
|
+
? DEFAULT_MODE_MAP[this.config.defaultMode]
|
|
117
|
+
: constants_1.OperatingMode.HEAT;
|
|
118
|
+
this._applyModeAndDefaults(mode, true, true).catch(err => this.platform.log.error(`[${config.name}] turn on failed: ${err}`));
|
|
109
119
|
}
|
|
110
120
|
});
|
|
111
121
|
this.fanService.getCharacteristic(Characteristic.RotationSpeed)
|
|
@@ -133,6 +143,32 @@ class BedJetAccessory {
|
|
|
133
143
|
// Start connecting — errors are logged but don't crash Homebridge
|
|
134
144
|
this.bedjet.connect().catch(err => this.platform.log.error(`[${config.name}] Initial connect failed: ${err}`));
|
|
135
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Set the operating mode, then optionally apply default temperature and fan
|
|
148
|
+
* speed from config. applyDefaults=true when turning on from off.
|
|
149
|
+
* applyMode=true when the mode itself should come from defaultMode config
|
|
150
|
+
* (fan turn-on path); false when HomeKit supplied the mode explicitly
|
|
151
|
+
* (thermostat path).
|
|
152
|
+
*/
|
|
153
|
+
async _applyModeAndDefaults(mode, applyDefaults, applyMode) {
|
|
154
|
+
const { config } = this;
|
|
155
|
+
// Determine HomeKit target state for pending optimistic value
|
|
156
|
+
const pendingState = mode === constants_1.OperatingMode.STANDBY ? 0
|
|
157
|
+
: mode === constants_1.OperatingMode.COOL || mode === constants_1.OperatingMode.DRY ? 2
|
|
158
|
+
: 1;
|
|
159
|
+
this.setPending(pendingState, 'pendingMode', 'pendingModeTimer', 5000);
|
|
160
|
+
await this.bedjet.setOperatingMode(mode);
|
|
161
|
+
if (applyDefaults) {
|
|
162
|
+
if (config.defaultTemperature !== undefined) {
|
|
163
|
+
this.setPending(config.defaultTemperature, 'pendingTemp', 'pendingTempTimer', 5000);
|
|
164
|
+
await this.bedjet.setTemperature(config.defaultTemperature);
|
|
165
|
+
}
|
|
166
|
+
if (config.defaultFanSpeed !== undefined) {
|
|
167
|
+
this.setPending(config.defaultFanSpeed, 'pendingFanSpeed', 'pendingFanSpeedTimer', 5000);
|
|
168
|
+
await this.bedjet.setFanSpeed(config.defaultFanSpeed);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
136
172
|
_syncHomeKit(state) {
|
|
137
173
|
const { Characteristic } = this.platform.api.hap;
|
|
138
174
|
// Clamp helper — keeps values within the bounds HomeKit expects
|
package/dist/bedjet/types.d.ts
CHANGED
|
@@ -17,9 +17,13 @@ export interface BedJetState {
|
|
|
17
17
|
unitsSetup?: boolean;
|
|
18
18
|
notificationCode?: number;
|
|
19
19
|
}
|
|
20
|
+
export type DefaultMode = 'heat' | 'turbo' | 'extendedHeat' | 'cool' | 'dry';
|
|
20
21
|
export interface BedJetConfig {
|
|
21
22
|
name: string;
|
|
22
23
|
address: string;
|
|
23
24
|
scanTimeout?: number;
|
|
25
|
+
defaultMode?: DefaultMode;
|
|
26
|
+
defaultTemperature?: number;
|
|
27
|
+
defaultFanSpeed?: number;
|
|
24
28
|
}
|
|
25
29
|
export declare const DEFAULT_STATE: BedJetState;
|
|
@@ -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
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge';
|
|
2
2
|
import { BedJet } from './bedjet/BedJet';
|
|
3
3
|
import { OperatingMode } from './bedjet/constants';
|
|
4
|
-
import type { BedJetConfig, BedJetState } from './bedjet/types';
|
|
4
|
+
import type { BedJetConfig, BedJetState, DefaultMode } from './bedjet/types';
|
|
5
5
|
import type { BedJetPlatform } from './platform';
|
|
6
6
|
|
|
7
|
+
const DEFAULT_MODE_MAP: Record<DefaultMode, OperatingMode> = {
|
|
8
|
+
heat: OperatingMode.HEAT,
|
|
9
|
+
turbo: OperatingMode.TURBO,
|
|
10
|
+
extendedHeat: OperatingMode.EXTENDED_HEAT,
|
|
11
|
+
cool: OperatingMode.COOL,
|
|
12
|
+
dry: OperatingMode.DRY,
|
|
13
|
+
};
|
|
14
|
+
|
|
7
15
|
// OperatingMode → CurrentHeatingCoolingState value
|
|
8
16
|
const CURRENT_STATE_MAP: Record<OperatingMode, number> = {
|
|
9
17
|
[OperatingMode.STANDBY]: 0, // OFF
|
|
@@ -107,8 +115,12 @@ export class BedJetAccessory {
|
|
|
107
115
|
})
|
|
108
116
|
.onSet((value: CharacteristicValue) => {
|
|
109
117
|
this.setPending(value as number, 'pendingMode', 'pendingModeTimer');
|
|
118
|
+
const wasOff = this.bedjet.state.operatingMode === OperatingMode.STANDBY
|
|
119
|
+
|| this.bedjet.state.operatingMode === OperatingMode.WAIT;
|
|
120
|
+
const turningOn = (value as number) !== 0;
|
|
110
121
|
const mode = TARGET_TO_MODE[value as number] ?? OperatingMode.STANDBY;
|
|
111
|
-
|
|
122
|
+
// Apply the requested mode, then defaults (temp/fan) if turning on from off
|
|
123
|
+
this._applyModeAndDefaults(mode, wasOff && turningOn, false).catch(err =>
|
|
112
124
|
this.platform.log.error(`[${config.name}] setOperatingMode failed: ${err}`),
|
|
113
125
|
);
|
|
114
126
|
});
|
|
@@ -124,20 +136,19 @@ export class BedJetAccessory {
|
|
|
124
136
|
: Characteristic.Active.INACTIVE,
|
|
125
137
|
)
|
|
126
138
|
.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');
|
|
130
139
|
if (value === Characteristic.Active.INACTIVE) {
|
|
140
|
+
this.setPending(0, 'pendingMode', 'pendingModeTimer');
|
|
131
141
|
this.bedjet.setOperatingMode(OperatingMode.STANDBY).catch(err =>
|
|
132
142
|
this.platform.log.error(`[${config.name}] setOperatingMode(STANDBY) failed: ${err}`),
|
|
133
143
|
);
|
|
134
|
-
} else {
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
this.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
} else if (this.bedjet.state.operatingMode === OperatingMode.STANDBY) {
|
|
145
|
+
// Turning on — use configured default mode and apply all defaults
|
|
146
|
+
const mode = this.config.defaultMode
|
|
147
|
+
? DEFAULT_MODE_MAP[this.config.defaultMode]
|
|
148
|
+
: OperatingMode.HEAT;
|
|
149
|
+
this._applyModeAndDefaults(mode, true, true).catch(err =>
|
|
150
|
+
this.platform.log.error(`[${config.name}] turn on failed: ${err}`),
|
|
151
|
+
);
|
|
141
152
|
}
|
|
142
153
|
});
|
|
143
154
|
|
|
@@ -174,6 +185,41 @@ export class BedJetAccessory {
|
|
|
174
185
|
);
|
|
175
186
|
}
|
|
176
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Set the operating mode, then optionally apply default temperature and fan
|
|
190
|
+
* speed from config. applyDefaults=true when turning on from off.
|
|
191
|
+
* applyMode=true when the mode itself should come from defaultMode config
|
|
192
|
+
* (fan turn-on path); false when HomeKit supplied the mode explicitly
|
|
193
|
+
* (thermostat path).
|
|
194
|
+
*/
|
|
195
|
+
private async _applyModeAndDefaults(
|
|
196
|
+
mode: OperatingMode,
|
|
197
|
+
applyDefaults: boolean,
|
|
198
|
+
applyMode: boolean,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
const { config } = this;
|
|
201
|
+
|
|
202
|
+
// Determine HomeKit target state for pending optimistic value
|
|
203
|
+
const pendingState =
|
|
204
|
+
mode === OperatingMode.STANDBY ? 0
|
|
205
|
+
: mode === OperatingMode.COOL || mode === OperatingMode.DRY ? 2
|
|
206
|
+
: 1;
|
|
207
|
+
this.setPending(pendingState, 'pendingMode', 'pendingModeTimer', 5000);
|
|
208
|
+
|
|
209
|
+
await this.bedjet.setOperatingMode(mode);
|
|
210
|
+
|
|
211
|
+
if (applyDefaults) {
|
|
212
|
+
if (config.defaultTemperature !== undefined) {
|
|
213
|
+
this.setPending(config.defaultTemperature, 'pendingTemp', 'pendingTempTimer', 5000);
|
|
214
|
+
await this.bedjet.setTemperature(config.defaultTemperature);
|
|
215
|
+
}
|
|
216
|
+
if (config.defaultFanSpeed !== undefined) {
|
|
217
|
+
this.setPending(config.defaultFanSpeed, 'pendingFanSpeed', 'pendingFanSpeedTimer', 5000);
|
|
218
|
+
await this.bedjet.setFanSpeed(config.defaultFanSpeed);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
177
223
|
private _syncHomeKit(state: BedJetState): void {
|
|
178
224
|
const { Characteristic } = this.platform.api.hap;
|
|
179
225
|
|
package/src/bedjet/types.ts
CHANGED
|
@@ -20,10 +20,15 @@ export interface BedJetState {
|
|
|
20
20
|
notificationCode?: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export type DefaultMode = 'heat' | 'turbo' | 'extendedHeat' | 'cool' | 'dry';
|
|
24
|
+
|
|
23
25
|
export interface BedJetConfig {
|
|
24
26
|
name: string;
|
|
25
|
-
address: string;
|
|
26
|
-
scanTimeout?: number;
|
|
27
|
+
address: string; // BLE MAC e.g. "AA:BB:CC:DD:EE:FF"
|
|
28
|
+
scanTimeout?: number; // seconds (default 30)
|
|
29
|
+
defaultMode?: DefaultMode; // mode to activate when turned on from HomeKit
|
|
30
|
+
defaultTemperature?: number; // °C, applied on turn-on
|
|
31
|
+
defaultFanSpeed?: number; // percent 5–100, applied on turn-on
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
export const DEFAULT_STATE: BedJetState = {
|