@homebridge-plugins/homebridge-august 3.4.1 → 3.4.2-beta.1
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/CHANGELOG.md +23 -0
- package/README.md +1 -2
- package/dist/Platform.HAP.d.ts +4 -1
- package/dist/Platform.HAP.d.ts.map +1 -1
- package/dist/Platform.HAP.js +50 -5
- package/dist/Platform.HAP.js.map +1 -1
- package/dist/devices/device.js +2 -2
- package/dist/devices/device.js.map +1 -1
- package/dist/devices/lock.d.ts.map +1 -1
- package/dist/devices/lock.js +23 -10
- package/dist/devices/lock.js.map +1 -1
- package/dist/homebridge-ui/public/index.html +2 -3
- package/dist/platform.matter.d.ts +1 -0
- package/dist/platform.matter.d.ts.map +1 -1
- package/dist/platform.matter.js +27 -1
- package/dist/platform.matter.js.map +1 -1
- package/package.json +7 -20
- package/dist/devices/device.test.d.ts +0 -2
- package/dist/devices/device.test.d.ts.map +0 -1
- package/dist/devices/device.test.js +0 -294
- package/dist/devices/device.test.js.map +0 -1
- package/test/connectivity-manager.test.ts +0 -592
- package/test/platform.matter.test.ts +0 -743
- package/test/platform.polling.test.ts +0 -313
- package/test/platform.test.ts +0 -642
- package/test/redact.test.ts +0 -86
- package/test/settings.test.ts +0 -148
|
@@ -1,743 +0,0 @@
|
|
|
1
|
-
/* Copyright(C) 2021-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
|
|
2
|
-
*
|
|
3
|
-
* platform.matter.test.ts: homebridge-august Matter platform tests.
|
|
4
|
-
*/
|
|
5
|
-
import type { API, Logging, MatterAccessory, PlatformAccessory } from 'homebridge'
|
|
6
|
-
|
|
7
|
-
import type { AugustPlatformConfig } from '../src/settings.js'
|
|
8
|
-
|
|
9
|
-
import { readFileSync } from 'node:fs'
|
|
10
|
-
|
|
11
|
-
import August from 'august-yale'
|
|
12
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
13
|
-
|
|
14
|
-
import { AugustMatterPlatform } from '../src/platform.matter.js'
|
|
15
|
-
|
|
16
|
-
// Mock the august-yale module
|
|
17
|
-
vi.mock('august-yale', () => {
|
|
18
|
-
// eslint-disable-next-line prefer-arrow-callback
|
|
19
|
-
const MockAugust = vi.fn().mockImplementation(function () {
|
|
20
|
-
return {
|
|
21
|
-
end: vi.fn(),
|
|
22
|
-
lock: vi.fn().mockResolvedValue(undefined),
|
|
23
|
-
unlock: vi.fn().mockResolvedValue(undefined),
|
|
24
|
-
details: vi.fn(),
|
|
25
|
-
}
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
const MockConstructor = MockAugust as any
|
|
29
|
-
MockConstructor.details = vi.fn()
|
|
30
|
-
MockConstructor.authorize = vi.fn()
|
|
31
|
-
MockConstructor.validate = vi.fn()
|
|
32
|
-
MockConstructor.subscribe = vi.fn().mockResolvedValue(() => {}) // returns unsubscribe fn by default
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
default: MockConstructor,
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
// Mock file system
|
|
40
|
-
vi.mock('node:fs', () => ({
|
|
41
|
-
readFileSync: vi.fn(),
|
|
42
|
-
writeFileSync: vi.fn(),
|
|
43
|
-
}))
|
|
44
|
-
|
|
45
|
-
describe('augustMatterPlatform', () => {
|
|
46
|
-
let platform: AugustMatterPlatform
|
|
47
|
-
let mockApi: API
|
|
48
|
-
let mockLog: Logging
|
|
49
|
-
let mockConfig: AugustPlatformConfig
|
|
50
|
-
let mockMatterApi: any
|
|
51
|
-
|
|
52
|
-
beforeEach(() => {
|
|
53
|
-
vi.clearAllMocks()
|
|
54
|
-
|
|
55
|
-
mockMatterApi = {
|
|
56
|
-
uuid: { generate: vi.fn((id: string) => `matter-uuid-${id}`) },
|
|
57
|
-
deviceTypes: { DoorLock: { deviceType: 11 } },
|
|
58
|
-
types: {
|
|
59
|
-
DoorLock: {
|
|
60
|
-
LockState: {
|
|
61
|
-
NotFullyLocked: 0,
|
|
62
|
-
Locked: 1,
|
|
63
|
-
Unlocked: 2,
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
registerPlatformAccessories: vi.fn().mockResolvedValue(undefined),
|
|
68
|
-
updatePlatformAccessories: vi.fn().mockResolvedValue(undefined),
|
|
69
|
-
unregisterPlatformAccessories: vi.fn().mockResolvedValue(undefined),
|
|
70
|
-
updateAccessoryState: vi.fn().mockResolvedValue(undefined),
|
|
71
|
-
getAccessoryState: vi.fn().mockResolvedValue(undefined),
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
mockApi = {
|
|
75
|
-
hap: {
|
|
76
|
-
uuid: { generate: vi.fn(() => 'hap-uuid') },
|
|
77
|
-
},
|
|
78
|
-
user: {
|
|
79
|
-
configPath: vi.fn(() => '/mock/config.json'),
|
|
80
|
-
},
|
|
81
|
-
on: vi.fn(),
|
|
82
|
-
registerPlatformAccessories: vi.fn(),
|
|
83
|
-
updatePlatformAccessories: vi.fn(),
|
|
84
|
-
unregisterPlatformAccessories: vi.fn(),
|
|
85
|
-
publishExternalAccessories: vi.fn(),
|
|
86
|
-
isMatterAvailable: vi.fn(() => true),
|
|
87
|
-
isMatterEnabled: vi.fn(() => true),
|
|
88
|
-
matter: mockMatterApi,
|
|
89
|
-
} as unknown as API
|
|
90
|
-
|
|
91
|
-
mockLog = {
|
|
92
|
-
info: vi.fn(),
|
|
93
|
-
warn: vi.fn(),
|
|
94
|
-
error: vi.fn(),
|
|
95
|
-
debug: vi.fn(),
|
|
96
|
-
success: vi.fn(),
|
|
97
|
-
} as any
|
|
98
|
-
|
|
99
|
-
mockConfig = {
|
|
100
|
-
platform: 'August',
|
|
101
|
-
name: 'Test August',
|
|
102
|
-
credentials: {
|
|
103
|
-
augustId: 'test@example.com',
|
|
104
|
-
password: 'testpassword',
|
|
105
|
-
countryCode: 'US',
|
|
106
|
-
installId: 'test-install-id',
|
|
107
|
-
isValidated: true,
|
|
108
|
-
},
|
|
109
|
-
options: {
|
|
110
|
-
logging: 'debug',
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify({
|
|
115
|
-
platforms: [mockConfig],
|
|
116
|
-
}))
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
vi.restoreAllMocks()
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
describe('configureAccessory', () => {
|
|
124
|
-
it('should stage (not immediately unregister) cached HAP accessories for deferred cleanup', async () => {
|
|
125
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
126
|
-
|
|
127
|
-
const hapAccessory = {
|
|
128
|
-
UUID: 'hap-uuid-1',
|
|
129
|
-
displayName: 'Front Door',
|
|
130
|
-
} as unknown as PlatformAccessory
|
|
131
|
-
|
|
132
|
-
await platform.configureAccessory(hapAccessory)
|
|
133
|
-
|
|
134
|
-
// HAP accessory should NOT be unregistered immediately; deferred until Matter registration succeeds
|
|
135
|
-
expect(mockApi.unregisterPlatformAccessories).not.toHaveBeenCalled()
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('should unregister staged HAP accessory after successful Matter registration', async () => {
|
|
139
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
140
|
-
|
|
141
|
-
const device = {
|
|
142
|
-
lockId: 'lock-abc',
|
|
143
|
-
LockName: 'Front Door',
|
|
144
|
-
SerialNumber: 'SN123',
|
|
145
|
-
skuNumber: 'AUG-SL05',
|
|
146
|
-
currentFirmwareVersion: '1.2.3',
|
|
147
|
-
hide_device: false,
|
|
148
|
-
homeKitEnabled: false,
|
|
149
|
-
} as any
|
|
150
|
-
|
|
151
|
-
// Stage a HAP accessory with UUID matching what matterApi.uuid.generate will return for this lockId
|
|
152
|
-
const hapAccessory = {
|
|
153
|
-
UUID: 'matter-uuid-lock-abc',
|
|
154
|
-
displayName: 'Front Door',
|
|
155
|
-
} as unknown as PlatformAccessory
|
|
156
|
-
await platform.configureAccessory(hapAccessory)
|
|
157
|
-
|
|
158
|
-
await (platform as any).Lock(device)
|
|
159
|
-
|
|
160
|
-
// After Matter registration succeeds, the staged HAP accessory should be unregistered
|
|
161
|
-
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(
|
|
162
|
-
'homebridge-august',
|
|
163
|
-
'August',
|
|
164
|
-
[hapAccessory],
|
|
165
|
-
)
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
it('should not add HAP accessories to the accessories cache', async () => {
|
|
169
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
170
|
-
|
|
171
|
-
const hapAccessory = {
|
|
172
|
-
UUID: 'hap-uuid-2',
|
|
173
|
-
displayName: 'Back Door',
|
|
174
|
-
} as unknown as PlatformAccessory
|
|
175
|
-
|
|
176
|
-
await platform.configureAccessory(hapAccessory)
|
|
177
|
-
|
|
178
|
-
// accessories array from the HAP base class should remain empty
|
|
179
|
-
expect(platform.accessories).toHaveLength(0)
|
|
180
|
-
})
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
describe('configureMatterAccessory', () => {
|
|
184
|
-
it('should store Matter accessories in the matterAccessories map', () => {
|
|
185
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
186
|
-
|
|
187
|
-
const matterAccessory = {
|
|
188
|
-
UUID: 'matter-uuid-lock1',
|
|
189
|
-
displayName: 'Front Door',
|
|
190
|
-
deviceType: { deviceType: 11 },
|
|
191
|
-
serialNumber: 'ABC123',
|
|
192
|
-
manufacturer: 'August Home Inc.',
|
|
193
|
-
model: 'AUG-SL05',
|
|
194
|
-
context: { lockId: 'lock1' },
|
|
195
|
-
} as unknown as MatterAccessory
|
|
196
|
-
|
|
197
|
-
platform.configureMatterAccessory(matterAccessory)
|
|
198
|
-
|
|
199
|
-
expect(platform.matterAccessories.has('matter-uuid-lock1')).toBe(true)
|
|
200
|
-
expect(platform.matterAccessories.get('matter-uuid-lock1')).toBe(matterAccessory)
|
|
201
|
-
})
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
describe('lock (Matter accessory registration)', () => {
|
|
205
|
-
it('should register a new Matter DoorLock accessory when Matter API is available', async () => {
|
|
206
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
207
|
-
|
|
208
|
-
const device = {
|
|
209
|
-
lockId: 'lock-abc',
|
|
210
|
-
LockName: 'Front Door',
|
|
211
|
-
SerialNumber: 'SN123',
|
|
212
|
-
skuNumber: 'AUG-SL05',
|
|
213
|
-
currentFirmwareVersion: '1.2.3',
|
|
214
|
-
hide_device: false,
|
|
215
|
-
homeKitEnabled: false,
|
|
216
|
-
} as any
|
|
217
|
-
|
|
218
|
-
// Expose the protected Lock method for testing
|
|
219
|
-
await (platform as any).Lock(device)
|
|
220
|
-
|
|
221
|
-
expect(mockMatterApi.registerPlatformAccessories).toHaveBeenCalledOnce()
|
|
222
|
-
const [pluginName, platformName, accessories] = mockMatterApi.registerPlatformAccessories.mock.calls[0]
|
|
223
|
-
expect(pluginName).toBe('homebridge-august')
|
|
224
|
-
expect(platformName).toBe('August')
|
|
225
|
-
expect(accessories).toHaveLength(1)
|
|
226
|
-
expect(accessories[0].deviceType).toBe(mockMatterApi.deviceTypes.DoorLock)
|
|
227
|
-
expect(accessories[0].manufacturer).toBe('August Home Inc.')
|
|
228
|
-
expect(accessories[0].serialNumber).toBe('SN123')
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
it('should restore an existing cached Matter accessory', async () => {
|
|
232
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
233
|
-
|
|
234
|
-
const uuid = 'matter-uuid-lock-abc'
|
|
235
|
-
const cachedAccessory: MatterAccessory = {
|
|
236
|
-
UUID: uuid,
|
|
237
|
-
displayName: 'Front Door',
|
|
238
|
-
deviceType: { deviceType: 11 } as any,
|
|
239
|
-
serialNumber: 'SN123',
|
|
240
|
-
manufacturer: 'August Home Inc.',
|
|
241
|
-
model: 'AUG-SL05',
|
|
242
|
-
context: { lockId: 'lock-abc' },
|
|
243
|
-
}
|
|
244
|
-
platform.configureMatterAccessory(cachedAccessory)
|
|
245
|
-
|
|
246
|
-
const device = {
|
|
247
|
-
lockId: 'lock-abc',
|
|
248
|
-
LockName: 'Front Door',
|
|
249
|
-
SerialNumber: 'SN123',
|
|
250
|
-
skuNumber: 'AUG-SL05',
|
|
251
|
-
currentFirmwareVersion: '1.2.3',
|
|
252
|
-
hide_device: false,
|
|
253
|
-
homeKitEnabled: false,
|
|
254
|
-
} as any
|
|
255
|
-
|
|
256
|
-
await (platform as any).Lock(device)
|
|
257
|
-
|
|
258
|
-
// re-registers to update handlers
|
|
259
|
-
expect(mockMatterApi.registerPlatformAccessories).toHaveBeenCalledOnce()
|
|
260
|
-
// Should have logged the "Restoring" message
|
|
261
|
-
expect(mockLog.info).toHaveBeenCalledWith(expect.stringContaining('Restoring existing Matter accessory'))
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
it('should unregister stale Matter accessory when device is hidden', async () => {
|
|
265
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
266
|
-
|
|
267
|
-
const uuid = 'matter-uuid-hidden-lock'
|
|
268
|
-
mockMatterApi.uuid.generate.mockReturnValueOnce(uuid)
|
|
269
|
-
|
|
270
|
-
const cachedAccessory: MatterAccessory = {
|
|
271
|
-
UUID: uuid,
|
|
272
|
-
displayName: 'Hidden Lock',
|
|
273
|
-
deviceType: { deviceType: 11 } as any,
|
|
274
|
-
serialNumber: 'SN456',
|
|
275
|
-
manufacturer: 'August Home Inc.',
|
|
276
|
-
model: 'AUG-SL05',
|
|
277
|
-
context: { lockId: 'hidden-lock' },
|
|
278
|
-
}
|
|
279
|
-
platform.configureMatterAccessory(cachedAccessory)
|
|
280
|
-
|
|
281
|
-
const device = {
|
|
282
|
-
lockId: 'hidden-lock',
|
|
283
|
-
LockName: 'Hidden Lock',
|
|
284
|
-
SerialNumber: 'SN456',
|
|
285
|
-
hide_device: true, // hidden device
|
|
286
|
-
homeKitEnabled: false,
|
|
287
|
-
} as any
|
|
288
|
-
|
|
289
|
-
await (platform as any).Lock(device)
|
|
290
|
-
|
|
291
|
-
// Should unregister the stale accessory
|
|
292
|
-
expect(mockMatterApi.unregisterPlatformAccessories).toHaveBeenCalledWith(
|
|
293
|
-
'homebridge-august',
|
|
294
|
-
'August',
|
|
295
|
-
[cachedAccessory],
|
|
296
|
-
)
|
|
297
|
-
expect(platform.matterAccessories.has(uuid)).toBe(false)
|
|
298
|
-
expect(mockMatterApi.registerPlatformAccessories).not.toHaveBeenCalled()
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
it('should not register when Matter API is unavailable', async () => {
|
|
302
|
-
const apiWithoutMatter = {
|
|
303
|
-
...mockApi,
|
|
304
|
-
matter: null as any,
|
|
305
|
-
}
|
|
306
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, apiWithoutMatter as any)
|
|
307
|
-
|
|
308
|
-
const device = {
|
|
309
|
-
lockId: 'lock-xyz',
|
|
310
|
-
LockName: 'Some Lock',
|
|
311
|
-
hide_device: false,
|
|
312
|
-
homeKitEnabled: false,
|
|
313
|
-
} as any
|
|
314
|
-
|
|
315
|
-
await (platform as any).Lock(device)
|
|
316
|
-
|
|
317
|
-
expect(mockMatterApi.registerPlatformAccessories).not.toHaveBeenCalled()
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
it('should set initial lockState to 0 (NotFullyLocked) until first poll resolves', async () => {
|
|
321
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
322
|
-
|
|
323
|
-
const device = {
|
|
324
|
-
lockId: 'lock-nullstate',
|
|
325
|
-
LockName: 'Null State Lock',
|
|
326
|
-
SerialNumber: 'SN789',
|
|
327
|
-
hide_device: false,
|
|
328
|
-
homeKitEnabled: false,
|
|
329
|
-
} as any
|
|
330
|
-
|
|
331
|
-
await (platform as any).Lock(device)
|
|
332
|
-
|
|
333
|
-
const registeredAccessory = mockMatterApi.registerPlatformAccessories.mock.calls[0][2][0] as MatterAccessory
|
|
334
|
-
expect(registeredAccessory.clusters?.doorLock?.lockState).toBe(0)
|
|
335
|
-
})
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
describe('fetchAndUpdateMatterLockState', () => {
|
|
339
|
-
// Helper: install a mock connectivity manager whose execute() runs
|
|
340
|
-
// the supplied fn against a mock August client. This mirrors the
|
|
341
|
-
// production path (platform.connectivity.execute(label, fn) ->
|
|
342
|
-
// fn(client)) without spinning up the real ConnectivityManager.
|
|
343
|
-
function installConnectivityMock(p: AugustMatterPlatform, client: any) {
|
|
344
|
-
;(p as any).connectivity = {
|
|
345
|
-
getState: () => 'healthy',
|
|
346
|
-
execute: vi.fn(async (_label: string, fn: (c: any) => Promise<any>) => {
|
|
347
|
-
try {
|
|
348
|
-
return await fn(client)
|
|
349
|
-
} catch {
|
|
350
|
-
// execute() returns undefined on failure rather than
|
|
351
|
-
// rethrowing (unless throwOnOffline:true). Match that.
|
|
352
|
-
return undefined
|
|
353
|
-
}
|
|
354
|
-
}),
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
it('should update lockState to 1 (Locked) when lock is locked', async () => {
|
|
359
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
360
|
-
|
|
361
|
-
const mockDetailsResult = { LockStatus: { state: { locked: true, unlocked: false } } }
|
|
362
|
-
installConnectivityMock(platform, { details: vi.fn().mockResolvedValue(mockDetailsResult) })
|
|
363
|
-
|
|
364
|
-
const device = { lockId: 'lock-test', LockName: 'Test Lock' } as any
|
|
365
|
-
|
|
366
|
-
await platform.fetchAndUpdateMatterLockState(device, 'test-uuid', mockMatterApi)
|
|
367
|
-
|
|
368
|
-
expect(mockMatterApi.updateAccessoryState).toHaveBeenCalledWith('test-uuid', 'doorLock', { lockState: 1 })
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
it('should update lockState to 2 (Unlocked) when lock is unlocked', async () => {
|
|
372
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
373
|
-
|
|
374
|
-
const mockDetailsResult = { LockStatus: { state: { locked: false, unlocked: true } } }
|
|
375
|
-
installConnectivityMock(platform, { details: vi.fn().mockResolvedValue(mockDetailsResult) })
|
|
376
|
-
|
|
377
|
-
const device = { lockId: 'lock-test', LockName: 'Test Lock' } as any
|
|
378
|
-
|
|
379
|
-
await platform.fetchAndUpdateMatterLockState(device, 'test-uuid', mockMatterApi)
|
|
380
|
-
|
|
381
|
-
expect(mockMatterApi.updateAccessoryState).toHaveBeenCalledWith('test-uuid', 'doorLock', { lockState: 2 })
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
it('should update lockState to 0 (NotFullyLocked) for ambiguous state', async () => {
|
|
385
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
386
|
-
|
|
387
|
-
const mockDetailsResult = { LockStatus: { state: { locked: false, unlocked: false } } }
|
|
388
|
-
installConnectivityMock(platform, { details: vi.fn().mockResolvedValue(mockDetailsResult) })
|
|
389
|
-
|
|
390
|
-
const device = { lockId: 'lock-test', LockName: 'Test Lock' } as any
|
|
391
|
-
|
|
392
|
-
await platform.fetchAndUpdateMatterLockState(device, 'test-uuid', mockMatterApi)
|
|
393
|
-
|
|
394
|
-
expect(mockMatterApi.updateAccessoryState).toHaveBeenCalledWith('test-uuid', 'doorLock', { lockState: 0 })
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
it('should not throw when August API details call fails', async () => {
|
|
398
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
399
|
-
|
|
400
|
-
installConnectivityMock(platform, { details: vi.fn().mockRejectedValue(new Error('API error')) })
|
|
401
|
-
|
|
402
|
-
const device = { lockId: 'lock-test', LockName: 'Test Lock' } as any
|
|
403
|
-
|
|
404
|
-
await expect(
|
|
405
|
-
platform.fetchAndUpdateMatterLockState(device, 'test-uuid', mockMatterApi),
|
|
406
|
-
).resolves.toBeUndefined()
|
|
407
|
-
|
|
408
|
-
expect(mockMatterApi.updateAccessoryState).not.toHaveBeenCalled()
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
it('should not call updateAccessoryState when connectivity is not initialized', async () => {
|
|
412
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
413
|
-
// connectivity is not set
|
|
414
|
-
|
|
415
|
-
const device = { lockId: 'lock-test', LockName: 'Test Lock' } as any
|
|
416
|
-
|
|
417
|
-
await expect(
|
|
418
|
-
platform.fetchAndUpdateMatterLockState(device, 'test-uuid', mockMatterApi),
|
|
419
|
-
).resolves.toBeUndefined()
|
|
420
|
-
|
|
421
|
-
expect(mockMatterApi.updateAccessoryState).not.toHaveBeenCalled()
|
|
422
|
-
})
|
|
423
|
-
|
|
424
|
-
it('should not throw when execute returns undefined (offline / failed)', async () => {
|
|
425
|
-
// The previous "retry with session refresh" test asserted that the
|
|
426
|
-
// matter path called refreshAugustSession() after a 502. That
|
|
427
|
-
// explicit retry layer is gone — ConnectivityManager handles
|
|
428
|
-
// retry/probe/rebuild internally. From the matter path's
|
|
429
|
-
// perspective, the only observable is: execute() either returned
|
|
430
|
-
// a result, or it returned undefined.
|
|
431
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
432
|
-
;(platform as any).connectivity = {
|
|
433
|
-
getState: () => 'offline',
|
|
434
|
-
execute: vi.fn().mockResolvedValue(undefined),
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const device = { lockId: 'lock-test', LockName: 'Test Lock' } as any
|
|
438
|
-
|
|
439
|
-
await expect(
|
|
440
|
-
platform.fetchAndUpdateMatterLockState(device, 'test-uuid', mockMatterApi),
|
|
441
|
-
).resolves.toBeUndefined()
|
|
442
|
-
|
|
443
|
-
expect(mockMatterApi.updateAccessoryState).not.toHaveBeenCalled()
|
|
444
|
-
})
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
describe('pubNub subscription lifecycle (Matter path equivalent of PR #206)', () => {
|
|
448
|
-
it('should capture unsubscribe function from August.subscribe()', async () => {
|
|
449
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
450
|
-
|
|
451
|
-
const mockUnsubscribe = vi.fn()
|
|
452
|
-
vi.mocked(August.subscribe).mockResolvedValueOnce(mockUnsubscribe)
|
|
453
|
-
|
|
454
|
-
const device = {
|
|
455
|
-
lockId: 'lock-sub',
|
|
456
|
-
LockName: 'Sub Lock',
|
|
457
|
-
SerialNumber: 'SN001',
|
|
458
|
-
hide_device: false,
|
|
459
|
-
homeKitEnabled: false,
|
|
460
|
-
} as any
|
|
461
|
-
|
|
462
|
-
await (platform as any).Lock(device)
|
|
463
|
-
|
|
464
|
-
// The unsubscribe should be stored in the internal map
|
|
465
|
-
const uuid = mockMatterApi.uuid.generate('lock-sub')
|
|
466
|
-
const stored = (platform as any).matterPubNubUnsubscribes.get(uuid)
|
|
467
|
-
expect(stored).toBe(mockUnsubscribe)
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
it('should tear down PubNub subscription when device is hidden (stale accessory removal)', async () => {
|
|
471
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
472
|
-
|
|
473
|
-
const uuid = 'matter-uuid-hidden-sub'
|
|
474
|
-
mockMatterApi.uuid.generate.mockReturnValueOnce(uuid)
|
|
475
|
-
|
|
476
|
-
const mockUnsubscribe = vi.fn()
|
|
477
|
-
;(platform as any).matterPubNubUnsubscribes.set(uuid, mockUnsubscribe)
|
|
478
|
-
|
|
479
|
-
const cachedAccessory: MatterAccessory = {
|
|
480
|
-
UUID: uuid,
|
|
481
|
-
displayName: 'Hidden Lock',
|
|
482
|
-
deviceType: { deviceType: 11 } as any,
|
|
483
|
-
serialNumber: 'SN002',
|
|
484
|
-
manufacturer: 'August Home Inc.',
|
|
485
|
-
model: 'AUG-SL05',
|
|
486
|
-
context: { lockId: 'hidden-sub' },
|
|
487
|
-
}
|
|
488
|
-
platform.configureMatterAccessory(cachedAccessory)
|
|
489
|
-
|
|
490
|
-
const device = {
|
|
491
|
-
lockId: 'hidden-sub',
|
|
492
|
-
LockName: 'Hidden Lock',
|
|
493
|
-
hide_device: true,
|
|
494
|
-
homeKitEnabled: false,
|
|
495
|
-
} as any
|
|
496
|
-
|
|
497
|
-
await (platform as any).Lock(device)
|
|
498
|
-
|
|
499
|
-
expect(mockUnsubscribe).toHaveBeenCalledOnce()
|
|
500
|
-
expect((platform as any).matterPubNubUnsubscribes.has(uuid)).toBe(false)
|
|
501
|
-
})
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
describe('polling subscription lifecycle', () => {
|
|
505
|
-
it('should track the polling subscription so it can be disposed', async () => {
|
|
506
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
507
|
-
|
|
508
|
-
const device = {
|
|
509
|
-
lockId: 'lock-poll-track',
|
|
510
|
-
LockName: 'Tracked Lock',
|
|
511
|
-
SerialNumber: 'SN100',
|
|
512
|
-
hide_device: false,
|
|
513
|
-
homeKitEnabled: false,
|
|
514
|
-
} as any
|
|
515
|
-
|
|
516
|
-
await (platform as any).Lock(device)
|
|
517
|
-
|
|
518
|
-
const uuid = mockMatterApi.uuid.generate('lock-poll-track')
|
|
519
|
-
const stored = (platform as any).matterPollingSubscriptions.get(uuid)
|
|
520
|
-
expect(stored).toBeDefined()
|
|
521
|
-
expect(typeof stored.unsubscribe).toBe('function')
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
it('should dispose previous polling subscription before starting a new one (idempotent)', async () => {
|
|
525
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
526
|
-
|
|
527
|
-
const device = {
|
|
528
|
-
lockId: 'lock-poll-idem',
|
|
529
|
-
LockName: 'Idempotent Lock',
|
|
530
|
-
SerialNumber: 'SN101',
|
|
531
|
-
hide_device: false,
|
|
532
|
-
homeKitEnabled: false,
|
|
533
|
-
} as any
|
|
534
|
-
|
|
535
|
-
await (platform as any).Lock(device)
|
|
536
|
-
|
|
537
|
-
const uuid = mockMatterApi.uuid.generate('lock-poll-idem')
|
|
538
|
-
const firstSub = (platform as any).matterPollingSubscriptions.get(uuid)
|
|
539
|
-
const firstUnsubSpy = vi.spyOn(firstSub, 'unsubscribe')
|
|
540
|
-
|
|
541
|
-
// Call polling again (simulating re-registration)
|
|
542
|
-
;(platform as any).startMatterStatusPolling(device, uuid, mockMatterApi)
|
|
543
|
-
|
|
544
|
-
// Previous subscription should have been disposed
|
|
545
|
-
expect(firstUnsubSpy).toHaveBeenCalledOnce()
|
|
546
|
-
// A new subscription should be stored
|
|
547
|
-
const secondSub = (platform as any).matterPollingSubscriptions.get(uuid)
|
|
548
|
-
expect(secondSub).toBeDefined()
|
|
549
|
-
expect(secondSub).not.toBe(firstSub)
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
it('should dispose polling subscription when device is hidden', async () => {
|
|
553
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
554
|
-
|
|
555
|
-
const uuid = 'matter-uuid-hidden-poll'
|
|
556
|
-
mockMatterApi.uuid.generate.mockReturnValueOnce(uuid)
|
|
557
|
-
|
|
558
|
-
const fakeSubscription = { unsubscribe: vi.fn() }
|
|
559
|
-
;(platform as any).matterPollingSubscriptions.set(uuid, fakeSubscription)
|
|
560
|
-
|
|
561
|
-
const device = {
|
|
562
|
-
lockId: 'hidden-poll',
|
|
563
|
-
LockName: 'Hidden Poll Lock',
|
|
564
|
-
hide_device: true,
|
|
565
|
-
homeKitEnabled: false,
|
|
566
|
-
} as any
|
|
567
|
-
|
|
568
|
-
await (platform as any).Lock(device)
|
|
569
|
-
|
|
570
|
-
expect(fakeSubscription.unsubscribe).toHaveBeenCalledOnce()
|
|
571
|
-
expect((platform as any).matterPollingSubscriptions.has(uuid)).toBe(false)
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
it('should not start polling when refreshRate is 0', async () => {
|
|
575
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
576
|
-
;(platform as any).platformRefreshRate = 0
|
|
577
|
-
|
|
578
|
-
const device = {
|
|
579
|
-
lockId: 'lock-no-poll',
|
|
580
|
-
LockName: 'No Poll Lock',
|
|
581
|
-
SerialNumber: 'SN102',
|
|
582
|
-
hide_device: false,
|
|
583
|
-
homeKitEnabled: false,
|
|
584
|
-
} as any
|
|
585
|
-
|
|
586
|
-
await (platform as any).Lock(device)
|
|
587
|
-
|
|
588
|
-
const uuid = mockMatterApi.uuid.generate('lock-no-poll')
|
|
589
|
-
expect((platform as any).matterPollingSubscriptions.has(uuid)).toBe(false)
|
|
590
|
-
})
|
|
591
|
-
})
|
|
592
|
-
|
|
593
|
-
describe('pendingHapCleanup sweep after discovery', () => {
|
|
594
|
-
it('should unregister staged HAP accessories that were not handled during discovery', async () => {
|
|
595
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
596
|
-
|
|
597
|
-
// Stage two HAP accessories from a previous session
|
|
598
|
-
const orphanedHap1 = { UUID: 'hap-orphan-1', displayName: 'Removed Lock 1' } as unknown as PlatformAccessory
|
|
599
|
-
const orphanedHap2 = { UUID: 'hap-orphan-2', displayName: 'Removed Lock 2' } as unknown as PlatformAccessory
|
|
600
|
-
await platform.configureAccessory(orphanedHap1)
|
|
601
|
-
await platform.configureAccessory(orphanedHap2)
|
|
602
|
-
|
|
603
|
-
// Stub super.discoverDevices so it's a no-op (doesn't touch pendingHapCleanup)
|
|
604
|
-
const baseProto = Object.getPrototypeOf(Object.getPrototypeOf(platform))
|
|
605
|
-
vi.spyOn(baseProto, 'discoverDevices').mockResolvedValue(undefined as never)
|
|
606
|
-
|
|
607
|
-
await platform.discoverDevices()
|
|
608
|
-
|
|
609
|
-
// Both staged HAP accessories should have been unregistered since neither
|
|
610
|
-
// had a matching Lock() call during discovery
|
|
611
|
-
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(
|
|
612
|
-
'homebridge-august',
|
|
613
|
-
'August',
|
|
614
|
-
expect.arrayContaining([orphanedHap1, orphanedHap2]),
|
|
615
|
-
)
|
|
616
|
-
// Map should be empty after the sweep
|
|
617
|
-
expect((platform as any).pendingHapCleanup.size).toBe(0)
|
|
618
|
-
})
|
|
619
|
-
|
|
620
|
-
it('should leave handled HAP accessories alone (those already removed by Lock())', async () => {
|
|
621
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
622
|
-
|
|
623
|
-
// Simulate a successful Lock() having already cleaned its entry
|
|
624
|
-
// (pendingHapCleanup is empty)
|
|
625
|
-
const baseProto = Object.getPrototypeOf(Object.getPrototypeOf(platform))
|
|
626
|
-
vi.spyOn(baseProto, 'discoverDevices').mockResolvedValue(undefined as never)
|
|
627
|
-
|
|
628
|
-
await platform.discoverDevices()
|
|
629
|
-
|
|
630
|
-
// unregisterPlatformAccessories should not be called — nothing to sweep
|
|
631
|
-
expect(mockApi.unregisterPlatformAccessories).not.toHaveBeenCalled()
|
|
632
|
-
})
|
|
633
|
-
|
|
634
|
-
it('should only sweep the orphans, not the locks that were successfully migrated', async () => {
|
|
635
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
636
|
-
|
|
637
|
-
// One lock that will be migrated (has matching active lock in account)
|
|
638
|
-
// and one orphan that no longer exists in the account
|
|
639
|
-
const migrated = { UUID: 'hap-migrated', displayName: 'Still Active' } as unknown as PlatformAccessory
|
|
640
|
-
const orphan = { UUID: 'hap-orphan', displayName: 'Deleted From Account' } as unknown as PlatformAccessory
|
|
641
|
-
await platform.configureAccessory(migrated)
|
|
642
|
-
await platform.configureAccessory(orphan)
|
|
643
|
-
|
|
644
|
-
// Simulate a successful Lock() call for the 'migrated' UUID by removing
|
|
645
|
-
// that entry from pendingHapCleanup directly (mimicking what Lock() does).
|
|
646
|
-
const baseProto = Object.getPrototypeOf(Object.getPrototypeOf(platform))
|
|
647
|
-
vi.spyOn(baseProto, 'discoverDevices').mockImplementation(async () => {
|
|
648
|
-
(platform as any).pendingHapCleanup.delete('hap-migrated')
|
|
649
|
-
})
|
|
650
|
-
|
|
651
|
-
await platform.discoverDevices()
|
|
652
|
-
|
|
653
|
-
// Only the orphan should have been unregistered in the sweep
|
|
654
|
-
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledOnce()
|
|
655
|
-
const swept = (mockApi.unregisterPlatformAccessories as any).mock.calls[0][2]
|
|
656
|
-
expect(swept).toContain(orphan)
|
|
657
|
-
expect(swept).not.toContain(migrated)
|
|
658
|
-
})
|
|
659
|
-
})
|
|
660
|
-
|
|
661
|
-
describe('fetchAndUpdateMatterLockState (battery → PowerSource)', () => {
|
|
662
|
-
// Install a stub connectivity manager whose execute() runs the poll fn
|
|
663
|
-
// against a fake august client, matching the suite's conventions.
|
|
664
|
-
function withDetails(p: any, detail: any) {
|
|
665
|
-
const client = { details: vi.fn().mockResolvedValue(detail) }
|
|
666
|
-
p.connectivity = { execute: vi.fn(async (_label: string, fn: (c: any) => Promise<any>) => fn(client)) }
|
|
667
|
-
return client
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
it('pushes the battery level to the PowerSource cluster on poll', async () => {
|
|
671
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
672
|
-
withDetails(platform, { battery: 0.53 })
|
|
673
|
-
|
|
674
|
-
await (platform as any).fetchAndUpdateMatterLockState(
|
|
675
|
-
{ lockId: 'lock-1', LockName: 'Front Door' } as any,
|
|
676
|
-
'matter-uuid-lock-1',
|
|
677
|
-
mockMatterApi,
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
expect(mockMatterApi.updateAccessoryState).toHaveBeenCalledWith(
|
|
681
|
-
'matter-uuid-lock-1',
|
|
682
|
-
'powerSource',
|
|
683
|
-
expect.objectContaining({
|
|
684
|
-
batPercentRemaining: 106, // round(0.53 * 200)
|
|
685
|
-
batChargeLevel: 0, // OK
|
|
686
|
-
batReplacementNeeded: false,
|
|
687
|
-
}),
|
|
688
|
-
)
|
|
689
|
-
})
|
|
690
|
-
|
|
691
|
-
it('pushes the battery even when the detail carries no lock state, and flags a low battery', async () => {
|
|
692
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
693
|
-
withDetails(platform, { battery: 0.08 }) // low, and no LockStatus.state at all
|
|
694
|
-
|
|
695
|
-
await (platform as any).fetchAndUpdateMatterLockState(
|
|
696
|
-
{ lockId: 'lock-2', LockName: 'Kitchen Door' } as any,
|
|
697
|
-
'matter-uuid-lock-2',
|
|
698
|
-
mockMatterApi,
|
|
699
|
-
)
|
|
700
|
-
|
|
701
|
-
// Regression guard: a stateless detail response — the norm for this
|
|
702
|
-
// endpoint — must not drop the battery update.
|
|
703
|
-
expect(mockMatterApi.updateAccessoryState).toHaveBeenCalledWith(
|
|
704
|
-
'matter-uuid-lock-2',
|
|
705
|
-
'powerSource',
|
|
706
|
-
expect.objectContaining({
|
|
707
|
-
batPercentRemaining: 16, // round(0.08 * 200)
|
|
708
|
-
batChargeLevel: 2, // Critical (<10%)
|
|
709
|
-
batReplacementNeeded: true, // <15%
|
|
710
|
-
}),
|
|
711
|
-
)
|
|
712
|
-
// ...and it must not push a doorLock state it doesn't have.
|
|
713
|
-
expect(mockMatterApi.updateAccessoryState).not.toHaveBeenCalledWith(
|
|
714
|
-
'matter-uuid-lock-2',
|
|
715
|
-
'doorLock',
|
|
716
|
-
expect.anything(),
|
|
717
|
-
)
|
|
718
|
-
})
|
|
719
|
-
|
|
720
|
-
it('leaves PowerSource untouched when the detail has no battery field', async () => {
|
|
721
|
-
platform = new AugustMatterPlatform(mockLog, mockConfig, mockApi)
|
|
722
|
-
withDetails(platform, { LockStatus: { state: { locked: true } } }) // no battery
|
|
723
|
-
|
|
724
|
-
await (platform as any).fetchAndUpdateMatterLockState(
|
|
725
|
-
{ lockId: 'lock-3', LockName: 'Guest Door' } as any,
|
|
726
|
-
'matter-uuid-lock-3',
|
|
727
|
-
mockMatterApi,
|
|
728
|
-
)
|
|
729
|
-
|
|
730
|
-
expect(mockMatterApi.updateAccessoryState).not.toHaveBeenCalledWith(
|
|
731
|
-
'matter-uuid-lock-3',
|
|
732
|
-
'powerSource',
|
|
733
|
-
expect.anything(),
|
|
734
|
-
)
|
|
735
|
-
// lock state is present, so the doorLock update still happens as before.
|
|
736
|
-
expect(mockMatterApi.updateAccessoryState).toHaveBeenCalledWith(
|
|
737
|
-
'matter-uuid-lock-3',
|
|
738
|
-
'doorLock',
|
|
739
|
-
expect.objectContaining({ lockState: 1 }),
|
|
740
|
-
)
|
|
741
|
-
})
|
|
742
|
-
})
|
|
743
|
-
})
|