@switchbot/homebridge-switchbot 5.0.0-beta.29 → 5.0.0-beta.30

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.
@@ -11,11 +11,15 @@ export declare class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
11
11
  private discoveredDevices;
12
12
  private accessoryInstances;
13
13
  private refreshTimers;
14
+ private platformRefreshTimer?;
15
+ private deviceStatusCache;
16
+ private perDeviceRefreshSet;
14
17
  private bleEventHandler;
15
18
  private mqttClient;
16
19
  private webhookEventListener;
17
20
  private webhookEventHandler;
18
21
  private platformLogging?;
22
+ private apiTracker?;
19
23
  infoLog: (...args: any[]) => void;
20
24
  successLog: (...args: any[]) => void;
21
25
  debugSuccessLog: (...args: any[]) => void;
@@ -31,6 +35,8 @@ export declare class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
31
35
  * Normalize a deviceId for matching (uppercase alphanumerics only)
32
36
  */
33
37
  private normalizeDeviceId;
38
+ /** Determine the platform-level batch interval in seconds */
39
+ private getPlatformBatchInterval;
34
40
  /**
35
41
  * Clear per-device resources: refresh timers, accessory instance registry, BLE handlers
36
42
  */
@@ -146,5 +152,18 @@ export declare class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
146
152
  * like managing multiple logical components within a single device.
147
153
  */
148
154
  private registerCustomDevices;
155
+ /**
156
+ * Start platform-level refresh timer to batch all device status updates
157
+ */
158
+ private startPlatformRefreshTimer;
159
+ /**
160
+ * Batch refresh all devices - still makes individual API calls but batches them together
161
+ * Note: SwitchBot API doesn't support true batch status calls, but we can parallelize them
162
+ */
163
+ private batchRefreshAllDevices;
164
+ /** Refresh a single device with retry and backoff; returns status object if successful */
165
+ private refreshSingleDeviceWithRetry;
166
+ /** Simple concurrency limiter for an array of items */
167
+ private runWithConcurrency;
149
168
  }
150
169
  //# sourceMappingURL=platform-matter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"platform-matter.d.ts","sourceRoot":"","sources":["../src/platform-matter.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"platform-matter.d.ts","sourceRoot":"","sources":["../src/platform-matter.ts"],"names":[],"mappings":"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"}
@@ -2,7 +2,7 @@ import asyncmqtt from 'async-mqtt';
2
2
  import { SwitchBotBLE, SwitchBotOpenAPI } from 'node-switchbot';
3
3
  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';
4
4
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
5
- import { cleanDeviceConfig, createPlatformLogger, formatDeviceIdAsMac, hs2rgb, makeBLESender, makeOpenAPISender, rgb2hs, sleep } from './utils.js';
5
+ import { ApiRequestTracker, cleanDeviceConfig, createPlatformLogger, formatDeviceIdAsMac, hs2rgb, makeBLESender, makeOpenAPISender, rgb2hs, sleep } from './utils.js';
6
6
  export class SwitchBotMatterPlatform {
7
7
  log;
8
8
  config;
@@ -21,6 +21,12 @@ export class SwitchBotMatterPlatform {
21
21
  accessoryInstances = new Map();
22
22
  // Refresh timers keyed by normalized deviceId
23
23
  refreshTimers = new Map();
24
+ // Platform-level refresh timer for batch status updates
25
+ platformRefreshTimer;
26
+ // Device status cache from last refresh
27
+ deviceStatusCache = new Map();
28
+ // Devices that have explicit per-device refresh timers (normalized deviceId)
29
+ perDeviceRefreshSet = new Set();
24
30
  // BLE event handlers keyed by device MAC (formatted)
25
31
  bleEventHandler = {};
26
32
  // MQTT and Webhook properties (mirror HAP behavior so Matter platform can
@@ -31,6 +37,8 @@ export class SwitchBotMatterPlatform {
31
37
  // Platform logging toggle (can be controlled via UI or config)
32
38
  // Use same shape as HAP platform: string values like 'debug', 'debugMode', 'standard', or 'none'
33
39
  platformLogging;
40
+ // API request tracking (persistent across restarts)
41
+ apiTracker;
34
42
  // Platform-provided logging helpers (attached in constructor)
35
43
  infoLog;
36
44
  successLog;
@@ -132,6 +140,14 @@ export class SwitchBotMatterPlatform {
132
140
  catch (e) {
133
141
  this.errorLog('Failed to initialize SwitchBot OpenAPI:', e?.message ?? e);
134
142
  }
143
+ // Initialize API request tracking
144
+ try {
145
+ this.apiTracker = new ApiRequestTracker(this.api, this.log, 'SwitchBot Matter');
146
+ this.apiTracker.startHourlyLogging();
147
+ }
148
+ catch (e) {
149
+ this.errorLog('Failed to initialize API request tracking:', e?.message ?? e);
150
+ }
135
151
  try {
136
152
  this.switchBotBLE = new SwitchBotBLE();
137
153
  if (!this.config.options?.disableLogsforBLE && this.switchBotBLE?.on) {
@@ -170,6 +186,10 @@ export class SwitchBotMatterPlatform {
170
186
  this.api.on('shutdown', async () => {
171
187
  try {
172
188
  this.infoLog('Homebridge shutting down: clearing refresh timers and BLE handlers');
189
+ // Stop API tracking hourly logging
190
+ if (this.apiTracker) {
191
+ this.apiTracker.stopHourlyLogging();
192
+ }
173
193
  // Clear all refresh timers
174
194
  for (const [nid, t] of Array.from(this.refreshTimers.entries())) {
175
195
  try {
@@ -180,6 +200,16 @@ export class SwitchBotMatterPlatform {
180
200
  }
181
201
  this.refreshTimers.delete(nid);
182
202
  }
203
+ // Clear platform-level refresh timer
204
+ try {
205
+ if (this.platformRefreshTimer) {
206
+ clearInterval(this.platformRefreshTimer);
207
+ this.platformRefreshTimer = undefined;
208
+ }
209
+ }
210
+ catch (e) {
211
+ this.debugLog(`Failed to clear platform refresh timer: ${e?.message ?? e}`);
212
+ }
183
213
  // Clear accessory instances registry
184
214
  try {
185
215
  this.accessoryInstances.clear();
@@ -245,6 +275,13 @@ export class SwitchBotMatterPlatform {
245
275
  normalizeDeviceId(deviceId) {
246
276
  return (deviceId ?? '').toUpperCase().replace(/[^A-Z0-9]+/g, '');
247
277
  }
278
+ /** Determine the platform-level batch interval in seconds */
279
+ getPlatformBatchInterval() {
280
+ const opt = this.config.options;
281
+ const val = opt?.matterBatchRefreshRate ?? opt?.refreshRate ?? 300;
282
+ const n = Number(val);
283
+ return Number.isFinite(n) && n > 0 ? n : 300;
284
+ }
248
285
  /**
249
286
  * Clear per-device resources: refresh timers, accessory instance registry, BLE handlers
250
287
  */
@@ -661,40 +698,55 @@ export class SwitchBotMatterPlatform {
661
698
  }
662
699
  // Brightness
663
700
  if (parsed.brightness !== undefined) {
664
- const level = Math.round((Number(parsed.brightness) / 100) * 254);
701
+ const rawLevel = Math.round((Number(parsed.brightness) / 100) * 254);
702
+ const level = Math.max(0, Math.min(254, rawLevel));
703
+ this.debugLog(`[BLE Brightness Debug] Device ${dev.deviceId}: rawBrightness=${parsed.brightness}, calculated=${rawLevel}, clamped=${level}`);
665
704
  await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.LevelControl, { currentLevel: level });
666
705
  }
667
706
  // Color
668
707
  if (parsed.color !== undefined) {
669
708
  const { r, g, b } = parsed.color;
670
709
  const [h, s] = rgb2hs(r, g, b);
671
- await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.ColorControl, { currentHue: Math.round((h / 360) * 254), currentSaturation: Math.round((s / 100) * 254) });
710
+ const rawHue = Math.round((h / 360) * 254);
711
+ const rawSat = Math.round((s / 100) * 254);
712
+ const hue = Math.max(0, Math.min(254, rawHue));
713
+ const sat = Math.max(0, Math.min(254, rawSat));
714
+ this.debugLog(`[BLE Color Debug] Device ${dev.deviceId}: RGB=[${r},${g},${b}], HS=[${h},${s}], Matter=[${rawHue},${rawSat}], clamped=[${hue},${sat}]`);
715
+ await this.api.matter.updateAccessoryState(uuidLocal, this.api.matter.clusterNames.ColorControl, { currentHue: hue, currentSaturation: sat });
672
716
  }
673
717
  // Battery -> powerSource cluster (common mapping)
674
718
  if (parsed.battery !== undefined) {
675
- try {
676
- const percentage = Number(parsed.battery);
677
- const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
678
- let batChargeLevel = 0;
679
- if (percentage < 20) {
680
- batChargeLevel = 2;
681
- }
682
- else if (percentage < 40) {
683
- batChargeLevel = 1;
684
- }
719
+ // Skip battery updates for device types that don't support PowerSource cluster
720
+ const deviceType = String(dev?.deviceType ?? '');
721
+ const unsupportedTypes = ['Curtain', 'Curtain2', 'Curtain3', 'Curtain 2', 'Blind Tilt'];
722
+ if (unsupportedTypes.includes(deviceType)) {
723
+ this.debugLog(`Device ${dev.deviceId} type ${deviceType} does not support PowerSource cluster, skipping BLE battery update`);
724
+ }
725
+ else {
685
726
  try {
686
- await this.api.matter.updateAccessoryState(uuidLocal, 'powerSource', { batPercentRemaining, batChargeLevel });
687
- }
688
- catch (updateError) {
689
- // Silently skip if powerSource cluster doesn't exist on this device
690
- const msg = String(updateError?.message ?? updateError);
691
- if (!msg.includes('does not exist') && !msg.includes('not found')) {
692
- throw updateError;
727
+ const percentage = Number(parsed.battery);
728
+ const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
729
+ let batChargeLevel = 0;
730
+ if (percentage < 20) {
731
+ batChargeLevel = 2;
732
+ }
733
+ else if (percentage < 40) {
734
+ batChargeLevel = 1;
735
+ }
736
+ try {
737
+ await this.api.matter.updateAccessoryState(uuidLocal, 'powerSource', { batPercentRemaining, batChargeLevel });
738
+ }
739
+ catch (updateError) {
740
+ // Silently skip if powerSource cluster doesn't exist on this device
741
+ const msg = String(updateError?.message ?? updateError);
742
+ if (!msg.includes('does not exist') && !msg.includes('not found')) {
743
+ throw updateError;
744
+ }
693
745
  }
694
746
  }
695
- }
696
- catch (e) {
697
- this.debugLog(`Failed to update battery state for ${dev.deviceId}: ${e?.message ?? e}`);
747
+ catch (e) {
748
+ this.debugLog(`Failed to update battery state for ${dev.deviceId}: ${e?.message ?? e}`);
749
+ }
698
750
  }
699
751
  }
700
752
  // Temperature -> temperatureMeasurement
@@ -821,6 +873,7 @@ export class SwitchBotMatterPlatform {
821
873
  return;
822
874
  }
823
875
  try {
876
+ this.apiTracker?.track();
824
877
  const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(dev.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
825
878
  const respAny = response;
826
879
  const body = respAny?.body ?? respAny;
@@ -855,13 +908,15 @@ export class SwitchBotMatterPlatform {
855
908
  clearInterval(existing);
856
909
  this.refreshTimers.delete(nid);
857
910
  }
858
- const refreshRateSec = dev.refreshRate ?? this.config.options?.refreshRate ?? 300;
859
- if (this.switchBotAPI && refreshRateSec && Number(refreshRateSec) > 0) {
911
+ const platformInterval = this.getPlatformBatchInterval();
912
+ const hasDeviceInterval = typeof dev.refreshRate === 'number' && Number(dev.refreshRate) > 0;
913
+ if (this.switchBotAPI && (hasDeviceInterval || platformInterval > 0)) {
860
914
  // Immediate one-shot to populate initial state
861
915
  ;
862
916
  (async () => {
863
917
  try {
864
918
  this.infoLog(`Performing initial OpenAPI refresh for ${dev.deviceId}`);
919
+ this.apiTracker?.track();
865
920
  const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(dev.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
866
921
  const respAny = response;
867
922
  const body = respAny?.body ?? respAny;
@@ -885,32 +940,25 @@ export class SwitchBotMatterPlatform {
885
940
  this.errorLog(`Initial OpenAPI refresh failed for ${dev.deviceId}: ${e?.message ?? e}`);
886
941
  }
887
942
  })();
888
- const timer = setInterval(async () => {
889
- try {
890
- this.debugLog(`Performing periodic OpenAPI refresh for ${dev.deviceId}`);
891
- const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(dev.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
892
- const respAny = response;
893
- const body = respAny?.body ?? respAny;
943
+ if (hasDeviceInterval) {
944
+ // Create a per-device timer and exclude it from batch
945
+ const interval = Number(dev.refreshRate);
946
+ this.perDeviceRefreshSet.add(nid);
947
+ const timer = setInterval(async () => {
894
948
  try {
895
- const s = JSON.stringify(body);
896
- this.debugLog(`Periodic OpenAPI refresh for ${dev.deviceId} returned statusCode=${statusCode} body=${s}`);
949
+ await this.refreshSingleDeviceWithRetry(dev);
897
950
  }
898
951
  catch (e) {
899
- this.debugLog(`Periodic OpenAPI refresh for ${dev.deviceId} returned statusCode=${statusCode} (body not stringifiable)`);
900
- }
901
- if (statusCode === 100 || statusCode === 200) {
902
- const status = body?.status ?? body;
903
- await this.applyStatusToAccessory(uuid, dev, status);
904
- }
905
- else {
906
- this.debugLog(`Periodic OpenAPI refresh returned unexpected statusCode=${statusCode} for ${dev.deviceId}`);
952
+ this.debugLog(`Per-device refresh failed for ${dev.deviceId}: ${e?.message ?? e}`);
907
953
  }
908
- }
909
- catch (e) {
910
- this.errorLog(`Periodic OpenAPI refresh failed for ${dev.deviceId}: ${e?.message ?? e}`);
911
- }
912
- }, Number(refreshRateSec) * 1000);
913
- this.refreshTimers.set(nid, timer);
954
+ }, interval * 1000);
955
+ this.refreshTimers.set(nid, timer);
956
+ this.infoLog(`Started per-device refresh timer for ${dev.deviceId} at ${interval}s`);
957
+ }
958
+ else {
959
+ // Start platform-level batched refresh timer (only once)
960
+ this.startPlatformRefreshTimer(platformInterval);
961
+ }
914
962
  }
915
963
  }
916
964
  catch (e) {
@@ -927,6 +975,7 @@ export class SwitchBotMatterPlatform {
927
975
  return;
928
976
  }
929
977
  try {
978
+ this.apiTracker?.track();
930
979
  const { response, statusCode } = await this.switchBotAPI.getDevices();
931
980
  this.debugLog(`SwitchBot getDevices response status: ${statusCode}`);
932
981
  if (statusCode === 100 || statusCode === 200) {
@@ -1029,6 +1078,7 @@ export class SwitchBotMatterPlatform {
1029
1078
  if (!this.switchBotAPI) {
1030
1079
  throw new Error('SwitchBot OpenAPI not initialized');
1031
1080
  }
1081
+ this.apiTracker?.track();
1032
1082
  const { response, statusCode } = await this.switchBotAPI.controlDevice(deviceObj.deviceId, bodyChange.command, bodyChange.parameter, bodyChange.commandType, this.config.credentials?.token, this.config.credentials?.secret);
1033
1083
  return { response, statusCode };
1034
1084
  }
@@ -1258,10 +1308,19 @@ export class SwitchBotMatterPlatform {
1258
1308
  // Brightness
1259
1309
  if (status?.brightness !== undefined) {
1260
1310
  const rawBrightness = Number(status.brightness);
1261
- const level = Math.round((rawBrightness / 100) * 254);
1262
- const clampedLevel = Math.max(0, Math.min(254, level));
1263
- this.debugLog(`[Brightness Debug] Device ${dev.deviceId}: rawBrightness=${rawBrightness}, calculated=${level}, clamped=${clampedLevel}`);
1264
- await safeUpdate(this.api.matter.clusterNames.LevelControl, { currentLevel: clampedLevel }, 'updateBrightness');
1311
+ const clampedBrightness = Math.max(0, Math.min(100, rawBrightness));
1312
+ // If instance has updateBrightness method, it expects percentage (0-100)
1313
+ // Otherwise, updateAccessoryState expects Matter-scaled value (0-254)
1314
+ if (instance && typeof instance.updateBrightness === 'function') {
1315
+ this.debugLog(`[Brightness Debug] Device ${dev.deviceId}: calling updateBrightness with percent=${clampedBrightness}`);
1316
+ await safeUpdate(this.api.matter.clusterNames.LevelControl, { currentLevel: clampedBrightness }, 'updateBrightness');
1317
+ }
1318
+ else {
1319
+ const level = Math.round((clampedBrightness / 100) * 254);
1320
+ const clampedLevel = Math.max(0, Math.min(254, level));
1321
+ this.debugLog(`[Brightness Debug] Device ${dev.deviceId}: calling updateAccessoryState with rawBrightness=${rawBrightness}, level=${clampedLevel}`);
1322
+ await safeUpdate(this.api.matter.clusterNames.LevelControl, { currentLevel: clampedLevel });
1323
+ }
1265
1324
  }
1266
1325
  // Color
1267
1326
  if (status?.color !== undefined) {
@@ -1288,30 +1347,49 @@ export class SwitchBotMatterPlatform {
1288
1347
  b = Number.parseInt(hex.substring(4, 6), 16);
1289
1348
  }
1290
1349
  const [h, s] = rgb2hs(r, g, b);
1291
- const hue = Math.round((h / 360) * 254);
1292
- const sat = Math.round((s / 100) * 254);
1293
- const clampedHue = Math.max(0, Math.min(254, hue));
1294
- const clampedSat = Math.max(0, Math.min(254, sat));
1295
- this.debugLog(`[Color Debug] Device ${dev.deviceId}: color="${color}", RGB=[${r},${g},${b}], HS=[${h},${s}], Matter=[${hue},${sat}], clamped=[${clampedHue},${clampedSat}]`);
1296
- await safeUpdate(this.api.matter.clusterNames.ColorControl, { currentHue: clampedHue, currentSaturation: clampedSat }, 'updateHueSaturation');
1350
+ const clampedH = Math.max(0, Math.min(360, h));
1351
+ const clampedS = Math.max(0, Math.min(100, s));
1352
+ // If instance has updateHueSaturation method, it expects raw values (h: 0-360, s: 0-100)
1353
+ // Otherwise, updateAccessoryState expects Matter-scaled values (0-254)
1354
+ if (instance && typeof instance.updateHueSaturation === 'function') {
1355
+ this.debugLog(`[Color Debug] Device ${dev.deviceId}: calling updateHueSaturation with color="${color}", RGB=[${r},${g},${b}], HS=[${clampedH},${clampedS}]`);
1356
+ await safeUpdate(this.api.matter.clusterNames.ColorControl, { currentHue: clampedH, currentSaturation: clampedS }, 'updateHueSaturation');
1357
+ }
1358
+ else {
1359
+ const hue = Math.round((clampedH / 360) * 254);
1360
+ const sat = Math.round((clampedS / 100) * 254);
1361
+ const clampedHue = Math.max(0, Math.min(254, hue));
1362
+ const clampedSat = Math.max(0, Math.min(254, sat));
1363
+ this.debugLog(`[Color Debug] Device ${dev.deviceId}: calling updateAccessoryState with color="${color}", RGB=[${r},${g},${b}], HS=[${h},${s}], Matter=[${clampedHue},${clampedSat}]`);
1364
+ await safeUpdate(this.api.matter.clusterNames.ColorControl, { currentHue: clampedHue, currentSaturation: clampedSat });
1365
+ }
1297
1366
  }
1298
1367
  // Battery/powerSource (support many possible field names)
1368
+ // Note: Some device types like WindowBlind don't support PowerSource cluster
1299
1369
  if (status?.battery !== undefined || status?.batt !== undefined || status?.batteryLevel !== undefined || status?.batteryPercentage !== undefined || status?.battery_level !== undefined) {
1300
- try {
1301
- const percentage = Number(status?.battery ?? status?.batt ?? status?.batteryPercentage ?? status?.batteryLevel ?? status?.battery_level);
1302
- const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
1303
- let batChargeLevel = 0;
1304
- if (percentage < 20) {
1305
- batChargeLevel = 2;
1370
+ // Skip battery updates for device types that don't support PowerSource cluster
1371
+ const deviceType = String(status?.deviceType ?? dev?.deviceType ?? '');
1372
+ const unsupportedTypes = ['Curtain', 'Curtain2', 'Curtain3', 'Curtain 2', 'Blind Tilt'];
1373
+ if (unsupportedTypes.includes(deviceType)) {
1374
+ this.debugLog(`Device ${dev.deviceId} type ${deviceType} does not support PowerSource cluster, skipping battery update`);
1375
+ }
1376
+ else {
1377
+ try {
1378
+ const percentage = Number(status?.battery ?? status?.batt ?? status?.batteryPercentage ?? status?.batteryLevel ?? status?.battery_level);
1379
+ const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)));
1380
+ let batChargeLevel = 0;
1381
+ if (percentage < 20) {
1382
+ batChargeLevel = 2;
1383
+ }
1384
+ else if (percentage < 40) {
1385
+ batChargeLevel = 1;
1386
+ }
1387
+ const powerCluster = (this.api.matter?.clusterNames && this.api.matter.clusterNames.PowerSource) ? this.api.matter.clusterNames.PowerSource : 'powerSource';
1388
+ await safeUpdate(powerCluster, { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage');
1306
1389
  }
1307
- else if (percentage < 40) {
1308
- batChargeLevel = 1;
1390
+ catch (e) {
1391
+ this.debugLog(`Failed to apply battery status for ${dev.deviceId}: ${e?.message ?? e}`);
1309
1392
  }
1310
- const powerCluster = (this.api.matter?.clusterNames && this.api.matter.clusterNames.PowerSource) ? this.api.matter.clusterNames.PowerSource : 'powerSource';
1311
- await safeUpdate(powerCluster, { batPercentRemaining, batChargeLevel }, 'updateBatteryPercentage');
1312
- }
1313
- catch (e) {
1314
- this.debugLog(`Failed to apply battery status for ${dev.deviceId}: ${e?.message ?? e}`);
1315
1393
  }
1316
1394
  }
1317
1395
  // Temperature + thermostat
@@ -1923,5 +2001,112 @@ export class SwitchBotMatterPlatform {
1923
2001
  await this.api.matter.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessories);
1924
2002
  }
1925
2003
  }
2004
+ /**
2005
+ * Start platform-level refresh timer to batch all device status updates
2006
+ */
2007
+ startPlatformRefreshTimer(refreshRateSec) {
2008
+ // Only create timer once
2009
+ if (this.platformRefreshTimer) {
2010
+ return;
2011
+ }
2012
+ this.debugLog(`Starting platform-level refresh timer with interval ${refreshRateSec}s`);
2013
+ this.platformRefreshTimer = setInterval(async () => {
2014
+ await this.batchRefreshAllDevices();
2015
+ }, Number(refreshRateSec) * 1000);
2016
+ }
2017
+ /**
2018
+ * Batch refresh all devices - still makes individual API calls but batches them together
2019
+ * Note: SwitchBot API doesn't support true batch status calls, but we can parallelize them
2020
+ */
2021
+ async batchRefreshAllDevices() {
2022
+ if (!this.switchBotAPI) {
2023
+ return;
2024
+ }
2025
+ this.debugLog('Performing batched periodic OpenAPI refresh for all devices');
2026
+ // Build list from registered accessory instances (uuid) and discovered devices
2027
+ const devicesToRefresh = [];
2028
+ try {
2029
+ for (const [nid, instance] of this.accessoryInstances.entries()) {
2030
+ const uuid = instance?.uuid;
2031
+ if (!uuid) {
2032
+ continue;
2033
+ }
2034
+ const dev = this.discoveredDevices.find(d => this.normalizeDeviceId(d.deviceId) === nid);
2035
+ if (dev && !this.perDeviceRefreshSet.has(nid)) {
2036
+ devicesToRefresh.push({ uuid, dev });
2037
+ }
2038
+ }
2039
+ }
2040
+ catch (e) {
2041
+ this.errorLog(`Failed to enumerate devices for batch refresh: ${e?.message ?? e}`);
2042
+ return;
2043
+ }
2044
+ if (devicesToRefresh.length === 0) {
2045
+ this.debugLog('No devices to refresh');
2046
+ return;
2047
+ }
2048
+ this.debugLog(`Refreshing ${devicesToRefresh.length} devices in parallel batch`);
2049
+ const concurrency = Number(this.config.options?.matterBatchConcurrency ?? 5);
2050
+ await this.runWithConcurrency(devicesToRefresh, async ({ uuid, dev }) => {
2051
+ try {
2052
+ const status = await this.refreshSingleDeviceWithRetry(dev);
2053
+ if (status) {
2054
+ await this.applyStatusToAccessory(uuid, dev, status);
2055
+ }
2056
+ }
2057
+ catch (e) {
2058
+ this.errorLog(`Periodic OpenAPI refresh failed for ${dev.deviceId}: ${e?.message ?? e}`);
2059
+ }
2060
+ }, Number.isFinite(concurrency) && concurrency > 0 ? concurrency : 5);
2061
+ this.debugLog(`Batch refresh completed for ${devicesToRefresh.length} devices`);
2062
+ }
2063
+ /** Refresh a single device with retry and backoff; returns status object if successful */
2064
+ async refreshSingleDeviceWithRetry(dev, retries = 3, baseDelayMs = 500) {
2065
+ const deviceId = dev.deviceId;
2066
+ let attempt = 0;
2067
+ while (attempt <= retries) {
2068
+ try {
2069
+ this.apiTracker?.track();
2070
+ const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(deviceId, this.config.credentials?.token, this.config.credentials?.secret);
2071
+ const respAny = response;
2072
+ const body = respAny?.body ?? respAny;
2073
+ if (statusCode === 100 || statusCode === 200) {
2074
+ const status = body?.status ?? body;
2075
+ this.deviceStatusCache.set(this.normalizeDeviceId(deviceId), { status, timestamp: Date.now() });
2076
+ this.debugLog(`OpenAPI refresh succeeded for ${deviceId} (attempt ${attempt + 1})`);
2077
+ return status;
2078
+ }
2079
+ this.debugLog(`OpenAPI refresh unexpected statusCode=${statusCode} for ${deviceId} (attempt ${attempt + 1})`);
2080
+ }
2081
+ catch (e) {
2082
+ this.debugLog(`OpenAPI refresh error for ${deviceId} (attempt ${attempt + 1}): ${e?.message ?? e}`);
2083
+ }
2084
+ // backoff before next retry if any left
2085
+ attempt++;
2086
+ if (attempt <= retries) {
2087
+ const delay = baseDelayMs * (2 ** (attempt - 1));
2088
+ await sleep(delay);
2089
+ }
2090
+ }
2091
+ return null;
2092
+ }
2093
+ /** Simple concurrency limiter for an array of items */
2094
+ async runWithConcurrency(items, worker, concurrency) {
2095
+ const queue = items.slice();
2096
+ const workers = [];
2097
+ const runNext = async () => {
2098
+ const item = queue.shift();
2099
+ if (!item) {
2100
+ return;
2101
+ }
2102
+ await worker(item);
2103
+ return runNext();
2104
+ };
2105
+ const pool = Math.min(concurrency, Math.max(1, items.length));
2106
+ for (let i = 0; i < pool; i++) {
2107
+ workers.push(runNext());
2108
+ }
2109
+ await Promise.all(workers);
2110
+ }
1926
2111
  }
1927
2112
  //# sourceMappingURL=platform-matter.js.map