@switchbot/homebridge-switchbot 5.0.0-beta.133 → 5.0.0-beta.135

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 (93) hide show
  1. package/config.schema.json +0 -11
  2. package/dist/deviceFactory.d.ts +3 -1
  3. package/dist/deviceFactory.d.ts.map +1 -1
  4. package/dist/deviceFactory.js +3 -18
  5. package/dist/deviceFactory.js.map +1 -1
  6. package/dist/devices/genericDevice.d.ts +1 -1
  7. package/dist/devices/genericDevice.d.ts.map +1 -1
  8. package/dist/devices/genericDevice.js +67 -19
  9. package/dist/devices/genericDevice.js.map +1 -1
  10. package/dist/homebridge-ui/public/index.html +107 -124
  11. package/dist/homebridge-ui/public/js/api.d.ts +1 -4
  12. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  13. package/dist/homebridge-ui/public/js/api.js +1 -7
  14. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  15. package/dist/homebridge-ui/public/js/api.ts +1 -8
  16. package/dist/homebridge-ui/public/js/app.js +201 -90
  17. package/dist/homebridge-ui/public/js/app.js.map +2 -2
  18. package/dist/homebridge-ui/public/js/app.ts +5 -6
  19. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  20. package/dist/homebridge-ui/public/js/credentials.js +1 -1
  21. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  22. package/dist/homebridge-ui/public/js/credentials.ts +7 -3
  23. package/dist/homebridge-ui/public/js/devices-delete.js +1 -1
  24. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
  25. package/dist/homebridge-ui/public/js/devices-delete.ts +1 -1
  26. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  27. package/dist/homebridge-ui/public/js/devices.js +0 -1
  28. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  29. package/dist/homebridge-ui/public/js/devices.ts +0 -2
  30. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  31. package/dist/homebridge-ui/public/js/discovery.js +6 -3
  32. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  33. package/dist/homebridge-ui/public/js/discovery.ts +9 -3
  34. package/dist/homebridge-ui/public/js/logger.d.ts +1 -0
  35. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/logger.js +8 -3
  37. package/dist/homebridge-ui/public/js/logger.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/logger.ts +12 -6
  39. package/dist/homebridge-ui/public/js/modals.d.ts +4 -0
  40. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/modals.js +152 -59
  42. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/modals.ts +187 -60
  44. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  45. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  46. package/dist/homebridge-ui/public/js/render.js +63 -24
  47. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  48. package/dist/homebridge-ui/public/js/render.ts +102 -51
  49. package/dist/homebridge-ui/utils/logger.js +1 -1
  50. package/dist/homebridge-ui/utils/logger.js.map +1 -1
  51. package/dist/platform.d.ts.map +1 -1
  52. package/dist/platform.js +38 -30
  53. package/dist/platform.js.map +1 -1
  54. package/dist/switchbotClient.d.ts +5 -5
  55. package/dist/switchbotClient.d.ts.map +1 -1
  56. package/dist/switchbotClient.js +21 -12
  57. package/dist/switchbotClient.js.map +1 -1
  58. package/docs/variables/default.html +1 -1
  59. package/package.json +1 -1
  60. package/scripts/build-ui.js +4 -3
  61. package/scripts/generate-matter-maps.js +34 -17
  62. package/scripts/sync-device-types.mjs +4 -1
  63. package/src/deviceFactory.ts +3 -18
  64. package/src/devices/genericDevice.ts +69 -21
  65. package/src/homebridge-ui/public/index.html +107 -124
  66. package/src/homebridge-ui/public/js/api.ts +1 -8
  67. package/src/homebridge-ui/public/js/app.ts +5 -6
  68. package/src/homebridge-ui/public/js/credentials.ts +7 -3
  69. package/src/homebridge-ui/public/js/devices-delete.ts +1 -1
  70. package/src/homebridge-ui/public/js/devices.ts +0 -2
  71. package/src/homebridge-ui/public/js/discovery.ts +9 -3
  72. package/src/homebridge-ui/public/js/logger.ts +12 -6
  73. package/src/homebridge-ui/public/js/modals.ts +187 -60
  74. package/src/homebridge-ui/public/js/render.ts +102 -51
  75. package/src/homebridge-ui/utils/logger.ts +1 -1
  76. package/src/platform.ts +38 -29
  77. package/src/switchbotClient.ts +42 -27
  78. package/test/accessory-restore.spec.ts +9 -8
  79. package/test/device-mapping.spec.ts +4 -3
  80. package/test/deviceFactory.spec.ts +4 -3
  81. package/test/e2e/run-e2e.spec.ts +6 -10
  82. package/test/fan-swing.spec.ts +7 -6
  83. package/test/helpers/matter-harness.ts +18 -7
  84. package/test/lock-users.spec.ts +5 -5
  85. package/test/matter-childbridge.spec.ts +10 -7
  86. package/test/matter-descriptors.spec.ts +8 -8
  87. package/test/matter-device-state.spec.ts +52 -36
  88. package/test/matter-integration.spec.ts +53 -6
  89. package/test/platform.integration.spec.ts +7 -5
  90. package/test/switchbot-client-debounce.spec.ts +36 -40
  91. package/test/switchbot-client-openapi.spec.ts +22 -15
  92. package/test/switchbotClient.spec.ts +4 -3
  93. package/test/utils.spec.ts +2 -1
@@ -1,20 +1,24 @@
1
- import { describe, it, expect } from 'vitest'
2
- import { LightDevice, FanDevice, CurtainDevice, LockDevice, HumidifierMatterDevice } from '../src/devices/genericDevice'
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { CurtainDevice, FanDevice, HumidifierMatterDevice, LightDevice, LockDevice } from '../src/devices/genericDevice'
3
4
  import { makeFakeApiWithMatter } from './helpers/matter-harness'
4
5
 
5
6
  const api = makeFakeApiWithMatter()
6
7
 
7
- describe('Matter device state translations', () => {
8
- it('Light: reads and writes map to setState/getState', async () => {
9
- const device = new LightDevice({ id: 'l-1', type: 'light', name: 'L1' }, {})
8
+ describe('matter device state translations', () => {
9
+ it('light: reads and writes map to setState/getState', async () => {
10
+ const device = new LightDevice({ id: 'l-1', type: 'light', name: 'L1' }, { log: console })
10
11
  let lastSet: any = null
11
12
  device.getState = async () => ({ on: true, brightness: 75, hue: 120, saturation: 50, colorTemperature: 350 })
12
- device.setState = async (c: any) => { lastSet = c; return { success: true } }
13
+ device.setState = async (c: any) => {
14
+ lastSet = c
15
+ return { success: true }
16
+ }
13
17
 
14
18
  const desc = await device.createMatterAccessory(api)
15
19
  const levelCluster = desc.clusters.find((c: any) => c.clusterId === 0x0008 || c.type === 'LevelControl')
16
20
  expect(levelCluster).toBeDefined()
17
- const levelAttr = levelCluster.attributes.currentLevel || levelCluster.attributes["0x0000"] || Object.values(levelCluster.attributes)[0]
21
+ const levelAttr = levelCluster.attributes.currentLevel || levelCluster.attributes['0x0000'] || Object.values(levelCluster.attributes)[0]
18
22
  const read = await levelAttr.read()
19
23
  expect(read).toBe(75)
20
24
 
@@ -23,11 +27,14 @@ describe('Matter device state translations', () => {
23
27
  expect(lastSet).toMatchObject({ brightness: 30 })
24
28
  })
25
29
 
26
- it('Fan: reads speed and writes speed/oscillation', async () => {
27
- const device = new FanDevice({ id: 'f-1', type: 'fan', name: 'F1' }, {})
28
- let last: any = null
30
+ it('fan: reads speed and writes speed/oscillation', async () => {
31
+ const device = new FanDevice({ id: 'f-1', type: 'fan', name: 'F1' }, { log: console })
32
+ let lastFan: any = null
29
33
  device.getState = async () => ({ on: false, speed: 45, oscillating: true, swingMode: 'auto' })
30
- device.setState = async (c: any) => { last = c; return { success: true } }
34
+ device.setState = async (c: any) => {
35
+ lastFan = c
36
+ return { success: true }
37
+ }
31
38
 
32
39
  const desc = await device.createMatterAccessory(api)
33
40
  const fanCluster = desc.clusters.find((c: any) => c.clusterId === 0x0202 || c.type === 'FanControl')
@@ -37,65 +44,74 @@ describe('Matter device state translations', () => {
37
44
  expect(speed).toBe(45)
38
45
 
39
46
  await speedAttr.write(80)
40
- expect(last).toMatchObject({ speed: 80 })
47
+ expect(lastFan).toMatchObject({ speed: 80 })
41
48
 
42
49
  // oscillation write
43
50
  const osc = fanCluster.attributes.oscillation
44
51
  await osc.write(true)
45
- expect(last).toMatchObject({ oscillate: true })
52
+ expect(lastFan).toMatchObject({ oscillate: true })
46
53
  })
47
54
 
48
- it('Curtain: reads and writes position', async () => {
49
- const device = new CurtainDevice({ id: 'c-1', type: 'curtain', name: 'C1' }, {})
50
- let last: any = null
55
+ it('curtain: reads and writes position', async () => {
56
+ const device = new CurtainDevice({ id: 'c-1', type: 'curtain', name: 'C1' }, { log: console })
57
+ let lastCurtain: any = null
51
58
  device.getState = async () => ({ position: 42 })
52
- device.setState = async (c: any) => { last = c; return { success: true } }
59
+ device.setState = async (c: any) => {
60
+ lastCurtain = c
61
+ return { success: true }
62
+ }
53
63
 
54
64
  const desc = await device.createMatterAccessory(api)
55
- const shade = desc.clusters.find((c: any) => c.clusterId === 0x0102 || c.type === 'Shade')
65
+ const shade = desc.clusters.find((c: any) => c.clusterId === 0x0102 || c.type === 'WindowCovering')
56
66
  expect(shade).toBeDefined()
57
- const current = shade.attributes.currentPosition || shade.attributes["0x0000"]
67
+ const current = shade.attributes.currentPositionLiftPercent100ths
58
68
  const v = await current.read()
59
- expect(v).toBe(42)
69
+ expect(Math.round(v / 100)).toBe(42)
60
70
 
61
- const target = shade.attributes.targetPosition || shade.attributes["0x0001"]
62
- await target.write(10)
63
- expect(last).toMatchObject({ position: 10 })
71
+ const target = shade.attributes.targetPositionLiftPercent100ths
72
+ await target.write(4200)
73
+ expect(lastCurtain).toMatchObject({ position: 42 })
64
74
  })
65
75
 
66
- it('Lock: reads and writes locked state', async () => {
67
- const device = new LockDevice({ id: 'k-1', type: 'lock', name: 'K1' }, {})
68
- let last: any = null
76
+ it('lock: reads and writes locked state', async () => {
77
+ const device = new LockDevice({ id: 'k-1', type: 'lock', name: 'K1' }, { log: console })
78
+ let lastLock: any = null
69
79
  device.getState = async () => ({ locked: true })
70
- device.setState = async (c: any) => { last = c; return { success: true } }
80
+ device.setState = async (c: any) => {
81
+ lastLock = c
82
+ return { success: true }
83
+ }
71
84
 
72
85
  const desc = await device.createMatterAccessory(api)
73
86
  const lock = desc.clusters.find((c: any) => c.clusterId === 0x0101 || c.type === 'DoorLock')
74
87
  expect(lock).toBeDefined()
75
- const lockAttr = lock.attributes.lockState || lock.attributes["0x0000"]
88
+ const lockAttr = lock.attributes.lockState || lock.attributes['0x0000']
76
89
  const v = await lockAttr.read()
77
90
  expect(v).toBe(true)
78
91
 
79
92
  await lockAttr.write(false)
80
- expect(last).toMatchObject({ locked: false })
93
+ expect(lastLock).toMatchObject({ locked: false })
81
94
  })
82
95
 
83
- it('Humidifier: reads humidity and toggles on/off', async () => {
84
- const device = new HumidifierMatterDevice({ id: 'h-1', type: 'humidifier', name: 'H1' }, {})
85
- let last: any = null
96
+ it('humidifier: reads humidity and toggles on/off', async () => {
97
+ const device = new HumidifierMatterDevice({ id: 'h-1', type: 'humidifier', name: 'H1' }, { log: console })
98
+ let lastHum: any = null
86
99
  device.getState = async () => ({ humidity: 55, on: false })
87
- device.setState = async (c: any) => { last = c; return { success: true } }
100
+ device.setState = async (c: any) => {
101
+ lastHum = c
102
+ return { success: true }
103
+ }
88
104
 
89
105
  const desc = await device.createMatterAccessory(api)
90
106
  const rh = desc.clusters.find((c: any) => c.clusterId === 0x0405 || c.type === 'RelativeHumiditySensor')
91
107
  expect(rh).toBeDefined()
92
- const humAttr = rh.attributes.currentRelativeHumidity || rh.attributes["0x0000"]
108
+ const humAttr = rh.attributes.currentRelativeHumidity || rh.attributes['0x0000']
93
109
  const hv = await humAttr.read()
94
110
  expect(hv).toBe(55)
95
111
 
96
112
  const onCluster = desc.clusters.find((c: any) => c.clusterId === 0x0006 || c.type === 'OnOff')
97
- const onAttr = onCluster.attributes.onOff || onCluster.attributes["0x0000"]
113
+ const onAttr = onCluster.attributes.onOff || onCluster.attributes['0x0000']
98
114
  await onAttr.write(true)
99
- expect(last).toMatchObject({ on: true })
115
+ expect(lastHum).toMatchObject({ on: true })
100
116
  })
101
117
  })
@@ -6,6 +6,37 @@ import { makeFakeApiWithMatter, makeFakeApiWithoutMatter, makeFakeMatterApi } fr
6
6
  // Expanded Matter integration harness tests
7
7
 
8
8
  describe('matter integration harness', () => {
9
+ it('registers blind tilt Matter accessory with correct window covering tilt attributes', async () => {
10
+ const matterApi = makeFakeMatterApi()
11
+ const api = makeFakeApiWithMatter(matterApi)
12
+
13
+ const platform = new SwitchBotMatterPlatform(console as any, {
14
+ devices: [{ id: 'blindtilt-1', type: 'blindtilt', name: 'Blind Tilt 1' }],
15
+ enableMatter: true,
16
+ preferMatter: true,
17
+ log: console,
18
+ } as any, api as any)
19
+
20
+ await (platform as any).registerMatterAccessories()
21
+
22
+ const regs = matterApi.getRegistered()
23
+ expect(regs.length).toBeGreaterThanOrEqual(1)
24
+ const clusters = regs[0]?.clusters
25
+ // Debug: log the actual clusters structure
26
+ // eslint-disable-next-line no-console
27
+ console.log('DEBUG clusters (blindtilt):', JSON.stringify(clusters, null, 2))
28
+ // Support both array and object cluster representations
29
+ let wc = clusters?.windowCovering
30
+ if (!wc && Array.isArray(clusters)) {
31
+ wc = clusters.find((c: any) => c?.type === 'WindowCovering' || c?.clusterId === 0x0102)
32
+ }
33
+ expect(wc).toBeDefined()
34
+ expect(wc).toHaveProperty('currentPositionTiltPercent100ths')
35
+ expect(wc).toHaveProperty('targetPositionTiltPercent100ths')
36
+ expect(wc).toHaveProperty('operationalStatus')
37
+ expect(wc).toHaveProperty('endProductType')
38
+ expect(wc).toHaveProperty('configStatus')
39
+ })
9
40
  it('normalizes Color Bulb and Motion Sensor to Matter-supported categories', async () => {
10
41
  const matterApi = makeFakeMatterApi()
11
42
  const api = makeFakeApiWithMatter(matterApi)
@@ -17,6 +48,7 @@ describe('matter integration harness', () => {
17
48
  ],
18
49
  enableMatter: true,
19
50
  preferMatter: true,
51
+ log: console,
20
52
  } as any, api as any)
21
53
 
22
54
  await (platform as any).registerMatterAccessories()
@@ -28,7 +60,7 @@ describe('matter integration harness', () => {
28
60
  expect(ids).toContain('ms-1')
29
61
  })
30
62
 
31
- it('uses conformant window covering type for curtain Matter registration', async () => {
63
+ it('registers curtain Matter accessory with correct window covering attributes', async () => {
32
64
  const matterApi = makeFakeMatterApi()
33
65
  const api = makeFakeApiWithMatter(matterApi)
34
66
 
@@ -36,14 +68,28 @@ describe('matter integration harness', () => {
36
68
  devices: [{ id: 'curtain-1', type: 'curtain', name: 'Curtain 1' }],
37
69
  enableMatter: true,
38
70
  preferMatter: true,
71
+ log: console,
39
72
  } as any, api as any)
40
73
 
41
74
  await (platform as any).registerMatterAccessories()
42
75
 
43
76
  const regs = matterApi.getRegistered()
44
77
  expect(regs.length).toBeGreaterThanOrEqual(1)
45
- // const wcType = regs[0]?.clusters?.windowCovering?.type
46
- // expect(wcType).toBe(4) // 4 = Curtain (conformant for SwitchBot Curtain)
78
+ const clusters = regs[0]?.clusters
79
+ // Debug: log the actual clusters structure
80
+ // eslint-disable-next-line no-console
81
+ console.log('DEBUG clusters (curtain):', JSON.stringify(clusters, null, 2))
82
+ // Support both array and object cluster representations
83
+ let wc = clusters?.windowCovering
84
+ if (!wc && Array.isArray(clusters)) {
85
+ wc = clusters.find((c: any) => c?.type === 'WindowCovering' || c?.clusterId === 0x0102)
86
+ }
87
+ expect(wc).toBeDefined()
88
+ expect(wc).toHaveProperty('currentPositionLiftPercent100ths')
89
+ expect(wc).toHaveProperty('targetPositionLiftPercent100ths')
90
+ expect(wc).toHaveProperty('operationalStatus')
91
+ expect(wc).toHaveProperty('endProductType')
92
+ expect(wc).toHaveProperty('configStatus')
47
93
  })
48
94
 
49
95
  it('registers RGBIC Neon Wire Rope Light as lightstrip with conformant ColorControl defaults', async () => {
@@ -54,6 +100,7 @@ describe('matter integration harness', () => {
54
100
  devices: [{ id: 'neon-1', type: 'RGBIC Neon Wire Rope Light', name: 'Family Room Neon Wire Rope Light' }],
55
101
  enableMatter: true,
56
102
  preferMatter: true,
103
+ log: console,
57
104
  } as any, api as any)
58
105
 
59
106
  await (platform as any).registerMatterAccessories()
@@ -72,7 +119,7 @@ describe('matter integration harness', () => {
72
119
  const matterApi = makeFakeMatterApi()
73
120
  const api = makeFakeApiWithMatter(matterApi)
74
121
 
75
- const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'm-1', type: 'light', name: 'L1' }], enableMatter: true, preferMatter: true } as any, api as any)
122
+ const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'm-1', type: 'light', name: 'L1' }], enableMatter: true, preferMatter: true, log: console } as any, api as any)
76
123
 
77
124
  // Call register path directly
78
125
  if (typeof (platform as any).registerMatterAccessories === 'function') {
@@ -91,7 +138,7 @@ describe('matter integration harness', () => {
91
138
  it('falls back to HAP when Matter API unavailable', async () => {
92
139
  // Directly instantiate the HAP platform to exercise HAP fallback
93
140
  const api = makeFakeApiWithoutMatter()
94
- const platform = new SwitchBotHAPPlatform(console as any, { devices: [{ id: 'h-1', type: 'light', name: 'H1' }], enableMatter: true, preferMatter: true } as any, api as any)
141
+ const platform = new SwitchBotHAPPlatform(console as any, { devices: [{ id: 'h-1', type: 'light', name: 'H1' }], enableMatter: true, preferMatter: true, log: console } as any, api as any)
95
142
 
96
143
  // call loadDevices for HAP flow
97
144
  if (typeof (platform as any).loadDevices === 'function') {
@@ -109,7 +156,7 @@ describe('matter integration harness', () => {
109
156
  const matterApi = makeFakeMatterApi()
110
157
  const api = makeFakeApiWithMatter(matterApi)
111
158
 
112
- const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'r-1', type: 'light', name: 'R1' }], enableMatter: true, preferMatter: true } as any, api as any)
159
+ const platform = new SwitchBotMatterPlatform(console as any, { devices: [{ id: 'r-1', type: 'light', name: 'R1' }], enableMatter: true, preferMatter: true, log: console } as any, api as any)
113
160
 
114
161
  // Simulate a restored accessory present before registration
115
162
  const restored = { uuid: matterApi.uuid.generate('r-1'), displayName: 'Restored R1', context: { deviceId: 'r-1' } }
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { SwitchBotHAPPlatform } from '../src/platform'
3
4
 
4
5
  // Minimal mock logger
@@ -21,7 +22,7 @@ describe('platform integration (HAP)', () => {
21
22
  }
22
23
 
23
24
  // Simple platformAccessory constructor
24
- function PlatformAccessory(name: string, uuid: string) {
25
+ function PlatformAccessory(this: any, name: string, uuid: string) {
25
26
  this.displayName = name
26
27
  this.UUID = uuid
27
28
  this.services = []
@@ -37,10 +38,11 @@ describe('platform integration (HAP)', () => {
37
38
  hap,
38
39
  platformAccessory: PlatformAccessory as any,
39
40
  registerPlatformAccessories: (_p: string, _n: string, accs: any[]) => registered.push(...accs),
40
- on: (_ev: string, cb: Function) => { /* do not auto-run */ },
41
+ on: () => { /* do not auto-run */ },
41
42
  }
42
43
 
43
- const config: any = { devices: [{ id: 'dev1', type: 'light', name: 'L1' }] }
44
+ // Inject logger into config
45
+ const config: any = { devices: [{ id: 'dev1', type: 'light', name: 'L1' }], log }
44
46
 
45
47
  const platform = new SwitchBotHAPPlatform(log as any, config, api)
46
48
 
@@ -49,7 +51,7 @@ describe('platform integration (HAP)', () => {
49
51
 
50
52
  // Expect accessory created and registered
51
53
  expect(registered.length).toBeGreaterThanOrEqual(1)
52
- const keys = Array.from((platform as any).accessories.keys())
54
+ const keys = [...(platform as any).accessories.keys()]
53
55
  expect(keys.some((k: string) => k.includes('dev1'))).toBeTruthy()
54
56
  })
55
57
  })
@@ -1,27 +1,28 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
2
3
  import { SwitchBotClient } from '../src/switchbotClient'
3
4
 
4
- describe('SwitchBotClient debounce/coalesce', () => {
5
+ describe('switchBotClient debounce/coalesce', () => {
5
6
  let origFetch: any
6
7
 
7
8
  beforeEach(() => {
8
- origFetch = (global as any).fetch
9
+ origFetch = (globalThis as any).fetch
9
10
  vi.useFakeTimers()
10
11
  })
11
12
 
12
13
  afterEach(() => {
13
- (global as any).fetch = origFetch
14
+ (globalThis as any).fetch = origFetch
14
15
  vi.restoreAllMocks()
15
16
  vi.useRealTimers()
16
17
  })
17
18
 
18
19
  it('coalesces rapid writes for the same device (last write wins)', async () => {
19
20
  const respBody = { status: 'ok' }
20
- const fetchMock = vi.fn(async (url: string, opts: any) => ({ json: async () => respBody }))
21
- ;(global as any).fetch = fetchMock
21
+ const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
22
+ ;(globalThis as any).fetch = fetchMock
22
23
 
23
24
  // Set a short debounce in config to keep the test fast
24
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10 } as any)
25
+ const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
25
26
  await c.init()
26
27
 
27
28
  const body1 = { command: 'one' }
@@ -42,16 +43,20 @@ describe('SwitchBotClient debounce/coalesce', () => {
42
43
 
43
44
  // Only one network call should have been made
44
45
  expect(fetchMock).toHaveBeenCalledTimes(1)
45
- const calledBody = JSON.parse(fetchMock.mock.calls[0][1].body)
46
+ let calledBody = {}
47
+ const calls = fetchMock.mock.calls as any
48
+ if (calls.length > 0 && Array.isArray(calls[0]) && calls[0].length > 1 && calls[0][1]?.body) {
49
+ calledBody = JSON.parse(calls[0][1].body)
50
+ }
46
51
  expect(calledBody).toEqual(body2)
47
52
  })
48
53
 
49
54
  it('does not coalesce writes for different devices', async () => {
50
55
  const respBody = { status: 'ok' }
51
- const fetchMock = vi.fn(async (url: string, opts: any) => ({ json: async () => respBody }))
52
- ;(global as any).fetch = fetchMock
56
+ const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
57
+ ;(globalThis as any).fetch = fetchMock
53
58
 
54
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10 } as any)
59
+ const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
55
60
  await c.init()
56
61
 
57
62
  const p1 = c.setDeviceState('d-A', { command: 'a' })
@@ -61,21 +66,18 @@ describe('SwitchBotClient debounce/coalesce', () => {
61
66
  await Promise.resolve()
62
67
  await Promise.resolve()
63
68
 
64
- await Promise.all([p1, p2])
65
-
69
+ const [r1, r2] = await Promise.all([p1, p2])
70
+ expect(r1).toEqual(respBody)
71
+ expect(r2).toEqual(respBody)
66
72
  expect(fetchMock).toHaveBeenCalledTimes(2)
67
- const b1 = JSON.parse(fetchMock.mock.calls[0][1].body)
68
- const b2 = JSON.parse(fetchMock.mock.calls[1][1].body)
69
- expect(b1).toEqual({ command: 'a' })
70
- expect(b2).toEqual({ command: 'b' })
71
73
  })
72
74
 
73
75
  it('coalesces multiple rapid writes (three) and resolves all promises with last response', async () => {
74
- const respBody = { status: 'ok', val: 'last' }
75
- const fetchMock = vi.fn(async (url: string, opts: any) => ({ json: async () => respBody }))
76
- ;(global as any).fetch = fetchMock
76
+ const respBody = { status: 'ok' }
77
+ const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
78
+ ;(globalThis as any).fetch = fetchMock
77
79
 
78
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10 } as any)
80
+ const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
79
81
  await c.init()
80
82
 
81
83
  const p1 = c.setDeviceState('d-debounce', { command: 'one' })
@@ -90,21 +92,21 @@ describe('SwitchBotClient debounce/coalesce', () => {
90
92
  expect(r1).toEqual(respBody)
91
93
  expect(r2).toEqual(respBody)
92
94
  expect(r3).toEqual(respBody)
93
-
94
95
  expect(fetchMock).toHaveBeenCalledTimes(1)
95
- const calledBody = JSON.parse(fetchMock.mock.calls[0][1].body)
96
+ let calledBody = {}
97
+ const calls = fetchMock.mock.calls as any
98
+ if (calls.length > 0 && Array.isArray(calls[0]) && calls[0].length > 1 && calls[0][1]?.body) {
99
+ calledBody = JSON.parse(calls[0][1].body)
100
+ }
96
101
  expect(calledBody).toEqual({ command: 'three' })
97
102
  })
98
103
 
99
104
  it('separate writes outside debounce window produce multiple network calls', async () => {
100
- const respBody1 = { status: 'ok', seq: 1 }
101
- const respBody2 = { status: 'ok', seq: 2 }
102
- const fetchMock = vi.fn()
103
- .mockImplementationOnce(async () => ({ json: async () => respBody1 }))
104
- .mockImplementationOnce(async () => ({ json: async () => respBody2 }))
105
- ;(global as any).fetch = fetchMock
106
-
107
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10 } as any)
105
+ const respBody = { status: 'ok' }
106
+ const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
107
+ ;(globalThis as any).fetch = fetchMock
108
+
109
+ const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
108
110
  await c.init()
109
111
 
110
112
  const p1 = c.setDeviceState('d-debounce', { command: 'one' })
@@ -117,15 +119,9 @@ describe('SwitchBotClient debounce/coalesce', () => {
117
119
  await Promise.resolve()
118
120
  await Promise.resolve()
119
121
 
120
- const r1 = await p1
121
- const r2 = await p2
122
-
122
+ const [r1, r2] = await Promise.all([p1, p2])
123
+ expect(r1).toEqual(respBody)
124
+ expect(r2).toEqual(respBody)
123
125
  expect(fetchMock).toHaveBeenCalledTimes(2)
124
- const b1 = JSON.parse(fetchMock.mock.calls[0][1].body)
125
- const b2 = JSON.parse(fetchMock.mock.calls[1][1].body)
126
- expect(b1).toEqual({ command: 'one' })
127
- expect(b2).toEqual({ command: 'two' })
128
- expect(r1).toEqual(respBody1)
129
- expect(r2).toEqual(respBody2)
130
126
  })
131
127
  })
@@ -1,30 +1,31 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+
2
3
  import { SwitchBotClient } from '../src/switchbotClient'
3
4
 
4
- describe('SwitchBotClient OpenAPI fallback', () => {
5
+ describe('switchBotClient OpenAPI fallback', () => {
5
6
  let origFetch: any
6
7
 
7
8
  beforeEach(() => {
8
- origFetch = (global as any).fetch
9
+ origFetch = (globalThis as any).fetch
9
10
  })
10
11
 
11
12
  afterEach(() => {
12
- (global as any).fetch = origFetch
13
+ (globalThis as any).fetch = origFetch
13
14
  vi.restoreAllMocks()
14
15
  })
15
16
 
16
17
  it('throws when no client and no token', async () => {
17
- const c = new SwitchBotClient({} as any)
18
+ const c = new SwitchBotClient({ log: console, logger: console } as any)
18
19
  await c.init()
19
20
  await expect(c.getDevice('nope')).rejects.toThrow('No SwitchBot client available')
20
21
  })
21
22
 
22
23
  it('uses OpenAPI when token provided for getDevice', async () => {
23
24
  const fakeResp = { body: { device: { id: 'd1', state: 'ok' } } }
24
- const fetchMock = vi.fn(async (url: string, opts: any) => ({ json: async () => fakeResp }))
25
- ;(global as any).fetch = fetchMock
25
+ const fetchMock = vi.fn(async () => ({ json: async () => fakeResp }))
26
+ ;(globalThis as any).fetch = fetchMock
26
27
 
27
- const c = new SwitchBotClient({ openApiToken: 'token-123' } as any)
28
+ const c = new SwitchBotClient({ openApiToken: 'token-123', log: console, logger: console } as any)
28
29
  await c.init()
29
30
  const res = await c.getDevice('d1')
30
31
  expect(res).toEqual(fakeResp)
@@ -33,23 +34,29 @@ describe('SwitchBotClient OpenAPI fallback', () => {
33
34
 
34
35
  it('posts commands to OpenAPI for setDeviceState', async () => {
35
36
  const respBody = { status: 'ok' }
36
- const fetchMock = vi.fn(async (url: string, opts: any) => ({ json: async () => respBody }))
37
- ;(global as any).fetch = fetchMock
37
+ const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
38
+ ;(globalThis as any).fetch = fetchMock
38
39
 
39
- const c = new SwitchBotClient({ openApiToken: 't' } as any)
40
+ const c = new SwitchBotClient({ openApiToken: 't', log: console, logger: console } as any)
40
41
  await c.init()
41
42
  const body = { command: 'turnOn', parameter: 'default', commandType: 'command' }
42
43
  const out = await c.setDeviceState('d2', body)
43
44
  expect(out).toEqual(respBody)
44
45
  expect(fetchMock).toHaveBeenCalledWith('https://api.switch-bot.com/v1.0/devices/d2/commands', expect.objectContaining({ method: 'POST' }))
45
- const calledBody = JSON.parse((fetchMock.mock.calls[0][1].body))
46
+ let calledBody = {}
47
+ const calls = fetchMock.mock.calls as any
48
+ if (calls.length > 0 && calls[0][1]?.body) {
49
+ calledBody = JSON.parse(calls[0][1].body)
50
+ }
46
51
  expect(calledBody).toEqual(body)
47
52
  })
48
53
 
49
54
  it('propagates fetch errors', async () => {
50
- const fetchMock = vi.fn(async () => { throw new Error('network') })
51
- ;(global as any).fetch = fetchMock
52
- const c = new SwitchBotClient({ openApiToken: 't' } as any)
55
+ const fetchMock = vi.fn(async () => {
56
+ throw new Error('network')
57
+ })
58
+ ;(globalThis as any).fetch = fetchMock
59
+ const c = new SwitchBotClient({ openApiToken: 't', log: console, logger: console } as any)
53
60
  await c.init()
54
61
  await expect(c.getDevice('x')).rejects.toThrow('network')
55
62
  })
@@ -1,9 +1,10 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { SwitchBotClient } from '../src/switchbotClient'
3
4
 
4
- describe('SwitchBotClient', () => {
5
+ describe('switchBotClient', () => {
5
6
  it('init does not throw and getDevice throws when no token/client', async () => {
6
- const c = new SwitchBotClient({})
7
+ const c = new SwitchBotClient({ log: console, logger: console })
7
8
  await c.init()
8
9
  await expect(c.getDevice('nope')).rejects.toThrow()
9
10
  })
@@ -1,4 +1,5 @@
1
- import { describe, it, expect } from 'vitest'
1
+ import { describe, expect, it } from 'vitest'
2
+
2
3
  import { createPlatformProxy } from '../src/utils'
3
4
 
4
5
  describe('createPlatformProxy', () => {