@switchbot/homebridge-switchbot 5.0.0-beta.6 → 5.0.0-beta.61

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 (267) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +45 -3
  3. package/config.schema.json +866 -13754
  4. package/dist/devices-hap/airpurifier.d.ts.map +1 -1
  5. package/dist/devices-hap/airpurifier.js +12 -6
  6. package/dist/devices-hap/airpurifier.js.map +1 -1
  7. package/dist/devices-hap/blindtilt.js +3 -3
  8. package/dist/devices-hap/bot.d.ts.map +1 -1
  9. package/dist/devices-hap/bot.js +16 -5
  10. package/dist/devices-hap/bot.js.map +1 -1
  11. package/dist/devices-hap/ceilinglight.d.ts.map +1 -1
  12. package/dist/devices-hap/ceilinglight.js +13 -7
  13. package/dist/devices-hap/ceilinglight.js.map +1 -1
  14. package/dist/devices-hap/colorbulb.d.ts.map +1 -1
  15. package/dist/devices-hap/colorbulb.js +49 -9
  16. package/dist/devices-hap/colorbulb.js.map +1 -1
  17. package/dist/devices-hap/contact.js +3 -3
  18. package/dist/devices-hap/curtain.js +2 -2
  19. package/dist/devices-hap/curtain.js.map +1 -1
  20. package/dist/devices-hap/device.d.ts +18 -8
  21. package/dist/devices-hap/device.d.ts.map +1 -1
  22. package/dist/devices-hap/device.js +141 -69
  23. package/dist/devices-hap/device.js.map +1 -1
  24. package/dist/devices-hap/fan.d.ts.map +1 -1
  25. package/dist/devices-hap/fan.js +12 -6
  26. package/dist/devices-hap/fan.js.map +1 -1
  27. package/dist/devices-hap/hub.d.ts.map +1 -1
  28. package/dist/devices-hap/hub.js +6 -5
  29. package/dist/devices-hap/hub.js.map +1 -1
  30. package/dist/devices-hap/humidifier.d.ts +5 -0
  31. package/dist/devices-hap/humidifier.d.ts.map +1 -1
  32. package/dist/devices-hap/humidifier.js +92 -4
  33. package/dist/devices-hap/humidifier.js.map +1 -1
  34. package/dist/devices-hap/iosensor.d.ts.map +1 -1
  35. package/dist/devices-hap/iosensor.js +36 -21
  36. package/dist/devices-hap/iosensor.js.map +1 -1
  37. package/dist/devices-hap/lightstrip.d.ts.map +1 -1
  38. package/dist/devices-hap/lightstrip.js +38 -8
  39. package/dist/devices-hap/lightstrip.js.map +1 -1
  40. package/dist/devices-hap/lock.d.ts.map +1 -1
  41. package/dist/devices-hap/lock.js +14 -6
  42. package/dist/devices-hap/lock.js.map +1 -1
  43. package/dist/devices-hap/meter.d.ts.map +1 -1
  44. package/dist/devices-hap/meter.js +6 -5
  45. package/dist/devices-hap/meter.js.map +1 -1
  46. package/dist/devices-hap/meterplus.d.ts.map +1 -1
  47. package/dist/devices-hap/meterplus.js +6 -5
  48. package/dist/devices-hap/meterplus.js.map +1 -1
  49. package/dist/devices-hap/meterpro.d.ts.map +1 -1
  50. package/dist/devices-hap/meterpro.js +7 -6
  51. package/dist/devices-hap/meterpro.js.map +1 -1
  52. package/dist/devices-hap/motion.js +3 -3
  53. package/dist/devices-hap/plug.d.ts.map +1 -1
  54. package/dist/devices-hap/plug.js +11 -6
  55. package/dist/devices-hap/plug.js.map +1 -1
  56. package/dist/devices-hap/relayswitch.js +3 -3
  57. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +1 -1
  58. package/dist/devices-hap/robotvacuumcleaner.js +13 -6
  59. package/dist/devices-hap/robotvacuumcleaner.js.map +1 -1
  60. package/dist/devices-hap/waterdetector.js +3 -3
  61. package/dist/devices-matter/BaseMatterAccessory.d.ts +27 -0
  62. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +1 -1
  63. package/dist/devices-matter/BaseMatterAccessory.js +169 -5
  64. package/dist/devices-matter/BaseMatterAccessory.js.map +1 -1
  65. package/dist/devices-matter/ColorLightAccessory.d.ts.map +1 -1
  66. package/dist/devices-matter/ColorLightAccessory.js +12 -12
  67. package/dist/devices-matter/ColorLightAccessory.js.map +1 -1
  68. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +1 -1
  69. package/dist/devices-matter/ColorTemperatureLightAccessory.js +5 -7
  70. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +1 -1
  71. package/dist/devices-matter/DimmableLightAccessory.js +9 -9
  72. package/dist/devices-matter/DimmableLightAccessory.js.map +1 -1
  73. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +1 -1
  74. package/dist/devices-matter/ExtendedColorLightAccessory.js +14 -15
  75. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +1 -1
  76. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +1 -1
  77. package/dist/devices-matter/OnOffLightAccessory.js +8 -16
  78. package/dist/devices-matter/OnOffLightAccessory.js.map +1 -1
  79. package/dist/devices-matter/OnOffOutletAccessory.d.ts +2 -0
  80. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +1 -1
  81. package/dist/devices-matter/OnOffOutletAccessory.js +10 -7
  82. package/dist/devices-matter/OnOffOutletAccessory.js.map +1 -1
  83. package/dist/devices-matter/OnOffSwitchAccessory.js +2 -2
  84. package/dist/devices-matter/OnOffSwitchAccessory.js.map +1 -1
  85. package/dist/devices-matter/RoboticVacuumAccessory.d.ts +29 -43
  86. package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +1 -1
  87. package/dist/devices-matter/RoboticVacuumAccessory.js +287 -262
  88. package/dist/devices-matter/RoboticVacuumAccessory.js.map +1 -1
  89. package/dist/homebridge-ui/public/index.html +200 -18
  90. package/dist/homebridge-ui/server.js +0 -31
  91. package/dist/homebridge-ui/server.js.map +1 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +4 -7
  94. package/dist/index.js.map +1 -1
  95. package/dist/irdevice/irdevice.d.ts +11 -10
  96. package/dist/irdevice/irdevice.d.ts.map +1 -1
  97. package/dist/irdevice/irdevice.js +76 -35
  98. package/dist/irdevice/irdevice.js.map +1 -1
  99. package/dist/platform-hap.d.ts +26 -15
  100. package/dist/platform-hap.d.ts.map +1 -1
  101. package/dist/platform-hap.js +333 -153
  102. package/dist/platform-hap.js.map +1 -1
  103. package/dist/platform-matter.d.ts +93 -6
  104. package/dist/platform-matter.d.ts.map +1 -1
  105. package/dist/platform-matter.js +1822 -224
  106. package/dist/platform-matter.js.map +1 -1
  107. package/dist/settings.d.ts +58 -7
  108. package/dist/settings.d.ts.map +1 -1
  109. package/dist/settings.js.map +1 -1
  110. package/dist/test/apiRequestTracker.test.d.ts +2 -0
  111. package/dist/test/apiRequestTracker.test.d.ts.map +1 -0
  112. package/dist/test/apiRequestTracker.test.js +392 -0
  113. package/dist/test/apiRequestTracker.test.js.map +1 -0
  114. package/dist/test/hap/device-webhook-context.test.d.ts +2 -0
  115. package/dist/test/hap/device-webhook-context.test.d.ts.map +1 -0
  116. package/dist/test/hap/device-webhook-context.test.js +128 -0
  117. package/dist/test/hap/device-webhook-context.test.js.map +1 -0
  118. package/dist/test/hap/platform-hap.logging.test.d.ts +2 -0
  119. package/dist/test/hap/platform-hap.logging.test.d.ts.map +1 -0
  120. package/dist/test/hap/platform-hap.logging.test.js +33 -0
  121. package/dist/test/hap/platform-hap.logging.test.js.map +1 -0
  122. package/dist/test/hap/platform-hap.test.d.ts +2 -0
  123. package/dist/test/hap/platform-hap.test.d.ts.map +1 -0
  124. package/dist/test/hap/platform-hap.test.js +62 -0
  125. package/dist/test/hap/platform-hap.test.js.map +1 -0
  126. package/dist/test/helpers/platform-fixtures.d.ts +9 -0
  127. package/dist/test/helpers/platform-fixtures.d.ts.map +1 -0
  128. package/dist/test/helpers/platform-fixtures.js +30 -0
  129. package/dist/test/helpers/platform-fixtures.js.map +1 -0
  130. package/dist/test/homebridge-ui/server.test.d.ts +2 -0
  131. package/dist/test/homebridge-ui/server.test.d.ts.map +1 -0
  132. package/dist/test/homebridge-ui/server.test.js +445 -0
  133. package/dist/test/homebridge-ui/server.test.js.map +1 -0
  134. package/dist/{index.test.d.ts.map → test/index.test.d.ts.map} +1 -1
  135. package/dist/test/index.test.js +19 -0
  136. package/dist/test/index.test.js.map +1 -0
  137. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts +2 -0
  138. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts.map +1 -0
  139. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js +71 -0
  140. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js.map +1 -0
  141. package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.d.ts +2 -0
  142. package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.d.ts.map +1 -0
  143. package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.js +366 -0
  144. package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.js.map +1 -0
  145. package/dist/test/matter/platform-matter.additional.test.d.ts +2 -0
  146. package/dist/test/matter/platform-matter.additional.test.d.ts.map +1 -0
  147. package/dist/test/matter/platform-matter.additional.test.js +35 -0
  148. package/dist/test/matter/platform-matter.additional.test.js.map +1 -0
  149. package/dist/test/matter/platform-matter.bleparse.test.d.ts +2 -0
  150. package/dist/test/matter/platform-matter.bleparse.test.d.ts.map +1 -0
  151. package/dist/test/matter/platform-matter.bleparse.test.js +43 -0
  152. package/dist/test/matter/platform-matter.bleparse.test.js.map +1 -0
  153. package/dist/test/matter/platform-matter.cleanup.test.d.ts +2 -0
  154. package/dist/test/matter/platform-matter.cleanup.test.d.ts.map +1 -0
  155. package/dist/test/matter/platform-matter.cleanup.test.js +70 -0
  156. package/dist/test/matter/platform-matter.cleanup.test.js.map +1 -0
  157. package/dist/test/matter/platform-matter.keepstale.test.d.ts +2 -0
  158. package/dist/test/matter/platform-matter.keepstale.test.d.ts.map +1 -0
  159. package/dist/test/matter/platform-matter.keepstale.test.js +27 -0
  160. package/dist/test/matter/platform-matter.keepstale.test.js.map +1 -0
  161. package/dist/test/matter/platform-matter.logging.test.d.ts +2 -0
  162. package/dist/test/matter/platform-matter.logging.test.d.ts.map +1 -0
  163. package/dist/test/matter/platform-matter.logging.test.js +29 -0
  164. package/dist/test/matter/platform-matter.logging.test.js.map +1 -0
  165. package/dist/test/matter/platform-matter.mapping.test.d.ts +2 -0
  166. package/dist/test/matter/platform-matter.mapping.test.d.ts.map +1 -0
  167. package/dist/test/matter/platform-matter.mapping.test.js +43 -0
  168. package/dist/test/matter/platform-matter.mapping.test.js.map +1 -0
  169. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts +2 -0
  170. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts.map +1 -0
  171. package/dist/test/matter/platform-matter.openapi-mapping.test.js +84 -0
  172. package/dist/test/matter/platform-matter.openapi-mapping.test.js.map +1 -0
  173. package/dist/test/matter/platform-matter.test.d.ts +2 -0
  174. package/dist/test/matter/platform-matter.test.d.ts.map +1 -0
  175. package/dist/test/matter/platform-matter.test.js +117 -0
  176. package/dist/test/matter/platform-matter.test.js.map +1 -0
  177. package/dist/test/matter/platform-matter.unregister.test.d.ts +2 -0
  178. package/dist/test/matter/platform-matter.unregister.test.d.ts.map +1 -0
  179. package/dist/test/matter/platform-matter.unregister.test.js +30 -0
  180. package/dist/test/matter/platform-matter.unregister.test.js.map +1 -0
  181. package/dist/test/matter/platform-matter.webhook.test.d.ts +2 -0
  182. package/dist/test/matter/platform-matter.webhook.test.d.ts.map +1 -0
  183. package/dist/test/matter/platform-matter.webhook.test.js +46 -0
  184. package/dist/test/matter/platform-matter.webhook.test.js.map +1 -0
  185. package/dist/test/utils.test.d.ts +2 -0
  186. package/dist/test/utils.test.d.ts.map +1 -0
  187. package/dist/test/utils.test.js +95 -0
  188. package/dist/test/utils.test.js.map +1 -0
  189. package/dist/test/verifyconfig.test.d.ts.map +1 -0
  190. package/dist/{verifyconfig.test.js → test/verifyconfig.test.js} +2 -2
  191. package/dist/test/verifyconfig.test.js.map +1 -0
  192. package/dist/utils.d.ts +204 -3
  193. package/dist/utils.d.ts.map +1 -1
  194. package/dist/utils.js +713 -33
  195. package/dist/utils.js.map +1 -1
  196. package/docs/assets/highlight.css +14 -0
  197. package/docs/assets/main.js +2 -2
  198. package/docs/index.html +31 -2
  199. package/docs/variables/default.html +1 -1
  200. package/package.json +15 -15
  201. package/src/devices-hap/airpurifier.ts +11 -6
  202. package/src/devices-hap/blindtilt.ts +3 -3
  203. package/src/devices-hap/bot.ts +15 -5
  204. package/src/devices-hap/ceilinglight.ts +12 -7
  205. package/src/devices-hap/colorbulb.ts +46 -10
  206. package/src/devices-hap/contact.ts +3 -3
  207. package/src/devices-hap/curtain.ts +2 -2
  208. package/src/devices-hap/device.ts +149 -70
  209. package/src/devices-hap/fan.ts +11 -6
  210. package/src/devices-hap/hub.ts +6 -5
  211. package/src/devices-hap/humidifier.ts +97 -4
  212. package/src/devices-hap/iosensor.ts +36 -21
  213. package/src/devices-hap/lightstrip.ts +35 -8
  214. package/src/devices-hap/lock.ts +13 -6
  215. package/src/devices-hap/meter.ts +6 -5
  216. package/src/devices-hap/meterplus.ts +6 -5
  217. package/src/devices-hap/meterpro.ts +7 -6
  218. package/src/devices-hap/motion.ts +3 -3
  219. package/src/devices-hap/plug.ts +10 -6
  220. package/src/devices-hap/relayswitch.ts +3 -3
  221. package/src/devices-hap/robotvacuumcleaner.ts +12 -6
  222. package/src/devices-hap/waterdetector.ts +3 -3
  223. package/src/devices-matter/BaseMatterAccessory.ts +176 -5
  224. package/src/devices-matter/ColorLightAccessory.ts +12 -12
  225. package/src/devices-matter/ColorTemperatureLightAccessory.ts +5 -7
  226. package/src/devices-matter/DimmableLightAccessory.ts +9 -9
  227. package/src/devices-matter/ExtendedColorLightAccessory.ts +14 -15
  228. package/src/devices-matter/OnOffLightAccessory.ts +8 -16
  229. package/src/devices-matter/OnOffOutletAccessory.ts +12 -7
  230. package/src/devices-matter/OnOffSwitchAccessory.ts +2 -2
  231. package/src/devices-matter/RoboticVacuumAccessory.ts +340 -313
  232. package/src/homebridge-ui/public/index.html +200 -18
  233. package/src/homebridge-ui/server.ts +0 -34
  234. package/src/index.ts +4 -7
  235. package/src/irdevice/irdevice.ts +74 -35
  236. package/src/platform-hap.ts +365 -169
  237. package/src/platform-matter.ts +1872 -229
  238. package/src/settings.ts +62 -3
  239. package/src/test/apiRequestTracker.test.ts +417 -0
  240. package/src/test/hap/device-webhook-context.test.ts +136 -0
  241. package/src/test/hap/platform-hap.logging.test.ts +36 -0
  242. package/src/test/hap/platform-hap.test.ts +70 -0
  243. package/src/test/helpers/platform-fixtures.ts +33 -0
  244. package/src/test/homebridge-ui/server.test.ts +486 -0
  245. package/src/test/index.test.ts +24 -0
  246. package/src/test/matter/devices-matter/baseMatterAccessory.test.ts +88 -0
  247. package/src/test/matter/devices-matter/roboticVacuumAccessory.test.ts +453 -0
  248. package/src/test/matter/platform-matter.additional.test.ts +44 -0
  249. package/src/test/matter/platform-matter.bleparse.test.ts +47 -0
  250. package/src/test/matter/platform-matter.cleanup.test.ts +86 -0
  251. package/src/test/matter/platform-matter.keepstale.test.ts +37 -0
  252. package/src/test/matter/platform-matter.logging.test.ts +33 -0
  253. package/src/test/matter/platform-matter.mapping.test.ts +57 -0
  254. package/src/test/matter/platform-matter.openapi-mapping.test.ts +109 -0
  255. package/src/test/matter/platform-matter.test.ts +144 -0
  256. package/src/test/matter/platform-matter.unregister.test.ts +39 -0
  257. package/src/test/matter/platform-matter.webhook.test.ts +54 -0
  258. package/src/test/utils.test.ts +96 -0
  259. package/src/{verifyconfig.test.ts → test/verifyconfig.test.ts} +12 -11
  260. package/src/utils.ts +777 -36
  261. package/dist/index.test.js +0 -14
  262. package/dist/index.test.js.map +0 -1
  263. package/dist/verifyconfig.test.d.ts.map +0 -1
  264. package/dist/verifyconfig.test.js.map +0 -1
  265. package/src/index.test.ts +0 -19
  266. /package/dist/{index.test.d.ts → test/index.test.d.ts} +0 -0
  267. /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,19 +233,39 @@ 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
+ if (this.switchBotAPI) {
239
+ try {
240
+ const dailyApiLimit = this.config.options?.dailyApiLimit ?? 10000
241
+ const dailyApiReserveForCommands = this.config.options?.dailyApiReserveForCommands ?? 1000
242
+ const webhookOnlyOnReserve = this.config.options?.webhookOnlyOnReserve ?? false
243
+ this.apiTracker = new ApiRequestTracker(this.api, this.log, 'SwitchBot HAP', {
244
+ dailyLimit: dailyApiLimit,
245
+ reserveForCommands: dailyApiReserveForCommands,
246
+ pausePollingAtReserve: webhookOnlyOnReserve,
247
+ resetAtLocalMidnight: this.config.options?.dailyApiResetAtLocalMidnight ?? false,
248
+ })
249
+ this.apiTracker.startHourlyLogging()
250
+ this.debugLog('API request tracking initialized for OpenAPI usage')
251
+ } catch (e: any) {
252
+ this.errorLog(`Failed to initialize API request tracking: ${e.message ?? e}`)
253
+ }
254
+ }
255
+
206
256
  // run the method to discover / register your devices as accessories
207
257
  try {
208
258
  // Does the user have a version of Homebridge that is compatible with matter?
209
259
  if (!this.api.isMatterAvailable?.()) {
210
- this.log.debug(`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)'}`)
260
+ 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)'}`)
211
261
  }
212
262
  if (!this.api.isMatterEnabled?.()) {
213
- this.log.debug(`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)'}`)
263
+ 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)'}`)
214
264
  }
215
265
  if (!this.api.isMatterAvailable?.() && !this.api.isMatterEnabled?.()) {
216
266
  await this.discoverDevices()
217
267
  } else {
218
- this.log.info('Matter is enabled in Homebridge. SwitchBot Matter devices will be handled by the Matter platform.')
268
+ this.infoLog('Matter is enabled in Homebridge. SwitchBot Matter devices will be handled by the Matter platform.')
219
269
  }
220
270
  } catch (e: any) {
221
271
  this.errorLog(`Failed to Discover, Error Message: ${e.message ?? e}, Submit Bugs Here: ` + 'https://tinyurl.com/SwitchBotBug')
@@ -444,6 +494,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
444
494
  let retryCount = 0
445
495
  const maxRetries = this.platformMaxRetries ?? 5
446
496
  const delayBetweenRetries = this.platformDelayBetweenRetries || 5000
497
+ let rateLimitExceeded = false
447
498
 
448
499
  this.debugWarnLog(`Retry Count: ${retryCount}`)
449
500
  this.debugWarnLog(`Max Retries: ${this.platformMaxRetries}`)
@@ -451,13 +502,29 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
451
502
 
452
503
  while (retryCount < maxRetries) {
453
504
  try {
505
+ if (!this.apiTracker?.trySpend('discovery')) {
506
+ rateLimitExceeded = true
507
+ this.warnLog('OpenAPI daily budget reached (discovery blocked). Falling back to manual device configuration.')
508
+ break
509
+ }
454
510
  const { response, statusCode } = await this.switchBotAPI.getDevices()
455
511
  this.debugLog(`response: ${JSON.stringify(response)}`)
456
512
  if (this.isSuccessfulResponse(statusCode)) {
457
- await this.handleDevices(Array.isArray(response.body.deviceList) ? response.body.deviceList : [])
458
- await this.handleIRDevices(Array.isArray(response.body.infraredRemoteList) ? response.body.infraredRemoteList : [])
513
+ const deviceList = Array.isArray(response.body.deviceList) ? response.body.deviceList : []
514
+ const irDeviceList = Array.isArray(response.body.infraredRemoteList) ? response.body.infraredRemoteList : []
515
+ await this.handleDevices(deviceList)
516
+ await this.handleIRDevices(irDeviceList)
517
+ // Diagnostic: warn users if their device count + refresh rate may exceed daily limits
518
+ this.validateApiUsageConfig(deviceList.length, irDeviceList.length)
459
519
  break
460
520
  } else {
521
+ // Check if rate limit exceeded (429)
522
+ if (statusCode === 429) {
523
+ rateLimitExceeded = true
524
+ this.warnLog('OpenAPI rate limit (429) exceeded. Falling back to manual device configuration.')
525
+ this.warnLog('Webhook functionality will still work if devices are configured manually.')
526
+ break
527
+ }
461
528
  await this.handleErrorResponse(statusCode, retryCount, maxRetries, delayBetweenRetries)
462
529
  retryCount++
463
530
  }
@@ -467,9 +534,117 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
467
534
  this.debugErrorLog(`Failed to Discover Devices, Error: ${e.message ?? e}`)
468
535
  }
469
536
  }
537
+
538
+ // If rate limit exceeded or retries exhausted, try to load from manual config or cached accessories
539
+ if (rateLimitExceeded || retryCount >= maxRetries) {
540
+ const hasCachedAccessories = this.accessories.length > 0
541
+ const hasManualConfig = this.config.options?.devices || this.config.options?.irdevices
542
+
543
+ if (hasManualConfig || hasCachedAccessories) {
544
+ if (hasCachedAccessories) {
545
+ this.warnLog(`Found ${this.accessories.length} cached accessories from previous sessions.`)
546
+ this.warnLog('Reinstantiating device classes to enable webhook handlers...')
547
+ await this.restoreCachedAccessories()
548
+ }
549
+ if (hasManualConfig) {
550
+ this.warnLog('Attempting to load devices from manual configuration...')
551
+ await this.handleManualConfig()
552
+ }
553
+ if (hasCachedAccessories) {
554
+ this.infoLog('Cached accessories restored. Webhook functionality is active.')
555
+ this.infoLog('Device discovery will resume when API rate limit resets.')
556
+ }
557
+ } else {
558
+ this.errorLog('OpenAPI unavailable and no cached accessories or manual device configuration found.')
559
+ this.errorLog('Please configure devices manually in the plugin settings to use webhook functionality.')
560
+ }
561
+ }
562
+ }
563
+
564
+ /**
565
+ * Restore cached accessories by reinstantiating their device classes
566
+ * This ensures webhook handlers and other functionality are properly set up
567
+ */
568
+ private async restoreCachedAccessories() {
569
+ this.debugLog('Restoring cached accessories and setting up webhook handlers...')
570
+
571
+ for (const accessory of this.accessories) {
572
+ try {
573
+ const device = accessory.context.device
574
+ const deviceType = accessory.context.deviceType || device?.deviceType
575
+ const deviceId = accessory.context.deviceId || device?.deviceId
576
+
577
+ if (!device || !deviceType || !deviceId) {
578
+ this.debugWarnLog(`Skipping cached accessory ${accessory.displayName} - missing context data`)
579
+ continue
580
+ }
581
+
582
+ this.debugLog(`Reinstantiating ${deviceType} for cached accessory: ${accessory.displayName}`)
583
+
584
+ // Reinstantiate the device class based on deviceType
585
+ const deviceTypeHandlers: { [key: string]: new (platform: any, accessory: PlatformAccessory, device: any) => any } = {
586
+ 'Humidifier': Humidifier,
587
+ 'Humidifier2': Humidifier,
588
+ 'Hub 2': Hub,
589
+ 'Hub 3': Hub,
590
+ 'Bot': Bot,
591
+ 'Relay Switch 1': RelaySwitch,
592
+ 'Relay Switch 1PM': RelaySwitch,
593
+ 'Meter': Meter,
594
+ 'MeterPlus': MeterPlus,
595
+ 'Meter Plus (JP)': MeterPlus,
596
+ 'MeterPro': MeterPro,
597
+ 'MeterPro(CO2)': MeterPro,
598
+ 'WoIOSensor': IOSensor,
599
+ 'Water Detector': WaterDetector,
600
+ 'Motion Sensor': Motion,
601
+ 'Contact Sensor': Contact,
602
+ 'Curtain': Curtain,
603
+ 'Curtain3': Curtain,
604
+ 'WoRollerShade': Curtain,
605
+ 'Roller Shade': Curtain,
606
+ 'Blind Tilt': BlindTilt,
607
+ 'Plug': Plug,
608
+ 'Plug Mini (US)': Plug,
609
+ 'Plug Mini (JP)': Plug,
610
+ 'Smart Lock': Lock,
611
+ 'Smart Lock Pro': Lock,
612
+ 'Smart Lock Ultra': Lock,
613
+ 'Color Bulb': ColorBulb,
614
+ 'K10+': RobotVacuumCleaner,
615
+ 'K10+ Pro': RobotVacuumCleaner,
616
+ 'WoSweeper': RobotVacuumCleaner,
617
+ 'WoSweeperMini': RobotVacuumCleaner,
618
+ 'Robot Vacuum Cleaner S1': RobotVacuumCleaner,
619
+ 'Robot Vacuum Cleaner S1 Plus': RobotVacuumCleaner,
620
+ 'Robot Vacuum Cleaner S10': RobotVacuumCleaner,
621
+ 'Ceiling Light': CeilingLight,
622
+ 'Ceiling Light Pro': CeilingLight,
623
+ 'Strip Light': StripLight,
624
+ 'Battery Circulator Fan': Fan,
625
+ 'Air Purifier PM2.5': AirPurifier,
626
+ 'Air Purifier Table PM2.5': AirPurifier,
627
+ 'Air Purifier VOC': AirPurifier,
628
+ 'Air Purifier Table VOC': AirPurifier,
629
+ }
630
+
631
+ const DeviceClass = deviceTypeHandlers[deviceType]
632
+ if (DeviceClass) {
633
+ new DeviceClass(this, accessory, device)
634
+ this.debugSuccessLog(`Successfully restored ${deviceType}: ${accessory.displayName}`)
635
+ } else {
636
+ this.debugLog(`No handler for device type: ${deviceType}`)
637
+ }
638
+ } catch (e: any) {
639
+ this.errorLog(`Failed to restore cached accessory ${accessory.displayName}, Error: ${e.message ?? e}`)
640
+ }
641
+ }
642
+
643
+ this.infoLog(`Restored ${this.accessories.length} cached accessories with webhook support`)
470
644
  }
471
645
 
472
646
  private async handleManualConfig() {
647
+ // Handle regular devices
473
648
  if (this.config.options?.devices) {
474
649
  this.debugLog(`SwitchBot Device Manual Config Set: ${JSON.stringify(this.config.options?.devices)}`)
475
650
  const devices = this.config.options.devices.map((v: any) => v)
@@ -486,17 +661,41 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
486
661
  this.errorLog(`failed to format device ID as MAC, Error: ${error}`)
487
662
  }
488
663
  }
489
- } else {
664
+ }
665
+
666
+ // Handle IR devices
667
+ if (this.config.options?.irdevices) {
668
+ this.debugLog(`SwitchBot IR Device Manual Config Set: ${JSON.stringify(this.config.options?.irdevices)}`)
669
+ const irdevices = this.config.options.irdevices.map((v: any) => v)
670
+ for (const irdevice of irdevices) {
671
+ irdevice.remoteType = irdevice.configRemoteType !== undefined ? irdevice.configRemoteType : 'Unknown'
672
+ irdevice.deviceName = irdevice.configDeviceName !== undefined ? irdevice.configDeviceName : 'Unknown'
673
+ try {
674
+ this.debugLog(`IR deviceId: ${irdevice.deviceId}`)
675
+ if (irdevice.remoteType) {
676
+ await this.createIRDevice(irdevice)
677
+ }
678
+ } catch (error) {
679
+ this.errorLog(`failed to create IR device, Error: ${error}`)
680
+ }
681
+ }
682
+ }
683
+
684
+ if (!this.config.options?.devices && !this.config.options?.irdevices) {
490
685
  this.errorLog('Neither SwitchBot Token or Device Config are set.')
491
686
  }
492
687
  }
493
688
 
689
+ /**
690
+ * Check if an API status code indicates success
691
+ * @deprecated Use shared isSuccessfulStatusCode from utils.js instead
692
+ */
494
693
  private isSuccessfulResponse(apiStatusCode: number): boolean {
495
- return (apiStatusCode === 200 || apiStatusCode === 100)
694
+ return isSuccessfulStatusCode(apiStatusCode)
496
695
  }
497
696
 
498
697
  private async handleDevices(deviceLists: any[]) {
499
- if (!this.config.options?.devices && !this.config.options?.deviceConfig) {
698
+ if (!this.config.options?.devices) {
500
699
  this.debugLog(`SwitchBot Device Config Not Set: ${JSON.stringify(this.config.options?.devices)}`)
501
700
  if (deviceLists.length === 0) {
502
701
  this.debugLog('SwitchBot API Has No Devices With Cloud Services Enabled')
@@ -510,92 +709,118 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
510
709
  }
511
710
  }
512
711
  }
513
- } else if (this.config.options?.devices || this.config.options?.deviceConfig) {
712
+ } else {
514
713
  this.debugLog(`SwitchBot Device Config Set: ${JSON.stringify(this.config.options?.devices)}`)
515
714
 
516
- // Step 1: Check and assign configDeviceType to deviceType if deviceType is not present
517
- const devicesWithTypeConfigPromises = deviceLists.map(async (device) => {
715
+ // Check and assign configDeviceType to deviceType if deviceType is not present
716
+ const devicesWithTypeAssigned = deviceLists.map((device) => {
518
717
  if (!device.deviceType) {
519
718
  device.deviceType = device.configDeviceType !== undefined ? device.configDeviceType : 'Unknown'
520
719
  this.warnLog(`API is displaying no deviceType: ${device.deviceType}, So using configDeviceType: ${device.configDeviceType}`)
521
720
  }
522
-
523
- // Retrieve deviceTypeConfig for each device and merge it
524
- const deviceTypeConfig = this.config.options?.deviceConfig?.[device.deviceType] || {}
525
- return Object.assign({}, device, deviceTypeConfig)
721
+ return device
526
722
  })
527
723
 
528
- // Wait for all promises to resolve
529
- const devicesWithTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices
530
-
531
- const devices = this.mergeByDeviceId(this.config.options.devices ?? [], devicesWithTypeConfig ?? [])
724
+ // Apply device-type templates from config entries with applyToAllDevicesOfType=true
725
+ const devicesWithTemplates = applyDeviceTypeTemplates(
726
+ devicesWithTypeAssigned,
727
+ this.config.options.devices,
728
+ 'deviceType',
729
+ msg => this.debugLog(msg),
730
+ )
731
+
732
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
733
+ const devices = mergeByDeviceId(this.config.options.devices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
734
+
735
+ // Apply global webhook option to devices that don't have their own webhook setting
736
+ if (this.config.options?.webhook === true) {
737
+ for (const device of devices) {
738
+ if (device.webhook === undefined) {
739
+ device.webhook = true
740
+ this.debugLog(`Applying global webhook option to device: ${device.deviceName ?? device.deviceId}`)
741
+ }
742
+ }
743
+ }
532
744
 
533
745
  this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`)
534
746
 
535
747
  for (const device of devices) {
536
- const deviceIdConfig = this.config.options?.devices?.[device.deviceId] || {}
537
- const deviceWithConfig = Object.assign({}, device, deviceIdConfig)
538
-
539
748
  if (device.configDeviceName) {
540
749
  device.deviceName = device.configDeviceName
541
750
  }
542
- // Pass the merged device object to createDevice
543
- await this.createDevice(deviceWithConfig)
751
+ // Log effective webhook setting for diagnostics
752
+ try {
753
+ const effectiveWebhook = device.webhook !== undefined ? device.webhook : (this.config.options?.webhook === true ? true : undefined)
754
+ this.debugLog(`Effective webhook for device ${device.deviceName ?? device.deviceId}: ${String(effectiveWebhook)}`)
755
+ } catch (e: any) {
756
+ this.debugLog(`Failed logging effective webhook for ${device.deviceName ?? device.deviceId}: ${e?.message ?? e}`)
757
+ }
758
+ await this.createDevice(device)
544
759
  }
545
760
  }
546
761
  }
547
762
 
548
763
  private async handleIRDevices(irDeviceLists: any[]) {
549
- if (!this.config.options?.irdevices && !this.config.options?.irdeviceConfig) {
764
+ if (!this.config.options?.irdevices) {
550
765
  this.debugLog(`IR Device Config Not Set: ${JSON.stringify(this.config.options?.irdevices)}`)
551
766
  for (const device of irDeviceLists) {
552
767
  if (device.remoteType) {
553
768
  await this.createIRDevice(device)
554
769
  }
555
770
  }
556
- } else if (this.config.options?.irdevices || this.config.options?.irdeviceConfig) {
771
+ } else {
557
772
  this.debugLog(`IR Device Config Set: ${JSON.stringify(this.config.options?.irdevices)}`)
558
773
 
559
- // Step 1: Check and assign configRemoteType to remoteType if remoteType is not present
560
- const devicesWithTypeConfigPromises = irDeviceLists.map(async (device) => {
774
+ // Check and assign configRemoteType to remoteType if remoteType is not present
775
+ const devicesWithTypeAssigned = irDeviceLists.map((device) => {
561
776
  if (!device.remoteType && device.configRemoteType) {
562
777
  device.remoteType = device.configRemoteType
563
778
  this.warnLog(`API is displaying no remoteType: ${device.remoteType}, So using configRemoteType: ${device.configRemoteType}`)
564
779
  } else if (!device.remoteType && !device.configDeviceName) {
565
780
  this.errorLog('No remoteType or configRemoteType for device. No device will be created.')
566
- return null // Skip this device
781
+ return null
567
782
  }
568
-
569
- // Retrieve remoteTypeConfig for each device and merge it
570
- const remoteTypeConfig = this.config.options?.irdeviceConfig?.[device.remoteType] || {}
571
- return Object.assign({}, device, remoteTypeConfig)
572
- })
573
- // Wait for all promises to resolve
574
- const devicesWithRemoteTypeConfig = (await Promise.all(devicesWithTypeConfigPromises)).filter(device => device !== null) // Filter out skipped devices
575
-
576
- const devices = this.mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithRemoteTypeConfig ?? [])
783
+ return device
784
+ }).filter(device => device !== null) // Filter out skipped devices
785
+
786
+ // Apply remote-type templates from config entries with applyToAllDevicesOfType=true
787
+ const devicesWithTemplates = applyDeviceTypeTemplates(
788
+ devicesWithTypeAssigned,
789
+ this.config.options.irdevices,
790
+ 'remoteType',
791
+ msg => this.debugLog(msg),
792
+ )
793
+
794
+ const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
795
+ const devices = mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
796
+
797
+ // Apply global webhook option to IR devices that don't have their own webhook setting
798
+ if (this.config.options?.webhook === true) {
799
+ for (const device of devices) {
800
+ if (device.webhook === undefined) {
801
+ device.webhook = true
802
+ this.debugLog(`Applying global webhook option to IR device: ${device.deviceName ?? device.deviceId}`)
803
+ }
804
+ }
805
+ }
577
806
 
578
807
  this.debugLog(`IR Devices: ${JSON.stringify(devices)}`)
579
808
  for (const device of devices) {
580
- const irdeviceIdConfig = this.config.options?.irdevices?.[device.deviceId] || {}
581
- const irdeviceWithConfig = Object.assign({}, device, irdeviceIdConfig)
582
-
583
809
  if (device.configDeviceName) {
584
810
  device.deviceName = device.configDeviceName
585
811
  }
586
- await this.createIRDevice(irdeviceWithConfig)
812
+ // Log effective webhook setting for diagnostics (IR devices)
813
+ try {
814
+ const effectiveWebhook = device.webhook !== undefined ? device.webhook : (this.config.options?.webhook === true ? true : undefined)
815
+ this.debugLog(`Effective webhook for IR device ${device.deviceName ?? device.deviceId}: ${String(effectiveWebhook)}`)
816
+ } catch (e: any) {
817
+ this.debugLog(`Failed logging effective webhook for IR ${device.deviceName ?? device.deviceId}: ${e?.message ?? e}`)
818
+ }
819
+ await this.createIRDevice(device)
587
820
  }
588
821
  }
589
822
  }
590
823
 
591
- private mergeByDeviceId(a1: { deviceId: string }[], a2: any[]) {
592
- const normalizeDeviceId = (deviceId: string) => deviceId.toUpperCase().replace(/[^A-Z0-9]+/g, '')
593
- return a1.map((itm) => {
594
- const matchingItem = a2.find(item => normalizeDeviceId(item.deviceId) === normalizeDeviceId(itm.deviceId))
595
- return { ...matchingItem, ...itm }
596
- })
597
- }
598
-
599
824
  private async handleErrorResponse(apiStatusCode: number, retryCount: number, maxRetries: number, delayBetweenRetries: number) {
600
825
  await this.statusCode(apiStatusCode)
601
826
  if (apiStatusCode === 500) {
@@ -632,6 +857,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
632
857
  'Plug Mini (JP)': this.createPlug.bind(this),
633
858
  'Smart Lock': this.createLock.bind(this),
634
859
  'Smart Lock Pro': this.createLock.bind(this),
860
+ 'Smart Lock Ultra': this.createLock.bind(this),
635
861
  'Color Bulb': this.createColorBulb.bind(this),
636
862
  'K10+': this.createRobotVacuumCleaner.bind(this),
637
863
  'K10+ Pro': this.createRobotVacuumCleaner.bind(this),
@@ -1627,7 +1853,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
1627
1853
  existingAccessory.context.device = device
1628
1854
  existingAccessory.context.deviceId = device.deviceId
1629
1855
  existingAccessory.context.deviceType = device.deviceType
1630
- existingAccessory.context.model = device.deviceType === 'Smart Lock Pro' ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1856
+ existingAccessory.context.model = (device.deviceType === 'Smart Lock Pro' || device.deviceType === 'Smart Lock Ultra') ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1631
1857
  existingAccessory.displayName = device.configDeviceName
1632
1858
  ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName)
1633
1859
  : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName)
@@ -1653,7 +1879,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
1653
1879
  accessory.context.device = device
1654
1880
  accessory.context.deviceId = device.deviceId
1655
1881
  accessory.context.deviceType = device.deviceType
1656
- accessory.context.model = device.deviceType === 'Smart Lock Pro' ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1882
+ accessory.context.model = (device.deviceType === 'Smart Lock Pro' || device.deviceType === 'Smart Lock Ultra') ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1657
1883
  accessory.displayName = device.configDeviceName
1658
1884
  ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName)
1659
1885
  : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName)
@@ -2720,44 +2946,31 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2720
2946
  *
2721
2947
  * @param statusCode - The status code returned by the device.
2722
2948
  * @returns A promise that resolves when the logging is complete.
2949
+ * @deprecated Use shared logStatusCode from utils.js instead
2723
2950
  */
2724
2951
  async statusCode(statusCode: number): Promise<void> {
2725
- const messages: { [key: number]: string } = {
2726
- 151: `Command not supported by this device type, statusCode: ${statusCode}, Submit Feature Request Here:
2727
- https://tinyurl.com/SwitchBotFeatureRequest`,
2728
- 152: `Device not found, statusCode: ${statusCode}`,
2729
- 160: `Command is not supported, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`,
2730
- 161: `Device is offline, statusCode: ${statusCode}`,
2731
- 171: `is offline, statusCode: ${statusCode}`,
2732
- 190: `Requests reached the daily limit, statusCode: ${statusCode}`,
2733
- 100: `Command successfully sent, statusCode: ${statusCode}`,
2734
- 200: `Request successful, statusCode: ${statusCode}`,
2735
- 400: `Bad Request, The client has issued an invalid request. This is commonly used to specify validation errors in a request payload,
2736
- statusCode: ${statusCode}`,
2737
- 401: `Unauthorized, Authorization for the API is required, but the request has not been authenticated, statusCode: ${statusCode}`,
2738
- 403: `Forbidden, The request has been authenticated but does not have appropriate permissions, or a requested resource is not found,
2739
- statusCode: ${statusCode}`,
2740
- 404: `Not Found, Specifies the requested path does not exist, statusCode: ${statusCode}`,
2741
- 406: `Not Acceptable, The client has requested a MIME type via the Accept header for a value not supported by the server,
2742
- statusCode: ${statusCode}`,
2743
- 415: `Unsupported Media Type, The client has defined a contentType header that is not supported by the server, statusCode: ${statusCode}`,
2744
- 422: `Unprocessable Entity, The client has made a valid request, but the server cannot process it. This is often used for APIs for which
2745
- certain limits have been exceeded, statusCode: ${statusCode}`,
2746
- 429: `Too Many Requests, The client has exceeded the number of requests allowed for a given time window, statusCode: ${statusCode}`,
2747
- 500: `Internal Server Error, An unexpected error on the SmartThings servers has occurred. These errors should be rare,
2748
- statusCode: ${statusCode}`,
2749
- }
2750
-
2751
- const message = messages[statusCode] ?? `Unknown statusCode, statusCode: ${statusCode}, Submit Bugs Here: https://tinyurl.com/SwitchBotBug`
2752
-
2753
- if ([100, 200].includes(statusCode)) {
2754
- this.debugLog(message)
2755
- } else {
2756
- this.errorLog(message)
2757
- }
2952
+ await logStatusCode(statusCode, {
2953
+ debugLog: this.debugLog.bind(this),
2954
+ errorLog: this.errorLog.bind(this),
2955
+ })
2758
2956
  }
2759
2957
 
2760
2958
  async retryRequest(device: (device & devicesConfig) | (irdevice & irDevicesConfig), deviceMaxRetries: number, deviceDelayBetweenRetries: number): Promise<{ response: any, statusCode: deviceStatusRequest['statusCode'] }> {
2959
+ // Check API budget BEFORE attempting any retries - don't waste cycles on blocked requests
2960
+ if (!this.apiTracker?.trySpend('poll')) {
2961
+ // Don't log on every blocked request - the ApiRequestTracker handles periodic warnings
2962
+ return {
2963
+ response: {
2964
+ deviceId: '',
2965
+ deviceType: '',
2966
+ hubDeviceId: '',
2967
+ version: 0,
2968
+ deviceName: '',
2969
+ },
2970
+ statusCode: 429,
2971
+ }
2972
+ }
2973
+
2761
2974
  let retryCount = 0
2762
2975
  const maxRetries = deviceMaxRetries
2763
2976
  const delayBetweenRetries = deviceDelayBetweenRetries
@@ -2788,6 +3001,9 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2788
3001
  const delayBetweenRetries = deviceDelayBetweenRetries ?? 1000
2789
3002
  while (retryCount < maxRetries) {
2790
3003
  try {
3004
+ if (!this.apiTracker?.trySpend('command')) {
3005
+ return { response: {}, statusCode: 429 }
3006
+ }
2791
3007
  const { response, statusCode } = await this.switchBotAPI.controlDevice(
2792
3008
  device.deviceId,
2793
3009
  bodyChange.command,
@@ -2886,6 +3102,56 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2886
3102
  this.version = version
2887
3103
  }
2888
3104
 
3105
+ /**
3106
+ * Validate that the user's configuration won't exceed API limits
3107
+ * Warn if device count × polling frequency will hit daily limits
3108
+ */
3109
+ private validateApiUsageConfig(deviceCount: number, irDeviceCount: number): void {
3110
+ try {
3111
+ const totalDevices = deviceCount + irDeviceCount
3112
+ if (totalDevices === 0) {
3113
+ return
3114
+ }
3115
+
3116
+ const refreshRate = this.platformRefreshRate ?? 300 // seconds
3117
+ const dailyLimit = this.config.options?.dailyApiLimit ?? 10000
3118
+ const reserveForCommands = this.config.options?.dailyApiReserveForCommands ?? 1000
3119
+
3120
+ // Calculate polls per day (86400 seconds in a day)
3121
+ const pollsPerDevicePerDay = Math.floor(86400 / refreshRate)
3122
+ const totalPollsPerDay = pollsPerDevicePerDay * totalDevices
3123
+
3124
+ // Add discovery calls (typically 1-2 per day)
3125
+ const estimatedDiscoveryCalls = 2
3126
+ const totalEstimatedCalls = totalPollsPerDay + estimatedDiscoveryCalls
3127
+
3128
+ const usableLimit = dailyLimit - reserveForCommands
3129
+ const percentOfLimit = Math.round((totalEstimatedCalls / usableLimit) * 100)
3130
+
3131
+ this.debugLog(`[API Usage Diagnostic] ${totalDevices} devices × ${pollsPerDevicePerDay} polls/day = ${totalPollsPerDay} estimated daily polls`)
3132
+ this.debugLog(`[API Usage Diagnostic] With ${reserveForCommands} reserved for commands, usable limit is ${usableLimit}`)
3133
+
3134
+ if (totalEstimatedCalls > dailyLimit) {
3135
+ this.errorLog(`⚠️ API LIMIT WARNING: Your configuration will exceed the daily API limit!`)
3136
+ this.errorLog(` Devices: ${totalDevices} | Refresh rate: ${refreshRate}s | Estimated daily polls: ${totalEstimatedCalls}`)
3137
+ this.errorLog(` Daily limit: ${dailyLimit} | You will use ${percentOfLimit}% of available budget`)
3138
+ this.errorLog(` SOLUTION: Increase refreshRate to ${Math.ceil((totalDevices * 86400) / usableLimit)} seconds or higher`)
3139
+ this.errorLog(` OR: Enable webhooks and set 'webhookOnlyOnReserve: true' to reduce polling`)
3140
+ } else if (totalEstimatedCalls > usableLimit) {
3141
+ this.warnLog(`⚠️ API USAGE WARNING: Configuration may exceed usable daily API budget`)
3142
+ this.warnLog(` Devices: ${totalDevices} | Refresh rate: ${refreshRate}s | Estimated daily polls: ${totalEstimatedCalls}`)
3143
+ this.warnLog(` Usable limit (after reserve): ${usableLimit} | You will use ${percentOfLimit}% of budget`)
3144
+ this.warnLog(` Polling may pause when approaching limit. Consider increasing refreshRate to ${Math.ceil((totalDevices * 86400) / usableLimit)}s`)
3145
+ } else if (percentOfLimit > 75) {
3146
+ this.infoLog(`[API Usage] Using ${percentOfLimit}% of daily budget (${totalEstimatedCalls}/${usableLimit} calls). Monitor usage if adding more devices.`)
3147
+ } else {
3148
+ this.debugLog(`[API Usage] Configuration looks good: ${percentOfLimit}% of daily budget (${totalEstimatedCalls}/${usableLimit} calls)`)
3149
+ }
3150
+ } catch (e: any) {
3151
+ this.debugErrorLog(`Failed to validate API usage config: ${e.message ?? e}`)
3152
+ }
3153
+ }
3154
+
2889
3155
  /**
2890
3156
  * Validate and clean a string value for a Name Characteristic.
2891
3157
  * @param displayName - The display name of the accessory.
@@ -2924,74 +3190,4 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
2924
3190
  return value
2925
3191
  }
2926
3192
  }
2927
-
2928
- /**
2929
- * If device level logging is turned on, log to log.warn
2930
- * Otherwise send debug logs to log.debug
2931
- */
2932
- async infoLog(...log: any[]): Promise<void> {
2933
- if (await this.enablingPlatformLogging()) {
2934
- this.log.info(String(...log))
2935
- }
2936
- }
2937
-
2938
- async successLog(...log: any[]): Promise<void> {
2939
- if (await this.enablingPlatformLogging()) {
2940
- this.log.success(String(...log))
2941
- }
2942
- }
2943
-
2944
- async debugSuccessLog(...log: any[]): Promise<void> {
2945
- if (await this.enablingPlatformLogging()) {
2946
- if (await this.loggingIsDebug()) {
2947
- this.log.success('[DEBUG]', String(...log))
2948
- }
2949
- }
2950
- }
2951
-
2952
- async warnLog(...log: any[]): Promise<void> {
2953
- if (await this.enablingPlatformLogging()) {
2954
- this.log.warn(String(...log))
2955
- }
2956
- }
2957
-
2958
- async debugWarnLog(...log: any[]): Promise<void> {
2959
- if (await this.enablingPlatformLogging()) {
2960
- if (await this.loggingIsDebug()) {
2961
- this.log.warn('[DEBUG]', String(...log))
2962
- }
2963
- }
2964
- }
2965
-
2966
- async errorLog(...log: any[]): Promise<void> {
2967
- if (await this.enablingPlatformLogging()) {
2968
- this.log.error(String(...log))
2969
- }
2970
- }
2971
-
2972
- async debugErrorLog(...log: any[]): Promise<void> {
2973
- if (await this.enablingPlatformLogging()) {
2974
- if (await this.loggingIsDebug()) {
2975
- this.log.error('[DEBUG]', String(...log))
2976
- }
2977
- }
2978
- }
2979
-
2980
- async debugLog(...log: any[]): Promise<void> {
2981
- if (await this.enablingPlatformLogging()) {
2982
- if (this.platformLogging === 'debug') {
2983
- this.log.info('[DEBUG]', String(...log))
2984
- } else if (this.platformLogging === 'debugMode') {
2985
- this.log.debug(String(...log))
2986
- }
2987
- }
2988
- }
2989
-
2990
- async loggingIsDebug(): Promise<boolean> {
2991
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug'
2992
- }
2993
-
2994
- async enablingPlatformLogging(): Promise<boolean> {
2995
- return this.platformLogging === 'debugMode' || this.platformLogging === 'debug' || this.platformLogging === 'standard'
2996
- }
2997
3193
  }