@switchbot/homebridge-switchbot 5.0.0-beta.62 → 5.0.0-beta.64

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.
@@ -1,453 +0,0 @@
1
- import type { API, Logger } from 'homebridge'
2
-
3
- import { beforeEach, describe, expect, it, vi } from 'vitest'
4
-
5
- // Mock the base accessory
6
- vi.mock('../../../devices-matter/BaseMatterAccessory.js', () => ({
7
- BaseMatterAccessory: class {
8
- api: any
9
- log: any
10
- config: any
11
- context: any
12
-
13
- constructor(api: API, log: Logger, config: any) {
14
- this.api = api
15
- this.log = log
16
- this.config = config
17
- this.context = config.context
18
- }
19
-
20
- logInfo = vi.fn()
21
- logWarn = vi.fn()
22
- logError = vi.fn()
23
- sendOpenAPICommand = vi.fn(async () => {})
24
- updateState = vi.fn()
25
- },
26
- }))
27
-
28
- describe('roboticVacuumAccessory', () => {
29
- let mockAPI: any
30
- let mockLogger: Logger
31
- let RoboticVacuumAccessory: any
32
-
33
- beforeEach(async () => {
34
- // Clear all mocks
35
- vi.clearAllMocks()
36
-
37
- // Mock API
38
- mockAPI = {
39
- matter: {
40
- uuid: {
41
- generate: vi.fn((serial: string) => `uuid-${serial}`),
42
- },
43
- deviceTypes: {
44
- RoboticVacuumCleaner: 'RoboticVacuumCleaner',
45
- },
46
- },
47
- }
48
-
49
- // Mock Logger
50
- mockLogger = {
51
- info: vi.fn(),
52
- warn: vi.fn(),
53
- error: vi.fn(),
54
- debug: vi.fn(),
55
- } as any
56
-
57
- // Dynamically import the class after mocks are set up
58
- const module = await import('../../../devices-matter/RoboticVacuumAccessory.js')
59
- RoboticVacuumAccessory = module.RoboticVacuumAccessory
60
- })
61
-
62
- describe('model detection', () => {
63
- it('should detect S1 model from Robot Vacuum Cleaner S1', () => {
64
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
65
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
66
- })
67
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
68
- expect((vacuum as any).capabilities.suctionKind).toBe('powLevel-0-3')
69
- expect((vacuum as any).capabilities.pause).toBe(false)
70
- })
71
-
72
- it('should detect S1 Plus model', () => {
73
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
74
- context: { deviceType: 'Robot Vacuum Cleaner S1 Plus' },
75
- })
76
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
77
- expect((vacuum as any).capabilities.pause).toBe(false)
78
- })
79
-
80
- it('should detect S1 Pro model', () => {
81
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
82
- context: { deviceType: 'Robot Vacuum Cleaner S1 Pro' },
83
- })
84
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
85
- })
86
-
87
- it('should detect S1 Mini model', () => {
88
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
89
- context: { deviceType: 'Robot Vacuum Cleaner S1 Mini' },
90
- })
91
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
92
- })
93
-
94
- it('should detect WoSweeper model', () => {
95
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
96
- context: { deviceType: 'WoSweeper' },
97
- })
98
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
99
- expect((vacuum as any).capabilities.suctionKind).toBe('powLevel-0-3')
100
- })
101
-
102
- it('should detect WoSweeperMini model', () => {
103
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
104
- context: { deviceType: 'WoSweeperMini' },
105
- })
106
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
107
- })
108
-
109
- it('should detect K10+ model', () => {
110
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
111
- context: { deviceType: 'K10+' },
112
- })
113
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
114
- expect((vacuum as any).capabilities.pause).toBe(false)
115
- })
116
-
117
- it('should detect K10+ Pro model', () => {
118
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
119
- context: { deviceType: 'K10+ Pro' },
120
- })
121
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
122
- expect((vacuum as any).capabilities.pause).toBe(false)
123
- })
124
-
125
- it('should detect K10+ Pro Combo model with mop capability', () => {
126
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
127
- context: { deviceType: 'Robot Vacuum Cleaner K10+ Pro Combo' },
128
- })
129
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-or-mop')
130
- expect((vacuum as any).capabilities.suctionKind).toBe('fanLevel-1-4')
131
- expect((vacuum as any).capabilities.pause).toBe(true)
132
- expect((vacuum as any).capabilities.waterLevel).toBe(false)
133
- })
134
-
135
- it('should detect S10 model with vacuum+mop and advanced commands', () => {
136
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
137
- context: { deviceType: 'Robot Vacuum Cleaner S10' },
138
- })
139
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-or-vacmop')
140
- expect((vacuum as any).capabilities.suctionKind).toBe('fanLevel-1-4')
141
- expect((vacuum as any).capabilities.pause).toBe(true)
142
- expect((vacuum as any).capabilities.waterLevel).toBe(true)
143
- expect((vacuum as any).capabilities.advancedCommands?.selfClean).toBe(true)
144
- expect((vacuum as any).capabilities.advancedCommands?.addWaterForHumi).toBe(true)
145
- })
146
-
147
- it('should detect S20 model with vacuum+mop and advanced commands', () => {
148
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
149
- context: { deviceType: 'Robot Vacuum Cleaner S20' },
150
- })
151
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-or-vacmop')
152
- expect((vacuum as any).capabilities.advancedCommands?.selfClean).toBe(true)
153
- })
154
-
155
- it('should detect K11+ model', () => {
156
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
157
- context: { deviceType: 'Robot Vacuum Cleaner K11+' },
158
- })
159
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-or-mop')
160
- expect((vacuum as any).capabilities.waterLevel).toBe(true)
161
- expect((vacuum as any).capabilities.advancedCommands?.setVolume).toBe(true)
162
- expect((vacuum as any).capabilities.advancedCommands?.selfClean).toBeUndefined()
163
- })
164
-
165
- it('should detect K20+ Pro model', () => {
166
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
167
- context: { deviceType: 'Robot Vacuum Cleaner K20 Plus Pro' },
168
- })
169
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-or-mop')
170
- expect((vacuum as any).capabilities.pause).toBe(true)
171
- })
172
-
173
- it('should default to basic vacuum for unknown models', () => {
174
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
175
- context: { deviceType: 'Unknown Model' },
176
- })
177
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
178
- expect((vacuum as any).capabilities.pause).toBe(false)
179
- expect((vacuum as any).capabilities.suctionKind).toBe('powLevel-0-3')
180
- })
181
-
182
- it('should handle missing context', () => {
183
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {})
184
- expect((vacuum as any).capabilities.cleanAction).toBe('vacuum-only')
185
- expect((vacuum as any).capabilities.pause).toBe(false)
186
- })
187
- })
188
-
189
- describe('capability matrix', () => {
190
- it('should configure correct clusters for basic vacuum (S1)', () => {
191
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
192
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
193
- })
194
- const clusters = (vacuum as any).config.clusters
195
-
196
- // Should have rvcRunMode with Idle and Cleaning only
197
- expect(clusters.rvcRunMode.supportedModes).toHaveLength(2)
198
- expect(clusters.rvcRunMode.supportedModes[0].label).toBe('Idle')
199
- expect(clusters.rvcRunMode.supportedModes[1].label).toBe('Cleaning')
200
-
201
- // Should have rvcCleanMode with suction levels
202
- expect(clusters.rvcCleanMode.supportedModes).toHaveLength(4)
203
- expect(clusters.rvcCleanMode.supportedModes.map((m: any) => m.label)).toEqual([
204
- 'Quiet',
205
- 'Standard',
206
- 'Strong',
207
- 'MAX',
208
- ])
209
-
210
- // Should not have serviceArea cluster
211
- expect(clusters.serviceArea).toBeUndefined()
212
- })
213
-
214
- it('should configure correct clusters for K10+ Pro Combo', () => {
215
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
216
- context: { deviceType: 'K10+ Pro Combo' },
217
- })
218
- const clusters = (vacuum as any).config.clusters
219
-
220
- // Should have vacuum/mop action modes
221
- expect(clusters.rvcCleanMode.supportedModes).toHaveLength(2)
222
- expect(clusters.rvcCleanMode.supportedModes.map((m: any) => m.label)).toEqual(['Vacuum', 'Mop'])
223
- })
224
-
225
- it('should configure correct clusters for S10', () => {
226
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
227
- context: { deviceType: 'Robot Vacuum Cleaner S10' },
228
- })
229
- const clusters = (vacuum as any).config.clusters
230
-
231
- // Should have vacuum and vacuum+mop modes
232
- expect(clusters.rvcCleanMode.supportedModes).toHaveLength(2)
233
- expect(clusters.rvcCleanMode.supportedModes.map((m: any) => m.label)).toEqual(['Vacuum', 'Vacuum & Mop'])
234
- })
235
-
236
- it('should include pause handler for models that support it', () => {
237
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
238
- context: { deviceType: 'K10+ Pro Combo' },
239
- })
240
- const handlers = (vacuum as any).config.handlers
241
-
242
- expect(handlers.rvcOperationalState.pause).toBeDefined()
243
- expect(handlers.rvcOperationalState.start).toBeDefined()
244
- expect(handlers.rvcOperationalState.stop).toBeDefined()
245
- expect(handlers.rvcOperationalState.goHome).toBeDefined()
246
- })
247
-
248
- it('should not include pause handler for basic models', () => {
249
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
250
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
251
- })
252
- const handlers = (vacuum as any).config.handlers
253
-
254
- expect(handlers.rvcOperationalState.pause).toBeUndefined()
255
- expect(handlers.rvcOperationalState.start).toBeDefined()
256
- })
257
- })
258
-
259
- describe('openAPI command mapping', () => {
260
- it('should send start command for basic vacuum', async () => {
261
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
262
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
263
- })
264
-
265
- await (vacuum as any).handleStart()
266
-
267
- expect((vacuum as any).sendOpenAPICommand).toHaveBeenCalledWith('start')
268
- })
269
-
270
- it('should send stop command for basic vacuum', async () => {
271
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
272
- context: { deviceType: 'K10+' },
273
- })
274
-
275
- await (vacuum as any).handleStop()
276
-
277
- expect((vacuum as any).sendOpenAPICommand).toHaveBeenCalledWith('stop')
278
- })
279
-
280
- it('should send dock command on goHome', async () => {
281
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
282
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
283
- })
284
-
285
- await (vacuum as any).handleGoHome()
286
-
287
- expect((vacuum as any).sendOpenAPICommand).toHaveBeenCalledWith('dock')
288
- })
289
-
290
- it('should send PowLevel command for basic vacuum suction change', async () => {
291
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
292
- context: { deviceType: 'WoSweeper' },
293
- })
294
-
295
- await (vacuum as any).handleChangeCleanMode({ newMode: 2 }) // Strong
296
-
297
- expect((vacuum as any).sendOpenAPICommand).toHaveBeenCalledWith('PowLevel', '2')
298
- })
299
-
300
- it('should send startClean with action for K10+ Pro Combo', async () => {
301
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
302
- context: { deviceType: 'K10+ Pro Combo' },
303
- })
304
-
305
- // Set to vacuum mode first
306
- await (vacuum as any).handleChangeCleanMode({ newMode: 0 })
307
- expect((vacuum as any).currentCleanAction).toBe('vacuum')
308
-
309
- await (vacuum as any).handleStart()
310
-
311
- const expectedPayload = JSON.stringify({
312
- action: 'vacuum',
313
- param: { fanLevel: 2 },
314
- })
315
- expect((vacuum as any).sendOpenAPICommand).toHaveBeenCalledWith('startClean', expectedPayload)
316
- })
317
-
318
- it('should send startClean with sweep_mop action for S10 vacuum+mop mode', async () => {
319
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
320
- context: { deviceType: 'Robot Vacuum Cleaner S10' },
321
- })
322
-
323
- // Set to vacuum+mop mode
324
- await (vacuum as any).handleChangeCleanMode({ newMode: 1 })
325
- expect((vacuum as any).currentCleanAction).toBe('vacuum_mop')
326
-
327
- await (vacuum as any).handleStart()
328
-
329
- const calls = (vacuum as any).sendOpenAPICommand.mock.calls
330
- const startCleanCall = calls.find((call: any) => call[0] === 'startClean')
331
- expect(startCleanCall).toBeDefined()
332
-
333
- const payload = JSON.parse(startCleanCall[1])
334
- expect(payload.action).toBe('sweep_mop')
335
- expect(payload.param.fanLevel).toBe(2)
336
- expect(payload.param.waterLevel).toBe(1)
337
- })
338
-
339
- it('should include waterLevel for S20 models', async () => {
340
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
341
- context: { deviceType: 'Robot Vacuum Cleaner S20' },
342
- })
343
-
344
- await (vacuum as any).handleStart()
345
-
346
- const calls = (vacuum as any).sendOpenAPICommand.mock.calls
347
- const startCleanCall = calls.find((call: any) => call[0] === 'startClean')
348
- const payload = JSON.parse(startCleanCall[1])
349
- expect(payload.param.waterLevel).toBe(1)
350
- })
351
-
352
- it('should not include waterLevel for K10+ Pro Combo', async () => {
353
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
354
- context: { deviceType: 'K10+ Pro Combo' },
355
- })
356
-
357
- await (vacuum as any).handleStart()
358
-
359
- const calls = (vacuum as any).sendOpenAPICommand.mock.calls
360
- const startCleanCall = calls.find((call: any) => call[0] === 'startClean')
361
- const payload = JSON.parse(startCleanCall[1])
362
- expect(payload.param.waterLevel).toBeUndefined()
363
- })
364
-
365
- it('should send pause command for models that support it', async () => {
366
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
367
- context: { deviceType: 'S10' },
368
- })
369
-
370
- await (vacuum as any).handlePause()
371
-
372
- expect((vacuum as any).sendOpenAPICommand).toHaveBeenCalledWith('pause')
373
- })
374
-
375
- it('should update local action state for clean mode changes on K10+ Pro Combo', async () => {
376
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
377
- context: { deviceType: 'K10+ Pro Combo' },
378
- })
379
-
380
- // Change to mop mode
381
- await (vacuum as any).handleChangeCleanMode({ newMode: 1 })
382
- expect((vacuum as any).currentCleanAction).toBe('mop')
383
-
384
- // Change back to vacuum mode
385
- await (vacuum as any).handleChangeCleanMode({ newMode: 0 })
386
- expect((vacuum as any).currentCleanAction).toBe('vacuum')
387
-
388
- // Clean mode changes should just update state, not call OpenAPI
389
- expect((vacuum as any).sendOpenAPICommand).not.toHaveBeenCalled()
390
- })
391
- })
392
-
393
- describe('state updates', () => {
394
- it('should update run mode correctly', () => {
395
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
396
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
397
- })
398
-
399
- vacuum.updateRunMode(1) // Cleaning
400
-
401
- expect((vacuum as any).updateState).toHaveBeenCalledWith('rvcRunMode', { currentMode: 1 })
402
- })
403
-
404
- it('should update clean mode correctly', () => {
405
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
406
- context: { deviceType: 'K10+' },
407
- })
408
-
409
- vacuum.updateCleanMode(2) // Strong
410
-
411
- expect((vacuum as any).updateState).toHaveBeenCalledWith('rvcCleanMode', { currentMode: 2 })
412
- })
413
-
414
- it('should update operational state correctly', () => {
415
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
416
- context: { deviceType: 'S10' },
417
- })
418
-
419
- vacuum.updateOperationalState(64) // Seeking Charger
420
-
421
- expect((vacuum as any).updateState).toHaveBeenCalledWith('rvcOperationalState', { operationalState: 64 })
422
- })
423
-
424
- it('should update battery percentage without PowerSource cluster', async () => {
425
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
426
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
427
- })
428
-
429
- await vacuum.updateBatteryPercentage(75)
430
-
431
- // Should log but not update any cluster
432
- expect((vacuum as any).logInfo).toHaveBeenCalledWith(expect.stringContaining('75%'))
433
- expect((vacuum as any).context.batteryPercentage).toBe(75)
434
- })
435
- })
436
-
437
- describe('error handling', () => {
438
- it('should handle OpenAPI command failures gracefully', async () => {
439
- const vacuum = new RoboticVacuumAccessory(mockAPI, mockLogger, {
440
- context: { deviceType: 'Robot Vacuum Cleaner S1' },
441
- })
442
-
443
- // Mock failure
444
- ;(vacuum as any).sendOpenAPICommand = vi.fn(async () => {
445
- throw new Error('API Error')
446
- })
447
-
448
- await (vacuum as any).handleStart()
449
-
450
- expect((vacuum as any).logWarn).toHaveBeenCalledWith(expect.stringContaining('failed'))
451
- })
452
- })
453
- })