homebridge-winix-purifiers 2.2.1 → 2.2.3
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/dist/accessory.js +8 -3
- package/dist/device.js +17 -7
- package/dist/platform.js +13 -5
- package/package.json +4 -2
package/dist/accessory.js
CHANGED
|
@@ -14,7 +14,7 @@ const DEFAULT_POLL_INTERVAL_SECONDS = 30;
|
|
|
14
14
|
const MIN_POLL_INTERVAL_SECONDS = 15;
|
|
15
15
|
const MIN_AMBIENT_LIGHT = 0.0001;
|
|
16
16
|
class WinixPurifierAccessory {
|
|
17
|
-
constructor(platform, config, accessory, override, log) {
|
|
17
|
+
constructor(platform, config, accessory, override, log, deviceCount) {
|
|
18
18
|
this.platform = platform;
|
|
19
19
|
this.config = config;
|
|
20
20
|
this.accessory = accessory;
|
|
@@ -24,8 +24,13 @@ class WinixPurifierAccessory {
|
|
|
24
24
|
this.Characteristic = this.platform.Characteristic;
|
|
25
25
|
const { deviceId, deviceAlias } = accessory.context.device;
|
|
26
26
|
const configuredSeconds = config.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS;
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const minForDeviceCount = deviceCount * 10;
|
|
28
|
+
const effectiveSeconds = Math.max(configuredSeconds, MIN_POLL_INTERVAL_SECONDS, minForDeviceCount);
|
|
29
|
+
if (effectiveSeconds > configuredSeconds) {
|
|
30
|
+
this.log.info(`Poll interval adjusted from ${configuredSeconds}s to ${effectiveSeconds}s to avoid Winix API rate limiting with ${deviceCount} device(s)`);
|
|
31
|
+
}
|
|
32
|
+
const pollIntervalMs = effectiveSeconds * 1000;
|
|
33
|
+
this.device = new device_1.Device(deviceId, pollIntervalMs, this.log, this.platform.client);
|
|
29
34
|
this.servicesInUse = new Set();
|
|
30
35
|
const deviceSerial = override?.serialNumber ?? 'WNXAI00000000';
|
|
31
36
|
const deviceName = override?.nameDevice ?? deviceAlias;
|
package/dist/device.js
CHANGED
|
@@ -6,10 +6,11 @@ const MAX_BACKOFF_MS = 5 * 60 * 1000;
|
|
|
6
6
|
const COMMAND_DELAY_MS = 1500;
|
|
7
7
|
const UNREACHABLE_THRESHOLD = 3;
|
|
8
8
|
class Device {
|
|
9
|
-
constructor(deviceId, pollIntervalMs, log) {
|
|
9
|
+
constructor(deviceId, pollIntervalMs, log, client) {
|
|
10
10
|
this.deviceId = deviceId;
|
|
11
11
|
this.pollIntervalMs = pollIntervalMs;
|
|
12
12
|
this.log = log;
|
|
13
|
+
this.client = client;
|
|
13
14
|
this.hasReceivedData = false;
|
|
14
15
|
this.pollTimer = null;
|
|
15
16
|
this.consecutiveFailures = 0;
|
|
@@ -33,13 +34,17 @@ class Device {
|
|
|
33
34
|
async initialFetch() {
|
|
34
35
|
try {
|
|
35
36
|
this.log.debug('device:initialFetch()');
|
|
36
|
-
const newState = await
|
|
37
|
+
const newState = await this.client.getDeviceStatus(this.deviceId);
|
|
37
38
|
Object.assign(this.state, newState);
|
|
38
39
|
this.hasReceivedData = true;
|
|
39
40
|
this.consecutiveFailures = 0;
|
|
40
41
|
this.log.debug('device:initialFetch()', JSON.stringify(this.state));
|
|
41
42
|
}
|
|
42
43
|
catch (e) {
|
|
44
|
+
if (e instanceof winix_api_1.RateLimitError) {
|
|
45
|
+
this.log.warn('device:initialFetch() rate limited, using defaults');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
43
48
|
this.log.warn('device:initialFetch() failed, using defaults:', e.message);
|
|
44
49
|
}
|
|
45
50
|
}
|
|
@@ -92,7 +97,7 @@ class Device {
|
|
|
92
97
|
return;
|
|
93
98
|
}
|
|
94
99
|
this.log.debug('device:setPower()', initialPower, value);
|
|
95
|
-
await
|
|
100
|
+
await this.client.setPower(this.deviceId, value);
|
|
96
101
|
this.state.power = value;
|
|
97
102
|
// Side effects observed from device testing
|
|
98
103
|
if (value === winix_api_1.Power.Off) {
|
|
@@ -110,7 +115,7 @@ class Device {
|
|
|
110
115
|
return;
|
|
111
116
|
}
|
|
112
117
|
this.log.debug('device:setMode(%s)', value);
|
|
113
|
-
await
|
|
118
|
+
await this.client.setMode(this.deviceId, value);
|
|
114
119
|
this.state.mode = value;
|
|
115
120
|
// Side effects observed from device testing
|
|
116
121
|
if (value === winix_api_1.Mode.Auto) {
|
|
@@ -127,7 +132,7 @@ class Device {
|
|
|
127
132
|
await this.setMode(winix_api_1.Mode.Manual);
|
|
128
133
|
await new Promise(r => setTimeout(r, COMMAND_DELAY_MS));
|
|
129
134
|
}
|
|
130
|
-
await
|
|
135
|
+
await this.client.setAirflow(this.deviceId, value);
|
|
131
136
|
this.state.airflow = value;
|
|
132
137
|
// Side effects observed from device testing
|
|
133
138
|
if (value === winix_api_1.Airflow.Sleep) {
|
|
@@ -137,7 +142,7 @@ class Device {
|
|
|
137
142
|
async setPlasmawave(value) {
|
|
138
143
|
this.log.debug('device:setPlasmawave()', value);
|
|
139
144
|
await this.ensureOn();
|
|
140
|
-
await
|
|
145
|
+
await this.client.setPlasmawave(this.deviceId, value);
|
|
141
146
|
this.state.plasmawave = value;
|
|
142
147
|
}
|
|
143
148
|
// Private methods
|
|
@@ -150,7 +155,7 @@ class Device {
|
|
|
150
155
|
async poll() {
|
|
151
156
|
try {
|
|
152
157
|
this.log.debug('device:poll()');
|
|
153
|
-
const newState = await
|
|
158
|
+
const newState = await this.client.getDeviceStatus(this.deviceId);
|
|
154
159
|
Object.assign(this.state, newState);
|
|
155
160
|
this.hasReceivedData = true;
|
|
156
161
|
this.consecutiveFailures = 0;
|
|
@@ -158,6 +163,11 @@ class Device {
|
|
|
158
163
|
this.onUpdate?.();
|
|
159
164
|
}
|
|
160
165
|
catch (e) {
|
|
166
|
+
if (e instanceof winix_api_1.RateLimitError) {
|
|
167
|
+
this.log.warn('device:poll() rate limited, retrying on next interval');
|
|
168
|
+
this.schedulePoll(this.pollIntervalMs);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
161
171
|
this.consecutiveFailures++;
|
|
162
172
|
const backoffMs = Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveFailures), MAX_BACKOFF_MS);
|
|
163
173
|
this.log.error(`device:poll() error: ${e.message} (retry in ${Math.round(backoffMs / 1000)}s)`);
|
package/dist/platform.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.WinixPurifierPlatform = void 0;
|
|
|
4
4
|
const winix_1 = require("./winix");
|
|
5
5
|
const settings_1 = require("./settings");
|
|
6
6
|
const accessory_1 = require("./accessory");
|
|
7
|
+
const winix_api_1 = require("winix-api");
|
|
7
8
|
const logger_1 = require("./logger");
|
|
8
9
|
const errors_1 = require("./errors");
|
|
9
10
|
const encryption_1 = require("./encryption");
|
|
@@ -21,6 +22,7 @@ class WinixPurifierPlatform {
|
|
|
21
22
|
this.handlers = new Map();
|
|
22
23
|
this.deviceOverrides = (this.config.deviceOverrides ?? [])
|
|
23
24
|
.reduce((m, o) => m.set(o.deviceId, o), new Map());
|
|
25
|
+
this.client = new winix_api_1.WinixClient();
|
|
24
26
|
this.winix = new winix_1.WinixHandler(api.user.storagePath(), settings_1.ENCRYPTION_KEY);
|
|
25
27
|
this.api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, this.onFinishLaunching.bind(this));
|
|
26
28
|
}
|
|
@@ -79,16 +81,22 @@ class WinixPurifierPlatform {
|
|
|
79
81
|
const uuid = this.api.hap.uuid.generate(device.deviceId);
|
|
80
82
|
let accessory = this.accessories.get(uuid);
|
|
81
83
|
this.log.debug('Found', accessory ? 'existing' : 'new', 'accessory:', this.logName(device));
|
|
82
|
-
if (accessory) {
|
|
84
|
+
if (accessory && this.handlers.has(uuid)) {
|
|
85
|
+
// Existing device on refresh: update metadata only, keep handler + polling loop
|
|
83
86
|
accessory.context.device = device;
|
|
84
|
-
|
|
87
|
+
this.api.updatePlatformAccessories([accessory]);
|
|
88
|
+
}
|
|
89
|
+
else if (accessory) {
|
|
90
|
+
// First boot with cached accessory: create handler
|
|
91
|
+
accessory.context.device = device;
|
|
92
|
+
const handler = await this.createNewAccessoryHandler(accessory, devices.length);
|
|
85
93
|
this.handlers.set(uuid, handler);
|
|
86
94
|
this.api.updatePlatformAccessories([accessory]);
|
|
87
95
|
}
|
|
88
96
|
else {
|
|
89
97
|
accessory = new this.api.platformAccessory(device.deviceAlias, uuid, 19 /* Categories.AIR_PURIFIER */);
|
|
90
98
|
accessory.context.device = device;
|
|
91
|
-
const handler = await this.createNewAccessoryHandler(accessory);
|
|
99
|
+
const handler = await this.createNewAccessoryHandler(accessory, devices.length);
|
|
92
100
|
this.accessories.set(uuid, accessory);
|
|
93
101
|
this.handlers.set(uuid, handler);
|
|
94
102
|
accessoriesToAdd.push(accessory);
|
|
@@ -100,12 +108,12 @@ class WinixPurifierPlatform {
|
|
|
100
108
|
}
|
|
101
109
|
this.removeOldDevices(discoveredUUIDs);
|
|
102
110
|
}
|
|
103
|
-
async createNewAccessoryHandler(accessory) {
|
|
111
|
+
async createNewAccessoryHandler(accessory, deviceCount) {
|
|
104
112
|
// 🫣 suppress warning message about adding characteristics which aren't required / optional, since it isn't accurate
|
|
105
113
|
this.suppressCharacteristicWarnings(accessory);
|
|
106
114
|
const deviceOverride = this.deviceOverrides.get(accessory.context.device.deviceId);
|
|
107
115
|
const log = new logger_1.DeviceLogger(this.log, accessory.context.device);
|
|
108
|
-
const handler = new accessory_1.WinixPurifierAccessory(this, this.config, accessory, deviceOverride, log);
|
|
116
|
+
const handler = new accessory_1.WinixPurifierAccessory(this, this.config, accessory, deviceOverride, log, deviceCount);
|
|
109
117
|
this.unsuppressCharacteristicWarnings(accessory);
|
|
110
118
|
await handler.initialize();
|
|
111
119
|
return handler;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"displayName": "Winix Air Purifiers",
|
|
3
3
|
"name": "homebridge-winix-purifiers",
|
|
4
|
-
"version": "2.2.
|
|
4
|
+
"version": "2.2.3",
|
|
5
5
|
"description": "Homebridge plugin for Winix air purifiers",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"postversion": "git push && git push --tags && yarn clean",
|
|
45
45
|
"prepublishOnly": "yarn validate",
|
|
46
46
|
"test": "vitest run",
|
|
47
|
+
"test:integration": "vitest run --config vitest.integration.config.ts tests/integration/integration.test.ts --reporter=verbose",
|
|
48
|
+
"test:integration:rate-limit": "vitest run --config vitest.integration.config.ts tests/integration/integration-rate-limit.test.ts --reporter=verbose",
|
|
47
49
|
"test:watch": "vitest",
|
|
48
50
|
"test:coverage": "vitest run --coverage",
|
|
49
51
|
"validate": "yarn lint && yarn build && yarn test",
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
],
|
|
64
66
|
"dependencies": {
|
|
65
67
|
"@homebridge/plugin-ui-utils": "1.0.3",
|
|
66
|
-
"winix-api": "1.
|
|
68
|
+
"winix-api": "1.9.0"
|
|
67
69
|
},
|
|
68
70
|
"devDependencies": {
|
|
69
71
|
"@types/node": "20.11.0",
|