homebridge-tsvesync 1.5.1 → 1.5.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/CHANGELOG.md +16 -0
- package/dist/accessories/air-purifier.accessory.d.ts +109 -2
- package/dist/accessories/air-purifier.accessory.d.ts.map +1 -1
- package/dist/accessories/air-purifier.accessory.js +336 -68
- package/dist/accessories/air-purifier.accessory.js.map +1 -1
- package/dist/accessories/humidifier.accessory.d.ts +32 -0
- package/dist/accessories/humidifier.accessory.d.ts.map +1 -1
- package/dist/accessories/humidifier.accessory.js +92 -9
- package/dist/accessories/humidifier.accessory.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/accessories/air-purifier-batch-write.test.ts +531 -0
- package/src/__tests__/accessories/humidifier-batch-write.test.ts +174 -0
- package/src/accessories/air-purifier.accessory.ts +400 -77
- package/src/accessories/humidifier.accessory.ts +121 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.2 (2026-07-26)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **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.
|
|
7
|
+
- **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.
|
|
8
|
+
- **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.
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
|
|
13
|
+
### Dependencies
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
16
|
+
### Tests
|
|
17
|
+
- **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.
|
|
18
|
+
|
|
3
19
|
## 1.5.1 (2026-07-25)
|
|
4
20
|
|
|
5
21
|
### 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
|
*/
|
|
@@ -98,23 +138,90 @@ export declare class AirPurifierAccessory extends BaseAccessory {
|
|
|
98
138
|
* Update device states based on the latest details
|
|
99
139
|
*/
|
|
100
140
|
protected updateDeviceSpecificStates(details: any): Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Buffer a characteristic write and return a promise that settles once the
|
|
143
|
+
* whole batch has been applied. Every characteristic in the batch shares the
|
|
144
|
+
* same promise, so HomeKit gets a success/failure per write as usual.
|
|
145
|
+
*/
|
|
146
|
+
private queueWrite;
|
|
147
|
+
/**
|
|
148
|
+
* Turn one buffered batch of HomeKit writes into a single device intent.
|
|
149
|
+
*
|
|
150
|
+
* The ordering rules that matter (issue #42):
|
|
151
|
+
* - An explicit AUTO write wins over the mode implied by the speed slider.
|
|
152
|
+
* This accessory encodes sleep/manual/turbo into RotationSpeed, so every
|
|
153
|
+
* speed write is implicitly a mode write; without this rule an automation
|
|
154
|
+
* that sets "on + auto + 60%" ends up in manual.
|
|
155
|
+
* - RotationSpeed 0 only means "turn off" when the same batch didn't also
|
|
156
|
+
* ask for the device to be on or set a mode. Otherwise "on + auto + 0%"
|
|
157
|
+
* turns the device on and straight back off.
|
|
158
|
+
*/
|
|
159
|
+
private resolveWrite;
|
|
160
|
+
/**
|
|
161
|
+
* Write a commanded speed/mode back into the device's cached details.
|
|
162
|
+
*
|
|
163
|
+
* The bypass purifiers (Core series) don't update their own cache after a
|
|
164
|
+
* successful changeFanSpeed - unlike the bypassV2 models, which do - so
|
|
165
|
+
* device.speed stays stale until the next poll, and the poll interval has a
|
|
166
|
+
* two minute floor. That stale value makes getRotationSpeed() disagree with
|
|
167
|
+
* what we just set and snaps the slider back to the wrong notch.
|
|
168
|
+
*/
|
|
169
|
+
private cacheCommandedSpeed;
|
|
170
|
+
/**
|
|
171
|
+
* Record a mode we just commanded so a stale API read can't undo it.
|
|
172
|
+
*/
|
|
173
|
+
private rememberCommandedMode;
|
|
174
|
+
/**
|
|
175
|
+
* Resolve the mode to report to HomeKit, preferring what we last commanded
|
|
176
|
+
* over what the device currently reports. The override is dropped as soon as
|
|
177
|
+
* the device catches up, or once it expires - so a mode changed in the VeSync
|
|
178
|
+
* app still comes through.
|
|
179
|
+
*/
|
|
180
|
+
private resolveEffectiveMode;
|
|
181
|
+
/**
|
|
182
|
+
* Map a VeSync mode to the HomeKit TargetAirPurifierState value.
|
|
183
|
+
* Pet is an automatic mode, so it surfaces as AUTO like auto itself.
|
|
184
|
+
*/
|
|
185
|
+
private modeToTargetState;
|
|
186
|
+
/**
|
|
187
|
+
* Push the mode back to HomeKit immediately after we change it.
|
|
188
|
+
*
|
|
189
|
+
* The RotationSpeed slider implicitly carries a mode (sleep/manual/turbo),
|
|
190
|
+
* so dragging it can move the device out of auto. Without this the tile
|
|
191
|
+
* keeps showing the old mode until the next poll - and the poll interval has
|
|
192
|
+
* a two minute floor, so "Auto" would linger long after the device had
|
|
193
|
+
* actually switched to Manual.
|
|
194
|
+
*/
|
|
195
|
+
private syncTargetStateToHomeKit;
|
|
101
196
|
/**
|
|
102
197
|
* Get target state (MANUAL = 0, AUTO = 1)
|
|
103
198
|
*/
|
|
104
199
|
private getTargetState;
|
|
105
200
|
/**
|
|
106
|
-
* Set target state (
|
|
201
|
+
* Set target state (MANUAL = 0, AUTO = 1). Buffered so it can be reconciled
|
|
202
|
+
* with any Active/RotationSpeed writes sent in the same HomeKit request.
|
|
107
203
|
*/
|
|
108
204
|
private setTargetState;
|
|
205
|
+
private applyTargetState;
|
|
109
206
|
/**
|
|
110
207
|
* Get rotation speed
|
|
111
208
|
*/
|
|
112
209
|
private getRotationSpeed;
|
|
113
210
|
/**
|
|
114
|
-
* Set rotation speed
|
|
211
|
+
* Set rotation speed. Buffered so it can be reconciled with any
|
|
212
|
+
* Active/TargetAirPurifierState writes sent in the same HomeKit request -
|
|
213
|
+
* the slider implicitly carries a mode, so it must not clobber an explicit
|
|
214
|
+
* AUTO written alongside it.
|
|
115
215
|
*/
|
|
116
216
|
private setRotationSpeed;
|
|
217
|
+
private applyRotationSpeed;
|
|
117
218
|
private getActive;
|
|
219
|
+
/**
|
|
220
|
+
* Set active state. Buffered so it can be reconciled with any
|
|
221
|
+
* RotationSpeed/TargetAirPurifierState writes sent in the same HomeKit
|
|
222
|
+
* request - notably so a 0% slider can't turn the device straight back off.
|
|
223
|
+
*/
|
|
118
224
|
private setActive;
|
|
225
|
+
private applyActive;
|
|
119
226
|
}
|
|
120
227
|
//# 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;
|
|
1
|
+
{"version":3,"file":"air-purifier.accessory.d.ts","sourceRoot":"","sources":["../../src/accessories/air-purifier.accessory.ts"],"names":[],"mappings":"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"}
|