@switchbot/homebridge-switchbot 5.0.0-beta.43 → 5.0.0-beta.44

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 (109) hide show
  1. package/config.schema.json +61 -9
  2. package/dist/devices-hap/airpurifier.d.ts.map +1 -1
  3. package/dist/devices-hap/airpurifier.js +12 -6
  4. package/dist/devices-hap/airpurifier.js.map +1 -1
  5. package/dist/devices-hap/blindtilt.js +3 -3
  6. package/dist/devices-hap/bot.d.ts.map +1 -1
  7. package/dist/devices-hap/bot.js +16 -5
  8. package/dist/devices-hap/bot.js.map +1 -1
  9. package/dist/devices-hap/ceilinglight.d.ts.map +1 -1
  10. package/dist/devices-hap/ceilinglight.js +13 -7
  11. package/dist/devices-hap/ceilinglight.js.map +1 -1
  12. package/dist/devices-hap/colorbulb.d.ts.map +1 -1
  13. package/dist/devices-hap/colorbulb.js +49 -9
  14. package/dist/devices-hap/colorbulb.js.map +1 -1
  15. package/dist/devices-hap/contact.js +3 -3
  16. package/dist/devices-hap/curtain.js +2 -2
  17. package/dist/devices-hap/curtain.js.map +1 -1
  18. package/dist/devices-hap/device.d.ts.map +1 -1
  19. package/dist/devices-hap/device.js +20 -1
  20. package/dist/devices-hap/device.js.map +1 -1
  21. package/dist/devices-hap/fan.d.ts.map +1 -1
  22. package/dist/devices-hap/fan.js +12 -6
  23. package/dist/devices-hap/fan.js.map +1 -1
  24. package/dist/devices-hap/hub.d.ts.map +1 -1
  25. package/dist/devices-hap/hub.js +6 -5
  26. package/dist/devices-hap/hub.js.map +1 -1
  27. package/dist/devices-hap/humidifier.d.ts +5 -0
  28. package/dist/devices-hap/humidifier.d.ts.map +1 -1
  29. package/dist/devices-hap/humidifier.js +92 -4
  30. package/dist/devices-hap/humidifier.js.map +1 -1
  31. package/dist/devices-hap/iosensor.d.ts.map +1 -1
  32. package/dist/devices-hap/iosensor.js +36 -21
  33. package/dist/devices-hap/iosensor.js.map +1 -1
  34. package/dist/devices-hap/lightstrip.d.ts.map +1 -1
  35. package/dist/devices-hap/lightstrip.js +38 -8
  36. package/dist/devices-hap/lightstrip.js.map +1 -1
  37. package/dist/devices-hap/lock.d.ts.map +1 -1
  38. package/dist/devices-hap/lock.js +14 -6
  39. package/dist/devices-hap/lock.js.map +1 -1
  40. package/dist/devices-hap/meter.d.ts.map +1 -1
  41. package/dist/devices-hap/meter.js +6 -5
  42. package/dist/devices-hap/meter.js.map +1 -1
  43. package/dist/devices-hap/meterplus.d.ts.map +1 -1
  44. package/dist/devices-hap/meterplus.js +6 -5
  45. package/dist/devices-hap/meterplus.js.map +1 -1
  46. package/dist/devices-hap/meterpro.d.ts.map +1 -1
  47. package/dist/devices-hap/meterpro.js +7 -6
  48. package/dist/devices-hap/meterpro.js.map +1 -1
  49. package/dist/devices-hap/motion.js +3 -3
  50. package/dist/devices-hap/plug.d.ts.map +1 -1
  51. package/dist/devices-hap/plug.js +11 -6
  52. package/dist/devices-hap/plug.js.map +1 -1
  53. package/dist/devices-hap/relayswitch.js +3 -3
  54. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +1 -1
  55. package/dist/devices-hap/robotvacuumcleaner.js +13 -6
  56. package/dist/devices-hap/robotvacuumcleaner.js.map +1 -1
  57. package/dist/devices-hap/waterdetector.js +3 -3
  58. package/dist/homebridge-ui/public/index.html +13 -1
  59. package/dist/platform-hap.d.ts.map +1 -1
  60. package/dist/platform-hap.js +38 -2
  61. package/dist/platform-hap.js.map +1 -1
  62. package/dist/platform-matter.d.ts.map +1 -1
  63. package/dist/platform-matter.js +69 -2
  64. package/dist/platform-matter.js.map +1 -1
  65. package/dist/settings.d.ts +12 -1
  66. package/dist/settings.d.ts.map +1 -1
  67. package/dist/settings.js.map +1 -1
  68. package/dist/test/hap/device-webhook-context.test.d.ts +2 -0
  69. package/dist/test/hap/device-webhook-context.test.d.ts.map +1 -0
  70. package/dist/test/hap/device-webhook-context.test.js +128 -0
  71. package/dist/test/hap/device-webhook-context.test.js.map +1 -0
  72. package/dist/test/matter/platform-matter.webhook.test.d.ts +2 -0
  73. package/dist/test/matter/platform-matter.webhook.test.d.ts.map +1 -0
  74. package/dist/test/matter/platform-matter.webhook.test.js +46 -0
  75. package/dist/test/matter/platform-matter.webhook.test.js.map +1 -0
  76. package/dist/utils.d.ts.map +1 -1
  77. package/dist/utils.js +10 -4
  78. package/dist/utils.js.map +1 -1
  79. package/docs/variables/default.html +1 -1
  80. package/package.json +2 -2
  81. package/src/devices-hap/airpurifier.ts +11 -6
  82. package/src/devices-hap/blindtilt.ts +3 -3
  83. package/src/devices-hap/bot.ts +15 -5
  84. package/src/devices-hap/ceilinglight.ts +12 -7
  85. package/src/devices-hap/colorbulb.ts +46 -10
  86. package/src/devices-hap/contact.ts +3 -3
  87. package/src/devices-hap/curtain.ts +2 -2
  88. package/src/devices-hap/device.ts +20 -1
  89. package/src/devices-hap/fan.ts +11 -6
  90. package/src/devices-hap/hub.ts +6 -5
  91. package/src/devices-hap/humidifier.ts +97 -4
  92. package/src/devices-hap/iosensor.ts +36 -21
  93. package/src/devices-hap/lightstrip.ts +35 -8
  94. package/src/devices-hap/lock.ts +13 -6
  95. package/src/devices-hap/meter.ts +6 -5
  96. package/src/devices-hap/meterplus.ts +6 -5
  97. package/src/devices-hap/meterpro.ts +7 -6
  98. package/src/devices-hap/motion.ts +3 -3
  99. package/src/devices-hap/plug.ts +10 -6
  100. package/src/devices-hap/relayswitch.ts +3 -3
  101. package/src/devices-hap/robotvacuumcleaner.ts +12 -6
  102. package/src/devices-hap/waterdetector.ts +3 -3
  103. package/src/homebridge-ui/public/index.html +13 -1
  104. package/src/platform-hap.ts +38 -2
  105. package/src/platform-matter.ts +70 -2
  106. package/src/settings.ts +12 -1
  107. package/src/test/hap/device-webhook-context.test.ts +136 -0
  108. package/src/test/matter/platform-matter.webhook.test.ts +54 -0
  109. package/src/utils.ts +14 -4
@@ -606,6 +606,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
606
606
  'Plug Mini (JP)': Plug,
607
607
  'Smart Lock': Lock,
608
608
  'Smart Lock Pro': Lock,
609
+ 'Smart Lock Ultra': Lock,
609
610
  'Color Bulb': ColorBulb,
610
611
  'K10+': RobotVacuumCleaner,
611
612
  'K10+ Pro': RobotVacuumCleaner,
@@ -728,12 +729,29 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
728
729
  const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
729
730
  const devices = mergeByDeviceId(this.config.options.devices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
730
731
 
732
+ // Apply global webhook option to devices that don't have their own webhook setting
733
+ if (this.config.options?.webhook === true) {
734
+ for (const device of devices) {
735
+ if (device.webhook === undefined) {
736
+ device.webhook = true
737
+ this.debugLog(`Applying global webhook option to device: ${device.deviceName ?? device.deviceId}`)
738
+ }
739
+ }
740
+ }
741
+
731
742
  this.debugLog(`SwitchBot Devices: ${JSON.stringify(devices)}`)
732
743
 
733
744
  for (const device of devices) {
734
745
  if (device.configDeviceName) {
735
746
  device.deviceName = device.configDeviceName
736
747
  }
748
+ // Log effective webhook setting for diagnostics
749
+ try {
750
+ const effectiveWebhook = device.webhook !== undefined ? device.webhook : (this.config.options?.webhook === true ? true : undefined)
751
+ this.debugLog(`Effective webhook for device ${device.deviceName ?? device.deviceId}: ${String(effectiveWebhook)}`)
752
+ } catch (e: any) {
753
+ this.debugLog(`Failed logging effective webhook for ${device.deviceName ?? device.deviceId}: ${e?.message ?? e}`)
754
+ }
737
755
  await this.createDevice(device)
738
756
  }
739
757
  }
@@ -773,11 +791,28 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
773
791
  const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
774
792
  const devices = mergeByDeviceId(this.config.options.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
775
793
 
794
+ // Apply global webhook option to IR devices that don't have their own webhook setting
795
+ if (this.config.options?.webhook === true) {
796
+ for (const device of devices) {
797
+ if (device.webhook === undefined) {
798
+ device.webhook = true
799
+ this.debugLog(`Applying global webhook option to IR device: ${device.deviceName ?? device.deviceId}`)
800
+ }
801
+ }
802
+ }
803
+
776
804
  this.debugLog(`IR Devices: ${JSON.stringify(devices)}`)
777
805
  for (const device of devices) {
778
806
  if (device.configDeviceName) {
779
807
  device.deviceName = device.configDeviceName
780
808
  }
809
+ // Log effective webhook setting for diagnostics (IR devices)
810
+ try {
811
+ const effectiveWebhook = device.webhook !== undefined ? device.webhook : (this.config.options?.webhook === true ? true : undefined)
812
+ this.debugLog(`Effective webhook for IR device ${device.deviceName ?? device.deviceId}: ${String(effectiveWebhook)}`)
813
+ } catch (e: any) {
814
+ this.debugLog(`Failed logging effective webhook for IR ${device.deviceName ?? device.deviceId}: ${e?.message ?? e}`)
815
+ }
781
816
  await this.createIRDevice(device)
782
817
  }
783
818
  }
@@ -819,6 +854,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
819
854
  'Plug Mini (JP)': this.createPlug.bind(this),
820
855
  'Smart Lock': this.createLock.bind(this),
821
856
  'Smart Lock Pro': this.createLock.bind(this),
857
+ 'Smart Lock Ultra': this.createLock.bind(this),
822
858
  'Color Bulb': this.createColorBulb.bind(this),
823
859
  'K10+': this.createRobotVacuumCleaner.bind(this),
824
860
  'K10+ Pro': this.createRobotVacuumCleaner.bind(this),
@@ -1814,7 +1850,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
1814
1850
  existingAccessory.context.device = device
1815
1851
  existingAccessory.context.deviceId = device.deviceId
1816
1852
  existingAccessory.context.deviceType = device.deviceType
1817
- existingAccessory.context.model = device.deviceType === 'Smart Lock Pro' ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1853
+ existingAccessory.context.model = (device.deviceType === 'Smart Lock Pro' || device.deviceType === 'Smart Lock Ultra') ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1818
1854
  existingAccessory.displayName = device.configDeviceName
1819
1855
  ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName)
1820
1856
  : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName)
@@ -1840,7 +1876,7 @@ export class SwitchBotHAPPlatform implements DynamicPlatformPlugin {
1840
1876
  accessory.context.device = device
1841
1877
  accessory.context.deviceId = device.deviceId
1842
1878
  accessory.context.deviceType = device.deviceType
1843
- accessory.context.model = device.deviceType === 'Smart Lock Pro' ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1879
+ accessory.context.model = (device.deviceType === 'Smart Lock Pro' || device.deviceType === 'Smart Lock Ultra') ? SwitchBotModel.LockPro : SwitchBotModel.Lock
1844
1880
  accessory.displayName = device.configDeviceName
1845
1881
  ? await this.validateAndCleanDisplayName(device.configDeviceName, 'configDeviceName', device.configDeviceName)
1846
1882
  : await this.validateAndCleanDisplayName(device.deviceName, 'deviceName', device.deviceName)
@@ -445,6 +445,16 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
445
445
  const allowConfigOnly = Boolean(this.config.options?.allowConfigOnlyDevices)
446
446
  const merged = mergeByDeviceId(this.config.options?.devices ?? [], devicesWithTemplates ?? [], allowConfigOnly)
447
447
 
448
+ // Apply global webhook setting if not explicitly set on individual devices
449
+ if (this.config.options?.webhook === true) {
450
+ merged.forEach((device: any) => {
451
+ if (device.webhook === undefined) {
452
+ device.webhook = true
453
+ this.debugLog(`Applied global webhook setting to Matter device: ${device.deviceId}`)
454
+ }
455
+ })
456
+ }
457
+
448
458
  return merged
449
459
  }
450
460
 
@@ -483,6 +493,16 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
483
493
  type IRMerged = irdevice & irDevicesConfig
484
494
  const merged = mergeByDeviceId(this.config.options?.irdevices ?? [], devicesWithTemplates ?? [], allowConfigOnly) as IRMerged[]
485
495
 
496
+ // Apply global webhook setting if not explicitly set on individual IR devices
497
+ if (this.config.options?.webhook === true) {
498
+ merged.forEach((device: any) => {
499
+ if (device.webhook === undefined) {
500
+ device.webhook = true
501
+ this.debugLog(`Applied global webhook setting to Matter IR device: ${device.deviceId}`)
502
+ }
503
+ })
504
+ }
505
+
486
506
  return merged as unknown as irdevice[]
487
507
  }
488
508
 
@@ -534,9 +554,20 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
534
554
  // so accessories can decide how verbose they should be.
535
555
  deviceLogging: (dev as any)?.logging,
536
556
  platformLogging: this.platformLogging,
557
+ // Expose effective webhook setting for parity with HAP base device logic
558
+ // Prefer explicit per-device setting; otherwise fall back to global option
559
+ webhook: (dev as any)?.webhook !== undefined ? (dev as any).webhook : (this.config.options?.webhook === true ? true : undefined),
537
560
  },
538
561
  }
539
562
 
563
+ // Log effective webhook for diagnostics
564
+ try {
565
+ const effectiveWebhook = (dev as any)?.webhook !== undefined ? (dev as any).webhook : (this.config.options?.webhook === true ? true : undefined)
566
+ this.debugLog(`Effective webhook for Matter device ${displayName} (${dev.deviceId}): ${String(effectiveWebhook)}`)
567
+ } catch (e: any) {
568
+ this.debugLog(`Failed logging effective webhook for Matter device ${displayName} (${dev.deviceId}): ${e?.message ?? e}`)
569
+ }
570
+
540
571
  // Build platform-side helpers using shared factories so they can be reused/tested
541
572
  const sendOpenAPI = makeOpenAPISender(this.retryCommand.bind(this), dev, { maxRetries: this.config.options?.maxRetries ?? 1, delayBetweenRetries: this.config.options?.delayBetweenRetries ?? 1000 })
542
573
  const sendBLE = makeBLESender(this.switchBotBLE, dev, { bleRetries: (this.config.options as any)?.bleRetries ?? 2, bleRetryDelay: (this.config.options as any)?.bleRetryDelay ?? 500 })
@@ -612,6 +643,7 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
612
643
  // Locks
613
644
  'Smart Lock': DoorLockAccessory,
614
645
  'Smart Lock Pro': DoorLockAccessory,
646
+ 'Smart Lock Ultra': DoorLockAccessory,
615
647
 
616
648
  // Sensors
617
649
  'Motion Sensor': OccupancySensorAccessory,
@@ -1512,6 +1544,38 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1512
1544
  }
1513
1545
  }
1514
1546
 
1547
+ // Robot vacuum: some firmwares expose 'taskType' or 'task' with similar semantics
1548
+ // to runState (e.g., 'cleaning', 'mapping', 'idle'). Map to rvcRunMode accordingly.
1549
+ if (status?.taskType !== undefined || status?.task_type !== undefined || status?.task !== undefined) {
1550
+ try {
1551
+ const raw = status?.taskType ?? status?.task_type ?? status?.task
1552
+ let mode: number | undefined
1553
+ if (typeof raw === 'number') {
1554
+ mode = Number(raw)
1555
+ } else if (typeof raw === 'string') {
1556
+ const s = raw.toLowerCase()
1557
+ if (s.includes('clean')) {
1558
+ mode = 1 // Cleaning
1559
+ } else if (s.includes('map')) {
1560
+ mode = 2 // Mapping
1561
+ } else if (s.includes('idle') || s.includes('stop') || s.includes('dock') || s.includes('charge') || s.includes('docked')) {
1562
+ mode = 0 // Idle
1563
+ } else {
1564
+ const n = Number(raw)
1565
+ if (!Number.isNaN(n)) {
1566
+ mode = n
1567
+ }
1568
+ }
1569
+ }
1570
+
1571
+ if (mode !== undefined) {
1572
+ await safeUpdate('rvcRunMode', { currentMode: Number(mode) }, 'updateRunMode')
1573
+ }
1574
+ } catch (e: any) {
1575
+ this.debugLog(`Failed to apply taskType for ${dev.deviceId}: ${e?.message ?? e}`)
1576
+ }
1577
+ }
1578
+
1515
1579
  // Brightness
1516
1580
  if (status?.brightness !== undefined) {
1517
1581
  const rawBrightness = Number(status.brightness)
@@ -1572,7 +1636,8 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1572
1636
 
1573
1637
  // Battery/powerSource (support many possible field names)
1574
1638
  // Note: Some device types like WindowBlind don't support PowerSource cluster
1575
- if (status?.battery !== undefined || status?.batt !== undefined || status?.batteryLevel !== undefined || status?.batteryPercentage !== undefined || status?.battery_level !== undefined) {
1639
+ if (status?.battery !== undefined || status?.batt !== undefined || status?.batteryLevel !== undefined || status?.batteryPercentage !== undefined || status?.battery_level !== undefined
1640
+ || status?.baseBattery !== undefined || status?.base_battery !== undefined || status?.stationBattery !== undefined || status?.waterBaseBattery !== undefined || status?.dockBattery !== undefined) {
1576
1641
  // Skip battery updates for device types that don't support PowerSource cluster
1577
1642
  const deviceType = String(status?.deviceType ?? dev?.deviceType ?? '')
1578
1643
  const unsupportedTypes = ['Curtain', 'Curtain2', 'Curtain3', 'Curtain 2', 'Blind Tilt']
@@ -1581,7 +1646,10 @@ export class SwitchBotMatterPlatform implements DynamicPlatformPlugin {
1581
1646
  this.debugLog(`Device ${dev.deviceId} type ${deviceType} does not support PowerSource cluster, skipping battery update`)
1582
1647
  } else {
1583
1648
  try {
1584
- const percentage = Number(status?.battery ?? status?.batt ?? status?.batteryPercentage ?? status?.batteryLevel ?? status?.battery_level)
1649
+ const percentage = Number(
1650
+ status?.battery ?? status?.batt ?? status?.batteryPercentage ?? status?.batteryLevel ?? status?.battery_level
1651
+ ?? status?.baseBattery ?? status?.base_battery ?? status?.stationBattery ?? status?.waterBaseBattery ?? status?.dockBattery,
1652
+ )
1585
1653
  const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)))
1586
1654
  let batChargeLevel = 0
1587
1655
  if (percentage < 20) {
package/src/settings.ts CHANGED
@@ -53,6 +53,12 @@ export interface options {
53
53
  disableLogsforOpenAPI?: boolean
54
54
  hostname?: string
55
55
  webhookURL?: string
56
+ /**
57
+ * When true, enables webhook support for all devices by default.
58
+ * Individual devices can override this with their own webhook setting.
59
+ * Requires webhookURL to be configured.
60
+ */
61
+ webhook?: boolean
56
62
  maxRetries?: number
57
63
  delayBetweenRetries?: number
58
64
  refreshRate?: number
@@ -178,6 +184,11 @@ export interface humidifierConfig extends BaseDeviceConfig {
178
184
  hide_temperature?: boolean
179
185
  convertUnitTo?: string
180
186
  set_minStep?: number
187
+ /**
188
+ * When true (Humidifier2 only), exposes a Switch service in HomeKit
189
+ * to trigger the built-in Drying Filter mode via OpenAPI (setMode 8).
190
+ */
191
+ activate_dryingfilter?: boolean
181
192
  };
182
193
 
183
194
  export interface curtainConfig extends BaseDeviceConfig {
@@ -253,7 +264,7 @@ export interface ceilingLightConfig extends BaseDeviceConfig {
253
264
  };
254
265
 
255
266
  export interface lockConfig extends BaseDeviceConfig {
256
- configDeviceType: 'Smart Lock' | 'Smart Lock Pro'
267
+ configDeviceType: 'Smart Lock' | 'Smart Lock Pro' | 'Smart Lock Ultra'
257
268
  hide_contactsensor?: boolean
258
269
  activate_latchbutton?: boolean
259
270
  };
@@ -0,0 +1,136 @@
1
+ /* eslint-disable import/first */
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ // Mock modules used by HAP device base occasionally via platform
4
+ vi.mock('fakegato-history', () => ({ default: () => ({}) }))
5
+ vi.mock('homebridge-lib/EveHomeKitTypes', () => ({ EveHomeKitTypes: class { constructor() {} } }))
6
+
7
+ import { deviceBase } from '../../devices-hap/device.js'
8
+
9
+ // Minimal HAP stub to satisfy deviceBase constructor operations
10
+ function makeHapStub() {
11
+ class Service {
12
+ private _chars: Record<string, any> = {}
13
+ setCharacteristic(k: any, v: any) {
14
+ this._chars[k] = v
15
+ return this
16
+ }
17
+
18
+ getCharacteristic(k: any) {
19
+ return {
20
+ updateValue: (v: any) => {
21
+ this._chars[k] = v
22
+ return this
23
+ },
24
+ } as any
25
+ }
26
+ }
27
+ const Characteristic: any = {
28
+ Manufacturer: 'Manufacturer',
29
+ AppMatchingIdentifier: 'AppMatchingIdentifier',
30
+ Name: 'Name',
31
+ ConfiguredName: 'ConfiguredName',
32
+ Model: 'Model',
33
+ ProductData: 'ProductData',
34
+ SerialNumber: 'SerialNumber',
35
+ HardwareRevision: 'HardwareRevision',
36
+ SoftwareRevision: 'SoftwareRevision',
37
+ FirmwareRevision: 'FirmwareRevision',
38
+ On: 'On',
39
+ }
40
+ ;(Service as any).AccessoryInformation = class extends Service {}
41
+ ;(Service as any).Outlet = class extends Service {}
42
+ return { Service, Characteristic, Categories: { OUTLET: 7 } }
43
+ }
44
+
45
+ // Minimal PlatformAccessory stub
46
+ function makeAccessoryStub(hap: any, name = 'Test Accessory') {
47
+ const services: any[] = []
48
+ return {
49
+ displayName: name,
50
+ category: 0,
51
+ context: {},
52
+ getService(cls: any) {
53
+ // find existing or create
54
+ const svc = services.find(s => s instanceof cls)
55
+ if (svc) {
56
+ return svc
57
+ }
58
+ const s = new cls()
59
+ services.push(s)
60
+ return s
61
+ },
62
+ addService(cls: any) {
63
+ const s = new cls()
64
+ services.push(s)
65
+ return s
66
+ },
67
+ }
68
+ }
69
+
70
+ // Minimal SwitchBotHAPPlatform-like stub with required surface
71
+ function makePlatformStub(options: any, hap: any) {
72
+ const log = { info: vi.fn(), warn: vi.fn(), debug: vi.fn(), error: vi.fn(), success: vi.fn() }
73
+ return {
74
+ api: { hap },
75
+ log,
76
+ config: { options, credentials: {} },
77
+ debugMode: false,
78
+ // logging helpers used by deviceBase
79
+ infoLog: () => {},
80
+ successLog: () => {},
81
+ debugSuccessLog: () => {},
82
+ warnLog: () => {},
83
+ debugWarnLog: () => {},
84
+ errorLog: () => {},
85
+ debugErrorLog: () => {},
86
+ debugLog: () => {},
87
+ loggingIsDebug: async () => false,
88
+ enablingPlatformLogging: async () => true,
89
+ connectBLE: vi.fn(),
90
+ bleEventHandler: {},
91
+ webhookEventHandler: {},
92
+ }
93
+ }
94
+
95
+ // Create a tiny concrete subclass to instantiate deviceBase
96
+ class TestHAPDevice extends deviceBase {
97
+ // Override any methods that may be invoked by tests if needed
98
+ }
99
+
100
+ describe('hap device base webhook context', () => {
101
+ it('sets accessory.context.webhook=true when global webhook is enabled and device.webhook is undefined', async () => {
102
+ const hap = makeHapStub()
103
+ const accessory: any = makeAccessoryStub(hap, 'Plug Device')
104
+ const platform: any = makePlatformStub({ webhook: true, logging: 'debug' }, hap)
105
+
106
+ const dev: any = {
107
+ deviceId: 'DEV-HAP-1',
108
+ deviceType: 'Plug',
109
+ connectionType: 'OpenAPI',
110
+ // webhook intentionally undefined
111
+ }
112
+
113
+ const d = new TestHAPDevice(platform, accessory, dev)
114
+ // Assert context
115
+ expect(accessory.context.webhook).toBe(true)
116
+ // ensure no unused var warning
117
+ expect(d).toBeDefined()
118
+ })
119
+
120
+ it('keeps accessory.context.webhook=false when device.webhook=false even if global is true', async () => {
121
+ const hap = makeHapStub()
122
+ const accessory: any = makeAccessoryStub(hap, 'Plug Device 2')
123
+ const platform: any = makePlatformStub({ webhook: true, logging: 'debug' }, hap)
124
+
125
+ const dev: any = {
126
+ deviceId: 'DEV-HAP-2',
127
+ deviceType: 'Plug',
128
+ connectionType: 'OpenAPI',
129
+ webhook: false,
130
+ }
131
+
132
+ const d = new TestHAPDevice(platform, accessory, dev)
133
+ expect(accessory.context.webhook).toBe(false)
134
+ expect(d).toBeDefined()
135
+ })
136
+ })
@@ -0,0 +1,54 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { SwitchBotMatterPlatform } from '../../platform-matter.js'
4
+ import { makeApiStub, makeLogStub } from '../helpers/platform-fixtures.js'
5
+
6
+ /**
7
+ * Verify that Matter accessories receive the effective webhook flag
8
+ * in their context when a global webhook option is enabled and the
9
+ * per-device webhook setting is undefined.
10
+ */
11
+ describe('platform-matter webhook context propagation', () => {
12
+ it('sets context.webhook=true when global webhook is enabled and device.webhook is undefined', async () => {
13
+ const api: any = makeApiStub()
14
+ const log: any = makeLogStub()
15
+
16
+ const platform = new SwitchBotMatterPlatform(log as any, {
17
+ name: 'SwitchBot',
18
+ options: { webhook: true },
19
+ } as any, api)
20
+
21
+ // Minimal device that maps to a known Matter accessory constructor
22
+ const dev: any = {
23
+ deviceId: 'DEV-WH-1',
24
+ deviceName: 'Webhook Plug',
25
+ deviceType: 'Plug',
26
+ // webhook intentionally undefined to test fallback
27
+ }
28
+
29
+ const acc = await (platform as any).createAccessoryFromDevice(dev)
30
+ expect(acc).toBeDefined()
31
+ expect((acc as any).context?.webhook).toBe(true)
32
+ })
33
+
34
+ it('keeps explicit device.webhook=false even when global webhook is true', async () => {
35
+ const api: any = makeApiStub()
36
+ const log: any = makeLogStub()
37
+
38
+ const platform = new SwitchBotMatterPlatform(log as any, {
39
+ name: 'SwitchBot',
40
+ options: { webhook: true },
41
+ } as any, api)
42
+
43
+ const dev: any = {
44
+ deviceId: 'DEV-WH-2',
45
+ deviceName: 'No Webhook Plug',
46
+ deviceType: 'Plug',
47
+ webhook: false,
48
+ }
49
+
50
+ const acc = await (platform as any).createAccessoryFromDevice(dev)
51
+ expect(acc).toBeDefined()
52
+ expect((acc as any).context?.webhook).toBe(false)
53
+ })
54
+ })
package/src/utils.ts CHANGED
@@ -49,12 +49,22 @@ export function validHumidity(humidity: number, min?: number, max?: number): num
49
49
  * Converts the value to celsius if the temperature units are in Fahrenheit
50
50
  */
51
51
  export function convertUnits(value: number, unit: string, convert?: string): number {
52
- if (unit === 'CELSIUS' && convert === 'CELSIUS') {
52
+ // Convert only when source unit differs from target unit.
53
+ // Supported values for unit/convert: 'CELSIUS' | 'FAHRENHEIT'
54
+ if (!convert || unit === convert) {
55
+ return value
56
+ }
57
+
58
+ if (unit === 'CELSIUS' && convert === 'FAHRENHEIT') {
53
59
  return Math.round((value * 9) / 5 + 32)
54
- } else if (unit === 'FAHRENHEIT' && convert === 'FAHRENHEIT') {
55
- // celsius should be to the nearest 0.5 degree
56
- return Math.round((5 / 9) * (value - 32) * 2) / 2
57
60
  }
61
+
62
+ if (unit === 'FAHRENHEIT' && convert === 'CELSIUS') {
63
+ // Celsius should be to the nearest 0.5 degree
64
+ return Math.round(((value - 32) * 5) / 9 * 2) / 2
65
+ }
66
+
67
+ // Unknown unit combination: return as-is
58
68
  return value
59
69
  }
60
70