@switchbot/homebridge-switchbot 5.0.0-beta.4 → 5.0.0-beta.40

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.
Files changed (162) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +23 -3
  3. package/config.schema.json +722 -13684
  4. package/dist/devices-hap/device.d.ts +18 -8
  5. package/dist/devices-hap/device.d.ts.map +1 -1
  6. package/dist/devices-hap/device.js +121 -68
  7. package/dist/devices-hap/device.js.map +1 -1
  8. package/dist/devices-matter/BaseMatterAccessory.d.ts +27 -0
  9. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +1 -1
  10. package/dist/devices-matter/BaseMatterAccessory.js +169 -5
  11. package/dist/devices-matter/BaseMatterAccessory.js.map +1 -1
  12. package/dist/devices-matter/ColorLightAccessory.d.ts.map +1 -1
  13. package/dist/devices-matter/ColorLightAccessory.js +12 -12
  14. package/dist/devices-matter/ColorLightAccessory.js.map +1 -1
  15. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +1 -1
  16. package/dist/devices-matter/ColorTemperatureLightAccessory.js +5 -7
  17. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +1 -1
  18. package/dist/devices-matter/DimmableLightAccessory.js +9 -9
  19. package/dist/devices-matter/DimmableLightAccessory.js.map +1 -1
  20. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +1 -1
  21. package/dist/devices-matter/ExtendedColorLightAccessory.js +14 -15
  22. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +1 -1
  23. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +1 -1
  24. package/dist/devices-matter/OnOffLightAccessory.js +8 -16
  25. package/dist/devices-matter/OnOffLightAccessory.js.map +1 -1
  26. package/dist/devices-matter/OnOffOutletAccessory.d.ts +2 -0
  27. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +1 -1
  28. package/dist/devices-matter/OnOffOutletAccessory.js +10 -7
  29. package/dist/devices-matter/OnOffOutletAccessory.js.map +1 -1
  30. package/dist/devices-matter/OnOffSwitchAccessory.js +2 -2
  31. package/dist/devices-matter/OnOffSwitchAccessory.js.map +1 -1
  32. package/dist/homebridge-ui/public/index.html +48 -1
  33. package/dist/homebridge-ui/server.js +53 -8
  34. package/dist/homebridge-ui/server.js.map +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +4 -7
  37. package/dist/index.js.map +1 -1
  38. package/dist/irdevice/irdevice.d.ts +11 -10
  39. package/dist/irdevice/irdevice.d.ts.map +1 -1
  40. package/dist/irdevice/irdevice.js +76 -35
  41. package/dist/irdevice/irdevice.js.map +1 -1
  42. package/dist/platform-hap.d.ts +21 -15
  43. package/dist/platform-hap.d.ts.map +1 -1
  44. package/dist/platform-hap.js +246 -147
  45. package/dist/platform-hap.js.map +1 -1
  46. package/dist/platform-matter.d.ts +88 -6
  47. package/dist/platform-matter.d.ts.map +1 -1
  48. package/dist/platform-matter.js +1726 -243
  49. package/dist/platform-matter.js.map +1 -1
  50. package/dist/settings.d.ts +41 -6
  51. package/dist/settings.d.ts.map +1 -1
  52. package/dist/settings.js.map +1 -1
  53. package/dist/test/hap/platform-hap.logging.test.d.ts +2 -0
  54. package/dist/test/hap/platform-hap.logging.test.d.ts.map +1 -0
  55. package/dist/test/hap/platform-hap.logging.test.js +33 -0
  56. package/dist/test/hap/platform-hap.logging.test.js.map +1 -0
  57. package/dist/test/hap/platform-hap.test.d.ts +2 -0
  58. package/dist/test/hap/platform-hap.test.d.ts.map +1 -0
  59. package/dist/test/hap/platform-hap.test.js +62 -0
  60. package/dist/test/hap/platform-hap.test.js.map +1 -0
  61. package/dist/test/helpers/platform-fixtures.d.ts +9 -0
  62. package/dist/test/helpers/platform-fixtures.d.ts.map +1 -0
  63. package/dist/test/helpers/platform-fixtures.js +30 -0
  64. package/dist/test/helpers/platform-fixtures.js.map +1 -0
  65. package/dist/{index.test.d.ts.map → test/index.test.d.ts.map} +1 -1
  66. package/dist/test/index.test.js +19 -0
  67. package/dist/test/index.test.js.map +1 -0
  68. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts +2 -0
  69. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts.map +1 -0
  70. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js +71 -0
  71. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js.map +1 -0
  72. package/dist/test/matter/platform-matter.additional.test.d.ts +2 -0
  73. package/dist/test/matter/platform-matter.additional.test.d.ts.map +1 -0
  74. package/dist/test/matter/platform-matter.additional.test.js +35 -0
  75. package/dist/test/matter/platform-matter.additional.test.js.map +1 -0
  76. package/dist/test/matter/platform-matter.bleparse.test.d.ts +2 -0
  77. package/dist/test/matter/platform-matter.bleparse.test.d.ts.map +1 -0
  78. package/dist/test/matter/platform-matter.bleparse.test.js +43 -0
  79. package/dist/test/matter/platform-matter.bleparse.test.js.map +1 -0
  80. package/dist/test/matter/platform-matter.cleanup.test.d.ts +2 -0
  81. package/dist/test/matter/platform-matter.cleanup.test.d.ts.map +1 -0
  82. package/dist/test/matter/platform-matter.cleanup.test.js +70 -0
  83. package/dist/test/matter/platform-matter.cleanup.test.js.map +1 -0
  84. package/dist/test/matter/platform-matter.keepstale.test.d.ts +2 -0
  85. package/dist/test/matter/platform-matter.keepstale.test.d.ts.map +1 -0
  86. package/dist/test/matter/platform-matter.keepstale.test.js +27 -0
  87. package/dist/test/matter/platform-matter.keepstale.test.js.map +1 -0
  88. package/dist/test/matter/platform-matter.logging.test.d.ts +2 -0
  89. package/dist/test/matter/platform-matter.logging.test.d.ts.map +1 -0
  90. package/dist/test/matter/platform-matter.logging.test.js +29 -0
  91. package/dist/test/matter/platform-matter.logging.test.js.map +1 -0
  92. package/dist/test/matter/platform-matter.mapping.test.d.ts +2 -0
  93. package/dist/test/matter/platform-matter.mapping.test.d.ts.map +1 -0
  94. package/dist/test/matter/platform-matter.mapping.test.js +43 -0
  95. package/dist/test/matter/platform-matter.mapping.test.js.map +1 -0
  96. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts +2 -0
  97. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts.map +1 -0
  98. package/dist/test/matter/platform-matter.openapi-mapping.test.js +84 -0
  99. package/dist/test/matter/platform-matter.openapi-mapping.test.js.map +1 -0
  100. package/dist/test/matter/platform-matter.test.d.ts +2 -0
  101. package/dist/test/matter/platform-matter.test.d.ts.map +1 -0
  102. package/dist/test/matter/platform-matter.test.js +117 -0
  103. package/dist/test/matter/platform-matter.test.js.map +1 -0
  104. package/dist/test/matter/platform-matter.unregister.test.d.ts +2 -0
  105. package/dist/test/matter/platform-matter.unregister.test.d.ts.map +1 -0
  106. package/dist/test/matter/platform-matter.unregister.test.js +30 -0
  107. package/dist/test/matter/platform-matter.unregister.test.js.map +1 -0
  108. package/dist/test/utils.test.d.ts +2 -0
  109. package/dist/test/utils.test.d.ts.map +1 -0
  110. package/dist/test/utils.test.js +95 -0
  111. package/dist/test/utils.test.js.map +1 -0
  112. package/dist/test/verifyconfig.test.d.ts.map +1 -0
  113. package/dist/{verifyconfig.test.js → test/verifyconfig.test.js} +2 -2
  114. package/dist/test/verifyconfig.test.js.map +1 -0
  115. package/dist/utils.d.ts +196 -3
  116. package/dist/utils.d.ts.map +1 -1
  117. package/dist/utils.js +656 -30
  118. package/dist/utils.js.map +1 -1
  119. package/docs/assets/main.js +2 -2
  120. package/docs/index.html +20 -2
  121. package/docs/variables/default.html +1 -1
  122. package/package.json +14 -14
  123. package/src/devices-hap/device.ts +129 -69
  124. package/src/devices-matter/BaseMatterAccessory.ts +176 -5
  125. package/src/devices-matter/ColorLightAccessory.ts +12 -12
  126. package/src/devices-matter/ColorTemperatureLightAccessory.ts +5 -7
  127. package/src/devices-matter/DimmableLightAccessory.ts +9 -9
  128. package/src/devices-matter/ExtendedColorLightAccessory.ts +14 -15
  129. package/src/devices-matter/OnOffLightAccessory.ts +8 -16
  130. package/src/devices-matter/OnOffOutletAccessory.ts +12 -7
  131. package/src/devices-matter/OnOffSwitchAccessory.ts +2 -2
  132. package/src/homebridge-ui/public/index.html +48 -1
  133. package/src/homebridge-ui/server.ts +55 -8
  134. package/src/index.ts +4 -7
  135. package/src/irdevice/irdevice.ts +74 -35
  136. package/src/platform-hap.ts +270 -160
  137. package/src/platform-matter.ts +1768 -240
  138. package/src/settings.ts +45 -2
  139. package/src/test/hap/platform-hap.logging.test.ts +36 -0
  140. package/src/test/hap/platform-hap.test.ts +70 -0
  141. package/src/test/helpers/platform-fixtures.ts +33 -0
  142. package/src/test/index.test.ts +24 -0
  143. package/src/test/matter/devices-matter/baseMatterAccessory.test.ts +88 -0
  144. package/src/test/matter/platform-matter.additional.test.ts +44 -0
  145. package/src/test/matter/platform-matter.bleparse.test.ts +47 -0
  146. package/src/test/matter/platform-matter.cleanup.test.ts +86 -0
  147. package/src/test/matter/platform-matter.keepstale.test.ts +37 -0
  148. package/src/test/matter/platform-matter.logging.test.ts +33 -0
  149. package/src/test/matter/platform-matter.mapping.test.ts +57 -0
  150. package/src/test/matter/platform-matter.openapi-mapping.test.ts +109 -0
  151. package/src/test/matter/platform-matter.test.ts +144 -0
  152. package/src/test/matter/platform-matter.unregister.test.ts +39 -0
  153. package/src/test/utils.test.ts +96 -0
  154. package/src/{verifyconfig.test.ts → test/verifyconfig.test.ts} +12 -11
  155. package/src/utils.ts +714 -32
  156. package/dist/index.test.js +0 -14
  157. package/dist/index.test.js.map +0 -1
  158. package/dist/verifyconfig.test.d.ts.map +0 -1
  159. package/dist/verifyconfig.test.js.map +0 -1
  160. package/src/index.test.ts +0 -19
  161. /package/dist/{index.test.d.ts → test/index.test.d.ts} +0 -0
  162. /package/dist/{verifyconfig.test.d.ts → test/verifyconfig.test.d.ts} +0 -0
@@ -36,7 +36,7 @@ import { TV } from './irdevice/tv.js';
36
36
  import { VacuumCleaner } from './irdevice/vacuumcleaner.js';
37
37
  import { WaterHeater } from './irdevice/waterheater.js';
38
38
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js';
39
- import { cleanDeviceConfig, formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, safeStringify, sleep } from './utils.js';
39
+ import { ApiRequestTracker, applyDeviceTypeTemplates, createPlatformLogger, formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, isSuccessfulStatusCode, logStatusCode, mergeByDeviceId, safeStringify, sleep } from './utils.js';
40
40
  /**
41
41
  * HomebridgePlatform
42
42
  * This class is the main constructor for your plugin, this is where you should
@@ -47,6 +47,17 @@ export class SwitchBotHAPPlatform {
47
47
  accessories = [];
48
48
  api;
49
49
  log;
50
+ // Logging helper functions (attached from utils.createPlatformLogger in constructor)
51
+ infoLog;
52
+ successLog;
53
+ debugSuccessLog;
54
+ warnLog;
55
+ debugWarnLog;
56
+ errorLog;
57
+ debugErrorLog;
58
+ debugLog;
59
+ loggingIsDebug;
60
+ enablingPlatformLogging;
50
61
  // Configuration properties
51
62
  platformConfig;
52
63
  platformLogging;
@@ -64,6 +75,8 @@ export class SwitchBotHAPPlatform {
64
75
  // SwitchBot APIs
65
76
  switchBotAPI;
66
77
  switchBotBLE;
78
+ // API request tracking
79
+ apiTracker;
67
80
  // External APIs
68
81
  eve;
69
82
  fakegatoAPI;
@@ -73,9 +86,21 @@ export class SwitchBotHAPPlatform {
73
86
  constructor(log, config, api) {
74
87
  this.api = api;
75
88
  this.log = log;
89
+ // Attach shared platform logging helpers (moved to utils for reuse)
90
+ const _pl = createPlatformLogger(async () => this.platformLogging, this.log);
91
+ this.infoLog = _pl.infoLog;
92
+ this.successLog = _pl.successLog;
93
+ this.debugSuccessLog = _pl.debugSuccessLog;
94
+ this.warnLog = _pl.warnLog;
95
+ this.debugWarnLog = _pl.debugWarnLog;
96
+ this.errorLog = _pl.errorLog;
97
+ this.debugErrorLog = _pl.debugErrorLog;
98
+ this.debugLog = _pl.debugLog;
99
+ this.loggingIsDebug = _pl.loggingIsDebug;
100
+ this.enablingPlatformLogging = _pl.enablingPlatformLogging;
76
101
  // only load if configured
77
102
  if (!config) {
78
- this.log.error('No configuration found for the plugin, please check your config.');
103
+ this.errorLog('No configuration found for the plugin, please check your config.');
79
104
  return;
80
105
  }
81
106
  // Plugin options into our config variables.
@@ -86,23 +111,21 @@ export class SwitchBotHAPPlatform {
86
111
  options: config.options,
87
112
  devices: config.devices,
88
113
  };
89
- // Normalize deviceConfig to remove UI-inserted defaults (lots of false/empty values)
114
+ // Determine platform logging preference (match HAP behaviour as closely as
115
+ // possible using config values. We default to 'standard' when unspecified.)
116
+ this.platformLogging = (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard' || this.config.options?.logging === 'none')
117
+ ? this.config.options.logging
118
+ : 'standard';
119
+ // Unconditional diagnostic using the raw Homebridge `log` so it always
120
+ // appears regardless of the platform logging helpers' gating logic.
90
121
  try {
91
- if (this.config.options) {
92
- const cleaned = cleanDeviceConfig(this.config.options.deviceConfig);
93
- if (cleaned) {
94
- ;
95
- this.config.options.deviceConfig = cleaned;
96
- }
97
- else {
98
- // remove the empty deviceConfig so downstream checks treat it as absent
99
- delete this.config.options.deviceConfig;
100
- }
101
- }
122
+ this.log.debug?.(`[SwitchBot HAP] effective platformLogging=${String(this.platformLogging)}`);
102
123
  }
103
124
  catch (e) {
104
- this.debugErrorLog(`Failed to clean deviceConfig: ${e}`);
125
+ // swallow any logging errors — diagnostics are best-effort
105
126
  }
127
+ // Note: deviceConfig and irdeviceConfig have been removed from the platform.
128
+ // All device-specific configuration should be done via options.devices and options.irdevices arrays.
106
129
  // Plugin Configuration
107
130
  this.getPlatformLogSettings();
108
131
  this.getPlatformRateSettings();
@@ -171,9 +194,36 @@ export class SwitchBotHAPPlatform {
171
194
  // to start discovery of new accessories.
172
195
  this.api.on('didFinishLaunching', async () => {
173
196
  this.debugLog('Executed didFinishLaunching callback');
197
+ // Initialize API request tracking
198
+ try {
199
+ const dailyApiLimit = this.config.options?.dailyApiLimit ?? 10000;
200
+ const dailyApiReserveForCommands = this.config.options?.dailyApiReserveForCommands ?? 1000;
201
+ const webhookOnlyOnReserve = this.config.options?.webhookOnlyOnReserve ?? false;
202
+ this.apiTracker = new ApiRequestTracker(this.api, this.log, 'SwitchBot HAP', {
203
+ dailyLimit: dailyApiLimit,
204
+ reserveForCommands: dailyApiReserveForCommands,
205
+ pausePollingAtReserve: webhookOnlyOnReserve,
206
+ });
207
+ this.apiTracker.startHourlyLogging();
208
+ }
209
+ catch (e) {
210
+ this.errorLog(`Failed to initialize API request tracking: ${e.message ?? e}`);
211
+ }
174
212
  // run the method to discover / register your devices as accessories
175
213
  try {
176
- await this.discoverDevices();
214
+ // Does the user have a version of Homebridge that is compatible with matter?
215
+ if (!this.api.isMatterAvailable?.()) {
216
+ this.debugLog(`Matter is not available in this version of Homebridge. Please update Homebridge to use this plugin, ${this.api.isMatterAvailable?.() ? '' : ' (Matter is not available in this version of Homebridge)'}`);
217
+ }
218
+ if (!this.api.isMatterEnabled?.()) {
219
+ this.debugLog(`Matter is not enabled in Homebridge. Please enable Matter in the Homebridge settings to use this plugin, ${this.api.isMatterEnabled?.() ? '' : ' (Matter is not enabled in Homebridge)'}`);
220
+ }
221
+ if (!this.api.isMatterAvailable?.() && !this.api.isMatterEnabled?.()) {
222
+ await this.discoverDevices();
223
+ }
224
+ else {
225
+ this.infoLog('Matter is enabled in Homebridge. SwitchBot Matter devices will be handled by the Matter platform.');
226
+ }
177
227
  }
178
228
  catch (e) {
179
229
  this.errorLog(`Failed to Discover, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug');
@@ -401,11 +451,17 @@ export class SwitchBotHAPPlatform {
401
451
  let retryCount = 0;
402
452
  const maxRetries = this.platformMaxRetries ?? 5;
403
453
  const delayBetweenRetries = this.platformDelayBetweenRetries || 5000;
454
+ let rateLimitExceeded = false;
404
455
  this.debugWarnLog(`Retry Count: ${retryCount}`);
405
456
  this.debugWarnLog(`Max Retries: ${this.platformMaxRetries}`);
406
457
  this.debugWarnLog(`Delay Between Retries: ${this.platformDelayBetweenRetries}`);
407
458
  while (retryCount < maxRetries) {
408
459
  try {
460
+ if (!this.apiTracker?.trySpend('discovery')) {
461
+ rateLimitExceeded = true;
462
+ this.warnLog('OpenAPI daily budget reached (discovery blocked). Falling back to manual device configuration.');
463
+ break;
464
+ }
409
465
  const { response, statusCode } = await this.switchBotAPI.getDevices();
410
466
  this.debugLog(`response: ${JSON.stringify(response)}`);
411
467
  if (this.isSuccessfulResponse(statusCode)) {
@@ -414,6 +470,13 @@ export class SwitchBotHAPPlatform {
414
470
  break;
415
471
  }
416
472
  else {
473
+ // Check if rate limit exceeded (429)
474
+ if (statusCode === 429) {
475
+ rateLimitExceeded = true;
476
+ this.warnLog('OpenAPI rate limit (429) exceeded. Falling back to manual device configuration.');
477
+ this.warnLog('Webhook functionality will still work if devices are configured manually.');
478
+ break;
479
+ }
417
480
  await this.handleErrorResponse(statusCode, retryCount, maxRetries, delayBetweenRetries);
418
481
  retryCount++;
419
482
  }
@@ -424,8 +487,109 @@ export class SwitchBotHAPPlatform {
424
487
  this.debugErrorLog(`Failed to Discover Devices, Error: ${e.message ?? e}`);
425
488
  }
426
489
  }
490
+ // If rate limit exceeded or retries exhausted, try to load from manual config or cached accessories
491
+ if (rateLimitExceeded || retryCount >= maxRetries) {
492
+ const hasCachedAccessories = this.accessories.length > 0;
493
+ const hasManualConfig = this.config.options?.devices || this.config.options?.irdevices;
494
+ if (hasManualConfig || hasCachedAccessories) {
495
+ if (hasCachedAccessories) {
496
+ this.warnLog(`Found ${this.accessories.length} cached accessories from previous sessions.`);
497
+ this.warnLog('Reinstantiating device classes to enable webhook handlers...');
498
+ await this.restoreCachedAccessories();
499
+ }
500
+ if (hasManualConfig) {
501
+ this.warnLog('Attempting to load devices from manual configuration...');
502
+ await this.handleManualConfig();
503
+ }
504
+ if (hasCachedAccessories) {
505
+ this.infoLog('Cached accessories restored. Webhook functionality is active.');
506
+ this.infoLog('Device discovery will resume when API rate limit resets.');
507
+ }
508
+ }
509
+ else {
510
+ this.errorLog('OpenAPI unavailable and no cached accessories or manual device configuration found.');
511
+ this.errorLog('Please configure devices manually in the plugin settings to use webhook functionality.');
512
+ }
513
+ }
514
+ }
515
+ /**
516
+ * Restore cached accessories by reinstantiating their device classes
517
+ * This ensures webhook handlers and other functionality are properly set up
518
+ */
519
+ async restoreCachedAccessories() {
520
+ this.debugLog('Restoring cached accessories and setting up webhook handlers...');
521
+ for (const accessory of this.accessories) {
522
+ try {
523
+ const device = accessory.context.device;
524
+ const deviceType = accessory.context.deviceType || device?.deviceType;
525
+ const deviceId = accessory.context.deviceId || device?.deviceId;
526
+ if (!device || !deviceType || !deviceId) {
527
+ this.debugWarnLog(`Skipping cached accessory ${accessory.displayName} - missing context data`);
528
+ continue;
529
+ }
530
+ this.debugLog(`Reinstantiating ${deviceType} for cached accessory: ${accessory.displayName}`);
531
+ // Reinstantiate the device class based on deviceType
532
+ const deviceTypeHandlers = {
533
+ 'Humidifier': Humidifier,
534
+ 'Humidifier2': Humidifier,
535
+ 'Hub 2': Hub,
536
+ 'Hub 3': Hub,
537
+ 'Bot': Bot,
538
+ 'Relay Switch 1': RelaySwitch,
539
+ 'Relay Switch 1PM': RelaySwitch,
540
+ 'Meter': Meter,
541
+ 'MeterPlus': MeterPlus,
542
+ 'Meter Plus (JP)': MeterPlus,
543
+ 'MeterPro': MeterPro,
544
+ 'MeterPro(CO2)': MeterPro,
545
+ 'WoIOSensor': IOSensor,
546
+ 'Water Detector': WaterDetector,
547
+ 'Motion Sensor': Motion,
548
+ 'Contact Sensor': Contact,
549
+ 'Curtain': Curtain,
550
+ 'Curtain3': Curtain,
551
+ 'WoRollerShade': Curtain,
552
+ 'Roller Shade': Curtain,
553
+ 'Blind Tilt': BlindTilt,
554
+ 'Plug': Plug,
555
+ 'Plug Mini (US)': Plug,
556
+ 'Plug Mini (JP)': Plug,
557
+ 'Smart Lock': Lock,
558
+ 'Smart Lock Pro': Lock,
559
+ 'Color Bulb': ColorBulb,
560
+ 'K10+': RobotVacuumCleaner,
561
+ 'K10+ Pro': RobotVacuumCleaner,
562
+ 'WoSweeper': RobotVacuumCleaner,
563
+ 'WoSweeperMini': RobotVacuumCleaner,
564
+ 'Robot Vacuum Cleaner S1': RobotVacuumCleaner,
565
+ 'Robot Vacuum Cleaner S1 Plus': RobotVacuumCleaner,
566
+ 'Robot Vacuum Cleaner S10': RobotVacuumCleaner,
567
+ 'Ceiling Light': CeilingLight,
568
+ 'Ceiling Light Pro': CeilingLight,
569
+ 'Strip Light': StripLight,
570
+ 'Battery Circulator Fan': Fan,
571
+ 'Air Purifier PM2.5': AirPurifier,
572
+ 'Air Purifier Table PM2.5': AirPurifier,
573
+ 'Air Purifier VOC': AirPurifier,
574
+ 'Air Purifier Table VOC': AirPurifier,
575
+ };
576
+ const DeviceClass = deviceTypeHandlers[deviceType];
577
+ if (DeviceClass) {
578
+ new DeviceClass(this, accessory, device);
579
+ this.debugSuccessLog(`Successfully restored ${deviceType}: ${accessory.displayName}`);
580
+ }
581
+ else {
582
+ this.debugLog(`No handler for device type: ${deviceType}`);
583
+ }
584
+ }
585
+ catch (e) {
586
+ this.errorLog(`Failed to restore cached accessory ${accessory.displayName}, Error: ${e.message ?? e}`);
587
+ }
588
+ }
589
+ this.infoLog(`Restored ${this.accessories.length} cached accessories with webhook support`);
427
590
  }
428
591
  async handleManualConfig() {
592
+ // Handle regular devices
429
593
  if (this.config.options?.devices) {
430
594
  this.debugLog(`SwitchBot Device Manual Config Set: ${JSON.stringify(this.config.options?.devices)}`);
431
595
  const devices = this.config.options.devices.map((v) => v);
@@ -444,15 +608,37 @@ export class SwitchBotHAPPlatform {
444
608
  }
445
609
  }
446
610
  }
447
- else {
611
+ // Handle IR devices
612
+ if (this.config.options?.irdevices) {
613
+ this.debugLog(`SwitchBot IR Device Manual Config Set: ${JSON.stringify(this.config.options?.irdevices)}`);
614
+ const irdevices = this.config.options.irdevices.map((v) => v);
615
+ for (const irdevice of irdevices) {
616
+ irdevice.remoteType = irdevice.configRemoteType !== undefined ? irdevice.configRemoteType : 'Unknown';
617
+ irdevice.deviceName = irdevice.configDeviceName !== undefined ? irdevice.configDeviceName : 'Unknown';
618
+ try {
619
+ this.debugLog(`IR deviceId: ${irdevice.deviceId}`);
620
+ if (irdevice.remoteType) {
621
+ await this.createIRDevice(irdevice);
622
+ }
623
+ }
624
+ catch (error) {
625
+ this.errorLog(`failed to create IR device, Error: ${error}`);
626
+ }
627
+ }
628
+ }
629
+ if (!this.config.options?.devices && !this.config.options?.irdevices) {
448
630
  this.errorLog('Neither SwitchBot Token or Device Config are set.');
449
631
  }
450
632
  }
633
+ /**
634
+ * Check if an API status code indicates success
635
+ * @deprecated Use shared isSuccessfulStatusCode from utils.js instead
636
+ */
451
637
  isSuccessfulResponse(apiStatusCode) {
452
- return (apiStatusCode === 200 || apiStatusCode === 100);
638
+ return isSuccessfulStatusCode(apiStatusCode);
453
639
  }
454
640
  async handleDevices(deviceLists) {
455
- if (!this.config.options?.devices && !this.config.options?.deviceConfig) {
641
+ if (!this.config.options?.devices) {
456
642
  this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`);
457
643
  if (deviceLists.length === 0) {
458
644
  this.debugLog('SwitchBot API Has No Devices With Cloud Services Enabled');
@@ -468,35 +654,31 @@ export class SwitchBotHAPPlatform {
468
654
  }
469
655
  }
470
656
  }
471
- else if (this.config.options?.devices || this.config.options?.deviceConfig) {
657
+ else {
472
658
  this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`);
473
- // Step 1: Check and assign configDeviceType to deviceType if deviceType is not present
474
- const devicesWithTypeConfigPromises = deviceLists.map(async (device) => {
659
+ // Check and assign configDeviceType to deviceType if deviceType is not present
660
+ const devicesWithTypeAssigned = deviceLists.map((device) => {
475
661
  if (!device.deviceType) {
476
662
  device.deviceType = device.configDeviceType !== undefined ? device.configDeviceType : 'Unknown';
477
663
  this.warnLog(`API is displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`);
478
664
  }
479
- // Retrieve deviceTypeConfig for each device and merge it
480
- const deviceTypeConfig = this.config.options?.deviceConfig?.[device.deviceType] || {};
481
- return Object.assign({}, device, deviceTypeConfig);
665
+ return device;
482
666
  });
483
- // Wait for all promises to resolve
484
- const devicesWithTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null); // Filter out skipped devices
485
- const devices = this.mergeByDeviceId(this.config.options.devices ?? [], devicesWithTypeConfig ?? []);
667
+ // Apply device-type templates from config entries with applyToAllDevicesOfType=true
668
+ const devicesWithTemplates = applyDeviceTypeTemplates(devicesWithTypeAssigned, this.config.options.devices, 'deviceType', msg => this.debugLog(msg));
669
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices);
670
+ const devices = mergeByDeviceId(this.config.options.devices ?? [], devicesWithTemplates ?? [], allowConfigOnly);
486
671
  this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`);
487
672
  for (const device of devices) {
488
- const deviceIdConfig = this.config.options?.devices?.[device.deviceId] || {};
489
- const deviceWithConfig = Object.assign({}, device, deviceIdConfig);
490
673
  if (device.configDeviceName) {
491
674
  device.deviceName = device.configDeviceName;
492
675
  }
493
- // Pass the merged device object to createDevice
494
- await this.createDevice(deviceWithConfig);
676
+ await this.createDevice(device);
495
677
  }
496
678
  }
497
679
  }
498
680
  async handleIRDevices(irDeviceLists) {
499
- if (!this.config.options?.irdevices && !this.config.options?.irdeviceConfig) {
681
+ if (!this.config.options?.irdevices) {
500
682
  this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`);
501
683
  for (const device of irDeviceLists) {
502
684
  if (device.remoteType) {
@@ -504,43 +686,33 @@ export class SwitchBotHAPPlatform {
504
686
  }
505
687
  }
506
688
  }
507
- else if (this.config.options?.irdevices || this.config.options?.irdeviceConfig) {
689
+ else {
508
690
  this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`);
509
- // Step 1: Check and assign configRemoteType to remoteType if remoteType is not present
510
- const devicesWithTypeConfigPromises = irDeviceLists.map(async (device) => {
691
+ // Check and assign configRemoteType to remoteType if remoteType is not present
692
+ const devicesWithTypeAssigned = irDeviceLists.map((device) => {
511
693
  if (!device.remoteType && device.configRemoteType) {
512
694
  device.remoteType = device.configRemoteType;
513
695
  this.warnLog(`API is displaying no remoteType: ${device.remoteType}, So using configRemoteType: ${device.configRemoteType}`);
514
696
  }
515
697
  else if (!device.remoteType && !device.configDeviceName) {
516
698
  this.errorLog('No remoteType or configRemoteType for device. No device will be created.');
517
- return null; // Skip this device
699
+ return null;
518
700
  }
519
- // Retrieve remoteTypeConfig for each device and merge it
520
- const remoteTypeConfig = this.config.options?.irdeviceConfig?.[device.remoteType] || {};
521
- return Object.assign({}, device, remoteTypeConfig);
522
- });
523
- // Wait for all promises to resolve
524
- const devicesWithRemoteTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null); // Filter out skipped devices
525
- const devices = this.mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithRemoteTypeConfig ?? []);
701
+ return device;
702
+ }).filter(device => device !== null); // Filter out skipped devices
703
+ // Apply remote-type templates from config entries with applyToAllDevicesOfType=true
704
+ const devicesWithTemplates = applyDeviceTypeTemplates(devicesWithTypeAssigned, this.config.options.irdevices, 'remoteType', msg => this.debugLog(msg));
705
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices);
706
+ const devices = mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly);
526
707
  this.debugLog(`IR Devices: ${JSON.stringify(devices)}`);
527
708
  for (const device of devices) {
528
- const irdeviceIdConfig = this.config.options?.irdevices?.[device.deviceId] || {};
529
- const irdeviceWithConfig = Object.assign({}, device, irdeviceIdConfig);
530
709
  if (device.configDeviceName) {
531
710
  device.deviceName = device.configDeviceName;
532
711
  }
533
- await this.createIRDevice(irdeviceWithConfig);
712
+ await this.createIRDevice(device);
534
713
  }
535
714
  }
536
715
  }
537
- mergeByDeviceId(a1, a2) {
538
- const normalizeDeviceId = (deviceId) => deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '');
539
- return a1.map((itm) => {
540
- const matchingItem = a2.find(item => normalizeDeviceId(item.deviceId) === normalizeDeviceId(itm.deviceId));
541
- return { ...matchingItem, ...itm };
542
- });
543
- }
544
716
  async handleErrorResponse(apiStatusCode, retryCount, maxRetries, delayBetweenRetries) {
545
717
  await this.statusCode(apiStatusCode);
546
718
  if (apiStatusCode === 500) {
@@ -2596,40 +2768,13 @@ export class SwitchBotHAPPlatform {
2596
2768
  *
2597
2769
  * @param statusCode - The status code returned by the device.
2598
2770
  * @returns A promise that resolves when the logging is complete.
2771
+ * @deprecated Use shared logStatusCode from utils.js instead
2599
2772
  */
2600
2773
  async statusCode(statusCode) {
2601
- const messages = {
2602
- 151: `Command not supported by this device type, statusCode: ${statusCode}, Submit Feature Request Here:
2603
- https://tinyurl.com/SwitchBotFeatureRequest`,
2604
- 152: `Device not found, statusCode: ${statusCode}`,
2605
- 160: `Command is not supported, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`,
2606
- 161: `Device is offline, statusCode: ${statusCode}`,
2607
- 171: `is offline, statusCode: ${statusCode}`,
2608
- 190: `Requests reached the daily limit, statusCode: ${statusCode}`,
2609
- 100: `Command successfully sent, statusCode: ${statusCode}`,
2610
- 200: `Request successful, statusCode: ${statusCode}`,
2611
- 400: `Bad Request, The client has issued an invalid request. This is commonly used to specify validation errors in a request payload,
2612
- statusCode: ${statusCode}`,
2613
- 401: `Unauthorized, Authorization for the API is required, but the request has not been authenticated, statusCode: ${statusCode}`,
2614
- 403: `Forbidden, The request has been authenticated but does not have appropriate permissions, or a requested resource is not found,
2615
- statusCode: ${statusCode}`,
2616
- 404: `Not Found, Specifies the requested path does not exist, statusCode: ${statusCode}`,
2617
- 406: `Not Acceptable, The client has requested a MIME type via the Accept header for a value not supported by the server,
2618
- statusCode: ${statusCode}`,
2619
- 415: `Unsupported Media Type, The client has defined a contentType header that is not supported by the server, statusCode: ${statusCode}`,
2620
- 422: `Unprocessable Entity, The client has made a valid request, but the server cannot process it. This is often used for APIs for which
2621
- certain limits have been exceeded, statusCode: ${statusCode}`,
2622
- 429: `Too Many Requests, The client has exceeded the number of requests allowed for a given time window, statusCode: ${statusCode}`,
2623
- 500: `Internal Server Error, An unexpected error on the SmartThings servers has occurred. These errors should be rare,
2624
- statusCode: ${statusCode}`,
2625
- };
2626
- const message = messages[statusCode] ?? `Unknown statusCode, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`;
2627
- if ([100, 200].includes(statusCode)) {
2628
- this.debugLog(message);
2629
- }
2630
- else {
2631
- this.errorLog(message);
2632
- }
2774
+ await logStatusCode(statusCode, {
2775
+ debugLog: this.debugLog.bind(this),
2776
+ errorLog: this.errorLog.bind(this),
2777
+ });
2633
2778
  }
2634
2779
  async retryRequest(device, deviceMaxRetries, deviceDelayBetweenRetries) {
2635
2780
  let retryCount = 0;
@@ -2637,6 +2782,18 @@ export class SwitchBotHAPPlatform {
2637
2782
  const delayBetweenRetries = deviceDelayBetweenRetries;
2638
2783
  while (retryCount < maxRetries) {
2639
2784
  try {
2785
+ if (!this.apiTracker?.trySpend('poll')) {
2786
+ return {
2787
+ response: {
2788
+ deviceId: '',
2789
+ deviceType: '',
2790
+ hubDeviceId: '',
2791
+ version: 0,
2792
+ deviceName: '',
2793
+ },
2794
+ statusCode: 429,
2795
+ };
2796
+ }
2640
2797
  const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(device.deviceId, this.config.credentials?.token, this.config.credentials?.secret);
2641
2798
  this.debugLog(`response: ${JSON.stringify(response)}`);
2642
2799
  return { response, statusCode };
@@ -2662,6 +2819,9 @@ export class SwitchBotHAPPlatform {
2662
2819
  const delayBetweenRetries = deviceDelayBetweenRetries ?? 1000;
2663
2820
  while (retryCount < maxRetries) {
2664
2821
  try {
2822
+ if (!this.apiTracker?.trySpend('command')) {
2823
+ return { response: {}, statusCode: 429 };
2824
+ }
2665
2825
  const { response, statusCode } = await this.switchBotAPI.controlDevice(device.deviceId, bodyChange.command, bodyChange.parameter, bodyChange.commandType, this.config.credentials?.token, this.config.credentials?.secret);
2666
2826
  this.debugLog(`response: ${JSON.stringify(response)}`);
2667
2827
  return { response, statusCode };
@@ -2784,66 +2944,5 @@ export class SwitchBotHAPPlatform {
2784
2944
  return value;
2785
2945
  }
2786
2946
  }
2787
- /**
2788
- * If device level logging is turned on, log to log.warn
2789
- * Otherwise send debug logs to log.debug
2790
- */
2791
- async infoLog(...log) {
2792
- if (await this.enablingPlatformLogging()) {
2793
- this.log.info(String(...log));
2794
- }
2795
- }
2796
- async successLog(...log) {
2797
- if (await this.enablingPlatformLogging()) {
2798
- this.log.success(String(...log));
2799
- }
2800
- }
2801
- async debugSuccessLog(...log) {
2802
- if (await this.enablingPlatformLogging()) {
2803
- if (await this.loggingIsDebug()) {
2804
- this.log.success('[DEBUG]', String(...log));
2805
- }
2806
- }
2807
- }
2808
- async warnLog(...log) {
2809
- if (await this.enablingPlatformLogging()) {
2810
- this.log.warn(String(...log));
2811
- }
2812
- }
2813
- async debugWarnLog(...log) {
2814
- if (await this.enablingPlatformLogging()) {
2815
- if (await this.loggingIsDebug()) {
2816
- this.log.warn('[DEBUG]', String(...log));
2817
- }
2818
- }
2819
- }
2820
- async errorLog(...log) {
2821
- if (await this.enablingPlatformLogging()) {
2822
- this.log.error(String(...log));
2823
- }
2824
- }
2825
- async debugErrorLog(...log) {
2826
- if (await this.enablingPlatformLogging()) {
2827
- if (await this.loggingIsDebug()) {
2828
- this.log.error('[DEBUG]', String(...log));
2829
- }
2830
- }
2831
- }
2832
- async debugLog(...log) {
2833
- if (await this.enablingPlatformLogging()) {
2834
- if (this.platformLogging === 'debug') {
2835
- this.log.info('[DEBUG]', String(...log));
2836
- }
2837
- else if (this.platformLogging === 'debugMode') {
2838
- this.log.debug(String(...log));
2839
- }
2840
- }
2841
- }
2842
- async loggingIsDebug() {
2843
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug';
2844
- }
2845
- async enablingPlatformLogging() {
2846
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug' || this.platformLogging === 'standard';
2847
- }
2848
2947
  }
2849
2948
  //# sourceMappingURL=platform-hap.js.map