homebridge-warmup4ie-v2 3.2.0 → 3.3.0

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/CHANGELOG.md CHANGED
@@ -7,6 +7,54 @@ This package is a maintained fork of [`homebridge-warmup4ie`](https://github.com
7
7
 
8
8
  ---
9
9
 
10
+ ## [3.3.0] — 2026-05-05
11
+
12
+ Roadmap [Milestone 6](ROADMAP.md) batch 1 — incremental polish. No GraphQL
13
+ changes; everything uses data we already fetch. `config.json` unchanged.
14
+
15
+ ### Added
16
+ - **`StatusFault` characteristic** on each Thermostat. Reads
17
+ `room.isFaultAir | isFaultFloor1 | isFaultFloor2` (data already in the
18
+ normalized room shape from v3.0). Surfaces sensor disconnects in
19
+ HomeKit's accessory diagnostics — better than mysterious wrong
20
+ readings. NO_FAULT (0) when clean, GENERAL_FAULT (1) when any flag set.
21
+ - **`runMode` edge-case handling** in `state.js` for the rare modes the
22
+ GraphQL schema documents (`anti_frost`, `holiday`, `gradual`,
23
+ `fil_pilote`, `relay`, `previous`, `not_set`):
24
+ - `holiday` → `TargetHeatingCoolingState = OFF` (location-wide vacation
25
+ mode; user expectation = "off"). `Current` still tracks `currentTemp <
26
+ targetTemp` because heating to the holiday setpoint *is* heating.
27
+ - `anti_frost` → `Target = OFF` (frost protection is passive).
28
+ `Current` still tracks the temp delta.
29
+ - `gradual` (early-start ramp-up) → `Target = AUTO`.
30
+ - The four `not_set | fil_pilote | relay | previous` rare modes →
31
+ `Target = HEAT` (safe fallback; better than OFF when heating may be
32
+ happening).
33
+
34
+ ### Fixed
35
+ - **Defensive guard against transient empty rooms.** If Warmup returns
36
+ `owned[0].rooms = []` (a transient API hiccup; the user almost
37
+ certainly didn't actually delete every thermostat in the seconds
38
+ between polls), `reconcileAccessories` now logs a warning and skips
39
+ the unregister-stale step. Without this guard, a single bad poll
40
+ would rip every cached accessory out of the user's HomeKit rooms /
41
+ scenes / automations.
42
+
43
+ ### Documentation
44
+ - Per-thermostat metadata fields in `normalizeRoom` get inline comments
45
+ explaining what each is for and which roadmap milestone surfaces it.
46
+
47
+ ### Tests
48
+ - 89 total, 86 offline + 2 live + 1 destructive (skipped). Up from 75
49
+ in 3.2.0. Notable additions:
50
+ - `state-derivers.test.js`: 8 new test cases covering every documented
51
+ `runMode` value (and a few unknown-mode fallbacks).
52
+ - `platform-state.test.js`: `StatusFault` flow (NO_FAULT initially,
53
+ GENERAL_FAULT after a sensor flag flips on the next poll); empty-rooms
54
+ defensive guard (cached accessory survives a 0-room poll response).
55
+
56
+ ---
57
+
10
58
  ## [3.2.0] — 2026-05-05
11
59
 
12
60
  Roadmap [Milestone 5](ROADMAP.md) — **Eve / fakegato history graphs.**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-warmup4ie-v2",
3
3
  "displayName": "Homebridge Warmup Wi-Fi Thermostats",
4
- "version": "3.2.0",
4
+ "version": "3.3.0",
5
5
  "description": "Homebridge plugin for Warmup Wi-Fi underfloor-heating thermostats (4iE, 6iE, 7iE, Element, Terra). Maintained fork of homebridge-warmup4ie with fixes for Homebridge 2.0 and the broken Off control in 0.1.0–0.1.1.",
6
6
  "main": "src/index.js",
7
7
  "scripts": {
package/src/index.js CHANGED
@@ -123,6 +123,16 @@ warmup4iePlatform.prototype = {
123
123
  // register/unregister deltas to Homebridge so the on-disk cache + HomeKit
124
124
  // bridge match the Warmup account.
125
125
  reconcileAccessories: function (rooms) {
126
+ // Defensive: an empty rooms array is *probably* a transient Warmup
127
+ // hiccup (the user didn't actually delete every thermostat). Skip
128
+ // unregistering cached accessories — they'll show "Not Responding"
129
+ // until the next poll, but we don't rip their HomeKit tiles out of
130
+ // rooms / scenes / automations on a glitch.
131
+ if (rooms.length === 0 && this.accessories.size > 0) {
132
+ this.log.warn('Warmup returned 0 rooms but %s cached accessories exist — keeping cache untouched', this.accessories.size);
133
+ return;
134
+ }
135
+
126
136
  const seen = new Set();
127
137
  const toRegister = [];
128
138
 
@@ -298,6 +308,11 @@ function pushRoomState(accessory, room) {
298
308
  .updateValue(heatingState);
299
309
  thermo.getCharacteristic(Characteristic.TargetHeatingCoolingState)
300
310
  .updateValue(deriveTargetHeatingState(room));
311
+ // StatusFault: NO_FAULT (0) when sensors look fine, GENERAL_FAULT (1)
312
+ // when any of the per-thermostat fault flags is set. Calling
313
+ // .getCharacteristic on an optional characteristic auto-adds it.
314
+ thermo.getCharacteristic(Characteristic.StatusFault)
315
+ .updateValue(deriveStatusFault(room));
301
316
  temp.getCharacteristic(Characteristic.CurrentTemperature)
302
317
  .updateValue(Number(room.airTemp / 10));
303
318
 
@@ -410,6 +425,14 @@ function effectiveTargetTemp(room) {
410
425
  return room.targetTemp > room.minTemp ? room.targetTemp : room.minTemp;
411
426
  }
412
427
 
428
+ // HAP StatusFault: NO_FAULT (0) | GENERAL_FAULT (1). Surfaces sensor
429
+ // disconnects (air or floor probes) so the user gets a visible "fault" badge
430
+ // in HomeKit's accessory diagnostics rather than mysterious wrong readings.
431
+ function deriveStatusFault(room) {
432
+ const anyFault = room.isFaultAir || room.isFaultFloor1 || room.isFaultFloor2;
433
+ return anyFault ? Characteristic.StatusFault.GENERAL_FAULT : Characteristic.StatusFault.NO_FAULT;
434
+ }
435
+
413
436
  function uuidForRoom(roomId) {
414
437
  return uuid.generate(`warmup4ie:${roomId}`);
415
438
  }
package/src/lib/state.js CHANGED
@@ -5,19 +5,38 @@
5
5
  // HAP-NodeJS. Numeric values are HAP characteristic values:
6
6
  // CurrentHeatingCoolingState: 0=OFF, 1=HEAT, 2=COOL (we only emit 0 and 1)
7
7
  // TargetHeatingCoolingState: 0=OFF, 1=HEAT, 2=COOL, 3=AUTO (we only emit 0, 1, 3)
8
+ //
9
+ // `runMode` enum (per the GraphQL schema): not_set | off | schedule | override
10
+ // | fixed | anti_frost | holiday |
11
+ // | fil_pilote | gradual | relay | previous
8
12
 
9
13
  function deriveCurrentHeatingState(room) {
14
+ // OFF only when the device is genuinely off. For every other mode (including
15
+ // anti_frost and holiday — both still drive heating to a low setpoint) the
16
+ // signal is whether currentTemp is below targetTemp. Roadmap M6 will swap
17
+ // this for the more accurate `Thermostat4iE.parameters.outputStatus` relay
18
+ // signal once we re-add `parameters` to the GraphQL query.
10
19
  if (room.runMode === 'off') return 0;
11
20
  return room.currentTemp < room.targetTemp ? 1 : 0;
12
21
  }
13
22
 
14
23
  function deriveTargetHeatingState(room) {
15
24
  switch (room.runMode) {
16
- case 'off': return 0;
25
+ case 'off':
26
+ case 'holiday': // Location-wide vacation mode — user expectation: "off"
27
+ case 'anti_frost': // Frost protection — passive, low setpoint
28
+ return 0; // OFF
17
29
  case 'fixed':
18
- case 'override': return 1;
19
- case 'schedule': return 3;
20
- default: return 1;
30
+ case 'override':
31
+ return 1; // HEAT (user-set fixed temperature)
32
+ case 'schedule':
33
+ case 'gradual': // Early-start ramp-up to the next scheduled slot
34
+ return 3; // AUTO
35
+ default:
36
+ // Includes `not_set`, `fil_pilote`, `relay`, `previous` — rare modes
37
+ // we have no real signal on. HEAT is the safe fallback (better than
38
+ // showing OFF when heating is actually happening).
39
+ return 1;
21
40
  }
22
41
  }
23
42
 
@@ -270,9 +270,15 @@ function normalizeRoom(r) {
270
270
  fixedTemp: r.fixedTemp,
271
271
  energy: r.energy,
272
272
  cost: r.cost,
273
- // Fields surfaced for future M5/M6 use (Eve energy graphs, StatusFault).
274
- // M6 will additionally fetch `parameters { outputStatus }` for the
275
- // real "is currently heating" relay signal — not in the query today.
273
+ // Per-thermostat metadata. Fault flags drive the platform's `StatusFault`
274
+ // characteristic (M6 batch 1). The rest are queued for follow-up:
275
+ // - `floor1Temp`/`floor2Temp` — exposed as a separate sensor in M6+.
276
+ // - `deviceSN` — could replace `warmup4ie-<roomId>` as SerialNumber, but
277
+ // would force HomeKit to re-pair existing accessories. Not worth it.
278
+ // - `lastPoll` — drives `StatusActive` (offline detection) in M6+.
279
+ // - `parameters { outputStatus }` (relay state) is not in the GraphQL
280
+ // query yet — re-adding caused a 409 during v3 development; M6 will
281
+ // try again carefully and use it for `CurrentHeatingCoolingState`.
276
282
  floor1Temp: t.floor1Temp,
277
283
  floor2Temp: t.floor2Temp,
278
284
  isFaultAir: t.isFaultAir,