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 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**: Features Winix API response caching to minimize requests and avoid rate limiting.
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
- ### Manual Configuration
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
- ### Encrypting Your Password (For Manual Setup and HOOBS Users)
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
- ### Using HOOBS?
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 pollIntervalMs = Math.max(configuredSeconds, MIN_POLL_INTERVAL_SECONDS) * 1000;
28
- this.device = new device_1.Device(deviceId, pollIntervalMs, this.log);
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 winix_api_1.WinixAPI.getDeviceStatus(this.deviceId);
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 winix_api_1.WinixAPI.setPower(this.deviceId, value);
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 winix_api_1.WinixAPI.setMode(this.deviceId, value);
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 winix_api_1.WinixAPI.setAirflow(this.deviceId, value);
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 winix_api_1.WinixAPI.setPlasmawave(this.deviceId, value);
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 winix_api_1.WinixAPI.getDeviceStatus(this.deviceId);
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.0",
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.7.0"
66
+ "winix-api": "1.9.0"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "20.11.0",