@switchbot/homebridge-switchbot 5.0.0-beta.136 → 5.0.0-beta.138

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 (61) hide show
  1. package/dist/errors.d.ts +37 -4
  2. package/dist/errors.d.ts.map +1 -1
  3. package/dist/errors.js +31 -50
  4. package/dist/errors.js.map +1 -1
  5. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  6. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  7. package/dist/platform.d.ts +2 -1
  8. package/dist/platform.d.ts.map +1 -1
  9. package/dist/platform.js +1 -1
  10. package/dist/platform.js.map +1 -1
  11. package/dist/switchbotClient.d.ts +0 -4
  12. package/dist/switchbotClient.d.ts.map +1 -1
  13. package/dist/switchbotClient.js +16 -90
  14. package/dist/switchbotClient.js.map +1 -1
  15. package/docs/variables/default.html +1 -1
  16. package/package.json +3 -3
  17. package/src/errors.ts +30 -38
  18. package/src/homebridge-ui/endpoints/devices.ts +29 -14
  19. package/src/platform.ts +1 -1
  20. package/src/switchbotClient.ts +16 -90
  21. package/test/client/switchbot-client-debounce.spec.ts +35 -0
  22. package/test/client/switchbot-client-openapi.spec.ts +19 -0
  23. package/test/client/switchbotClient.spec.ts +23 -0
  24. package/test/device/device-mapping.spec.ts +23 -0
  25. package/test/device/deviceBase.spec.ts +26 -0
  26. package/test/device/deviceFactory-edge.spec.ts +15 -0
  27. package/test/device/deviceFactory.spec.ts +33 -0
  28. package/test/device/fan-swing.spec.ts +34 -0
  29. package/test/device/irdevice.spec.ts +9 -0
  30. package/test/device/lock-users.spec.ts +35 -0
  31. package/test/device/matter-descriptors.spec.ts +22 -0
  32. package/test/device/matter-device-state.spec.ts +37 -0
  33. package/test/errors/errors.spec.ts +10 -0
  34. package/test/homebridge-ui/server.spec.ts +9 -0
  35. package/test/platform/accessory-restore.spec.ts +36 -0
  36. package/test/platform/matter-childbridge.spec.ts +34 -0
  37. package/test/platform/matter-integration.spec.ts +33 -0
  38. package/test/platform/platform-edge.spec.ts +72 -0
  39. package/test/platform/platform.integration.spec.ts +34 -0
  40. package/test/utils/utils-extra.spec.ts +10 -0
  41. package/test/utils/utils.spec.ts +53 -0
  42. package/todo/TODO.md +55 -165
  43. package/test/accessory-restore.spec.ts +0 -74
  44. package/test/device-mapping.spec.ts +0 -38
  45. package/test/deviceFactory.spec.ts +0 -19
  46. package/test/fan-swing.spec.ts +0 -30
  47. package/test/lock-users.spec.ts +0 -44
  48. package/test/matter-childbridge.spec.ts +0 -62
  49. package/test/matter-descriptors.spec.ts +0 -110
  50. package/test/matter-device-state.spec.ts +0 -117
  51. package/test/matter-integration.spec.ts +0 -183
  52. package/test/platform.integration.spec.ts +0 -57
  53. package/test/switchbot-client-debounce.spec.ts +0 -127
  54. package/test/switchbot-client-openapi.spec.ts +0 -63
  55. package/test/switchbotClient.spec.ts +0 -11
  56. package/test/utils.spec.ts +0 -37
  57. package/todo/COMPLETED.md +0 -542
  58. package/todo/README.md +0 -59
  59. package/todo/mockups/filtering-sorting.svg +0 -112
  60. package/todo/mockups/modal-switchbot-theme.svg +0 -66
  61. package/todo/mockups/signal-strength-visualization.svg +0 -94
@@ -1,117 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { CurtainDevice, FanDevice, HumidifierMatterDevice, LightDevice, LockDevice } from '../src/devices/genericDevice'
4
- import { makeFakeApiWithMatter } from './helpers/matter-harness'
5
-
6
- const api = makeFakeApiWithMatter()
7
-
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 })
11
- let lastSet: any = null
12
- device.getState = async () => ({ on: true, brightness: 75, hue: 120, saturation: 50, colorTemperature: 350 })
13
- device.setState = async (c: any) => {
14
- lastSet = c
15
- return { success: true }
16
- }
17
-
18
- const desc = await device.createMatterAccessory(api)
19
- const levelCluster = desc.clusters.find((c: any) => c.clusterId === 0x0008 || c.type === 'LevelControl')
20
- expect(levelCluster).toBeDefined()
21
- const levelAttr = levelCluster.attributes.currentLevel || levelCluster.attributes['0x0000'] || Object.values(levelCluster.attributes)[0]
22
- const read = await levelAttr.read()
23
- expect(read).toBe(75)
24
-
25
- // write should call setState with brightness
26
- await levelAttr.write(30)
27
- expect(lastSet).toMatchObject({ brightness: 30 })
28
- })
29
-
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
33
- device.getState = async () => ({ on: false, speed: 45, oscillating: true, swingMode: 'auto' })
34
- device.setState = async (c: any) => {
35
- lastFan = c
36
- return { success: true }
37
- }
38
-
39
- const desc = await device.createMatterAccessory(api)
40
- const fanCluster = desc.clusters.find((c: any) => c.clusterId === 0x0202 || c.type === 'FanControl')
41
- expect(fanCluster).toBeDefined()
42
- const speedAttr = fanCluster.attributes.rotationSpeed || Object.values(fanCluster.attributes)[0]
43
- const speed = await speedAttr.read()
44
- expect(speed).toBe(45)
45
-
46
- await speedAttr.write(80)
47
- expect(lastFan).toMatchObject({ speed: 80 })
48
-
49
- // oscillation write
50
- const osc = fanCluster.attributes.oscillation
51
- await osc.write(true)
52
- expect(lastFan).toMatchObject({ oscillate: true })
53
- })
54
-
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
58
- device.getState = async () => ({ position: 42 })
59
- device.setState = async (c: any) => {
60
- lastCurtain = c
61
- return { success: true }
62
- }
63
-
64
- const desc = await device.createMatterAccessory(api)
65
- const shade = desc.clusters.find((c: any) => c.clusterId === 0x0102 || c.type === 'WindowCovering')
66
- expect(shade).toBeDefined()
67
- const current = shade.attributes.currentPositionLiftPercent100ths
68
- const v = await current.read()
69
- expect(Math.round(v / 100)).toBe(42)
70
-
71
- const target = shade.attributes.targetPositionLiftPercent100ths
72
- await target.write(4200)
73
- expect(lastCurtain).toMatchObject({ position: 42 })
74
- })
75
-
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
79
- device.getState = async () => ({ locked: true })
80
- device.setState = async (c: any) => {
81
- lastLock = c
82
- return { success: true }
83
- }
84
-
85
- const desc = await device.createMatterAccessory(api)
86
- const lock = desc.clusters.find((c: any) => c.clusterId === 0x0101 || c.type === 'DoorLock')
87
- expect(lock).toBeDefined()
88
- const lockAttr = lock.attributes.lockState || lock.attributes['0x0000']
89
- const v = await lockAttr.read()
90
- expect(v).toBe(true)
91
-
92
- await lockAttr.write(false)
93
- expect(lastLock).toMatchObject({ locked: false })
94
- })
95
-
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
99
- device.getState = async () => ({ humidity: 55, on: false })
100
- device.setState = async (c: any) => {
101
- lastHum = c
102
- return { success: true }
103
- }
104
-
105
- const desc = await device.createMatterAccessory(api)
106
- const rh = desc.clusters.find((c: any) => c.clusterId === 0x0405 || c.type === 'RelativeHumiditySensor')
107
- expect(rh).toBeDefined()
108
- const humAttr = rh.attributes.currentRelativeHumidity || rh.attributes['0x0000']
109
- const hv = await humAttr.read()
110
- expect(hv).toBe(55)
111
-
112
- const onCluster = desc.clusters.find((c: any) => c.clusterId === 0x0006 || c.type === 'OnOff')
113
- const onAttr = onCluster.attributes.onOff || onCluster.attributes['0x0000']
114
- await onAttr.write(true)
115
- expect(lastHum).toMatchObject({ on: true })
116
- })
117
- })
@@ -1,183 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import SwitchBotHAPPlatform, { SwitchBotMatterPlatform } from '../src/platform'
4
- import { makeFakeApiWithMatter, makeFakeApiWithoutMatter, makeFakeMatterApi } from './helpers/matter-harness'
5
-
6
- // Expanded Matter integration harness tests
7
-
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
- })
40
- it('normalizes Color Bulb and Motion Sensor to Matter-supported categories', async () => {
41
- const matterApi = makeFakeMatterApi()
42
- const api = makeFakeApiWithMatter(matterApi)
43
-
44
- const platform = new SwitchBotMatterPlatform(console as any, {
45
- devices: [
46
- { id: 'cb-1', type: 'Color Bulb', name: 'Color Bulb 1' },
47
- { id: 'ms-1', type: 'Motion Sensor', name: 'Motion Sensor 1' },
48
- ],
49
- enableMatter: true,
50
- preferMatter: true,
51
- log: console,
52
- } as any, api as any)
53
-
54
- await (platform as any).registerMatterAccessories()
55
-
56
- const regs = matterApi.getRegistered()
57
- expect(regs.length).toBeGreaterThanOrEqual(2)
58
- const ids = regs.map((r: any) => r?.context?.deviceId)
59
- expect(ids).toContain('cb-1')
60
- expect(ids).toContain('ms-1')
61
- })
62
-
63
- it('registers curtain Matter accessory with correct window covering attributes', async () => {
64
- const matterApi = makeFakeMatterApi()
65
- const api = makeFakeApiWithMatter(matterApi)
66
-
67
- const platform = new SwitchBotMatterPlatform(console as any, {
68
- devices: [{ id: 'curtain-1', type: 'curtain', name: 'Curtain 1' }],
69
- enableMatter: true,
70
- preferMatter: true,
71
- log: console,
72
- } as any, api as any)
73
-
74
- await (platform as any).registerMatterAccessories()
75
-
76
- const regs = matterApi.getRegistered()
77
- expect(regs.length).toBeGreaterThanOrEqual(1)
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')
93
- })
94
-
95
- it('registers RGBIC Neon Wire Rope Light as lightstrip with conformant ColorControl defaults', async () => {
96
- const matterApi = makeFakeMatterApi()
97
- const api = makeFakeApiWithMatter(matterApi)
98
-
99
- const platform = new SwitchBotMatterPlatform(console as any, {
100
- devices: [{ id: 'neon-1', type: 'RGBIC Neon Wire Rope Light', name: 'Family Room Neon Wire Rope Light' }],
101
- enableMatter: true,
102
- preferMatter: true,
103
- log: console,
104
- } as any, api as any)
105
-
106
- await (platform as any).registerMatterAccessories()
107
-
108
- const regs = matterApi.getRegistered()
109
- expect(regs.length).toBeGreaterThanOrEqual(1)
110
- const neon = regs.find((r: any) => r?.context?.deviceId === 'neon-1')
111
- expect(neon).toBeDefined()
112
- expect(neon?.context?.type).toBe('lightstrip')
113
- expect(neon?.clusters?.colorControl).toBeDefined()
114
- expect(neon?.clusters?.colorControl?.colorMode).toBe(0)
115
- })
116
-
117
- it('registers Matter accessories when Matter API available', async () => {
118
- // Instantiate the real Matter platform directly to test its registration path
119
- const matterApi = makeFakeMatterApi()
120
- const api = makeFakeApiWithMatter(matterApi)
121
-
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)
123
-
124
- // Call register path directly
125
- if (typeof (platform as any).registerMatterAccessories === 'function') {
126
- await (platform as any).registerMatterAccessories()
127
- } else if (typeof (platform as any).loadDevices === 'function') {
128
- await (platform as any).loadDevices()
129
- }
130
-
131
- const regs = matterApi.getRegistered()
132
- expect(regs.length).toBeGreaterThanOrEqual(1)
133
- // The platform registers serialized accessories; ensure deviceId is present in context
134
- expect(regs[0]).toHaveProperty('context')
135
- expect(regs[0].context).toHaveProperty('deviceId', 'm-1')
136
- })
137
-
138
- it('falls back to HAP when Matter API unavailable', async () => {
139
- // Directly instantiate the HAP platform to exercise HAP fallback
140
- const api = makeFakeApiWithoutMatter()
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)
142
-
143
- // call loadDevices for HAP flow
144
- if (typeof (platform as any).loadDevices === 'function') {
145
- await (platform as any).loadDevices()
146
- }
147
-
148
- // Platform should have an accessories map populated
149
- const accMap = (platform as any).accessories
150
- expect(accMap).toBeDefined()
151
- const keys = [...accMap.keys()]
152
- expect(keys.length).toBeGreaterThanOrEqual(1)
153
- })
154
-
155
- it('reuses restored Matter accessories instead of registering duplicates', async () => {
156
- const matterApi = makeFakeMatterApi()
157
- const api = makeFakeApiWithMatter(matterApi)
158
-
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)
160
-
161
- // Simulate a restored accessory present before registration
162
- const restored = { uuid: matterApi.uuid.generate('r-1'), displayName: 'Restored R1', context: { deviceId: 'r-1' } }
163
- if (typeof (platform as any).configureMatterAccessory === 'function') {
164
- await (platform as any).configureMatterAccessory(restored)
165
- } else if (typeof (platform as any).configureAccessory === 'function') {
166
- await (platform as any).configureAccessory(restored)
167
- }
168
-
169
- // Now call register path
170
- if (typeof (platform as any).registerMatterAccessories === 'function') {
171
- await (platform as any).registerMatterAccessories()
172
- } else if (typeof (platform as any).loadDevices === 'function') {
173
- await (platform as any).loadDevices()
174
- if (typeof (platform as any).registerMatterAccessories === 'function') {
175
- await (platform as any).registerMatterAccessories()
176
- }
177
- }
178
-
179
- const accs = (platform as any).accessories
180
- const found = [...accs.values()].some((a: any) => a && (a.uuid === restored.uuid || a.UUID === restored.uuid))
181
- expect(found).toBeTruthy()
182
- })
183
- })
@@ -1,57 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { SwitchBotHAPPlatform } from '../src/platform'
4
-
5
- // Minimal mock logger
6
- const log = {
7
- info: () => {},
8
- warn: () => {},
9
- error: () => {},
10
- debug: () => {},
11
- }
12
-
13
- describe('platform integration (HAP)', () => {
14
- it('registers HAP accessories using Homebridge API', async () => {
15
- const registered: any[] = []
16
-
17
- // Minimal hap stub
18
- const hap = {
19
- uuid: { generate: (s: string) => `uuid-${s}` },
20
- Service: { Switch: 'Switch', Lightbulb: 'Lightbulb' },
21
- Characteristic: { On: 'On', Brightness: 'Brightness' },
22
- }
23
-
24
- // Simple platformAccessory constructor
25
- function PlatformAccessory(this: any, name: string, uuid: string) {
26
- this.displayName = name
27
- this.UUID = uuid
28
- this.services = []
29
- this.getService = (type: any) => this.services.find((s: any) => s.type === type)
30
- this.addService = (type: any) => {
31
- const s: any = { type, characteristics: {} }
32
- this.services.push(s)
33
- return s
34
- }
35
- }
36
-
37
- const api: any = {
38
- hap,
39
- platformAccessory: PlatformAccessory as any,
40
- registerPlatformAccessories: (_p: string, _n: string, accs: any[]) => registered.push(...accs),
41
- on: () => { /* do not auto-run */ },
42
- }
43
-
44
- // Inject logger into config
45
- const config: any = { devices: [{ id: 'dev1', type: 'light', name: 'L1' }], log }
46
-
47
- const platform = new SwitchBotHAPPlatform(log as any, config, api)
48
-
49
- // call loadDevices directly to control flow
50
- await (platform as any).loadDevices()
51
-
52
- // Expect accessory created and registered
53
- expect(registered.length).toBeGreaterThanOrEqual(1)
54
- const keys = [...(platform as any).accessories.keys()]
55
- expect(keys.some((k: string) => k.includes('dev1'))).toBeTruthy()
56
- })
57
- })
@@ -1,127 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- import { SwitchBotClient } from '../src/switchbotClient'
4
-
5
- describe('switchBotClient debounce/coalesce', () => {
6
- let origFetch: any
7
-
8
- beforeEach(() => {
9
- origFetch = (globalThis as any).fetch
10
- vi.useFakeTimers()
11
- })
12
-
13
- afterEach(() => {
14
- (globalThis as any).fetch = origFetch
15
- vi.restoreAllMocks()
16
- vi.useRealTimers()
17
- })
18
-
19
- it('coalesces rapid writes for the same device (last write wins)', async () => {
20
- const respBody = { status: 'ok' }
21
- const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
22
- ;(globalThis as any).fetch = fetchMock
23
-
24
- // Set a short debounce in config to keep the test fast
25
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
26
- await c.init()
27
-
28
- const body1 = { command: 'one' }
29
- const body2 = { command: 'two' }
30
-
31
- const p1 = c.setDeviceState('d-debounce', body1)
32
- const p2 = c.setDeviceState('d-debounce', body2)
33
-
34
- // Fast-forward timers past debounce window
35
- vi.advanceTimersByTime(20)
36
- // Allow pending microtasks to resolve
37
- await Promise.resolve()
38
- await Promise.resolve()
39
-
40
- const [r1, r2] = await Promise.all([p1, p2])
41
- expect(r1).toEqual(respBody)
42
- expect(r2).toEqual(respBody)
43
-
44
- // Only one network call should have been made
45
- expect(fetchMock).toHaveBeenCalledTimes(1)
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
- }
51
- expect(calledBody).toEqual(body2)
52
- })
53
-
54
- it('does not coalesce writes for different devices', async () => {
55
- const respBody = { status: 'ok' }
56
- const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
57
- ;(globalThis as any).fetch = fetchMock
58
-
59
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
60
- await c.init()
61
-
62
- const p1 = c.setDeviceState('d-A', { command: 'a' })
63
- const p2 = c.setDeviceState('d-B', { command: 'b' })
64
-
65
- vi.advanceTimersByTime(20)
66
- await Promise.resolve()
67
- await Promise.resolve()
68
-
69
- const [r1, r2] = await Promise.all([p1, p2])
70
- expect(r1).toEqual(respBody)
71
- expect(r2).toEqual(respBody)
72
- expect(fetchMock).toHaveBeenCalledTimes(2)
73
- })
74
-
75
- it('coalesces multiple rapid writes (three) and resolves all promises with last response', async () => {
76
- const respBody = { status: 'ok' }
77
- const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
78
- ;(globalThis as any).fetch = fetchMock
79
-
80
- const c = new SwitchBotClient({ openApiToken: 't', writeDebounceMs: 10, log: console, logger: console } as any)
81
- await c.init()
82
-
83
- const p1 = c.setDeviceState('d-debounce', { command: 'one' })
84
- const p2 = c.setDeviceState('d-debounce', { command: 'two' })
85
- const p3 = c.setDeviceState('d-debounce', { command: 'three' })
86
-
87
- vi.advanceTimersByTime(20)
88
- await Promise.resolve()
89
- await Promise.resolve()
90
-
91
- const [r1, r2, r3] = await Promise.all([p1, p2, p3])
92
- expect(r1).toEqual(respBody)
93
- expect(r2).toEqual(respBody)
94
- expect(r3).toEqual(respBody)
95
- expect(fetchMock).toHaveBeenCalledTimes(1)
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
- }
101
- expect(calledBody).toEqual({ command: 'three' })
102
- })
103
-
104
- it('separate writes outside debounce window produce multiple network calls', async () => {
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)
110
- await c.init()
111
-
112
- const p1 = c.setDeviceState('d-debounce', { command: 'one' })
113
- vi.advanceTimersByTime(20)
114
- await Promise.resolve()
115
- await Promise.resolve()
116
-
117
- const p2 = c.setDeviceState('d-debounce', { command: 'two' })
118
- vi.advanceTimersByTime(20)
119
- await Promise.resolve()
120
- await Promise.resolve()
121
-
122
- const [r1, r2] = await Promise.all([p1, p2])
123
- expect(r1).toEqual(respBody)
124
- expect(r2).toEqual(respBody)
125
- expect(fetchMock).toHaveBeenCalledTimes(2)
126
- })
127
- })
@@ -1,63 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
-
3
- import { SwitchBotClient } from '../src/switchbotClient'
4
-
5
- describe('switchBotClient OpenAPI fallback', () => {
6
- let origFetch: any
7
-
8
- beforeEach(() => {
9
- origFetch = (globalThis as any).fetch
10
- })
11
-
12
- afterEach(() => {
13
- (globalThis as any).fetch = origFetch
14
- vi.restoreAllMocks()
15
- })
16
-
17
- it('throws when no client and no token', async () => {
18
- const c = new SwitchBotClient({ log: console, logger: console } as any)
19
- await c.init()
20
- await expect(c.getDevice('nope')).rejects.toThrow('No SwitchBot client available')
21
- })
22
-
23
- it('uses OpenAPI when token provided for getDevice', async () => {
24
- const fakeResp = { body: { device: { id: 'd1', state: 'ok' } } }
25
- const fetchMock = vi.fn(async () => ({ json: async () => fakeResp }))
26
- ;(globalThis as any).fetch = fetchMock
27
-
28
- const c = new SwitchBotClient({ openApiToken: 'token-123', log: console, logger: console } as any)
29
- await c.init()
30
- const res = await c.getDevice('d1')
31
- expect(res).toEqual(fakeResp)
32
- expect(fetchMock).toHaveBeenCalledWith('https://api.switch-bot.com/v1.0/devices/d1', { headers: { Authorization: 'token-123' } })
33
- })
34
-
35
- it('posts commands to OpenAPI for setDeviceState', async () => {
36
- const respBody = { status: 'ok' }
37
- const fetchMock = vi.fn(async () => ({ json: async () => respBody }))
38
- ;(globalThis as any).fetch = fetchMock
39
-
40
- const c = new SwitchBotClient({ openApiToken: 't', log: console, logger: console } as any)
41
- await c.init()
42
- const body = { command: 'turnOn', parameter: 'default', commandType: 'command' }
43
- const out = await c.setDeviceState('d2', body)
44
- expect(out).toEqual(respBody)
45
- expect(fetchMock).toHaveBeenCalledWith('https://api.switch-bot.com/v1.0/devices/d2/commands', expect.objectContaining({ method: 'POST' }))
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
- }
51
- expect(calledBody).toEqual(body)
52
- })
53
-
54
- it('propagates fetch errors', async () => {
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)
60
- await c.init()
61
- await expect(c.getDevice('x')).rejects.toThrow('network')
62
- })
63
- })
@@ -1,11 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { SwitchBotClient } from '../src/switchbotClient'
4
-
5
- describe('switchBotClient', () => {
6
- it('init does not throw and getDevice throws when no token/client', async () => {
7
- const c = new SwitchBotClient({ log: console, logger: console })
8
- await c.init()
9
- await expect(c.getDevice('nope')).rejects.toThrow()
10
- })
11
- })
@@ -1,37 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { createPlatformProxy } from '../src/utils'
4
-
5
- describe('createPlatformProxy', () => {
6
- it('returns Matter platform when preferMatter, enabled, and Matter API is available', () => {
7
- class HAP {}
8
- class Matter {}
9
- const ProxyCtor: any = createPlatformProxy(HAP, Matter)
10
- const api = {
11
- isMatterAvailable: () => true,
12
- isMatterEnabled: () => true,
13
- }
14
- const impl = new (ProxyCtor as any)(console, { preferMatter: true, enableMatter: true }, api)
15
- expect(impl).toBeInstanceOf(Matter)
16
- })
17
-
18
- it('returns HAP platform when preferMatter is false', () => {
19
- class HAP {}
20
- class Matter {}
21
- const ProxyCtor: any = createPlatformProxy(HAP, Matter)
22
- const impl = new (ProxyCtor as any)(console, { preferMatter: false, enableMatter: true }, undefined)
23
- expect(impl).toBeInstanceOf(HAP)
24
- })
25
-
26
- it('returns HAP platform when Matter API is unavailable', () => {
27
- class HAP {}
28
- class Matter {}
29
- const ProxyCtor: any = createPlatformProxy(HAP, Matter)
30
- const api = {
31
- isMatterAvailable: () => false,
32
- isMatterEnabled: () => false,
33
- }
34
- const impl = new (ProxyCtor as any)(console, { preferMatter: true, enableMatter: true }, api)
35
- expect(impl).toBeInstanceOf(HAP)
36
- })
37
- })