@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.
- package/dist/errors.d.ts +37 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +31 -50
- package/dist/errors.js.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/platform.d.ts +2 -1
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +1 -1
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts +0 -4
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +16 -90
- package/dist/switchbotClient.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +3 -3
- package/src/errors.ts +30 -38
- package/src/homebridge-ui/endpoints/devices.ts +29 -14
- package/src/platform.ts +1 -1
- package/src/switchbotClient.ts +16 -90
- package/test/client/switchbot-client-debounce.spec.ts +35 -0
- package/test/client/switchbot-client-openapi.spec.ts +19 -0
- package/test/client/switchbotClient.spec.ts +23 -0
- package/test/device/device-mapping.spec.ts +23 -0
- package/test/device/deviceBase.spec.ts +26 -0
- package/test/device/deviceFactory-edge.spec.ts +15 -0
- package/test/device/deviceFactory.spec.ts +33 -0
- package/test/device/fan-swing.spec.ts +34 -0
- package/test/device/irdevice.spec.ts +9 -0
- package/test/device/lock-users.spec.ts +35 -0
- package/test/device/matter-descriptors.spec.ts +22 -0
- package/test/device/matter-device-state.spec.ts +37 -0
- package/test/errors/errors.spec.ts +10 -0
- package/test/homebridge-ui/server.spec.ts +9 -0
- package/test/platform/accessory-restore.spec.ts +36 -0
- package/test/platform/matter-childbridge.spec.ts +34 -0
- package/test/platform/matter-integration.spec.ts +33 -0
- package/test/platform/platform-edge.spec.ts +72 -0
- package/test/platform/platform.integration.spec.ts +34 -0
- package/test/utils/utils-extra.spec.ts +10 -0
- package/test/utils/utils.spec.ts +53 -0
- package/todo/TODO.md +55 -165
- package/test/accessory-restore.spec.ts +0 -74
- package/test/device-mapping.spec.ts +0 -38
- package/test/deviceFactory.spec.ts +0 -19
- package/test/fan-swing.spec.ts +0 -30
- package/test/lock-users.spec.ts +0 -44
- package/test/matter-childbridge.spec.ts +0 -62
- package/test/matter-descriptors.spec.ts +0 -110
- package/test/matter-device-state.spec.ts +0 -117
- package/test/matter-integration.spec.ts +0 -183
- package/test/platform.integration.spec.ts +0 -57
- package/test/switchbot-client-debounce.spec.ts +0 -127
- package/test/switchbot-client-openapi.spec.ts +0 -63
- package/test/switchbotClient.spec.ts +0 -11
- package/test/utils.spec.ts +0 -37
- package/todo/COMPLETED.md +0 -542
- package/todo/README.md +0 -59
- package/todo/mockups/filtering-sorting.svg +0 -112
- package/todo/mockups/modal-switchbot-theme.svg +0 -66
- 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
|
-
})
|
package/test/utils.spec.ts
DELETED
|
@@ -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
|
-
})
|