homebridge-tsvesync 1.5.1 → 1.6.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
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0 (2026-07-27)
4
+
5
+ ### Added
6
+ - **Display Lock In Device Settings** ([#44](https://github.com/mickgiles/homebridge-tsvesync/issues/44)): Air purifiers now expose the panel lock (marketed as "Display Lock" on Core-series models, "Child Lock" elsewhere) through HomeKit's native `LockPhysicalControls` characteristic, so the toggle appears in the accessory's settings pane in the Home app rather than as an extra switch tile. It is registered only for devices whose profile advertises the feature and whose library class implements the setter; the polled device state is pushed on every refresh, failed writes are rejected so the Home app reverts the toggle, and a stale characteristic left by an older cached accessory is removed. Verified live against Core 200S and Core 300S hardware.
7
+
8
+ ### Changed
9
+ - **tsvesync 1.6.0**: Picks up the child-lock API alignment with pyvesync — the Core-series write payload the cloud actually accepts, the `childLockSwitch` payload for Vital 100S/200S and Everest Air, immediate getter read-back after a successful set, and correct `'on'`/`'off'` string parsing for the LV-PUR131S. Without these the new control would read stale state and, on non-Core models, silently fail to write.
10
+ - **Tower Fan Lock Control Removed**: The LTF-F422S API neither reports nor accepts child lock, so the fan accessory no longer registers `LockPhysicalControls` for devices that do not advertise the feature and removes the stale characteristic from cached accessories. Previously the toggle appeared to work but the device ignored it and the state always read unlocked.
11
+
12
+ ## 1.5.2 (2026-07-26)
13
+
14
+ ### Fixed
15
+ - **Auto Mode Honoured In Scenes And Automations** ([#42](https://github.com/mickgiles/homebridge-tsvesync/issues/42)): Setting a Vital 200S to Auto from an Apple Home automation landed in manual, sleep, or off depending purely on the slider position. HomeKit writes `Active`, `TargetAirPurifierState`, and `RotationSpeed` in a single HAP request and HAP-NodeJS dispatches them concurrently; because this accessory encodes sleep/manual/turbo into the speed slider, every speed write is implicitly a mode write and raced with the explicit Auto beside it. Writes are now buffered briefly and resolved into one intent — an explicit Auto beats the mode implied by the slider, `RotationSpeed` 0 only means "turn off" when nothing else in the batch asked for on or a mode, and an explicit `Active` 0 still wins outright.
16
+ - **Overlapping Write Bursts No Longer Interleave**: Batches are applied one at a time. Dragging the slider produces a burst spaced beyond the coalesce window, and un-serialised those API calls interleaved so the device settled on whichever response happened to land last.
17
+ - **Humidifier 0% Mist No Longer Cancels Power On**: The humidifier accessory had the same batching hazard — a 0% mist slider arriving alongside `Active` 1 turned the device straight back off. It now uses the same buffered-intent resolution as the air purifier.
18
+ - **Mode Changes Reported Back Immediately**: A slider drag can move a device out of auto, but `TargetAirPurifierState` was never pushed, so the tile kept showing "Auto" until the next poll — and the poll interval has a two minute floor. Failed `manualMode()` calls were also discarded, so a speed could be sent to a device still in auto; the failure is now surfaced instead.
19
+ - **Speed Readback No Longer Uses A Stale Cache**: The commanded speed is written through, so the slider stops snapping back to the previously reported value. Sleep was also gated on the device being off, so while running it reported the last manual speed instead of the sleep notch.
20
+ - **Core 300S Speed Model Corrected To Three Speeds**: The library declared four fan levels for the Core 300S, but the hardware is Low/Med/High plus Sleep, so the top two slider notches were identical. The real maximum is now used, giving 0/25/50/75/100 mapped 1:1 to off/Sleep/Low/Med/High. Because that changes `minStep` and HomeKit caches characteristic metadata on the home hub, percentages are mapped by which range they fall into rather than by rounding against the new step — rounding put a stale 60% at Low instead of Medium — and the controller's own value is echoed back so the slider does not fight it. Upgrading users get correct speeds immediately; only the tick marks need the hub to catch up.
21
+
22
+ ### Dependencies
23
+ - **tsvesync**: Updated to 1.5.2 for the corrected Core 200S/300S fan levels and the commanded-speed cache write-through that this accessory's readback path depends on.
24
+
25
+ ### Tests
26
+ - **Batched Write Coverage**: Adds `src/__tests__/accessories/air-purifier-batch-write.test.ts` and `src/__tests__/accessories/humidifier-batch-write.test.ts` covering scene and automation write batches, explicit power off, mode readback, stale-mode override expiry, speed readback against a stale library cache, stale slider grids from a not-yet-updated hub, and burst serialisation.
27
+
3
28
  ## 1.5.1 (2026-07-25)
4
29
 
5
30
  ### Fixed
@@ -3,6 +3,13 @@ import { BaseAccessory } from './base.accessory';
3
3
  import { TSVESyncPlatform } from '../platform';
4
4
  import { DeviceCapabilities, VeSyncAirPurifier } from '../types/device.types';
5
5
  export declare class AirPurifierAccessory extends BaseAccessory {
6
+ /**
7
+ * How long to buffer characteristic writes before applying them. HAP-NodeJS
8
+ * dispatches every characteristic in one request within the same tick, so a
9
+ * short window is enough to see the whole batch; it stays far below HAP's
10
+ * 3s slow-write warning threshold.
11
+ */
12
+ private static readonly WRITE_COALESCE_MS;
6
13
  private readonly capabilities;
7
14
  protected readonly device: VeSyncAirPurifier;
8
15
  private isAirBypassDevice;
@@ -11,6 +18,13 @@ export declare class AirPurifierAccessory extends BaseAccessory {
11
18
  private lastSetSpeed;
12
19
  private lastSetPercentage;
13
20
  private skipNextUpdate;
21
+ private pendingWrite;
22
+ private pendingWriteTimer;
23
+ private pendingWritePromise;
24
+ private writeChain;
25
+ private static readonly COMMANDED_MODE_TTL_MS;
26
+ private _lastCommandedMode;
27
+ private _lastCommandedModeTime;
14
28
  constructor(platform: TSVESyncPlatform, accessory: PlatformAccessory, device: VeSyncAirPurifier);
15
29
  /**
16
30
  * Feature detection system
@@ -25,10 +39,36 @@ export declare class AirPurifierAccessory extends BaseAccessory {
25
39
  */
26
40
  private getMaxFanSpeed;
27
41
  private isCore200S;
42
+ private isCore300S;
43
+ /**
44
+ * The real number of manual fan speeds, which is not always what the library
45
+ * advertises. tsvesync declares `levels: [1,2,3,4]` for the Core 200S and
46
+ * Core 300S, but both hardware families only have three manual speeds -
47
+ * verified against a Core300S, where changeFanSpeed(4) reports success and
48
+ * the device settles at 3. Trusting the declared 4 produces a dead top notch
49
+ * where 80% and 100% both mean speed 3.
50
+ */
51
+ private getEffectiveMaxFanSpeed;
28
52
  /**
29
53
  * Calculate the appropriate step size for discrete speed levels
30
54
  */
31
55
  private calculateRotationSpeedStep;
56
+ /**
57
+ * Total slider notches: sleep, each manual speed, and turbo when supported.
58
+ */
59
+ private getTotalNotches;
60
+ /**
61
+ * Map a percentage to a slider notch by which equal-width range it falls in,
62
+ * rather than by rounding against our own step.
63
+ *
64
+ * This has to stay correct even when the controller disagrees with us about
65
+ * the step. HomeKit caches characteristic metadata on the home hub, so after
66
+ * an upgrade that changes minStep a controller can keep sending values on the
67
+ * *old* grid for an unknown period. Rounding against our step maps those to
68
+ * the wrong speed (a 60% on an old 20% grid became Low instead of Medium);
69
+ * range mapping gets it right on either grid.
70
+ */
71
+ private percentageToNotch;
32
72
  /**
33
73
  * Convert device speed to percentage
34
74
  */
@@ -74,6 +114,17 @@ export declare class AirPurifierAccessory extends BaseAccessory {
74
114
  * for the rare device that supports pet but not auto.
75
115
  */
76
116
  private setPetMode;
117
+ /**
118
+ * The panel lock (marketed as "Display Lock" on Core-series purifiers,
119
+ * "Child Lock" elsewhere) maps to HomeKit's LockPhysicalControls, which
120
+ * the Home app shows in the accessory's settings pane rather than as a
121
+ * tile. Only wired when the device profile advertises the feature and the
122
+ * tsvesync class implements the setter.
123
+ */
124
+ private supportsChildLock;
125
+ private setupChildLock;
126
+ private getLockPhysicalControls;
127
+ private setLockPhysicalControls;
77
128
  /**
78
129
  * Extract filter life from device data in a centralized way
79
130
  */
@@ -98,23 +149,90 @@ export declare class AirPurifierAccessory extends BaseAccessory {
98
149
  * Update device states based on the latest details
99
150
  */
100
151
  protected updateDeviceSpecificStates(details: any): Promise<void>;
152
+ /**
153
+ * Buffer a characteristic write and return a promise that settles once the
154
+ * whole batch has been applied. Every characteristic in the batch shares the
155
+ * same promise, so HomeKit gets a success/failure per write as usual.
156
+ */
157
+ private queueWrite;
158
+ /**
159
+ * Turn one buffered batch of HomeKit writes into a single device intent.
160
+ *
161
+ * The ordering rules that matter (issue #42):
162
+ * - An explicit AUTO write wins over the mode implied by the speed slider.
163
+ * This accessory encodes sleep/manual/turbo into RotationSpeed, so every
164
+ * speed write is implicitly a mode write; without this rule an automation
165
+ * that sets "on + auto + 60%" ends up in manual.
166
+ * - RotationSpeed 0 only means "turn off" when the same batch didn't also
167
+ * ask for the device to be on or set a mode. Otherwise "on + auto + 0%"
168
+ * turns the device on and straight back off.
169
+ */
170
+ private resolveWrite;
171
+ /**
172
+ * Write a commanded speed/mode back into the device's cached details.
173
+ *
174
+ * The bypass purifiers (Core series) don't update their own cache after a
175
+ * successful changeFanSpeed - unlike the bypassV2 models, which do - so
176
+ * device.speed stays stale until the next poll, and the poll interval has a
177
+ * two minute floor. That stale value makes getRotationSpeed() disagree with
178
+ * what we just set and snaps the slider back to the wrong notch.
179
+ */
180
+ private cacheCommandedSpeed;
181
+ /**
182
+ * Record a mode we just commanded so a stale API read can't undo it.
183
+ */
184
+ private rememberCommandedMode;
185
+ /**
186
+ * Resolve the mode to report to HomeKit, preferring what we last commanded
187
+ * over what the device currently reports. The override is dropped as soon as
188
+ * the device catches up, or once it expires - so a mode changed in the VeSync
189
+ * app still comes through.
190
+ */
191
+ private resolveEffectiveMode;
192
+ /**
193
+ * Map a VeSync mode to the HomeKit TargetAirPurifierState value.
194
+ * Pet is an automatic mode, so it surfaces as AUTO like auto itself.
195
+ */
196
+ private modeToTargetState;
197
+ /**
198
+ * Push the mode back to HomeKit immediately after we change it.
199
+ *
200
+ * The RotationSpeed slider implicitly carries a mode (sleep/manual/turbo),
201
+ * so dragging it can move the device out of auto. Without this the tile
202
+ * keeps showing the old mode until the next poll - and the poll interval has
203
+ * a two minute floor, so "Auto" would linger long after the device had
204
+ * actually switched to Manual.
205
+ */
206
+ private syncTargetStateToHomeKit;
101
207
  /**
102
208
  * Get target state (MANUAL = 0, AUTO = 1)
103
209
  */
104
210
  private getTargetState;
105
211
  /**
106
- * Set target state (AUTO = 0, MANUAL = 1)
212
+ * Set target state (MANUAL = 0, AUTO = 1). Buffered so it can be reconciled
213
+ * with any Active/RotationSpeed writes sent in the same HomeKit request.
107
214
  */
108
215
  private setTargetState;
216
+ private applyTargetState;
109
217
  /**
110
218
  * Get rotation speed
111
219
  */
112
220
  private getRotationSpeed;
113
221
  /**
114
- * Set rotation speed
222
+ * Set rotation speed. Buffered so it can be reconciled with any
223
+ * Active/TargetAirPurifierState writes sent in the same HomeKit request -
224
+ * the slider implicitly carries a mode, so it must not clobber an explicit
225
+ * AUTO written alongside it.
115
226
  */
116
227
  private setRotationSpeed;
228
+ private applyRotationSpeed;
117
229
  private getActive;
230
+ /**
231
+ * Set active state. Buffered so it can be reconciled with any
232
+ * RotationSpeed/TargetAirPurifierState writes sent in the same HomeKit
233
+ * request - notably so a 0% slider can't turn the device straight back off.
234
+ */
118
235
  private setActive;
236
+ private applyActive;
119
237
  }
120
238
  //# sourceMappingURL=air-purifier.accessory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"air-purifier.accessory.d.ts","sourceRoot":"","sources":["../../src/accessories/air-purifier.accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,iBAAiB,EAAW,MAAM,YAAY,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAsC9E,qBAAa,oBAAqB,SAAQ,aAAa;IACrD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAC7C,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,cAAc,CAAkB;gBAGtC,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,iBAAiB;IAiD3B;;OAEG;IACH,OAAO,CAAC,UAAU;IAqIlB;;OAEG;IACH,OAAO,CAAC,+BAA+B;IAmBvC;;OAEG;IACH,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,UAAU;IAIlB;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAiBlC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAsBzB;;;;;;;;;;;;OAYG;YACW,oCAAoC;IAoClD;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA0BzB;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IAuI9B;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAc;IAE3D,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;YACW,UAAU;IAKxB;;;;OAIG;YACW,UAAU;IA0CxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA0BzB;;OAEG;YACW,yBAAyB;IAOvC;;OAEG;YACW,kBAAkB;IAOhC;;OAEG;YACW,eAAe;IAI7B;;OAEG;IACH,SAAS,CAAC,qBAAqB,IAAI,kBAAkB;IAcrD;;OAEG;cACa,0BAA0B,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAmIvE;;OAEG;YACW,cAAc;IAc5B;;OAEG;YACW,cAAc;IA2C5B;;OAEG;YACW,gBAAgB;IA6B9B;;OAEG;YACW,gBAAgB;YA0MhB,SAAS;YAIT,SAAS;CAoExB"}
1
+ {"version":3,"file":"air-purifier.accessory.d.ts","sourceRoot":"","sources":["../../src/accessories/air-purifier.accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,iBAAiB,EAAW,MAAM,YAAY,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAgD9E,qBAAa,oBAAqB,SAAQ,aAAa;IACrD;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAM;IAE/C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,SAAS,CAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAC;IAC7C,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,iBAAiB,CAAU;IACnC,OAAO,CAAC,cAAc,CAAU;IAChC,OAAO,CAAC,YAAY,CAAa;IACjC,OAAO,CAAC,iBAAiB,CAAa;IACtC,OAAO,CAAC,cAAc,CAAkB;IAOxC,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,mBAAmB,CAA8B;IAMzD,OAAO,CAAC,UAAU,CAAoC;IAQtD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IACtD,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,sBAAsB,CAAK;gBAGjC,QAAQ,EAAE,gBAAgB,EAC1B,SAAS,EAAE,iBAAiB,EAC5B,MAAM,EAAE,iBAAiB;IAiD3B;;OAEG;IACH,OAAO,CAAC,UAAU;IAyIlB;;OAEG;IACH,OAAO,CAAC,+BAA+B;IAmBvC;;OAEG;IACH,OAAO,CAAC,cAAc;IAiCtB,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,UAAU;IAIlB;;;;;;;OAOG;IACH,OAAO,CAAC,uBAAuB;IAO/B;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAalC;;OAEG;IACH,OAAO,CAAC,eAAe;IAKvB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,iBAAiB;IASzB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAsBzB;;;;;;;;;;;;OAYG;YACW,oCAAoC;IAoClD;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAyBzB;;OAEG;IACH,SAAS,CAAC,YAAY,IAAI,IAAI;IA4I9B;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAc;IAE3D,OAAO,CAAC,kBAAkB;IA2B1B;;OAEG;YACW,UAAU;IAKxB;;;;OAIG;YACW,UAAU;IA6CxB;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,cAAc;YAsBR,uBAAuB;YAMvB,uBAAuB;IAqBrC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA0BzB;;OAEG;YACW,yBAAyB;IAOvC;;OAEG;YACW,kBAAkB;IAOhC;;OAEG;YACW,eAAe;IAI7B;;OAEG;IACH,SAAS,CAAC,qBAAqB,IAAI,kBAAkB;IAcrD;;OAEG;cACa,0BAA0B,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IA0IvE;;;;OAIG;IACH,OAAO,CAAC,UAAU;IA4BlB;;;;;;;;;;;OAWG;YACW,YAAY;IAqD1B;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB;IAmB3B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAK7B;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAsB5B;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;;;;;;;OAQG;IACH,OAAO,CAAC,wBAAwB;IAYhC;;OAEG;YACW,cAAc;IAc5B;;;OAGG;YACW,cAAc;YAId,gBAAgB;IA+C9B;;OAEG;YACW,gBAAgB;IAwC9B;;;;;OAKG;YACW,gBAAgB;YAUhB,kBAAkB;YA2MlB,SAAS;IAIvB;;;;OAIG;YACW,SAAS;YAIT,WAAW;CAoE1B"}