@switchbot/homebridge-switchbot 5.0.0-beta.30 → 5.0.0-beta.32
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 +23 -3
- package/config.schema.json +28 -0
- package/dist/platform-matter.d.ts +2 -0
- package/dist/platform-matter.d.ts.map +1 -1
- package/dist/platform-matter.js +116 -7
- package/dist/platform-matter.js.map +1 -1
- package/dist/settings.d.ts +2 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/test/hap/platform-hap.logging.test.d.ts +2 -0
- package/dist/test/hap/platform-hap.logging.test.d.ts.map +1 -0
- package/dist/test/hap/platform-hap.logging.test.js +33 -0
- package/dist/test/hap/platform-hap.logging.test.js.map +1 -0
- package/dist/test/hap/platform-hap.test.d.ts +2 -0
- package/dist/test/hap/platform-hap.test.d.ts.map +1 -0
- package/dist/test/hap/platform-hap.test.js +62 -0
- package/dist/test/hap/platform-hap.test.js.map +1 -0
- package/dist/test/helpers/platform-fixtures.d.ts +5 -5
- package/dist/{index.test.d.ts.map → test/index.test.d.ts.map} +1 -1
- package/dist/{index.test.js → test/index.test.js} +2 -2
- package/dist/test/index.test.js.map +1 -0
- package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts.map +1 -0
- package/dist/test/{devices-matter → matter/devices-matter}/baseMatterAccessory.test.js +1 -1
- package/dist/test/matter/devices-matter/baseMatterAccessory.test.js.map +1 -0
- package/dist/test/matter/platform-matter.additional.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.additional.test.js → matter/platform-matter.additional.test.js} +2 -2
- package/dist/test/matter/platform-matter.additional.test.js.map +1 -0
- package/dist/test/matter/platform-matter.bleparse.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.bleparse.test.js → matter/platform-matter.bleparse.test.js} +2 -2
- package/dist/test/matter/platform-matter.bleparse.test.js.map +1 -0
- package/dist/test/matter/platform-matter.cleanup.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.cleanup.test.js → matter/platform-matter.cleanup.test.js} +3 -3
- package/dist/test/matter/platform-matter.cleanup.test.js.map +1 -0
- package/dist/test/matter/platform-matter.keepstale.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.keepstale.test.js → matter/platform-matter.keepstale.test.js} +2 -2
- package/dist/test/matter/platform-matter.keepstale.test.js.map +1 -0
- package/dist/test/matter/platform-matter.logging.test.d.ts +2 -0
- package/dist/test/matter/platform-matter.logging.test.d.ts.map +1 -0
- package/dist/test/matter/platform-matter.logging.test.js +29 -0
- package/dist/test/matter/platform-matter.logging.test.js.map +1 -0
- package/dist/test/matter/platform-matter.mapping.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.mapping.test.js → matter/platform-matter.mapping.test.js} +2 -2
- package/dist/test/matter/platform-matter.mapping.test.js.map +1 -0
- package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.openapi-mapping.test.js → matter/platform-matter.openapi-mapping.test.js} +2 -2
- package/dist/test/matter/platform-matter.openapi-mapping.test.js.map +1 -0
- package/dist/test/matter/platform-matter.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.test.js → matter/platform-matter.test.js} +3 -3
- package/dist/test/matter/platform-matter.test.js.map +1 -0
- package/dist/test/matter/platform-matter.unregister.test.d.ts.map +1 -0
- package/dist/test/{platform-matter.unregister.test.js → matter/platform-matter.unregister.test.js} +2 -2
- package/dist/test/matter/platform-matter.unregister.test.js.map +1 -0
- package/dist/{utils.test.d.ts.map → test/utils.test.d.ts.map} +1 -1
- package/dist/{utils.test.js → test/utils.test.js} +1 -1
- package/dist/test/utils.test.js.map +1 -0
- package/dist/test/verifyconfig.test.d.ts.map +1 -0
- package/dist/test/verifyconfig.test.js.map +1 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +37 -12
- package/dist/utils.js.map +1 -1
- package/docs/index.html +19 -1
- package/docs/variables/default.html +1 -1
- package/package.json +3 -3
- package/src/platform-matter.ts +118 -11
- package/src/settings.ts +2 -0
- package/src/test/hap/platform-hap.logging.test.ts +36 -0
- package/src/test/hap/platform-hap.test.ts +70 -0
- package/src/{index.test.ts → test/index.test.ts} +2 -2
- package/src/test/{devices-matter → matter/devices-matter}/baseMatterAccessory.test.ts +1 -1
- package/src/test/{platform-matter.additional.test.ts → matter/platform-matter.additional.test.ts} +2 -2
- package/src/test/{platform-matter.bleparse.test.ts → matter/platform-matter.bleparse.test.ts} +2 -2
- package/src/test/{platform-matter.cleanup.test.ts → matter/platform-matter.cleanup.test.ts} +3 -3
- package/src/test/{platform-matter.keepstale.test.ts → matter/platform-matter.keepstale.test.ts} +2 -2
- package/src/test/matter/platform-matter.logging.test.ts +33 -0
- package/src/test/{platform-matter.mapping.test.ts → matter/platform-matter.mapping.test.ts} +2 -2
- package/src/test/{platform-matter.openapi-mapping.test.ts → matter/platform-matter.openapi-mapping.test.ts} +2 -2
- package/src/test/{platform-matter.test.ts → matter/platform-matter.test.ts} +3 -3
- package/src/test/{platform-matter.unregister.test.ts → matter/platform-matter.unregister.test.ts} +2 -2
- package/src/{utils.test.ts → test/utils.test.ts} +1 -1
- package/src/{verifyconfig.test.ts → test/verifyconfig.test.ts} +1 -1
- package/src/utils.ts +37 -9
- package/dist/index.test.js.map +0 -1
- package/dist/test/devices-matter/baseMatterAccessory.test.d.ts.map +0 -1
- package/dist/test/devices-matter/baseMatterAccessory.test.js.map +0 -1
- package/dist/test/platform-matter.additional.test.d.ts.map +0 -1
- package/dist/test/platform-matter.additional.test.js.map +0 -1
- package/dist/test/platform-matter.bleparse.test.d.ts.map +0 -1
- package/dist/test/platform-matter.bleparse.test.js.map +0 -1
- package/dist/test/platform-matter.cleanup.test.d.ts.map +0 -1
- package/dist/test/platform-matter.cleanup.test.js.map +0 -1
- package/dist/test/platform-matter.keepstale.test.d.ts.map +0 -1
- package/dist/test/platform-matter.keepstale.test.js.map +0 -1
- package/dist/test/platform-matter.mapping.test.d.ts.map +0 -1
- package/dist/test/platform-matter.mapping.test.js.map +0 -1
- package/dist/test/platform-matter.openapi-mapping.test.d.ts.map +0 -1
- package/dist/test/platform-matter.openapi-mapping.test.js.map +0 -1
- package/dist/test/platform-matter.test.d.ts.map +0 -1
- package/dist/test/platform-matter.test.js.map +0 -1
- package/dist/test/platform-matter.unregister.test.d.ts.map +0 -1
- package/dist/test/platform-matter.unregister.test.js.map +0 -1
- package/dist/utils.test.js.map +0 -1
- package/dist/verifyconfig.test.d.ts.map +0 -1
- package/dist/verifyconfig.test.js.map +0 -1
- /package/dist/{index.test.d.ts → test/index.test.d.ts} +0 -0
- /package/dist/test/{devices-matter → matter/devices-matter}/baseMatterAccessory.test.d.ts +0 -0
- /package/dist/test/{platform-matter.additional.test.d.ts → matter/platform-matter.additional.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.bleparse.test.d.ts → matter/platform-matter.bleparse.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.cleanup.test.d.ts → matter/platform-matter.cleanup.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.keepstale.test.d.ts → matter/platform-matter.keepstale.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.mapping.test.d.ts → matter/platform-matter.mapping.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.openapi-mapping.test.d.ts → matter/platform-matter.openapi-mapping.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.test.d.ts → matter/platform-matter.test.d.ts} +0 -0
- /package/dist/test/{platform-matter.unregister.test.d.ts → matter/platform-matter.unregister.test.d.ts} +0 -0
- /package/dist/{utils.test.d.ts → test/utils.test.d.ts} +0 -0
- /package/dist/{verifyconfig.test.d.ts → test/verifyconfig.test.d.ts} +0 -0
- /package/dist/{verifyconfig.test.js → test/verifyconfig.test.js} +0 -0
package/README.md
CHANGED
|
@@ -52,16 +52,13 @@
|
|
|
52
52
|
## Troubleshooting
|
|
53
53
|
|
|
54
54
|
- ### If using Linux / Raspberry Pi OS
|
|
55
|
-
|
|
56
55
|
1. `bluetoothctl` must be installed on the device, otherwise it cannot communicate via Bluetooth. Enable it with `sudo bluetoothctl power on`.
|
|
57
56
|
|
|
58
57
|
2. If errors occur, while enabling it, restart the process:
|
|
59
|
-
|
|
60
58
|
- `rfkill block bluetooth`
|
|
61
59
|
- `rfkill unblock bluetooth`
|
|
62
60
|
|
|
63
61
|
3. Also make sure, that the computer can discover the SwitchBot device:
|
|
64
|
-
|
|
65
62
|
- `sudo bluetoothctl`
|
|
66
63
|
- `scan on`
|
|
67
64
|
|
|
@@ -218,6 +215,29 @@
|
|
|
218
215
|
- Supports Fan Device Type
|
|
219
216
|
- Allows for On/Off Controls
|
|
220
217
|
|
|
218
|
+
## Matter Platform
|
|
219
|
+
|
|
220
|
+
### Batched refresh and API load control
|
|
221
|
+
|
|
222
|
+
By default, the Matter platform uses a single batched refresh to update device status. You can tune or override this behavior with the following options under `options`:
|
|
223
|
+
|
|
224
|
+
- `matterBatchEnabled` (boolean, default true): enable/disable platform-level batched refresh. Devices with a per-device `refreshRate` still run their own timers.
|
|
225
|
+
- `matterBatchRefreshRate` (number, seconds): batch interval (falls back to `options.refreshRate`, then 300 if not set).
|
|
226
|
+
- `matterBatchConcurrency` (number): limit of parallel OpenAPI status calls during a batch (default 5).
|
|
227
|
+
- `matterBatchJitter` (number, seconds): random startup delay before the first batch to reduce synchronized spikes.
|
|
228
|
+
|
|
229
|
+
Device-level override:
|
|
230
|
+
|
|
231
|
+
- If a device sets `refreshRate` in its config, it uses a per-device timer and is excluded from the platform batch.
|
|
232
|
+
|
|
233
|
+
Reliability and rate-limiting:
|
|
234
|
+
|
|
235
|
+
- Each device status call retries with exponential backoff on non-success responses.
|
|
236
|
+
- After exhausting retries, the device enters a short cooldown before being retried; cooldowns are persisted across restarts.
|
|
237
|
+
- The batch worklist is randomized every cycle to further distribute API load.
|
|
238
|
+
|
|
239
|
+
These controls keep API usage smooth and predictable while preserving per-device control when needed.
|
|
240
|
+
|
|
221
241
|
## SwitchBot APIs
|
|
222
242
|
|
|
223
243
|
- [OpenWonderLabs/node-switchbot](https://github.com/OpenWonderLabs/node-switchbot)
|
package/config.schema.json
CHANGED
|
@@ -52,6 +52,18 @@
|
|
|
52
52
|
"placeholder": 5,
|
|
53
53
|
"description": "Maximum number of parallel OpenAPI status calls during a batch refresh."
|
|
54
54
|
},
|
|
55
|
+
"matterBatchEnabled": {
|
|
56
|
+
"title": "Enable Matter Batch Refresh",
|
|
57
|
+
"type": "boolean",
|
|
58
|
+
"default": true,
|
|
59
|
+
"description": "When true (default), the platform runs a single batched refresh timer. Devices with a per-device refreshRate still use their own timers."
|
|
60
|
+
},
|
|
61
|
+
"matterBatchJitter": {
|
|
62
|
+
"title": "Matter Batch Start Jitter (seconds)",
|
|
63
|
+
"type": "number",
|
|
64
|
+
"placeholder": 30,
|
|
65
|
+
"description": "Random delay added before starting the platform-level batch timer to reduce synchronized spikes."
|
|
66
|
+
},
|
|
55
67
|
"devices": {
|
|
56
68
|
"type": "array",
|
|
57
69
|
"items": {
|
|
@@ -14834,6 +14846,22 @@
|
|
|
14834
14846
|
"options.mqttPubOptions",
|
|
14835
14847
|
"options.maxRetries",
|
|
14836
14848
|
"options.delayBetweenRetries",
|
|
14849
|
+
{
|
|
14850
|
+
"key": "options.matterBatchRefreshRate",
|
|
14851
|
+
"description": "<em class='primary-text'>Specifies the interval, in seconds, for retrieving the latest device status from the SwitchBot API when batching Matter requests.</em>"
|
|
14852
|
+
},
|
|
14853
|
+
{
|
|
14854
|
+
"key": "options.matterBatchConcurrency",
|
|
14855
|
+
"description": "<em class='primary-text'>Specifies the maximum number of Matter requests to process concurrently.</em>"
|
|
14856
|
+
},
|
|
14857
|
+
{
|
|
14858
|
+
"key": "options.matterBatchEnabled",
|
|
14859
|
+
"description": "<em class='primary-text'>If true, enables batching of Matter requests to optimize performance. Default: false.</em>"
|
|
14860
|
+
},
|
|
14861
|
+
{
|
|
14862
|
+
"key": "options.matterBatchJitter",
|
|
14863
|
+
"description": "<em class='primary-text'>Specifies the maximum random jitter, in seconds, to add to the batch processing interval to prevent synchronization issues. Default: 0.</em>"
|
|
14864
|
+
},
|
|
14837
14865
|
{
|
|
14838
14866
|
"key": "options.refreshRate",
|
|
14839
14867
|
"description": "<em class='primary-text'>Specifies the interval, in seconds, for retrieving the latest device status from the SwitchBot API.</em>"
|
|
@@ -13,6 +13,8 @@ export declare class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
|
|
|
13
13
|
private refreshTimers;
|
|
14
14
|
private platformRefreshTimer?;
|
|
15
15
|
private deviceStatusCache;
|
|
16
|
+
private backoffCooldowns;
|
|
17
|
+
private backoffFilePath?;
|
|
16
18
|
private perDeviceRefreshSet;
|
|
17
19
|
private bleEventHandler;
|
|
18
20
|
private mqttClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-matter.d.ts","sourceRoot":"","sources":["../src/platform-matter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,GAAG,EACH,qBAAqB,EACrB,OAAO,EAEP,yBAAyB,EAC1B,MAAM,YAAY,CAAA;AAEnB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAExD,OAAO,KAAK,EAAiB,uBAAuB,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"platform-matter.d.ts","sourceRoot":"","sources":["../src/platform-matter.ts"],"names":[],"mappings":"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"}
|
package/dist/platform-matter.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import asyncmqtt from 'async-mqtt';
|
|
2
4
|
import { SwitchBotBLE, SwitchBotOpenAPI } from 'node-switchbot';
|
|
3
5
|
import { ColorLightAccessory, ColorTemperatureLightAccessory, ContactSensorAccessory, DimmableLightAccessory, DoorLockAccessory, ExtendedColorLightAccessory, FanAccessory, HumiditySensorAccessory, LeakSensorAccessory, LightSensorAccessory, OccupancySensorAccessory, OnOffLightAccessory, OnOffOutletAccessory, OnOffSwitchAccessory, PowerStripAccessory, RoboticVacuumAccessory, SmokeCOAlarmAccessory, TemperatureSensorAccessory, ThermostatAccessory, VenetianBlindAccessory, WindowBlindAccessory, } from './devices-matter/index.js';
|
|
@@ -25,6 +27,9 @@ export class SwitchBotMatterPlatform {
|
|
|
25
27
|
platformRefreshTimer;
|
|
26
28
|
// Device status cache from last refresh
|
|
27
29
|
deviceStatusCache = new Map();
|
|
30
|
+
// Backoff cooldowns persisted across restarts: normalized deviceId -> nextAllowedAt(ms)
|
|
31
|
+
backoffCooldowns = new Map();
|
|
32
|
+
backoffFilePath;
|
|
28
33
|
// Devices that have explicit per-device refresh timers (normalized deviceId)
|
|
29
34
|
perDeviceRefreshSet = new Set();
|
|
30
35
|
// BLE event handlers keyed by device MAC (formatted)
|
|
@@ -236,6 +241,22 @@ export class SwitchBotMatterPlatform {
|
|
|
236
241
|
catch (e) {
|
|
237
242
|
this.debugLog(`Failed to stop BLE scanning: ${e?.message ?? e}`);
|
|
238
243
|
}
|
|
244
|
+
// Persist backoff cooldowns to disk
|
|
245
|
+
try {
|
|
246
|
+
if (this.backoffFilePath) {
|
|
247
|
+
const obj = {};
|
|
248
|
+
for (const [k, v] of this.backoffCooldowns.entries()) {
|
|
249
|
+
if (Number.isFinite(v)) {
|
|
250
|
+
obj[k] = v;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
writeFileSync(this.backoffFilePath, JSON.stringify(obj, null, 2));
|
|
254
|
+
this.debugLog(`Saved ${Object.keys(obj).length} backoff cooldown entries`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
this.debugLog(`Failed to save backoff state: ${e?.message ?? e}`);
|
|
259
|
+
}
|
|
239
260
|
}
|
|
240
261
|
catch (e) {
|
|
241
262
|
this.debugLog('Shutdown cleanup failed: %s', e?.message ?? e);
|
|
@@ -268,6 +289,32 @@ export class SwitchBotMatterPlatform {
|
|
|
268
289
|
catch (e) {
|
|
269
290
|
this.errorLog(`Setup Webhook, Error Message: ${e?.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug');
|
|
270
291
|
}
|
|
292
|
+
// Initialize backoff persistence file path and load any saved cooldowns
|
|
293
|
+
try {
|
|
294
|
+
const storagePath = this.api.user.storagePath();
|
|
295
|
+
this.backoffFilePath = join(storagePath, `${PLUGIN_NAME.replace('@', '').replace('/', '-')}-matter-backoff.json`);
|
|
296
|
+
if (existsSync(this.backoffFilePath)) {
|
|
297
|
+
try {
|
|
298
|
+
const raw = readFileSync(this.backoffFilePath, 'utf8');
|
|
299
|
+
const parsed = JSON.parse(raw);
|
|
300
|
+
if (parsed && typeof parsed === 'object') {
|
|
301
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
302
|
+
const ts = Number(v);
|
|
303
|
+
if (Number.isFinite(ts)) {
|
|
304
|
+
this.backoffCooldowns.set(String(k), ts);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
this.debugLog(`Loaded ${this.backoffCooldowns.size} backoff cooldown entries`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
this.debugLog(`Failed to load backoff state: ${e?.message ?? e}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
this.debugLog(`Failed to initialize backoff persistence: ${e?.message ?? e}`);
|
|
317
|
+
}
|
|
271
318
|
}
|
|
272
319
|
/**
|
|
273
320
|
* Normalize a deviceId for matching (uppercase alphanumerics only)
|
|
@@ -946,6 +993,12 @@ export class SwitchBotMatterPlatform {
|
|
|
946
993
|
this.perDeviceRefreshSet.add(nid);
|
|
947
994
|
const timer = setInterval(async () => {
|
|
948
995
|
try {
|
|
996
|
+
// Skip if device is under cooldown
|
|
997
|
+
const now = Date.now();
|
|
998
|
+
const nextAllowed = this.backoffCooldowns.get(nid) ?? 0;
|
|
999
|
+
if (now < nextAllowed) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
949
1002
|
await this.refreshSingleDeviceWithRetry(dev);
|
|
950
1003
|
}
|
|
951
1004
|
catch (e) {
|
|
@@ -1234,11 +1287,35 @@ export class SwitchBotMatterPlatform {
|
|
|
1234
1287
|
// Helper to safely call instance methods or fallback to api.matter.updateAccessoryState
|
|
1235
1288
|
const safeUpdate = async (cluster, attributes, methodName) => {
|
|
1236
1289
|
try {
|
|
1290
|
+
// Special-case: powerSource cluster is optional on many devices (e.g., Curtains/Blinds).
|
|
1291
|
+
// To avoid noisy Matter server errors ("Behavior ID powerSource does not exist"),
|
|
1292
|
+
// always use the direct updateAccessoryState path wrapped in a guard for this cluster,
|
|
1293
|
+
// even when an accessory instance is present.
|
|
1294
|
+
const powerClusterName = (this.api.matter?.clusterNames && this.api.matter.clusterNames.PowerSource)
|
|
1295
|
+
? this.api.matter.clusterNames.PowerSource
|
|
1296
|
+
: 'powerSource';
|
|
1297
|
+
const isPowerSourceCluster = cluster === powerClusterName || cluster === 'powerSource';
|
|
1298
|
+
// If the accessory instance declares supported clusters, skip updates for clusters
|
|
1299
|
+
// not present to avoid triggering Matter server errors and logs.
|
|
1300
|
+
let clusterSupported = true;
|
|
1301
|
+
if (instance && instance.clusters) {
|
|
1302
|
+
const declared = instance.clusters;
|
|
1303
|
+
if (Array.isArray(declared)) {
|
|
1304
|
+
clusterSupported = declared.includes(cluster);
|
|
1305
|
+
}
|
|
1306
|
+
else if (typeof declared === 'object') {
|
|
1307
|
+
clusterSupported = Object.prototype.hasOwnProperty.call(declared, cluster);
|
|
1308
|
+
}
|
|
1309
|
+
if (!clusterSupported) {
|
|
1310
|
+
this.debugLog(`Cluster ${cluster} not declared on accessory for ${dev.deviceId}, skipping update`);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1237
1314
|
if (instance && methodName && typeof instance[methodName] === 'function') {
|
|
1238
1315
|
// prefer device-specific update helpers when available
|
|
1239
1316
|
await instance[methodName](...(Object.values(attributes)));
|
|
1240
1317
|
}
|
|
1241
|
-
else if (instance && typeof instance.updateState === 'function') {
|
|
1318
|
+
else if (!isPowerSourceCluster && instance && typeof instance.updateState === 'function') {
|
|
1242
1319
|
// some accessories expose updateState that accepts cluster and attributes
|
|
1243
1320
|
await instance.updateState(cluster, attributes);
|
|
1244
1321
|
}
|
|
@@ -1708,7 +1785,7 @@ export class SwitchBotMatterPlatform {
|
|
|
1708
1785
|
return;
|
|
1709
1786
|
}
|
|
1710
1787
|
// If no discovered devices are available, do not register example/demo accessories.
|
|
1711
|
-
this.infoLog('No discovered SwitchBot devices found
|
|
1788
|
+
this.infoLog('No discovered SwitchBot devices found.');
|
|
1712
1789
|
this.debugLog('═'.repeat(80));
|
|
1713
1790
|
this.debugLog('Finished registering Matter accessories');
|
|
1714
1791
|
this.debugLog('═'.repeat(80));
|
|
@@ -2009,10 +2086,26 @@ export class SwitchBotMatterPlatform {
|
|
|
2009
2086
|
if (this.platformRefreshTimer) {
|
|
2010
2087
|
return;
|
|
2011
2088
|
}
|
|
2012
|
-
|
|
2013
|
-
this.
|
|
2014
|
-
|
|
2015
|
-
|
|
2089
|
+
// Respect user toggle
|
|
2090
|
+
if (this.config.options?.matterBatchEnabled === false) {
|
|
2091
|
+
this.infoLog('Matter batch refresh is disabled by configuration');
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const jitterSec = Number(this.config.options?.matterBatchJitter ?? 0);
|
|
2095
|
+
const intervalMs = Number(refreshRateSec) * 1000;
|
|
2096
|
+
const jitterMs = Number.isFinite(jitterSec) && jitterSec > 0 ? Math.floor(Math.random() * jitterSec * 1000) : 0;
|
|
2097
|
+
// Start after optional jitter, then schedule recurring interval
|
|
2098
|
+
setTimeout(async () => {
|
|
2099
|
+
try {
|
|
2100
|
+
await this.batchRefreshAllDevices();
|
|
2101
|
+
}
|
|
2102
|
+
catch (e) {
|
|
2103
|
+
this.debugLog(`Initial batch refresh failed: ${e?.message ?? e}`);
|
|
2104
|
+
}
|
|
2105
|
+
this.platformRefreshTimer = setInterval(async () => {
|
|
2106
|
+
await this.batchRefreshAllDevices();
|
|
2107
|
+
}, intervalMs);
|
|
2108
|
+
}, jitterMs);
|
|
2016
2109
|
}
|
|
2017
2110
|
/**
|
|
2018
2111
|
* Batch refresh all devices - still makes individual API calls but batches them together
|
|
@@ -2032,7 +2125,10 @@ export class SwitchBotMatterPlatform {
|
|
|
2032
2125
|
continue;
|
|
2033
2126
|
}
|
|
2034
2127
|
const dev = this.discoveredDevices.find(d => this.normalizeDeviceId(d.deviceId) === nid);
|
|
2035
|
-
|
|
2128
|
+
// Skip devices with per-device timers and those in cooldown
|
|
2129
|
+
const now = Date.now();
|
|
2130
|
+
const nextAllowed = this.backoffCooldowns.get(nid) ?? 0;
|
|
2131
|
+
if (dev && !this.perDeviceRefreshSet.has(nid) && now >= nextAllowed) {
|
|
2036
2132
|
devicesToRefresh.push({ uuid, dev });
|
|
2037
2133
|
}
|
|
2038
2134
|
}
|
|
@@ -2046,6 +2142,11 @@ export class SwitchBotMatterPlatform {
|
|
|
2046
2142
|
return;
|
|
2047
2143
|
}
|
|
2048
2144
|
this.debugLog(`Refreshing ${devicesToRefresh.length} devices in parallel batch`);
|
|
2145
|
+
// Randomize order to reduce synchronized spikes
|
|
2146
|
+
for (let i = devicesToRefresh.length - 1; i > 0; i--) {
|
|
2147
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
2148
|
+
[devicesToRefresh[i], devicesToRefresh[j]] = [devicesToRefresh[j], devicesToRefresh[i]];
|
|
2149
|
+
}
|
|
2049
2150
|
const concurrency = Number(this.config.options?.matterBatchConcurrency ?? 5);
|
|
2050
2151
|
await this.runWithConcurrency(devicesToRefresh, async ({ uuid, dev }) => {
|
|
2051
2152
|
try {
|
|
@@ -2088,6 +2189,14 @@ export class SwitchBotMatterPlatform {
|
|
|
2088
2189
|
await sleep(delay);
|
|
2089
2190
|
}
|
|
2090
2191
|
}
|
|
2192
|
+
// Set a cooldown after exhausting retries to avoid hammering problematic devices
|
|
2193
|
+
try {
|
|
2194
|
+
const nid = this.normalizeDeviceId(deviceId);
|
|
2195
|
+
const cooldownMs = Math.max(60_000, baseDelayMs * (2 ** retries) * 10);
|
|
2196
|
+
this.backoffCooldowns.set(nid, Date.now() + cooldownMs);
|
|
2197
|
+
this.debugLog(`Applied cooldown for ${deviceId}: ${Math.round(cooldownMs / 1000)}s`);
|
|
2198
|
+
}
|
|
2199
|
+
catch { }
|
|
2091
2200
|
return null;
|
|
2092
2201
|
}
|
|
2093
2202
|
/** Simple concurrency limiter for an array of items */
|