homebridge-winix-purifiers 2.2.3 → 2.2.5
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/device.js +10 -1
- package/dist/platform.js +1 -1
- package/dist/winix.js +19 -2
- package/package.json +3 -2
package/dist/device.js
CHANGED
|
@@ -168,9 +168,18 @@ class Device {
|
|
|
168
168
|
this.schedulePoll(this.pollIntervalMs);
|
|
169
169
|
return;
|
|
170
170
|
}
|
|
171
|
+
// Winix occasionally returns "no data" for a healthy device. Treat as
|
|
172
|
+
// transient: keep last-known state and don't count toward unreachable.
|
|
173
|
+
if (e instanceof winix_api_1.NoDataError) {
|
|
174
|
+
this.log.debug('device:poll() got NoDataError, retaining last-known state');
|
|
175
|
+
this.schedulePoll(this.pollIntervalMs);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
171
178
|
this.consecutiveFailures++;
|
|
172
179
|
const backoffMs = Math.min(this.pollIntervalMs * Math.pow(2, this.consecutiveFailures), MAX_BACKOFF_MS);
|
|
173
|
-
|
|
180
|
+
const err = e;
|
|
181
|
+
this.log.error(`device:poll() error: ${err.constructor?.name ?? 'Error'}: ${err.message} ` +
|
|
182
|
+
`(retry in ${Math.round(backoffMs / 1000)}s)`);
|
|
174
183
|
this.schedulePoll(backoffMs);
|
|
175
184
|
return;
|
|
176
185
|
}
|
package/dist/platform.js
CHANGED
|
@@ -22,7 +22,6 @@ class WinixPurifierPlatform {
|
|
|
22
22
|
this.handlers = new Map();
|
|
23
23
|
this.deviceOverrides = (this.config.deviceOverrides ?? [])
|
|
24
24
|
.reduce((m, o) => m.set(o.deviceId, o), new Map());
|
|
25
|
-
this.client = new winix_api_1.WinixClient();
|
|
26
25
|
this.winix = new winix_1.WinixHandler(api.user.storagePath(), settings_1.ENCRYPTION_KEY);
|
|
27
26
|
this.api.on("didFinishLaunching" /* APIEvent.DID_FINISH_LAUNCHING */, this.onFinishLaunching.bind(this));
|
|
28
27
|
}
|
|
@@ -52,6 +51,7 @@ class WinixPurifierPlatform {
|
|
|
52
51
|
this.log.error('error getting devices:', e.message);
|
|
53
52
|
return;
|
|
54
53
|
}
|
|
54
|
+
this.client = new winix_api_1.WinixClient(this.winix.getIdentityId());
|
|
55
55
|
await this.discoverDevices();
|
|
56
56
|
// refresh devices on the configured interval
|
|
57
57
|
const refreshIntervalMs = (this.config.deviceRefreshIntervalMinutes ?? DEFAULT_DEVICE_REFRESH_INTERVAL_MINUTES) * 60 * 1000;
|
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
|
/**
|
|
@@ -95,6 +97,12 @@ class WinixHandler {
|
|
|
95
97
|
return '';
|
|
96
98
|
}
|
|
97
99
|
}
|
|
100
|
+
getIdentityId() {
|
|
101
|
+
if (!this.winix) {
|
|
102
|
+
throw new UnauthenticatedError();
|
|
103
|
+
}
|
|
104
|
+
return this.winix.getIdentityId();
|
|
105
|
+
}
|
|
98
106
|
async getDevices() {
|
|
99
107
|
if (!this.winix || !this.auth) {
|
|
100
108
|
throw new UnauthenticatedError();
|
|
@@ -104,10 +112,19 @@ class WinixHandler {
|
|
|
104
112
|
devices = await this.winix.getDevices();
|
|
105
113
|
}
|
|
106
114
|
catch (e) {
|
|
107
|
-
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) {
|
|
108
125
|
throw e;
|
|
109
126
|
}
|
|
110
|
-
|
|
127
|
+
this.lastReloginAt = Date.now();
|
|
111
128
|
await this.login(this.auth.username, this.auth.password);
|
|
112
129
|
return await this.winix.getDevices();
|
|
113
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.5",
|
|
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": "
|
|
69
|
+
"winix-api": "2.0.1"
|
|
69
70
|
},
|
|
70
71
|
"devDependencies": {
|
|
71
72
|
"@types/node": "20.11.0",
|