homebridge-winix-purifiers 2.2.0 → 2.2.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/README.md +13 -4
- package/dist/accessory.js +8 -3
- package/dist/device.js +17 -7
- package/dist/platform.js +6 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
HomeKit.
|
|
32
32
|
- **Filter Management**: Exposes the remaining filter life and provides an alert when it's time to change the filter,
|
|
33
33
|
configurable to trigger at a specified percentage of remaining filter life.
|
|
34
|
-
- **Efficiency**:
|
|
34
|
+
- **Efficiency**: Polls the Winix API in the background on a configurable interval to minimize requests and keep state fresh.
|
|
35
35
|
- **Reliability**: Automatically refreshes device list on a configurable interval to ensure devices are always
|
|
36
36
|
up-to-date.
|
|
37
37
|
- **Seamless Authentication**: Automatically refreshes your Winix authentication token in the background, so you no
|
|
@@ -78,7 +78,8 @@ Simply provide your Winix account credentials for automatic device discovery and
|
|
|
78
78
|
|
|
79
79
|
<img src="./assets/link-account.gif" alt="Link Account" />
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
<details>
|
|
82
|
+
<summary><h3>Manual Configuration</h3></summary>
|
|
82
83
|
|
|
83
84
|
While not recommended, if manual setup is required, add the following to the `platforms` section of your `config.json`:
|
|
84
85
|
|
|
@@ -144,7 +145,10 @@ While not recommended, if manual setup is required, add the following to the `pl
|
|
|
144
145
|
| `deviceOverrides[].nameSleepSwitch` | `"Sleep"` | The display name of the Sleep switch. Optional. |
|
|
145
146
|
| `platform` | `"WinixPurifiers"` | Must always be `"WinixPurifiers"` in order for the plugin to load this config. |
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
</details>
|
|
149
|
+
|
|
150
|
+
<details>
|
|
151
|
+
<summary><h3>Encrypting Your Password (For Manual Setup and HOOBS Users)</h3></summary>
|
|
148
152
|
|
|
149
153
|
If you're configuring the plugin manually or using HOOBS, you need to store your Winix password securely in the
|
|
150
154
|
configuration file. To do this, you must first encrypt your password using the provided `encrypt-password` script.
|
|
@@ -189,9 +193,12 @@ The script will output your encrypted password. Copy the result and paste it int
|
|
|
189
193
|
|
|
190
194
|
This ensures that your password is securely stored within the configuration file.
|
|
191
195
|
|
|
196
|
+
</details>
|
|
197
|
+
|
|
192
198
|
## FAQ
|
|
193
199
|
|
|
194
|
-
|
|
200
|
+
<details>
|
|
201
|
+
<summary><h3>Using HOOBS?</h3></summary>
|
|
195
202
|
|
|
196
203
|
If you're using [HOOBS](https://hoobs.org), you can install the plugin directly from the HOOBS interface. You will not
|
|
197
204
|
be able to use the custom configuration UI, since it is not supported in HOOBS. You will need to use the manual
|
|
@@ -201,6 +208,8 @@ details on obtaining the required `auth` values. See
|
|
|
201
208
|
[Encrypting Your Password](#encrypting-your-password-for-manual-setup-and-hoobs-users) for instructions on encrypting
|
|
202
209
|
your Winix password.
|
|
203
210
|
|
|
211
|
+
</details>
|
|
212
|
+
|
|
204
213
|
### Missing “Auto/Manual” switch in Home app?
|
|
205
214
|
|
|
206
215
|
Please see [this issue](https://github.com/regaw-leinad/homebridge-winix-purifiers/issues/1) for more details.
|
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
|
}
|
|
@@ -81,14 +83,14 @@ class WinixPurifierPlatform {
|
|
|
81
83
|
this.log.debug('Found', accessory ? 'existing' : 'new', 'accessory:', this.logName(device));
|
|
82
84
|
if (accessory) {
|
|
83
85
|
accessory.context.device = device;
|
|
84
|
-
const handler = await this.createNewAccessoryHandler(accessory);
|
|
86
|
+
const handler = await this.createNewAccessoryHandler(accessory, devices.length);
|
|
85
87
|
this.handlers.set(uuid, handler);
|
|
86
88
|
this.api.updatePlatformAccessories([accessory]);
|
|
87
89
|
}
|
|
88
90
|
else {
|
|
89
91
|
accessory = new this.api.platformAccessory(device.deviceAlias, uuid, 19 /* Categories.AIR_PURIFIER */);
|
|
90
92
|
accessory.context.device = device;
|
|
91
|
-
const handler = await this.createNewAccessoryHandler(accessory);
|
|
93
|
+
const handler = await this.createNewAccessoryHandler(accessory, devices.length);
|
|
92
94
|
this.accessories.set(uuid, accessory);
|
|
93
95
|
this.handlers.set(uuid, handler);
|
|
94
96
|
accessoriesToAdd.push(accessory);
|
|
@@ -100,12 +102,12 @@ class WinixPurifierPlatform {
|
|
|
100
102
|
}
|
|
101
103
|
this.removeOldDevices(discoveredUUIDs);
|
|
102
104
|
}
|
|
103
|
-
async createNewAccessoryHandler(accessory) {
|
|
105
|
+
async createNewAccessoryHandler(accessory, deviceCount) {
|
|
104
106
|
// 🫣 suppress warning message about adding characteristics which aren't required / optional, since it isn't accurate
|
|
105
107
|
this.suppressCharacteristicWarnings(accessory);
|
|
106
108
|
const deviceOverride = this.deviceOverrides.get(accessory.context.device.deviceId);
|
|
107
109
|
const log = new logger_1.DeviceLogger(this.log, accessory.context.device);
|
|
108
|
-
const handler = new accessory_1.WinixPurifierAccessory(this, this.config, accessory, deviceOverride, log);
|
|
110
|
+
const handler = new accessory_1.WinixPurifierAccessory(this, this.config, accessory, deviceOverride, log, deviceCount);
|
|
109
111
|
this.unsuppressCharacteristicWarnings(accessory);
|
|
110
112
|
await handler.initialize();
|
|
111
113
|
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.2",
|
|
5
5
|
"description": "Homebridge plugin for Winix air purifiers",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
],
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@homebridge/plugin-ui-utils": "1.0.3",
|
|
66
|
-
"winix-api": "1.
|
|
66
|
+
"winix-api": "1.9.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@types/node": "20.11.0",
|