@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.
- package/config.schema.json +12 -0
- package/dist/platform-hap.d.ts +1 -0
- package/dist/platform-hap.d.ts.map +1 -1
- package/dist/platform-hap.js +14 -1
- package/dist/platform-hap.js.map +1 -1
- package/dist/platform-matter.d.ts +19 -0
- package/dist/platform-matter.d.ts.map +1 -1
- package/dist/platform-matter.js +255 -70
- 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/utils.d.ts +40 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +126 -0
- package/dist/utils.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/platform-hap.ts +16 -1
- package/src/platform-matter.ts +262 -66
- package/src/settings.ts +3 -0
- package/src/utils.ts +137 -4
|
@@ -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":"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;AA+B3E,qBAAa,uBAAwB,YAAW,qBAAqB;
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
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
|
-
|
|
697
|
-
|
|
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
|
|
859
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
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(`
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
|
1292
|
-
const
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
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
|
-
|
|
1308
|
-
|
|
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
|