@switchbot/homebridge-switchbot 5.0.0-beta.37 → 5.0.0-beta.39

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.
@@ -11,7 +11,10 @@ class PluginUiServer extends HomebridgePluginUiServer {
11
11
  */
12
12
  this.onRequest('getCachedAccessories', () => {
13
13
  try {
14
- const plugin = 'homebridge-switchbot'
14
+ // Some Homebridge versions store cached accessories with the scoped
15
+ // plugin name ("@switchbot/homebridge-switchbot"); others may use
16
+ // the unscoped id ("homebridge-switchbot"). Check both.
17
+ const pluginNames = ['@switchbot/homebridge-switchbot', 'homebridge-switchbot']
15
18
  const devicesToReturn = []
16
19
 
17
20
  // The path and file of the cached accessories
@@ -22,11 +25,12 @@ class PluginUiServer extends HomebridgePluginUiServer {
22
25
  // read the cached accessories file
23
26
  const cachedAccessories: any[] = JSON.parse(fs.readFileSync(accFile, 'utf8'))
24
27
 
25
- cachedAccessories.forEach((accessory: any) => {
26
- // Check the accessory is from this plugin
27
- if (accessory.plugin === plugin) {
28
- // Add the cached accessory to the array
29
- devicesToReturn.push(accessory.accessory as never)
28
+ cachedAccessories.forEach((entry: any) => {
29
+ // entry shape varies by UI version
30
+ const pluginName = entry.plugin || entry?.accessory?.plugin || entry?.accessory?.pluginName
31
+ const acc = entry.accessory ?? entry
32
+ if (pluginNames.includes(pluginName)) {
33
+ devicesToReturn.push(acc as never)
30
34
  }
31
35
  })
32
36
  }
@@ -40,7 +44,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
40
44
  // Provide Matter cached accessories if Homebridge stores them separately.
41
45
  this.onRequest('getCachedMatterAccessories', () => {
42
46
  try {
43
- const plugin = 'homebridge-switchbot'
47
+ const pluginNames = ['@switchbot/homebridge-switchbot', 'homebridge-switchbot']
44
48
  const devicesToReturn: any[] = []
45
49
 
46
50
  const accFile = `${this.homebridgeStoragePath}/accessories/cachedAccessories`
@@ -56,7 +60,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
56
60
  // Entry shape varies between Homebridge versions; try common locations
57
61
  const pluginName = entry.plugin || entry?.accessory?.plugin || entry?.accessory?.pluginName
58
62
  const acc = entry.accessory ?? entry
59
- if (pluginName === plugin) {
63
+ if (pluginNames.includes(pluginName)) {
60
64
  devicesToReturn.push(acc as never)
61
65
  }
62
66
  })
@@ -483,6 +483,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
483
483
  let retryCount = 0
484
484
  const maxRetries = this.platformMaxRetries ?? 5
485
485
  const delayBetweenRetries = this.platformDelayBetweenRetries || 5000
486
+ let rateLimitExceeded = false
486
487
 
487
488
  this.debugWarnLog(`Retry Count: ${retryCount}`)
488
489
  this.debugWarnLog(`Max Retries: ${this.platformMaxRetries}`)
@@ -498,6 +499,13 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
498
499
  await this.handleIRDevices(Array.isArray(response.body.infraredRemoteList) ? response.body.infraredRemoteList : [])
499
500
  break
500
501
  } else {
502
+ // Check if rate limit exceeded (429)
503
+ if (statusCode === 429) {
504
+ rateLimitExceeded = true
505
+ this.warnLog('OpenAPI rate limit (429) exceeded. Falling back to manual device configuration.')
506
+ this.warnLog('Webhook functionality will still work if devices are configured manually.')
507
+ break
508
+ }
501
509
  await this.handleErrorResponse(statusCode, retryCount, maxRetries, delayBetweenRetries)
502
510
  retryCount++
503
511
  }
@@ -507,9 +515,116 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
507
515
  this.debugErrorLog(`Failed to Discover Devices, Error: ${e.message ?? e}`)
508
516
  }
509
517
  }
518
+
519
+ // If rate limit exceeded or retries exhausted, try to load from manual config or cached accessories
520
+ if (rateLimitExceeded || retryCount >= maxRetries) {
521
+ const hasCachedAccessories = this.accessories.length > 0
522
+ const hasManualConfig = this.config.options?.devices || this.config.options?.irdevices
523
+
524
+ if (hasManualConfig || hasCachedAccessories) {
525
+ if (hasCachedAccessories) {
526
+ this.warnLog(`Found ${this.accessories.length} cached accessories from previous sessions.`)
527
+ this.warnLog('Reinstantiating device classes to enable webhook handlers...')
528
+ await this.restoreCachedAccessories()
529
+ }
530
+ if (hasManualConfig) {
531
+ this.warnLog('Attempting to load devices from manual configuration...')
532
+ await this.handleManualConfig()
533
+ }
534
+ if (hasCachedAccessories) {
535
+ this.infoLog('Cached accessories restored. Webhook functionality is active.')
536
+ this.infoLog('Device discovery will resume when API rate limit resets.')
537
+ }
538
+ } else {
539
+ this.errorLog('OpenAPI unavailable and no cached accessories or manual device configuration found.')
540
+ this.errorLog('Please configure devices manually in the plugin settings to use webhook functionality.')
541
+ }
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Restore cached accessories by reinstantiating their device classes
547
+ * This ensures webhook handlers and other functionality are properly set up
548
+ */
549
+ private async restoreCachedAccessories() {
550
+ this.debugLog('Restoring cached accessories and setting up webhook handlers...')
551
+
552
+ for (const accessory of this.accessories) {
553
+ try {
554
+ const device = accessory.context.device
555
+ const deviceType = accessory.context.deviceType || device?.deviceType
556
+ const deviceId = accessory.context.deviceId || device?.deviceId
557
+
558
+ if (!device || !deviceType || !deviceId) {
559
+ this.debugWarnLog(`Skipping cached accessory ${accessory.displayName} - missing context data`)
560
+ continue
561
+ }
562
+
563
+ this.debugLog(`Reinstantiating ${deviceType} for cached accessory: ${accessory.displayName}`)
564
+
565
+ // Reinstantiate the device class based on deviceType
566
+ const deviceTypeHandlers: { [key: string]: new (platform: any, accessory: PlatformAccessory, device: any) => any } = {
567
+ 'Humidifier': Humidifier,
568
+ 'Humidifier2': Humidifier,
569
+ 'Hub 2': Hub,
570
+ 'Hub 3': Hub,
571
+ 'Bot': Bot,
572
+ 'Relay Switch 1': RelaySwitch,
573
+ 'Relay Switch 1PM': RelaySwitch,
574
+ 'Meter': Meter,
575
+ 'MeterPlus': MeterPlus,
576
+ 'Meter Plus (JP)': MeterPlus,
577
+ 'MeterPro': MeterPro,
578
+ 'MeterPro(CO2)': MeterPro,
579
+ 'WoIOSensor': IOSensor,
580
+ 'Water Detector': WaterDetector,
581
+ 'Motion Sensor': Motion,
582
+ 'Contact Sensor': Contact,
583
+ 'Curtain': Curtain,
584
+ 'Curtain3': Curtain,
585
+ 'WoRollerShade': Curtain,
586
+ 'Roller Shade': Curtain,
587
+ 'Blind Tilt': BlindTilt,
588
+ 'Plug': Plug,
589
+ 'Plug Mini (US)': Plug,
590
+ 'Plug Mini (JP)': Plug,
591
+ 'Smart Lock': Lock,
592
+ 'Smart Lock Pro': Lock,
593
+ 'Color Bulb': ColorBulb,
594
+ 'K10+': RobotVacuumCleaner,
595
+ 'K10+ Pro': RobotVacuumCleaner,
596
+ 'WoSweeper': RobotVacuumCleaner,
597
+ 'WoSweeperMini': RobotVacuumCleaner,
598
+ 'Robot Vacuum Cleaner S1': RobotVacuumCleaner,
599
+ 'Robot Vacuum Cleaner S1 Plus': RobotVacuumCleaner,
600
+ 'Robot Vacuum Cleaner S10': RobotVacuumCleaner,
601
+ 'Ceiling Light': CeilingLight,
602
+ 'Ceiling Light Pro': CeilingLight,
603
+ 'Strip Light': StripLight,
604
+ 'Battery Circulator Fan': Fan,
605
+ 'Air Purifier PM2.5': AirPurifier,
606
+ 'Air Purifier Table PM2.5': AirPurifier,
607
+ 'Air Purifier VOC': AirPurifier,
608
+ 'Air Purifier Table VOC': AirPurifier,
609
+ }
610
+
611
+ const DeviceClass = deviceTypeHandlers[deviceType]
612
+ if (DeviceClass) {
613
+ new DeviceClass(this, accessory, device)
614
+ this.debugSuccessLog(`Successfully restored ${deviceType}: ${accessory.displayName}`)
615
+ } else {
616
+ this.debugLog(`No handler for device type: ${deviceType}`)
617
+ }
618
+ } catch (e: any) {
619
+ this.errorLog(`Failed to restore cached accessory ${accessory.displayName}, Error: ${e.message ?? e}`)
620
+ }
621
+ }
622
+
623
+ this.infoLog(`Restored ${this.accessories.length} cached accessories with webhook support`)
510
624
  }
511
625
 
512
626
  private async handleManualConfig() {
627
+ // Handle regular devices
513
628
  if (this.config.options?.devices) {
514
629
  this.debugLog(`SwitchBot Device Manual Config Set: ${JSON.stringify(this.config.options?.devices)}`)
515
630
  const devices = this.config.options.devices.map((v: any) => v)
@@ -526,7 +641,27 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
526
641
  this.errorLog(`failed to format device ID as MAC, Error: ${error}`)
527
642
  }
528
643
  }
529
- } else {
644
+ }
645
+
646
+ // Handle IR devices
647
+ if (this.config.options?.irdevices) {
648
+ this.debugLog(`SwitchBot IR Device Manual Config Set: ${JSON.stringify(this.config.options?.irdevices)}`)
649
+ const irdevices = this.config.options.irdevices.map((v: any) => v)
650
+ for (const irdevice of irdevices) {
651
+ irdevice.remoteType = irdevice.configRemoteType !== undefined ? irdevice.configRemoteType : 'Unknown'
652
+ irdevice.deviceName = irdevice.configDeviceName !== undefined ? irdevice.configDeviceName : 'Unknown'
653
+ try {
654
+ this.debugLog(`IR deviceId: ${irdevice.deviceId}`)
655
+ if (irdevice.remoteType) {
656
+ await this.createIRDevice(irdevice)
657
+ }
658
+ } catch (error) {
659
+ this.errorLog(`failed to create IR device, Error: ${error}`)
660
+ }
661
+ }
662
+ }
663
+
664
+ if (!this.config.options?.devices && !this.config.options?.irdevices) {
530
665
  this.errorLog('Neither SwitchBot Token or Device Config are set.')
531
666
  }
532
667
  }
@@ -447,7 +447,7 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
447
447
  * Merge discovered IR devices with per-device overrides from `config.options.irdevices`.
448
448
  * Also applies device-type templates when applyToAllDevicesOfType is set.
449
449
  */
450
- private async mergeDiscoveredIRDevices(discovered: irdevice[]): Promise<any[]> {
450
+ private async mergeDiscoveredIRDevices(discovered: irdevice[]): Promise<irdevice[]> {
451
451
  // If there's no per-device config, return discovered as-is
452
452
  if (!this.config.options?.irdevices) {
453
453
  return discovered
@@ -475,9 +475,10 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
475
475
 
476
476
  // Merge per-device overrides by matching deviceId
477
477
  const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
478
- const merged = mergeByDeviceId(this.config.options?.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
478
+ type IRMerged = irdevice & irDevicesConfig
479
+ const merged = mergeByDeviceId(this.config.options?.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly) as IRMerged[]
479
480
 
480
- return merged
481
+ return merged as unknown as irdevice[]
481
482
  }
482
483
 
483
484
  /**
@@ -1075,6 +1076,25 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1075
1076
  this.debugLog(`Failed to schedule refresh for ${dev.deviceId}: ${e?.message ?? e}`)
1076
1077
  }
1077
1078
 
1079
+ // Register webhook handler for this device
1080
+ try {
1081
+ if (dev.webhook && dev.deviceId) {
1082
+ this.debugLog(`Registering webhook handler for Matter device: ${dev.deviceId}`)
1083
+ this.webhookEventHandler[dev.deviceId] = async (context: any) => {
1084
+ try {
1085
+ this.debugLog(`Received webhook for Matter device ${dev.deviceId}: ${JSON.stringify(context)}`)
1086
+ // Apply webhook status update to the accessory
1087
+ await this.applyStatusToAccessory(uuid, dev, context)
1088
+ } catch (e: any) {
1089
+ this.errorLog(`Failed to handle webhook for ${dev.deviceId}: ${e?.message ?? e}`)
1090
+ }
1091
+ }
1092
+ this.debugSuccessLog(`Webhook handler registered for ${dev.deviceId}`)
1093
+ }
1094
+ } catch (e: any) {
1095
+ this.debugLog(`Failed to register webhook handler for ${dev.deviceId}: ${e?.message ?? e}`)
1096
+ }
1097
+
1078
1098
  return instance.toAccessory()
1079
1099
  }
1080
1100
 
@@ -1107,6 +1127,12 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1107
1127
  }
1108
1128
  } else {
1109
1129
  this.warnLog(`SwitchBot getDevices returned status ${statusCode}`)
1130
+ // If rate limit exceeded (429), log specific message
1131
+ if (statusCode === 429) {
1132
+ this.warnLog('OpenAPI rate limit (429) exceeded during discovery.')
1133
+ this.warnLog('Webhook functionality will still work for manually configured devices.')
1134
+ this.warnLog('Device state updates will be limited until rate limit resets.')
1135
+ }
1110
1136
  }
1111
1137
  } catch (e: any) {
1112
1138
  this.errorLog('Failed to discover SwitchBot devices:', e?.message ?? e)
@@ -1864,14 +1890,77 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1864
1890
  return
1865
1891
  }
1866
1892
 
1867
- // If no discovered devices are available, do not register example/demo accessories.
1868
- this.infoLog('No discovered SwitchBot devices found.')
1893
+ // If no discovered devices are available, check for cached Matter accessories
1894
+ const hasCachedAccessories = this.matterAccessories.size > 0
1895
+ if (hasCachedAccessories) {
1896
+ this.infoLog(`No devices discovered via OpenAPI, but found ${this.matterAccessories.size} cached Matter accessories.`)
1897
+ this.infoLog('Cached accessories will continue to function with webhook updates.')
1898
+ this.infoLog('Restoring webhook handlers for cached Matter accessories...')
1899
+ await this.restoreCachedMatterAccessoryWebhooks()
1900
+ this.infoLog('Device discovery will resume when API becomes available.')
1901
+ } else {
1902
+ this.infoLog('No discovered SwitchBot devices found.')
1903
+ }
1869
1904
 
1870
1905
  this.debugLog('═'.repeat(80))
1871
1906
  this.debugLog('Finished registering Matter accessories')
1872
1907
  this.debugLog('═'.repeat(80))
1873
1908
  }
1874
1909
 
1910
+ /**
1911
+ * Restore webhook handlers for cached Matter accessories
1912
+ * This ensures webhook functionality continues to work even when device discovery fails
1913
+ */
1914
+ private async restoreCachedMatterAccessoryWebhooks() {
1915
+ this.debugLog('Restoring webhook handlers for cached Matter accessories...')
1916
+
1917
+ let restoredCount = 0
1918
+ for (const [uuid, accessory] of this.matterAccessories.entries()) {
1919
+ try {
1920
+ const context = (accessory as any)?.context
1921
+ const deviceId = context?.deviceId
1922
+ const webhook = context?.webhook
1923
+
1924
+ if (!deviceId) {
1925
+ this.debugLog(`Skipping cached accessory ${accessory.displayName} - no deviceId in context`)
1926
+ continue
1927
+ }
1928
+
1929
+ // Only register webhook if the device had webhook enabled
1930
+ if (webhook) {
1931
+ this.debugLog(`Restoring webhook handler for Matter device: ${deviceId}`)
1932
+
1933
+ // Create a minimal device object from cached context for webhook handling
1934
+ const dev: any = {
1935
+ deviceId,
1936
+ deviceName: context?.name || accessory.displayName,
1937
+ deviceType: context?.deviceType,
1938
+ webhook: true,
1939
+ }
1940
+
1941
+ this.webhookEventHandler[deviceId] = async (webhookContext: any) => {
1942
+ try {
1943
+ this.debugLog(`Received webhook for cached Matter device ${deviceId}: ${JSON.stringify(webhookContext)}`)
1944
+ // Apply webhook status update to the accessory
1945
+ await this.applyStatusToAccessory(uuid, dev, webhookContext)
1946
+ } catch (e: any) {
1947
+ this.errorLog(`Failed to handle webhook for cached device ${deviceId}: ${e?.message ?? e}`)
1948
+ }
1949
+ }
1950
+
1951
+ restoredCount++
1952
+ this.debugSuccessLog(`Webhook handler restored for ${deviceId}`)
1953
+ } else {
1954
+ this.debugLog(`Device ${deviceId} does not have webhook enabled, skipping`)
1955
+ }
1956
+ } catch (e: any) {
1957
+ this.errorLog(`Failed to restore webhook handler for cached accessory ${accessory.displayName}: ${e?.message ?? e}`)
1958
+ }
1959
+ }
1960
+
1961
+ this.infoLog(`Restored webhook handlers for ${restoredCount} cached Matter accessories`)
1962
+ }
1963
+
1875
1964
  /**
1876
1965
  * Remove accessories that are disabled in config
1877
1966
  */