homebridge-winix-purifiers 2.2.4 → 2.2.6
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/characteristic.js +10 -1
- package/dist/device.js +23 -7
- package/dist/winix.js +13 -2
- package/package.json +3 -2
package/dist/characteristic.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.CharacteristicWrapper = exports.CharacteristicManager = void 0;
|
|
4
|
+
const winix_api_1 = require("winix-api");
|
|
4
5
|
const errors_1 = require("./errors");
|
|
5
6
|
/**
|
|
6
7
|
* Manager to provide a consistent way to get and set characteristics
|
|
@@ -62,7 +63,15 @@ class CharacteristicWrapper {
|
|
|
62
63
|
}
|
|
63
64
|
catch (e) {
|
|
64
65
|
(0, errors_1.assertError)(e);
|
|
65
|
-
|
|
66
|
+
// Known transient upstream conditions (rate limit, 5xx, DNS, malformed body)
|
|
67
|
+
// still fail the HAP call so HomeKit knows the action didn't land, but log at
|
|
68
|
+
// warn without the implication of a plugin bug.
|
|
69
|
+
if (e instanceof winix_api_1.RateLimitError || e instanceof winix_api_1.UpstreamUnavailableError) {
|
|
70
|
+
this.log.warn(`Winix API transient: ${e.constructor.name}: ${e.message}`);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.log.error('error calling Winix API:', e.message);
|
|
74
|
+
}
|
|
66
75
|
throw new this.platform.HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */);
|
|
67
76
|
}
|
|
68
77
|
}
|
package/dist/device.js
CHANGED
|
@@ -5,6 +5,9 @@ const winix_api_1 = require("winix-api");
|
|
|
5
5
|
const MAX_BACKOFF_MS = 5 * 60 * 1000;
|
|
6
6
|
const COMMAND_DELAY_MS = 1500;
|
|
7
7
|
const UNREACHABLE_THRESHOLD = 3;
|
|
8
|
+
// 90s clears WinixClient's internal 60s rate-limit cooldown plus a small buffer,
|
|
9
|
+
// so the first poll after a rate-limited initialFetch actually has a chance to succeed.
|
|
10
|
+
const FAILED_FETCH_RETRY_MS = 90 * 1000;
|
|
8
11
|
class Device {
|
|
9
12
|
constructor(deviceId, pollIntervalMs, log, client) {
|
|
10
13
|
this.deviceId = deviceId;
|
|
@@ -41,8 +44,8 @@ class Device {
|
|
|
41
44
|
this.log.debug('device:initialFetch()', JSON.stringify(this.state));
|
|
42
45
|
}
|
|
43
46
|
catch (e) {
|
|
44
|
-
if (e instanceof winix_api_1.RateLimitError) {
|
|
45
|
-
this.log.warn(
|
|
47
|
+
if (e instanceof winix_api_1.RateLimitError || e instanceof winix_api_1.UpstreamUnavailableError) {
|
|
48
|
+
this.log.warn(`device:initialFetch() ${e.constructor.name}, using defaults (will retry soon)`);
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
this.log.warn('device:initialFetch() failed, using defaults:', e.message);
|
|
@@ -50,10 +53,14 @@ class Device {
|
|
|
50
53
|
}
|
|
51
54
|
startPolling(onUpdate) {
|
|
52
55
|
this.onUpdate = onUpdate;
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
// If initialFetch failed (rate limited, upstream unavailable, etc.), the device
|
|
57
|
+
// is unreachable in HomeKit until the first successful poll. Skip the full-interval
|
|
58
|
+
// jitter and retry soon to close that window. Otherwise stagger the first poll
|
|
59
|
+
// with a random delay to avoid all devices hitting the API at the same time.
|
|
60
|
+
const delay = this.hasReceivedData
|
|
61
|
+
? Math.floor(Math.random() * this.pollIntervalMs)
|
|
62
|
+
: Math.min(FAILED_FETCH_RETRY_MS, this.pollIntervalMs);
|
|
63
|
+
this.schedulePoll(delay);
|
|
57
64
|
}
|
|
58
65
|
resetPollTimer(delayMs = 3000) {
|
|
59
66
|
this.schedulePoll(delayMs);
|
|
@@ -168,9 +175,18 @@ class Device {
|
|
|
168
175
|
this.schedulePoll(this.pollIntervalMs);
|
|
169
176
|
return;
|
|
170
177
|
}
|
|
178
|
+
// Transient upstream conditions (Winix "no data", 5xx, DNS/connect failures,
|
|
179
|
+
// malformed bodies). Keep last-known state and don't count toward unreachable.
|
|
180
|
+
if (e instanceof winix_api_1.NoDataError || e instanceof winix_api_1.UpstreamUnavailableError) {
|
|
181
|
+
this.log.debug(`device:poll() ${e.constructor.name}, retaining last-known state`);
|
|
182
|
+
this.schedulePoll(this.pollIntervalMs);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
171
185
|
this.consecutiveFailures++;
|
|
172
186
|
const backoffMs = Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveFailures), MAX_BACKOFF_MS);
|
|
173
|
-
|
|
187
|
+
const err = e;
|
|
188
|
+
this.log.error(`device:poll() error: ${err.constructor?.name ?? 'Error'}: ${err.message} ` +
|
|
189
|
+
`(retry in ${Math.round(backoffMs / 1000)}s)`);
|
|
174
190
|
this.schedulePoll(backoffMs);
|
|
175
191
|
return;
|
|
176
192
|
}
|
package/dist/winix.js
CHANGED
|
@@ -10,6 +10,7 @@ const promises_1 = require("node:fs/promises");
|
|
|
10
10
|
const node_path_1 = __importDefault(require("node:path"));
|
|
11
11
|
const TOKEN_DIRECTORY_NAME = 'winix-purifiers';
|
|
12
12
|
const TOKEN_FILE_NAME = 'token.json';
|
|
13
|
+
const MIN_RELOGIN_INTERVAL_MS = 5 * 60 * 1000;
|
|
13
14
|
class NotConfiguredError extends Error {
|
|
14
15
|
constructor() {
|
|
15
16
|
super('Account not configured');
|
|
@@ -25,6 +26,7 @@ exports.UnauthenticatedError = UnauthenticatedError;
|
|
|
25
26
|
class WinixHandler {
|
|
26
27
|
constructor(storagePath, encryptionKey) {
|
|
27
28
|
this.encryptionKey = encryptionKey;
|
|
29
|
+
this.lastReloginAt = 0;
|
|
28
30
|
this.refreshTokenPath = node_path_1.default.join(storagePath, TOKEN_DIRECTORY_NAME, TOKEN_FILE_NAME);
|
|
29
31
|
}
|
|
30
32
|
/**
|
|
@@ -110,10 +112,19 @@ class WinixHandler {
|
|
|
110
112
|
devices = await this.winix.getDevices();
|
|
111
113
|
}
|
|
112
114
|
catch (e) {
|
|
113
|
-
if (
|
|
115
|
+
if (e instanceof winix_api_1.RefreshTokenExpiredError) {
|
|
116
|
+
await this.login(this.auth.username, this.auth.password);
|
|
117
|
+
return await this.winix.getDevices();
|
|
118
|
+
}
|
|
119
|
+
if (!(e instanceof winix_api_1.MobileSessionInvalidError)) {
|
|
120
|
+
throw e;
|
|
121
|
+
}
|
|
122
|
+
// Mobile session invalidated (MULTI LOGIN / "user is not valid"). Re-login
|
|
123
|
+
// is throttled to prevent ping-pong with another active session.
|
|
124
|
+
if (Date.now() - this.lastReloginAt < MIN_RELOGIN_INTERVAL_MS) {
|
|
114
125
|
throw e;
|
|
115
126
|
}
|
|
116
|
-
|
|
127
|
+
this.lastReloginAt = Date.now();
|
|
117
128
|
await this.login(this.auth.username, this.auth.password);
|
|
118
129
|
return await this.winix.getDevices();
|
|
119
130
|
}
|
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.6",
|
|
5
5
|
"description": "Homebridge plugin for Winix air purifiers",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"test": "vitest run",
|
|
47
47
|
"test:integration": "vitest run --config vitest.integration.config.ts tests/integration/integration.test.ts --reporter=verbose",
|
|
48
48
|
"test:integration:rate-limit": "vitest run --config vitest.integration.config.ts tests/integration/integration-rate-limit.test.ts --reporter=verbose",
|
|
49
|
+
"test:integration:session-recovery": "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --config vitest.integration.config.ts tests/integration/integration-session-recovery.test.ts --reporter=verbose",
|
|
49
50
|
"test:watch": "vitest",
|
|
50
51
|
"test:coverage": "vitest run --coverage",
|
|
51
52
|
"validate": "yarn lint && yarn build && yarn test",
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
],
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"@homebridge/plugin-ui-utils": "1.0.3",
|
|
68
|
-
"winix-api": "2.0.
|
|
69
|
+
"winix-api": "2.0.2"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@types/node": "20.11.0",
|