@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
@@ -55,7 +55,7 @@ import { TV } from './irdevice/tv.js'
55
55
  import { VacuumCleaner } from './irdevice/vacuumcleaner.js'
56
56
  import { WaterHeater } from './irdevice/waterheater.js'
57
57
  import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
58
- import { cleanDeviceConfig, formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, safeStringify, sleep } from './utils.js'
58
+ import { ApiRequestTracker, applyDeviceTypeTemplates, createPlatformLogger, formatDeviceIdAsMac, isBlindTiltDevice, isCurtainDevice, isSuccessfulStatusCode, logStatusCode, mergeByDeviceId, safeStringify, sleep } from './utils.js'
59
59
 
60
60
  /**
61
61
  * HomebridgePlatform
@@ -68,6 +68,18 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
68
68
  public readonly api: API
69
69
  public readonly log: Logging
70
70
 
71
+ // Logging helper functions (attached from utils.createPlatformLogger in constructor)
72
+ infoLog!: (...log: any[]) => Promise<void>
73
+ successLog!: (...log: any[]) => Promise<void>
74
+ debugSuccessLog!: (...log: any[]) => Promise<void>
75
+ warnLog!: (...log: any[]) => Promise<void>
76
+ debugWarnLog!: (...log: any[]) => Promise<void>
77
+ errorLog!: (...log: any[]) => Promise<void>
78
+ debugErrorLog!: (...log: any[]) => Promise<void>
79
+ debugLog!: (...log: any[]) => Promise<void>
80
+ loggingIsDebug!: () => Promise<boolean>
81
+ enablingPlatformLogging!: () => Promise<boolean>
82
+
71
83
  // Configuration properties
72
84
  platformConfig!: SwitchBotPlatformConfig
73
85
  platformLogging!: options['logging']
@@ -88,6 +100,9 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
88
100
  switchBotAPI!: SwitchBotOpenAPI
89
101
  switchBotBLE!: SwitchBotBLE
90
102
 
103
+ // API request tracking
104
+ private apiTracker?: ApiRequestTracker
105
+
91
106
  // External APIs
92
107
  public readonly eve: any
93
108
  public readonly fakegatoAPI: any
@@ -104,9 +119,22 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
104
119
  this.api = api
105
120
  this.log = log
106
121
 
122
+ // Attach shared platform logging helpers (moved to utils for reuse)
123
+ const _pl = createPlatformLogger(async () => this.platformLogging, this.log)
124
+ this.infoLog = _pl.infoLog
125
+ this.successLog = _pl.successLog
126
+ this.debugSuccessLog = _pl.debugSuccessLog
127
+ this.warnLog = _pl.warnLog
128
+ this.debugWarnLog = _pl.debugWarnLog
129
+ this.errorLog = _pl.errorLog
130
+ this.debugErrorLog = _pl.debugErrorLog
131
+ this.debugLog = _pl.debugLog
132
+ this.loggingIsDebug = _pl.loggingIsDebug
133
+ this.enablingPlatformLogging = _pl.enablingPlatformLogging
134
+
107
135
  // only load if configured
108
136
  if (!config) {
109
- this.log.error('No configuration found for the plugin, please check your config.')
137
+ this.errorLog('No configuration found for the plugin, please check your config.')
110
138
  return
111
139
  }
112
140
 
@@ -119,21 +147,23 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
119
147
  devices: config.devices as { deviceId: string }[],
120
148
  }
121
149
 
122
- // Normalize deviceConfig to remove UI-inserted defaults (lots of false/empty values)
150
+ // Determine platform logging preference (match HAP behaviour as closely as
151
+ // possible using config values. We default to 'standard' when unspecified.)
152
+ this.platformLogging = (this.config.options?.logging === 'debug' || this.config.options?.logging === 'standard' || this.config.options?.logging === 'none')
153
+ ? this.config.options.logging
154
+ : 'standard'
155
+
156
+ // Unconditional diagnostic using the raw Homebridge `log` so it always
157
+ // appears regardless of the platform logging helpers' gating logic.
123
158
  try {
124
- if ((this.config as any).options) {
125
- const cleaned = cleanDeviceConfig((this.config as any).options.deviceConfig)
126
- if (cleaned) {
127
- ;(this.config as any).options.deviceConfig = cleaned
128
- } else {
129
- // remove the empty deviceConfig so downstream checks treat it as absent
130
- delete (this.config as any).options.deviceConfig
131
- }
132
- }
133
- } catch (e) {
134
- this.debugErrorLog(`Failed to clean deviceConfig: ${e}`)
159
+ this.log.debug?.(`[SwitchBot HAP] effective platformLogging=${String(this.platformLogging)}`)
160
+ } catch (e: any) {
161
+ // swallow any logging errors — diagnostics are best-effort
135
162
  }
136
163
 
164
+ // Note: deviceConfig and irdeviceConfig have been removed from the platform.
165
+ // All device-specific configuration should be done via options.devices and options.irdevices arrays.
166
+
137
167
  // Plugin Configuration
138
168
  this.getPlatformLogSettings()
139
169
  this.getPlatformRateSettings()
@@ -203,9 +233,36 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
203
233
  // to start discovery of new accessories.
204
234
  this.api.on('didFinishLaunching', async () => {
205
235
  this.debugLog('Executed didFinishLaunching callback')
236
+
237
+ // Initialize API request tracking
238
+ try {
239
+ const dailyApiLimit = this.config.options?.dailyApiLimit ?? 10000
240
+ const dailyApiReserveForCommands = this.config.options?.dailyApiReserveForCommands ?? 1000
241
+ const webhookOnlyOnReserve = this.config.options?.webhookOnlyOnReserve ?? false
242
+ this.apiTracker = new ApiRequestTracker(this.api, this.log, 'SwitchBot HAP', {
243
+ dailyLimit: dailyApiLimit,
244
+ reserveForCommands: dailyApiReserveForCommands,
245
+ pausePollingAtReserve: webhookOnlyOnReserve,
246
+ })
247
+ this.apiTracker.startHourlyLogging()
248
+ } catch (e: any) {
249
+ this.errorLog(`Failed to initialize API request tracking: ${e.message ?? e}`)
250
+ }
251
+
206
252
  // run the method to discover / register your devices as accessories
207
253
  try {
208
- await this.discoverDevices()
254
+ // Does the user have a version of Homebridge that is compatible with matter?
255
+ if (!this.api.isMatterAvailable?.()) {
256
+ 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)'}`)
257
+ }
258
+ if (!this.api.isMatterEnabled?.()) {
259
+ 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)'}`)
260
+ }
261
+ if (!this.api.isMatterAvailable?.() && !this.api.isMatterEnabled?.()) {
262
+ await this.discoverDevices()
263
+ } else {
264
+ this.infoLog('Matter is enabled in Homebridge. SwitchBot Matter devices will be handled by the Matter platform.')
265
+ }
209
266
  } catch (e: any) {
210
267
  this.errorLog(`Failed to Discover, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug')
211
268
  this.debugErrorLog(`Failed to Discover, Error: ${e.message ?? e}`)
@@ -433,6 +490,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
433
490
  let retryCount = 0
434
491
  const maxRetries = this.platformMaxRetries ?? 5
435
492
  const delayBetweenRetries = this.platformDelayBetweenRetries || 5000
493
+ let rateLimitExceeded = false
436
494
 
437
495
  this.debugWarnLog(`Retry Count: ${retryCount}`)
438
496
  this.debugWarnLog(`Max Retries: ${this.platformMaxRetries}`)
@@ -440,6 +498,11 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
440
498
 
441
499
  while (retryCount < maxRetries) {
442
500
  try {
501
+ if (!this.apiTracker?.trySpend('discovery')) {
502
+ rateLimitExceeded = true
503
+ this.warnLog('OpenAPI daily budget reached (discovery blocked). Falling back to manual device configuration.')
504
+ break
505
+ }
443
506
  const { response, statusCode } = await this.switchBotAPI.getDevices()
444
507
  this.debugLog(`response: ${JSON.stringify(response)}`)
445
508
  if (this.isSuccessfulResponse(statusCode)) {
@@ -447,6 +510,13 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
447
510
  await this.handleIRDevices(Array.isArray(response.body.infraredRemoteList) ? response.body.infraredRemoteList : [])
448
511
  break
449
512
  } else {
513
+ // Check if rate limit exceeded (429)
514
+ if (statusCode === 429) {
515
+ rateLimitExceeded = true
516
+ this.warnLog('OpenAPI rate limit (429) exceeded. Falling back to manual device configuration.')
517
+ this.warnLog('Webhook functionality will still work if devices are configured manually.')
518
+ break
519
+ }
450
520
  await this.handleErrorResponse(statusCode, retryCount, maxRetries, delayBetweenRetries)
451
521
  retryCount++
452
522
  }
@@ -456,9 +526,116 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
456
526
  this.debugErrorLog(`Failed to Discover Devices, Error: ${e.message ?? e}`)
457
527
  }
458
528
  }
529
+
530
+ // If rate limit exceeded or retries exhausted, try to load from manual config or cached accessories
531
+ if (rateLimitExceeded || retryCount >= maxRetries) {
532
+ const hasCachedAccessories = this.accessories.length > 0
533
+ const hasManualConfig = this.config.options?.devices || this.config.options?.irdevices
534
+
535
+ if (hasManualConfig || hasCachedAccessories) {
536
+ if (hasCachedAccessories) {
537
+ this.warnLog(`Found ${this.accessories.length} cached accessories from previous sessions.`)
538
+ this.warnLog('Reinstantiating device classes to enable webhook handlers...')
539
+ await this.restoreCachedAccessories()
540
+ }
541
+ if (hasManualConfig) {
542
+ this.warnLog('Attempting to load devices from manual configuration...')
543
+ await this.handleManualConfig()
544
+ }
545
+ if (hasCachedAccessories) {
546
+ this.infoLog('Cached accessories restored. Webhook functionality is active.')
547
+ this.infoLog('Device discovery will resume when API rate limit resets.')
548
+ }
549
+ } else {
550
+ this.errorLog('OpenAPI unavailable and no cached accessories or manual device configuration found.')
551
+ this.errorLog('Please configure devices manually in the plugin settings to use webhook functionality.')
552
+ }
553
+ }
554
+ }
555
+
556
+ /**
557
+ * Restore cached accessories by reinstantiating their device classes
558
+ * This ensures webhook handlers and other functionality are properly set up
559
+ */
560
+ private async restoreCachedAccessories() {
561
+ this.debugLog('Restoring cached accessories and setting up webhook handlers...')
562
+
563
+ for (const accessory of this.accessories) {
564
+ try {
565
+ const device = accessory.context.device
566
+ const deviceType = accessory.context.deviceType || device?.deviceType
567
+ const deviceId = accessory.context.deviceId || device?.deviceId
568
+
569
+ if (!device || !deviceType || !deviceId) {
570
+ this.debugWarnLog(`Skipping cached accessory ${accessory.displayName} - missing context data`)
571
+ continue
572
+ }
573
+
574
+ this.debugLog(`Reinstantiating ${deviceType} for cached accessory: ${accessory.displayName}`)
575
+
576
+ // Reinstantiate the device class based on deviceType
577
+ const deviceTypeHandlers: { [key: string]: new (platform: any, accessory: PlatformAccessory, device: any) => any } = {
578
+ 'Humidifier': Humidifier,
579
+ 'Humidifier2': Humidifier,
580
+ 'Hub 2': Hub,
581
+ 'Hub 3': Hub,
582
+ 'Bot': Bot,
583
+ 'Relay Switch 1': RelaySwitch,
584
+ 'Relay Switch 1PM': RelaySwitch,
585
+ 'Meter': Meter,
586
+ 'MeterPlus': MeterPlus,
587
+ 'Meter Plus (JP)': MeterPlus,
588
+ 'MeterPro': MeterPro,
589
+ 'MeterPro(CO2)': MeterPro,
590
+ 'WoIOSensor': IOSensor,
591
+ 'Water Detector': WaterDetector,
592
+ 'Motion Sensor': Motion,
593
+ 'Contact Sensor': Contact,
594
+ 'Curtain': Curtain,
595
+ 'Curtain3': Curtain,
596
+ 'WoRollerShade': Curtain,
597
+ 'Roller Shade': Curtain,
598
+ 'Blind Tilt': BlindTilt,
599
+ 'Plug': Plug,
600
+ 'Plug Mini (US)': Plug,
601
+ 'Plug Mini (JP)': Plug,
602
+ 'Smart Lock': Lock,
603
+ 'Smart Lock Pro': Lock,
604
+ 'Color Bulb': ColorBulb,
605
+ 'K10+': RobotVacuumCleaner,
606
+ 'K10+ Pro': RobotVacuumCleaner,
607
+ 'WoSweeper': RobotVacuumCleaner,
608
+ 'WoSweeperMini': RobotVacuumCleaner,
609
+ 'Robot Vacuum Cleaner S1': RobotVacuumCleaner,
610
+ 'Robot Vacuum Cleaner S1 Plus': RobotVacuumCleaner,
611
+ 'Robot Vacuum Cleaner S10': RobotVacuumCleaner,
612
+ 'Ceiling Light': CeilingLight,
613
+ 'Ceiling Light Pro': CeilingLight,
614
+ 'Strip Light': StripLight,
615
+ 'Battery Circulator Fan': Fan,
616
+ 'Air Purifier PM2.5': AirPurifier,
617
+ 'Air Purifier Table PM2.5': AirPurifier,
618
+ 'Air Purifier VOC': AirPurifier,
619
+ 'Air Purifier Table VOC': AirPurifier,
620
+ }
621
+
622
+ const DeviceClass = deviceTypeHandlers[deviceType]
623
+ if (DeviceClass) {
624
+ new DeviceClass(this, accessory, device)
625
+ this.debugSuccessLog(`Successfully restored ${deviceType}: ${accessory.displayName}`)
626
+ } else {
627
+ this.debugLog(`No handler for device type: ${deviceType}`)
628
+ }
629
+ } catch (e: any) {
630
+ this.errorLog(`Failed to restore cached accessory ${accessory.displayName}, Error: ${e.message ?? e}`)
631
+ }
632
+ }
633
+
634
+ this.infoLog(`Restored ${this.accessories.length} cached accessories with webhook support`)
459
635
  }
460
636
 
461
637
  private async handleManualConfig() {
638
+ // Handle regular devices
462
639
  if (this.config.options?.devices) {
463
640
  this.debugLog(`SwitchBot Device Manual Config Set: ${JSON.stringify(this.config.options?.devices)}`)
464
641
  const devices = this.config.options.devices.map((v: any) => v)
@@ -475,17 +652,41 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
475
652
  this.errorLog(`failed to format device ID as MAC, Error: ${error}`)
476
653
  }
477
654
  }
478
- } else {
655
+ }
656
+
657
+ // Handle IR devices
658
+ if (this.config.options?.irdevices) {
659
+ this.debugLog(`SwitchBot IR Device Manual Config Set: ${JSON.stringify(this.config.options?.irdevices)}`)
660
+ const irdevices = this.config.options.irdevices.map((v: any) => v)
661
+ for (const irdevice of irdevices) {
662
+ irdevice.remoteType = irdevice.configRemoteType !== undefined ? irdevice.configRemoteType : 'Unknown'
663
+ irdevice.deviceName = irdevice.configDeviceName !== undefined ? irdevice.configDeviceName : 'Unknown'
664
+ try {
665
+ this.debugLog(`IR deviceId: ${irdevice.deviceId}`)
666
+ if (irdevice.remoteType) {
667
+ await this.createIRDevice(irdevice)
668
+ }
669
+ } catch (error) {
670
+ this.errorLog(`failed to create IR device, Error: ${error}`)
671
+ }
672
+ }
673
+ }
674
+
675
+ if (!this.config.options?.devices && !this.config.options?.irdevices) {
479
676
  this.errorLog('Neither SwitchBot Token or Device Config are set.')
480
677
  }
481
678
  }
482
679
 
680
+ /**
681
+ * Check if an API status code indicates success
682
+ * @deprecated Use shared isSuccessfulStatusCode from utils.js instead
683
+ */
483
684
  private isSuccessfulResponse(apiStatusCode: number): boolean {
484
- return (apiStatusCode === 200 || apiStatusCode === 100)
685
+ return isSuccessfulStatusCode(apiStatusCode)
485
686
  }
486
687
 
487
688
  private async handleDevices(deviceLists: any[]) {
488
- if (!this.config.options?.devices && !this.config.options?.deviceConfig) {
689
+ if (!this.config.options?.devices) {
489
690
  this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`)
490
691
  if (deviceLists.length === 0) {
491
692
  this.debugLog('SwitchBot API Has No Devices With Cloud Services Enabled')
@@ -499,92 +700,84 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
499
700
  }
500
701
  }
501
702
  }
502
- } else if (this.config.options?.devices || this.config.options?.deviceConfig) {
703
+ } else {
503
704
  this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`)
504
705
 
505
- // Step 1: Check and assign configDeviceType to deviceType if deviceType is not present
506
- const devicesWithTypeConfigPromises = deviceLists.map(async (device) => {
706
+ // Check and assign configDeviceType to deviceType if deviceType is not present
707
+ const devicesWithTypeAssigned = deviceLists.map((device) => {
507
708
  if (!device.deviceType) {
508
709
  device.deviceType = device.configDeviceType !== undefined ? device.configDeviceType : 'Unknown'
509
710
  this.warnLog(`API is displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`)
510
711
  }
511
-
512
- // Retrieve deviceTypeConfig for each device and merge it
513
- const deviceTypeConfig = this.config.options?.deviceConfig?.[device.deviceType] || {}
514
- return Object.assign({}, device, deviceTypeConfig)
712
+ return device
515
713
  })
516
714
 
517
- // Wait for all promises to resolve
518
- const devicesWithTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices
715
+ // Apply device-type templates from config entries with applyToAllDevicesOfType=true
716
+ const devicesWithTemplates = applyDeviceTypeTemplates(
717
+ devicesWithTypeAssigned,
718
+ this.config.options.devices,
719
+ 'deviceType',
720
+ msg => this.debugLog(msg),
721
+ )
519
722
 
520
- const devices = this.mergeByDeviceId(this.config.options.devices ?? [], devicesWithTypeConfig ?? [])
723
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
724
+ const devices = mergeByDeviceId(this.config.options.devices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
521
725
 
522
726
  this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`)
523
727
 
524
728
  for (const device of devices) {
525
- const deviceIdConfig = this.config.options?.devices?.[device.deviceId] || {}
526
- const deviceWithConfig = Object.assign({}, device, deviceIdConfig)
527
-
528
729
  if (device.configDeviceName) {
529
730
  device.deviceName = device.configDeviceName
530
731
  }
531
- // Pass the merged device object to createDevice
532
- await this.createDevice(deviceWithConfig)
732
+ await this.createDevice(device)
533
733
  }
534
734
  }
535
735
  }
536
736
 
537
737
  private async handleIRDevices(irDeviceLists: any[]) {
538
- if (!this.config.options?.irdevices && !this.config.options?.irdeviceConfig) {
738
+ if (!this.config.options?.irdevices) {
539
739
  this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`)
540
740
  for (const device of irDeviceLists) {
541
741
  if (device.remoteType) {
542
742
  await this.createIRDevice(device)
543
743
  }
544
744
  }
545
- } else if (this.config.options?.irdevices || this.config.options?.irdeviceConfig) {
745
+ } else {
546
746
  this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`)
547
747
 
548
- // Step 1: Check and assign configRemoteType to remoteType if remoteType is not present
549
- const devicesWithTypeConfigPromises = irDeviceLists.map(async (device) => {
748
+ // Check and assign configRemoteType to remoteType if remoteType is not present
749
+ const devicesWithTypeAssigned = irDeviceLists.map((device) => {
550
750
  if (!device.remoteType && device.configRemoteType) {
551
751
  device.remoteType = device.configRemoteType
552
752
  this.warnLog(`API is displaying no remoteType: ${device.remoteType}, So using configRemoteType: ${device.configRemoteType}`)
553
753
  } else if (!device.remoteType && !device.configDeviceName) {
554
754
  this.errorLog('No remoteType or configRemoteType for device. No device will be created.')
555
- return null // Skip this device
755
+ return null
556
756
  }
757
+ return device
758
+ }).filter(device => device !== null) // Filter out skipped devices
557
759
 
558
- // Retrieve remoteTypeConfig for each device and merge it
559
- const remoteTypeConfig = this.config.options?.irdeviceConfig?.[device.remoteType] || {}
560
- return Object.assign({}, device, remoteTypeConfig)
561
- })
562
- // Wait for all promises to resolve
563
- const devicesWithRemoteTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices
760
+ // Apply remote-type templates from config entries with applyToAllDevicesOfType=true
761
+ const devicesWithTemplates = applyDeviceTypeTemplates(
762
+ devicesWithTypeAssigned,
763
+ this.config.options.irdevices,
764
+ 'remoteType',
765
+ msg => this.debugLog(msg),
766
+ )
564
767
 
565
- const devices = this.mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithRemoteTypeConfig ?? [])
768
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
769
+ const devices = mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
566
770
 
567
771
  this.debugLog(`IR Devices: ${JSON.stringify(devices)}`)
568
772
  for (const device of devices) {
569
- const irdeviceIdConfig = this.config.options?.irdevices?.[device.deviceId] || {}
570
- const irdeviceWithConfig = Object.assign({}, device, irdeviceIdConfig)
571
-
572
773
  if (device.configDeviceName) {
573
774
  device.deviceName = device.configDeviceName
574
775
  }
575
- await this.createIRDevice(irdeviceWithConfig)
776
+ await this.createIRDevice(device)
576
777
  }
577
778
  }
578
779
  }
579
780
 
580
- private mergeByDeviceId(a1: { deviceId: string }[], a2: any[]) {
581
- const normalizeDeviceId = (deviceId: string) => deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '')
582
- return a1.map((itm) => {
583
- const matchingItem = a2.find(item => normalizeDeviceId(item.deviceId) === normalizeDeviceId(itm.deviceId))
584
- return { ...matchingItem, ...itm }
585
- })
586
- }
587
-
588
781
  private async handleErrorResponse(apiStatusCode: number, retryCount: number, maxRetries: number, delayBetweenRetries: number) {
589
782
  await this.statusCode(apiStatusCode)
590
783
  if (apiStatusCode === 500) {
@@ -2709,41 +2902,13 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2709
2902
  *
2710
2903
  * @param statusCode - The status code returned by the device.
2711
2904
  * @returns A promise that resolves when the logging is complete.
2905
+ * @deprecated Use shared logStatusCode from utils.js instead
2712
2906
  */
2713
2907
  async statusCode(statusCode: number): Promise<void> {
2714
- const messages: { [key: number]: string } = {
2715
- 151: `Command not supported by this device type, statusCode: ${statusCode}, Submit Feature Request Here:
2716
- https://tinyurl.com/SwitchBotFeatureRequest`,
2717
- 152: `Device not found, statusCode: ${statusCode}`,
2718
- 160: `Command is not supported, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`,
2719
- 161: `Device is offline, statusCode: ${statusCode}`,
2720
- 171: `is offline, statusCode: ${statusCode}`,
2721
- 190: `Requests reached the daily limit, statusCode: ${statusCode}`,
2722
- 100: `Command successfully sent, statusCode: ${statusCode}`,
2723
- 200: `Request successful, statusCode: ${statusCode}`,
2724
- 400: `Bad Request, The client has issued an invalid request. This is commonly used to specify validation errors in a request payload,
2725
- statusCode: ${statusCode}`,
2726
- 401: `Unauthorized, Authorization for the API is required, but the request has not been authenticated, statusCode: ${statusCode}`,
2727
- 403: `Forbidden, The request has been authenticated but does not have appropriate permissions, or a requested resource is not found,
2728
- statusCode: ${statusCode}`,
2729
- 404: `Not Found, Specifies the requested path does not exist, statusCode: ${statusCode}`,
2730
- 406: `Not Acceptable, The client has requested a MIME type via the Accept header for a value not supported by the server,
2731
- statusCode: ${statusCode}`,
2732
- 415: `Unsupported Media Type, The client has defined a contentType header that is not supported by the server, statusCode: ${statusCode}`,
2733
- 422: `Unprocessable Entity, The client has made a valid request, but the server cannot process it. This is often used for APIs for which
2734
- certain limits have been exceeded, statusCode: ${statusCode}`,
2735
- 429: `Too Many Requests, The client has exceeded the number of requests allowed for a given time window, statusCode: ${statusCode}`,
2736
- 500: `Internal Server Error, An unexpected error on the SmartThings servers has occurred. These errors should be rare,
2737
- statusCode: ${statusCode}`,
2738
- }
2739
-
2740
- const message = messages[statusCode] ?? `Unknown statusCode, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`
2741
-
2742
- if ([100, 200].includes(statusCode)) {
2743
- this.debugLog(message)
2744
- } else {
2745
- this.errorLog(message)
2746
- }
2908
+ await logStatusCode(statusCode, {
2909
+ debugLog: this.debugLog.bind(this),
2910
+ errorLog: this.errorLog.bind(this),
2911
+ })
2747
2912
  }
2748
2913
 
2749
2914
  async retryRequest(device: (device & devicesConfig) | (irdevice & irDevicesConfig), deviceMaxRetries: number, deviceDelayBetweenRetries: number): Promise<{ response: any, statusCode: deviceStatusRequest['statusCode'] }> {
@@ -2752,6 +2917,18 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2752
2917
  const delayBetweenRetries = deviceDelayBetweenRetries
2753
2918
  while (retryCount < maxRetries) {
2754
2919
  try {
2920
+ if (!this.apiTracker?.trySpend('poll')) {
2921
+ return {
2922
+ response: {
2923
+ deviceId: '',
2924
+ deviceType: '',
2925
+ hubDeviceId: '',
2926
+ version: 0,
2927
+ deviceName: '',
2928
+ },
2929
+ statusCode: 429,
2930
+ }
2931
+ }
2755
2932
  const { response, statusCode } = await this.switchBotAPI.getDeviceStatus(device.deviceId, this.config.credentials?.token, this.config.credentials?.secret)
2756
2933
  this.debugLog(`response: ${JSON.stringify(response)}`)
2757
2934
  return { response, statusCode }
@@ -2777,6 +2954,9 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2777
2954
  const delayBetweenRetries = deviceDelayBetweenRetries ?? 1000
2778
2955
  while (retryCount < maxRetries) {
2779
2956
  try {
2957
+ if (!this.apiTracker?.trySpend('command')) {
2958
+ return { response: {}, statusCode: 429 }
2959
+ }
2780
2960
  const { response, statusCode } = await this.switchBotAPI.controlDevice(
2781
2961
  device.deviceId,
2782
2962
  bodyChange.command,
@@ -2913,74 +3093,4 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2913
3093
  return value
2914
3094
  }
2915
3095
  }
2916
-
2917
- /**
2918
- * If device level logging is turned on, log to log.warn
2919
- * Otherwise send debug logs to log.debug
2920
- */
2921
- async infoLog(...log: any[]): Promise<void> {
2922
- if (await this.enablingPlatformLogging()) {
2923
- this.log.info(String(...log))
2924
- }
2925
- }
2926
-
2927
- async successLog(...log: any[]): Promise<void> {
2928
- if (await this.enablingPlatformLogging()) {
2929
- this.log.success(String(...log))
2930
- }
2931
- }
2932
-
2933
- async debugSuccessLog(...log: any[]): Promise<void> {
2934
- if (await this.enablingPlatformLogging()) {
2935
- if (await this.loggingIsDebug()) {
2936
- this.log.success('[DEBUG]', String(...log))
2937
- }
2938
- }
2939
- }
2940
-
2941
- async warnLog(...log: any[]): Promise<void> {
2942
- if (await this.enablingPlatformLogging()) {
2943
- this.log.warn(String(...log))
2944
- }
2945
- }
2946
-
2947
- async debugWarnLog(...log: any[]): Promise<void> {
2948
- if (await this.enablingPlatformLogging()) {
2949
- if (await this.loggingIsDebug()) {
2950
- this.log.warn('[DEBUG]', String(...log))
2951
- }
2952
- }
2953
- }
2954
-
2955
- async errorLog(...log: any[]): Promise<void> {
2956
- if (await this.enablingPlatformLogging()) {
2957
- this.log.error(String(...log))
2958
- }
2959
- }
2960
-
2961
- async debugErrorLog(...log: any[]): Promise<void> {
2962
- if (await this.enablingPlatformLogging()) {
2963
- if (await this.loggingIsDebug()) {
2964
- this.log.error('[DEBUG]', String(...log))
2965
- }
2966
- }
2967
- }
2968
-
2969
- async debugLog(...log: any[]): Promise<void> {
2970
- if (await this.enablingPlatformLogging()) {
2971
- if (this.platformLogging === 'debug') {
2972
- this.log.info('[DEBUG]', String(...log))
2973
- } else if (this.platformLogging === 'debugMode') {
2974
- this.log.debug(String(...log))
2975
- }
2976
- }
2977
- }
2978
-
2979
- async loggingIsDebug(): Promise<boolean> {
2980
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug'
2981
- }
2982
-
2983
- async enablingPlatformLogging(): Promise<boolean> {
2984
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug' || this.platformLogging === 'standard'
2985
- }
2986
3096
  }