@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.
- package/dist/homebridge-ui/server.js +12 -8
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/platform-hap.d.ts +5 -0
- package/dist/platform-hap.d.ts.map +1 -1
- package/dist/platform-hap.js +128 -1
- package/dist/platform-hap.js.map +1 -1
- package/dist/platform-matter.d.ts +5 -0
- package/dist/platform-matter.d.ts.map +1 -1
- package/dist/platform-matter.js +87 -2
- package/dist/platform-matter.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/homebridge-ui/server.ts +12 -8
- package/src/platform-hap.ts +136 -1
- package/src/platform-matter.ts +94 -5
|
@@ -11,7 +11,10 @@ class PluginUiServer extends HomebridgePluginUiServer {
|
|
|
11
11
|
*/
|
|
12
12
|
this.onRequest('getCachedAccessories', () => {
|
|
13
13
|
try {
|
|
14
|
-
|
|
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((
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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
|
|
63
|
+
if (pluginNames.includes(pluginName)) {
|
|
60
64
|
devicesToReturn.push(acc as never)
|
|
61
65
|
}
|
|
62
66
|
})
|
package/src/platform-hap.ts
CHANGED
|
@@ -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
|
-
}
|
|
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
|
}
|
package/src/platform-matter.ts
CHANGED
|
@@ -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<
|
|
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
|
-
|
|
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,
|
|
1868
|
-
this.
|
|
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
|
*/
|