@switchbot/homebridge-switchbot 5.0.0-beta.3 → 5.0.0-beta.31

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 (153) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +23 -3
  3. package/config.schema.json +70 -4
  4. package/dist/devices-hap/device.d.ts +1 -0
  5. package/dist/devices-hap/device.d.ts.map +1 -1
  6. package/dist/devices-hap/device.js +70 -30
  7. package/dist/devices-hap/device.js.map +1 -1
  8. package/dist/devices-matter/BaseMatterAccessory.d.ts +23 -0
  9. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +1 -1
  10. package/dist/devices-matter/BaseMatterAccessory.js +167 -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 +35 -0
  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 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/index.test.js +7 -2
  39. package/dist/index.test.js.map +1 -1
  40. package/dist/irdevice/irdevice.d.ts +11 -10
  41. package/dist/irdevice/irdevice.d.ts.map +1 -1
  42. package/dist/irdevice/irdevice.js +76 -35
  43. package/dist/irdevice/irdevice.js.map +1 -1
  44. package/dist/platform-hap.d.ts +11 -14
  45. package/dist/platform-hap.d.ts.map +1 -1
  46. package/dist/platform-hap.js +64 -64
  47. package/dist/platform-hap.js.map +1 -1
  48. package/dist/platform-matter.d.ts +87 -6
  49. package/dist/platform-matter.d.ts.map +1 -1
  50. package/dist/platform-matter.js +1845 -84
  51. package/dist/platform-matter.js.map +1 -1
  52. package/dist/settings.d.ts +11 -0
  53. package/dist/settings.d.ts.map +1 -1
  54. package/dist/settings.js.map +1 -1
  55. package/dist/test/hap/platform-hap.logging.test.d.ts +2 -0
  56. package/dist/test/hap/platform-hap.logging.test.d.ts.map +1 -0
  57. package/dist/test/hap/platform-hap.logging.test.js +33 -0
  58. package/dist/test/hap/platform-hap.logging.test.js.map +1 -0
  59. package/dist/test/hap/platform-hap.test.d.ts +2 -0
  60. package/dist/test/hap/platform-hap.test.d.ts.map +1 -0
  61. package/dist/test/hap/platform-hap.test.js +62 -0
  62. package/dist/test/hap/platform-hap.test.js.map +1 -0
  63. package/dist/test/helpers/platform-fixtures.d.ts +9 -0
  64. package/dist/test/helpers/platform-fixtures.d.ts.map +1 -0
  65. package/dist/test/helpers/platform-fixtures.js +30 -0
  66. package/dist/test/helpers/platform-fixtures.js.map +1 -0
  67. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts +2 -0
  68. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts.map +1 -0
  69. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js +71 -0
  70. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js.map +1 -0
  71. package/dist/test/matter/platform-matter.additional.test.d.ts +2 -0
  72. package/dist/test/matter/platform-matter.additional.test.d.ts.map +1 -0
  73. package/dist/test/matter/platform-matter.additional.test.js +35 -0
  74. package/dist/test/matter/platform-matter.additional.test.js.map +1 -0
  75. package/dist/test/matter/platform-matter.bleparse.test.d.ts +2 -0
  76. package/dist/test/matter/platform-matter.bleparse.test.d.ts.map +1 -0
  77. package/dist/test/matter/platform-matter.bleparse.test.js +43 -0
  78. package/dist/test/matter/platform-matter.bleparse.test.js.map +1 -0
  79. package/dist/test/matter/platform-matter.cleanup.test.d.ts +2 -0
  80. package/dist/test/matter/platform-matter.cleanup.test.d.ts.map +1 -0
  81. package/dist/test/matter/platform-matter.cleanup.test.js +70 -0
  82. package/dist/test/matter/platform-matter.cleanup.test.js.map +1 -0
  83. package/dist/test/matter/platform-matter.keepstale.test.d.ts +2 -0
  84. package/dist/test/matter/platform-matter.keepstale.test.d.ts.map +1 -0
  85. package/dist/test/matter/platform-matter.keepstale.test.js +27 -0
  86. package/dist/test/matter/platform-matter.keepstale.test.js.map +1 -0
  87. package/dist/test/matter/platform-matter.logging.test.d.ts +2 -0
  88. package/dist/test/matter/platform-matter.logging.test.d.ts.map +1 -0
  89. package/dist/test/matter/platform-matter.logging.test.js +29 -0
  90. package/dist/test/matter/platform-matter.logging.test.js.map +1 -0
  91. package/dist/test/matter/platform-matter.mapping.test.d.ts +2 -0
  92. package/dist/test/matter/platform-matter.mapping.test.d.ts.map +1 -0
  93. package/dist/test/matter/platform-matter.mapping.test.js +43 -0
  94. package/dist/test/matter/platform-matter.mapping.test.js.map +1 -0
  95. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts +2 -0
  96. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts.map +1 -0
  97. package/dist/test/matter/platform-matter.openapi-mapping.test.js +84 -0
  98. package/dist/test/matter/platform-matter.openapi-mapping.test.js.map +1 -0
  99. package/dist/test/matter/platform-matter.test.d.ts +2 -0
  100. package/dist/test/matter/platform-matter.test.d.ts.map +1 -0
  101. package/dist/test/matter/platform-matter.test.js +117 -0
  102. package/dist/test/matter/platform-matter.test.js.map +1 -0
  103. package/dist/test/matter/platform-matter.unregister.test.d.ts +2 -0
  104. package/dist/test/matter/platform-matter.unregister.test.d.ts.map +1 -0
  105. package/dist/test/matter/platform-matter.unregister.test.js +30 -0
  106. package/dist/test/matter/platform-matter.unregister.test.js.map +1 -0
  107. package/dist/utils.d.ts +127 -0
  108. package/dist/utils.d.ts.map +1 -1
  109. package/dist/utils.js +405 -0
  110. package/dist/utils.js.map +1 -1
  111. package/dist/utils.test.d.ts +2 -0
  112. package/dist/utils.test.d.ts.map +1 -0
  113. package/dist/utils.test.js +95 -0
  114. package/dist/utils.test.js.map +1 -0
  115. package/dist/verifyconfig.test.js +2 -2
  116. package/dist/verifyconfig.test.js.map +1 -1
  117. package/docs/assets/main.js +2 -2
  118. package/docs/index.html +20 -2
  119. package/docs/variables/default.html +1 -1
  120. package/package.json +14 -14
  121. package/src/devices-hap/device.ts +68 -30
  122. package/src/devices-matter/BaseMatterAccessory.ts +168 -5
  123. package/src/devices-matter/ColorLightAccessory.ts +12 -12
  124. package/src/devices-matter/ColorTemperatureLightAccessory.ts +5 -7
  125. package/src/devices-matter/DimmableLightAccessory.ts +9 -9
  126. package/src/devices-matter/ExtendedColorLightAccessory.ts +14 -15
  127. package/src/devices-matter/OnOffLightAccessory.ts +8 -16
  128. package/src/devices-matter/OnOffOutletAccessory.ts +12 -7
  129. package/src/devices-matter/OnOffSwitchAccessory.ts +2 -2
  130. package/src/homebridge-ui/public/index.html +48 -1
  131. package/src/homebridge-ui/server.ts +37 -0
  132. package/src/index.test.ts +7 -2
  133. package/src/index.ts +4 -5
  134. package/src/irdevice/irdevice.ts +74 -35
  135. package/src/platform-hap.ts +68 -73
  136. package/src/platform-matter.ts +1879 -87
  137. package/src/settings.ts +15 -0
  138. package/src/test/hap/platform-hap.logging.test.ts +36 -0
  139. package/src/test/hap/platform-hap.test.ts +70 -0
  140. package/src/test/helpers/platform-fixtures.ts +33 -0
  141. package/src/test/matter/devices-matter/baseMatterAccessory.test.ts +88 -0
  142. package/src/test/matter/platform-matter.additional.test.ts +44 -0
  143. package/src/test/matter/platform-matter.bleparse.test.ts +47 -0
  144. package/src/test/matter/platform-matter.cleanup.test.ts +86 -0
  145. package/src/test/matter/platform-matter.keepstale.test.ts +37 -0
  146. package/src/test/matter/platform-matter.logging.test.ts +33 -0
  147. package/src/test/matter/platform-matter.mapping.test.ts +57 -0
  148. package/src/test/matter/platform-matter.openapi-mapping.test.ts +109 -0
  149. package/src/test/matter/platform-matter.test.ts +144 -0
  150. package/src/test/matter/platform-matter.unregister.test.ts +39 -0
  151. package/src/utils.test.ts +96 -0
  152. package/src/utils.ts +419 -3
  153. package/src/verifyconfig.test.ts +11 -10
package/src/utils.ts CHANGED
@@ -1,10 +1,14 @@
1
+ import type { API, Logging } from 'homebridge'
2
+ import type { blindTilt, curtain, curtain3, device } from 'node-switchbot'
3
+
4
+ import type { devicesConfig } from './settings.js'
5
+
1
6
  /* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
7
  *
3
8
  * util.ts: @switchbot/homebridge-switchbot platform class.
4
9
  */
5
- import type { blindTilt, curtain, curtain3, device } from 'node-switchbot'
6
-
7
- import type { devicesConfig } from './settings.js'
10
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
11
+ import { join } from 'node:path'
8
12
 
9
13
  export enum BlindTiltMappingMode {
10
14
  OnlyUp = 'only_up',
@@ -716,3 +720,415 @@ export function cleanDeviceConfig(deviceConfig?: Record<string, Record<string, a
716
720
  }
717
721
  return undefined
718
722
  }
723
+
724
+ /**
725
+ * Factory that returns a function to send OpenAPI commands using a retry wrapper.
726
+ *
727
+ * @param retryCommandFunc - bound function that calls platform.retryCommand(device, bodyChange, maxRetries, delay)
728
+ * @param deviceObj - the device object to operate on
729
+ * @param opts - optional overrides for maxRetries and delayBetweenRetries
730
+ * @param opts.maxRetries - override for maxRetries
731
+ * @param opts.delayBetweenRetries - override for delayBetweenRetries
732
+ */
733
+ export function makeOpenAPISender(retryCommandFunc: any, deviceObj: any, opts?: { maxRetries?: number, delayBetweenRetries?: number }) {
734
+ return async (command: string, parameter = 'default') => {
735
+ const bodyChange: any = { command, parameter, commandType: 'command' }
736
+ return retryCommandFunc(deviceObj, bodyChange, opts?.maxRetries, opts?.delayBetweenRetries)
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Factory that returns a function to perform BLE actions using a SwitchBotBLE client.
742
+ * Handles discovery retries and method invocation on the discovered device instance.
743
+ *
744
+ * @param switchBotBLE - instance of SwitchBotBLE (may be undefined)
745
+ * @param deviceObj - the device object (used to obtain bleModel/deviceId)
746
+ * @param opts - optional retry settings
747
+ * @param opts.bleRetries - number of BLE discovery retries
748
+ * @param opts.bleRetryDelay - delay between BLE retries in ms
749
+ */
750
+ export function makeBLESender(switchBotBLE: any, deviceObj: any, opts?: { bleRetries?: number, bleRetryDelay?: number }) {
751
+ return async (methodName: string, ...args: any[]) => {
752
+ if (!switchBotBLE) {
753
+ throw new Error('Platform BLE not available')
754
+ }
755
+ const id = formatDeviceIdAsMac(deviceObj.deviceId)
756
+ const maxRetries = opts?.bleRetries ?? 2
757
+ const retryDelay = opts?.bleRetryDelay ?? 500
758
+ let attempt = 0
759
+ while (attempt < maxRetries) {
760
+ try {
761
+ const list = await switchBotBLE.discover({ model: (deviceObj as any).bleModel, id })
762
+ if (!Array.isArray(list) || list.length === 0) {
763
+ throw new Error('BLE device not found')
764
+ }
765
+ const deviceInst: any = list[0]
766
+ if (typeof deviceInst[methodName] !== 'function') {
767
+ throw new TypeError(`BLE method ${methodName} not available on device`)
768
+ }
769
+ return await deviceInst[methodName](...args)
770
+ } catch (e: any) {
771
+ attempt++
772
+ if (attempt >= maxRetries) {
773
+ throw e
774
+ }
775
+ await sleep(retryDelay)
776
+ }
777
+ }
778
+ throw new Error('BLE operation failed')
779
+ }
780
+ }
781
+
782
+ /**
783
+ * Decide effective connection type for a device given platform options.
784
+ * Mirrors the logic previously in platform-matter.
785
+ */
786
+ export function chooseConnectionType(platformOptions: any, deviceObj: any): 'BLE' | 'OpenAPI' {
787
+ if (deviceObj?.connectionType) {
788
+ return deviceObj.connectionType === 'BLE' ? 'BLE' : 'OpenAPI'
789
+ }
790
+ if (platformOptions?.BLE && (deviceObj?.bleModel || (typeof deviceObj?.deviceId === 'string' && deviceObj.deviceId.length > 0))) {
791
+ return 'BLE'
792
+ }
793
+ return 'OpenAPI'
794
+ }
795
+
796
+ /**
797
+ * Detect whether Matter is enabled/available on the provided Homebridge API object.
798
+ * This encapsulates the multi-fallback detection used across the project.
799
+ */
800
+ /**
801
+ * Detect whether Matter is enabled on the provided Homebridge API object.
802
+ * Returns an object with an `enabled` boolean and an optional `reason` string
803
+ * describing which check matched (useful for diagnostics).
804
+ */
805
+ export function detectMatter(apiObj: API): { enabled: boolean, reason?: string } {
806
+ try {
807
+ const maybe = (apiObj as any).isMatterEnabled
808
+ if (typeof maybe === 'function') {
809
+ return { enabled: Boolean(maybe.call(apiObj)), reason: 'api.isMatterEnabled() returned truthy' }
810
+ }
811
+ if (typeof maybe !== 'undefined') {
812
+ return { enabled: Boolean(maybe), reason: 'api.isMatterEnabled property present' }
813
+ }
814
+
815
+ const server = (apiObj as any).server ?? (apiObj as any).homebridgeServer ?? (apiObj as any).homebridge_server
816
+ const serverMaybe = server?.isMatterEnabled
817
+ if (typeof serverMaybe === 'function') {
818
+ return { enabled: Boolean(serverMaybe.call(server)), reason: 'server.isMatterEnabled() returned truthy' }
819
+ }
820
+ if (typeof server?.isMatterEnabled !== 'undefined') {
821
+ return { enabled: Boolean(server.isMatterEnabled), reason: 'server.isMatterEnabled property present' }
822
+ }
823
+ } catch (e: any) {
824
+ return { enabled: false, reason: `error during detection: ${String(e?.message ?? e)}` }
825
+ }
826
+ return { enabled: false, reason: 'no isMatterEnabled API or server fallback detected' }
827
+ }
828
+
829
+ /**
830
+ * Backwards-compatible boolean wrapper for detectMatter.
831
+ */
832
+ export function detectMatterEnabled(apiObj: API): boolean {
833
+ return detectMatter(apiObj).enabled
834
+ }
835
+
836
+ /**
837
+ * Create platform logging helpers used by both HAP and Matter platforms.
838
+ *
839
+ * getPlatformLogging may be either a synchronous string-returning function or an
840
+ * async function that resolves to the current platform logging setting. The
841
+ * returned helpers mirror the instance methods previously implemented on the
842
+ * HAP platform (infoLog, warnLog, errorLog, debugLog, etc.).
843
+ */
844
+ export function createPlatformLogger(getPlatformLogging: () => string | Promise<string | undefined>, log: Logging) {
845
+ const getPL = async () => {
846
+ try {
847
+ return await getPlatformLogging()
848
+ } catch {
849
+ return undefined
850
+ }
851
+ }
852
+
853
+ const loggingIsDebug = async () => {
854
+ const pl = await getPL()
855
+ return pl === 'debugMode' || pl === 'debug'
856
+ }
857
+
858
+ const enablingPlatformLogging = async () => {
859
+ const pl = await getPL()
860
+ return pl === 'debugMode' || pl === 'debug' || pl === 'standard'
861
+ }
862
+
863
+ const formatArgs = (args: any[]): string => {
864
+ return args
865
+ .map((a: any) => {
866
+ if (typeof a === 'string') {
867
+ return a
868
+ }
869
+ try {
870
+ return JSON.stringify(a)
871
+ } catch {
872
+ return String(a)
873
+ }
874
+ })
875
+ .join(' ')
876
+ }
877
+
878
+ return {
879
+ // Format arbitrary arguments into a single string to ensure values are not dropped
880
+ // when loggers only accept a single message parameter.
881
+ // Prefer readable JSON for objects, fall back to String() on errors.
882
+ // Example: infoLog('Loaded', accessory.displayName) => "Loaded My Light"
883
+ infoLog: async (...args: any[]) => {
884
+ if (await enablingPlatformLogging()) {
885
+ const msg = formatArgs(args)
886
+ log.info(msg)
887
+ }
888
+ },
889
+ successLog: async (...args: any[]) => {
890
+ if (await enablingPlatformLogging()) {
891
+ const msg = formatArgs(args)
892
+ // Some Logging implementations expose `success` — call if present
893
+ ;(log as any).success?.(msg) ?? log.info(msg)
894
+ }
895
+ },
896
+ debugSuccessLog: async (...args: any[]) => {
897
+ if (await enablingPlatformLogging()) {
898
+ if (await loggingIsDebug()) {
899
+ const msg = formatArgs(args)
900
+ ;(log as any).success?.(`[DEBUG] ${msg}`) ?? log.info(`[DEBUG] ${msg}`)
901
+ }
902
+ }
903
+ },
904
+ warnLog: async (...args: any[]) => {
905
+ if (await enablingPlatformLogging()) {
906
+ const msg = formatArgs(args)
907
+ log.warn(msg)
908
+ }
909
+ },
910
+ debugWarnLog: async (...args: any[]) => {
911
+ if (await enablingPlatformLogging()) {
912
+ if (await loggingIsDebug()) {
913
+ const msg = formatArgs(args)
914
+ log.warn(`[DEBUG] ${msg}`)
915
+ }
916
+ }
917
+ },
918
+ errorLog: async (...args: any[]) => {
919
+ if (await enablingPlatformLogging()) {
920
+ const msg = formatArgs(args)
921
+ log.error(msg)
922
+ }
923
+ },
924
+ debugErrorLog: async (...args: any[]) => {
925
+ if (await enablingPlatformLogging()) {
926
+ if (await loggingIsDebug()) {
927
+ const msg = formatArgs(args)
928
+ log.error(`[DEBUG] ${msg}`)
929
+ }
930
+ }
931
+ },
932
+ debugLog: async (...args: any[]) => {
933
+ if (await enablingPlatformLogging()) {
934
+ const pl = await getPL()
935
+ if (pl === 'debug') {
936
+ const msg = formatArgs(args)
937
+ log.info(`[DEBUG] ${msg}`)
938
+ } else if (pl === 'debugMode') {
939
+ const msg = formatArgs(args)
940
+ log.debug(msg)
941
+ }
942
+ }
943
+ },
944
+ loggingIsDebug,
945
+ enablingPlatformLogging,
946
+ }
947
+ }
948
+
949
+ /**
950
+ * Create a Platform proxy class that selects between two platform constructors
951
+ * (HAP vs Matter) at runtime using `detectMatter`. Returns a class suitable
952
+ * for passing to `api.registerPlatform`.
953
+ */
954
+ export function createPlatformProxy(HAPCtor: any, MatterCtor: any) {
955
+ return class PlatformProxy {
956
+ delegate: any
957
+
958
+ constructor(public readonly log: any, public readonly config: any, public readonly api: API) {
959
+ const matterInfo = detectMatter(this.api)
960
+ const isMatter = matterInfo.enabled
961
+ const reason = matterInfo.reason ? ` Reason: ${matterInfo.reason}` : ''
962
+ this.log.info?.(`Homebridge SwitchBot Plugin initializing in ${isMatter ? 'Matter' : 'HAP'} mode.`)
963
+ this.log.debug?.(`Homebridge SwitchBot Plugin initializing in ${isMatter ? 'Matter' : 'HAP'} mode.${reason}`)
964
+ const PlatformCtor = isMatter ? MatterCtor : HAPCtor
965
+ this.delegate = new PlatformCtor(this.log, this.config, this.api)
966
+ }
967
+
968
+ configureAccessory(accessory: any): void {
969
+ try {
970
+ if (this.delegate && typeof this.delegate.configureAccessory === 'function') {
971
+ return this.delegate.configureAccessory(accessory)
972
+ }
973
+ } catch (e) {
974
+ // swallow — preserve previous behaviour where delegate errors don't bubble here
975
+ }
976
+ }
977
+
978
+ configureMatterAccessory?(accessory: any): void {
979
+ try {
980
+ if (this.delegate && typeof this.delegate.configureMatterAccessory === 'function') {
981
+ return this.delegate.configureMatterAccessory(accessory)
982
+ }
983
+ } catch (e) {
984
+ // swallow — delegate may not implement this or may throw
985
+ }
986
+ }
987
+
988
+ get accessories(): any {
989
+ try {
990
+ return this.delegate?.accessories
991
+ } catch (e) {
992
+ return undefined
993
+ }
994
+ }
995
+
996
+ get matterAccessories(): any {
997
+ try {
998
+ return this.delegate?.matterAccessories
999
+ } catch (e) {
1000
+ return undefined
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * API Request Tracker - Persistent tracking of SwitchBot API calls
1008
+ * Tracks requests per day with automatic midnight rollover
1009
+ */
1010
+ export class ApiRequestTracker {
1011
+ private count = 0
1012
+ private date = ''
1013
+ private statsFile = ''
1014
+ private hourlyTimer?: NodeJS.Timeout
1015
+ private log: Logging
1016
+
1017
+ constructor(api: API, log: Logging, pluginName = 'SwitchBot') {
1018
+ this.log = log
1019
+ this.statsFile = join(api.user.storagePath(), `${pluginName.toLowerCase()}-api-stats.json`)
1020
+ this.load()
1021
+ }
1022
+
1023
+ /**
1024
+ * Load API request statistics from persistent storage
1025
+ */
1026
+ private load(): void {
1027
+ try {
1028
+ const today = new Date().toISOString().split('T')[0]
1029
+
1030
+ if (existsSync(this.statsFile)) {
1031
+ const data = JSON.parse(readFileSync(this.statsFile, 'utf8'))
1032
+
1033
+ // If it's a new day, reset the counter
1034
+ if (data.date === today) {
1035
+ this.count = data.count || 0
1036
+ this.date = data.date
1037
+ this.log.warn?.(`[API Stats] Loaded: ${this.count} requests today (${today})`)
1038
+ } else {
1039
+ this.log.error?.(`[API Stats] New day detected. Previous: ${data.count || 0} requests on ${data.date}`)
1040
+ this.count = 0
1041
+ this.date = today
1042
+ this.save()
1043
+ }
1044
+ } else {
1045
+ this.log.debug?.('[API Stats] No existing stats file, starting fresh')
1046
+ this.count = 0
1047
+ this.date = today
1048
+ this.save()
1049
+ }
1050
+ } catch (e: any) {
1051
+ this.log.error?.(`[API Stats] Failed to load stats: ${e?.message ?? e}`)
1052
+ this.count = 0
1053
+ this.date = new Date().toISOString().split('T')[0]
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Save API request statistics to persistent storage
1059
+ */
1060
+ private save(): void {
1061
+ try {
1062
+ const data = {
1063
+ date: this.date,
1064
+ count: this.count,
1065
+ lastUpdated: new Date().toISOString(),
1066
+ }
1067
+ writeFileSync(this.statsFile, JSON.stringify(data, null, 2), 'utf8')
1068
+ } catch (e: any) {
1069
+ this.log.debug?.(`[API Stats] Failed to save stats: ${e?.message ?? e}`)
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * Increment API request counter and save
1075
+ */
1076
+ public track(): void {
1077
+ const today = new Date().toISOString().split('T')[0]
1078
+
1079
+ // Reset counter if it's a new day
1080
+ if (this.date !== today) {
1081
+ this.log.debug?.(`[API Stats] Day rollover: ${this.count} requests on ${this.date}`)
1082
+ this.count = 0
1083
+ this.date = today
1084
+ }
1085
+
1086
+ this.count++
1087
+ this.save()
1088
+ }
1089
+
1090
+ /**
1091
+ * Start hourly logging of API request count
1092
+ */
1093
+ public startHourlyLogging(): void {
1094
+ // Log immediately on startup
1095
+ this.log.info?.(`[API Stats] Today (${this.date}): ${this.count} API requests`)
1096
+
1097
+ // Then log every hour
1098
+ this.hourlyTimer = setInterval(() => {
1099
+ const today = new Date().toISOString().split('T')[0]
1100
+ if (this.date !== today) {
1101
+ // Day rollover
1102
+ this.log.info?.(`[API Stats] Day rollover - Previous day (${this.date}): ${this.count} API requests`)
1103
+ this.count = 0
1104
+ this.date = today
1105
+ this.save()
1106
+ }
1107
+ this.log.info?.(`[API Stats] Today (${this.date}): ${this.count} API requests`)
1108
+ }, 60 * 60 * 1000) // Every hour
1109
+ }
1110
+
1111
+ /**
1112
+ * Stop hourly logging
1113
+ */
1114
+ public stopHourlyLogging(): void {
1115
+ if (this.hourlyTimer) {
1116
+ clearInterval(this.hourlyTimer)
1117
+ this.hourlyTimer = undefined
1118
+ }
1119
+ }
1120
+
1121
+ /**
1122
+ * Get current count
1123
+ */
1124
+ public getCount(): number {
1125
+ return this.count
1126
+ }
1127
+
1128
+ /**
1129
+ * Get current date
1130
+ */
1131
+ public getDate(): string {
1132
+ return this.date
1133
+ }
1134
+ }
@@ -1,6 +1,7 @@
1
- import { describe, expect, it, vi } from 'vitest'
2
1
  import type { SwitchBotPlatformConfig } from './settings.js'
3
2
 
3
+ import { describe, expect, it } from 'vitest'
4
+
4
5
  // Create a minimal mock of the SwitchBotPlatform to test verifyConfig
5
6
  class MockSwitchBotPlatform {
6
7
  config: SwitchBotPlatformConfig
@@ -102,10 +103,10 @@ describe('verifyConfig fix for reboot loop', () => {
102
103
 
103
104
  // This should NOT throw an error anymore - it should log instead
104
105
  await expect(platform.verifyConfig()).resolves.not.toThrow()
105
-
106
+
106
107
  // Verify that the error was logged instead of thrown
107
108
  expect(platform.errorLogCalls).toContain(
108
- 'The devices config section is missing the *Device ID* in the config. Please check your config.'
109
+ 'The devices config section is missing the *Device ID* in the config. Please check your config.',
109
110
  )
110
111
  })
111
112
 
@@ -132,10 +133,10 @@ describe('verifyConfig fix for reboot loop', () => {
132
133
 
133
134
  // This should NOT throw an error anymore - it should log instead
134
135
  await expect(platform.verifyConfig()).resolves.not.toThrow()
135
-
136
+
136
137
  // Verify that the error was logged instead of thrown
137
138
  expect(platform.errorLogCalls).toContain(
138
- 'The devices config section is missing the *Device Type* in the config. Please check your config.'
139
+ 'The devices config section is missing the *Device Type* in the config. Please check your config.',
139
140
  )
140
141
  })
141
142
 
@@ -187,11 +188,11 @@ describe('verifyConfig fix for reboot loop', () => {
187
188
 
188
189
  // Should not throw or log device config errors with valid config
189
190
  await expect(platform.verifyConfig()).resolves.not.toThrow()
190
-
191
+
191
192
  // Should not have device config errors
192
- expect(platform.errorLogCalls.filter(msg =>
193
- msg.includes('missing the *Device ID*') ||
194
- msg.includes('missing the *Device Type*')
193
+ expect(platform.errorLogCalls.filter(msg =>
194
+ msg.includes('missing the *Device ID*')
195
+ || msg.includes('missing the *Device Type*'),
195
196
  )).toHaveLength(0)
196
197
  })
197
- })
198
+ })