@homebridge-plugins/homebridge-august 3.2.3 → 3.3.0-beta.0
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/.github/npm-version-script-esm.js +97 -0
- package/CHANGELOG.md +15 -0
- package/dist/Platform.Base.d.ts +288 -0
- package/dist/Platform.Base.d.ts.map +1 -0
- package/dist/Platform.Base.js +797 -0
- package/dist/Platform.Base.js.map +1 -0
- package/dist/Platform.HAP.d.ts +20 -139
- package/dist/Platform.HAP.d.ts.map +1 -1
- package/dist/Platform.HAP.js +46 -733
- package/dist/Platform.HAP.js.map +1 -1
- package/dist/connectivity-manager.d.ts +3 -3
- package/dist/connectivity-manager.d.ts.map +1 -1
- package/dist/connectivity-manager.js +57 -14
- package/dist/connectivity-manager.js.map +1 -1
- package/dist/devices/device.d.ts +5 -16
- package/dist/devices/device.d.ts.map +1 -1
- package/dist/devices/device.js +18 -75
- package/dist/devices/device.js.map +1 -1
- package/dist/devices/lock.d.ts +3 -3
- package/dist/devices/lock.d.ts.map +1 -1
- package/dist/devices/lock.js +41 -41
- package/dist/devices/lock.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/package.json +8 -7
- package/dist/platform.matter.d.ts +18 -20
- package/dist/platform.matter.d.ts.map +1 -1
- package/dist/platform.matter.js +74 -80
- package/dist/platform.matter.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +8 -7
- package/test/device.test.ts +384 -0
- package/test/{platform.test.ts → platform.hap.test.ts} +23 -23
- package/test/platform.polling.test.ts +5 -5
- package/.github/scripts/compute-version-override.cjs +0 -51
- package/.github/scripts/normalize-changelog-header.cjs +0 -86
- package/.github/scripts/npm-version-script-esm-auto.js +0 -201
- package/.github/scripts/resolve-version-bump-esm.js +0 -118
- 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 -590
- package/test/platform.matter.test.ts +0 -660
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import type { CharacteristicValue, Service } from 'homebridge'
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
// Create a simplified test implementation that mimics the updateCharacteristic behavior
|
|
6
|
+
class MockDeviceWithCharacteristics {
|
|
7
|
+
accessory = { context: {} as Record<string, CharacteristicValue>, displayName: 'Test Lock' }
|
|
8
|
+
debugLog = vi.fn()
|
|
9
|
+
debugWarnLog = vi.fn()
|
|
10
|
+
infoLog = vi.fn()
|
|
11
|
+
|
|
12
|
+
async updateCharacteristic(
|
|
13
|
+
Service: Service,
|
|
14
|
+
ServiceName: string,
|
|
15
|
+
Characteristic: any,
|
|
16
|
+
CharacteristicValue: CharacteristicValue,
|
|
17
|
+
CharacteristicName: string,
|
|
18
|
+
Value?: CharacteristicValue,
|
|
19
|
+
StatusMatch?: string,
|
|
20
|
+
StatusDoesNotMatch?: string,
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
if (CharacteristicValue === undefined) {
|
|
23
|
+
await this.debugLog(`${CharacteristicName}: ${CharacteristicValue}`)
|
|
24
|
+
} else {
|
|
25
|
+
const contextKey = `${ServiceName}${CharacteristicName}`
|
|
26
|
+
await this.debugWarnLog(`context before: ${this.accessory.context[contextKey]}`)
|
|
27
|
+
const contextBefore = this.accessory.context[contextKey]
|
|
28
|
+
this.accessory.context[contextKey] = CharacteristicValue
|
|
29
|
+
await this.debugWarnLog(`context after: ${this.accessory.context[contextKey]}`)
|
|
30
|
+
await this.debugLog(`updateCharacteristic ${CharacteristicName}: ${CharacteristicValue} (${CharacteristicValue === Value ? StatusMatch : StatusDoesNotMatch})`)
|
|
31
|
+
if (contextBefore !== CharacteristicValue) {
|
|
32
|
+
// Value actually changed - push update to HomeKit
|
|
33
|
+
Service.updateCharacteristic(Characteristic, CharacteristicValue)
|
|
34
|
+
// Only log state change messages when we had a prior known value (not initial discovery)
|
|
35
|
+
if (contextBefore !== undefined && StatusMatch && StatusDoesNotMatch) {
|
|
36
|
+
await this.infoLog(`was ${CharacteristicValue === Value ? StatusMatch : StatusDoesNotMatch}`)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create a simplified test implementation that mimics the statusCode behavior
|
|
44
|
+
class MockDevice {
|
|
45
|
+
debugLog = vi.fn()
|
|
46
|
+
debugErrorLog = vi.fn()
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if error is a network timeout or session expiration that requires session refresh
|
|
50
|
+
* Handles 502 Bad Gateway, 401 Unauthorized, 503 Service Unavailable, and network timeouts
|
|
51
|
+
*/
|
|
52
|
+
isTimeoutError(error: { message: string, name?: string, statusCode?: number }): boolean {
|
|
53
|
+
// Check for august-yale TimeoutError by name (stable contract — works regardless
|
|
54
|
+
// of message format changes) and Node.js network timeouts by message
|
|
55
|
+
if (error.name === 'TimeoutError' || error.message?.includes('ETIMEDOUT')) {
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check for status codes that indicate session expiration or temporary server issues
|
|
60
|
+
// 502 Bad Gateway - typically indicates session has expired or server issues
|
|
61
|
+
// 503 Service Unavailable - temporary server issues
|
|
62
|
+
// 401 Unauthorized - session has expired
|
|
63
|
+
if (error.statusCode && (error.statusCode === 502 || error.statusCode === 503 || error.statusCode === 401)) {
|
|
64
|
+
return true
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check for error messages containing these status codes (from tiny-json-http error format)
|
|
68
|
+
// Format: "POST failed with: 502" or "GET failed with: 401"
|
|
69
|
+
if (error.message) {
|
|
70
|
+
const sessionErrorPatterns = [
|
|
71
|
+
/failed with: 502/i,
|
|
72
|
+
/failed with: 503/i,
|
|
73
|
+
/failed with: 401/i,
|
|
74
|
+
/Bad Gateway/i,
|
|
75
|
+
/Service Unavailable/i,
|
|
76
|
+
/Unauthorized/i,
|
|
77
|
+
]
|
|
78
|
+
return sessionErrorPatterns.some(pattern => pattern.test(error.message))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async statusCode(action: string, error: { message: string, constructor?: { name: string } }): Promise<void> {
|
|
85
|
+
const statusCodeString = error.message || '' // Convert statusCode to a string, handle undefined/null
|
|
86
|
+
|
|
87
|
+
// Check if the error is an AggregateError or doesn't contain a status code
|
|
88
|
+
if (error.constructor?.name === 'AggregateError' || !/^\d{3}/.test(statusCodeString)) {
|
|
89
|
+
await this.debugErrorLog(`${action} failed with ${error.constructor?.name || 'Error'}: ${statusCodeString}`)
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const statusCode = statusCodeString.slice(0, 3)
|
|
94
|
+
const logMap: Record<string, string> = {
|
|
95
|
+
100: `Command successfully sent, statusCode: ${statusCodeString}`,
|
|
96
|
+
200: `Request successful, statusCode: ${statusCodeString}`,
|
|
97
|
+
400: `Bad Request, statusCode: ${statusCodeString}`,
|
|
98
|
+
429: `Too Many Requests, exceeded the number of requests allowed for a given time window, statusCode: ${statusCodeString}`,
|
|
99
|
+
}
|
|
100
|
+
const logMessage = logMap[statusCode]
|
|
101
|
+
?? `Unknown statusCode: ${statusCodeString}, Submit Bugs Here: https://tinyurl.com/AugustYaleBug`
|
|
102
|
+
await this.debugLog(logMessage)
|
|
103
|
+
if (!logMap[statusCode]) {
|
|
104
|
+
await this.debugErrorLog(`failed ${action}, Error: ${error}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
describe('statusCode error handling', () => {
|
|
110
|
+
let mockDevice: MockDevice
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
mockDevice = new MockDevice()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('isTimeoutError method', () => {
|
|
117
|
+
it('should detect august-yale TimeoutError by error.name', () => {
|
|
118
|
+
const error = { message: 'Request timed out after 30000ms', name: 'TimeoutError' }
|
|
119
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should detect august-yale TimeoutError regardless of message content', () => {
|
|
123
|
+
const error = { message: 'anything', name: 'TimeoutError' }
|
|
124
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('should detect ETIMEDOUT network errors', () => {
|
|
128
|
+
const error = { message: 'ETIMEDOUT connection timed out' }
|
|
129
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should detect 502 Bad Gateway status code', () => {
|
|
133
|
+
const error = { message: 'POST failed with: 502', statusCode: 502 }
|
|
134
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should detect 502 in error message', () => {
|
|
138
|
+
const error = { message: 'POST failed with: 502' }
|
|
139
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should detect 503 Service Unavailable status code', () => {
|
|
143
|
+
const error = { message: 'GET failed with: 503', statusCode: 503 }
|
|
144
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should detect 503 in error message', () => {
|
|
148
|
+
const error = { message: 'Service Unavailable' }
|
|
149
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should detect 401 Unauthorized status code', () => {
|
|
153
|
+
const error = { message: 'POST failed with: 401', statusCode: 401 }
|
|
154
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should detect 401 in error message', () => {
|
|
158
|
+
const error = { message: 'Unauthorized access' }
|
|
159
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('should detect Bad Gateway in error message', () => {
|
|
163
|
+
const error = { message: 'Bad Gateway' }
|
|
164
|
+
expect(mockDevice.isTimeoutError(error)).toBe(true)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should return false for non-timeout errors', () => {
|
|
168
|
+
const error = { message: '404 Not Found' }
|
|
169
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should return false for 400 Bad Request', () => {
|
|
173
|
+
const error = { message: '400 Bad Request', statusCode: 400 }
|
|
174
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should return false for 429 Too Many Requests', () => {
|
|
178
|
+
const error = { message: '429 Too Many Requests', statusCode: 429 }
|
|
179
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should return false for empty message', () => {
|
|
183
|
+
const error = { message: '' }
|
|
184
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('should return false for errors with non-timeout name', () => {
|
|
188
|
+
const error = { message: 'something failed', name: 'Error' }
|
|
189
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should not match 401 in non-HTTP status contexts', () => {
|
|
193
|
+
const error = { message: 'Expected 4012 records but found 401' }
|
|
194
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('should not match 502 in non-HTTP status contexts', () => {
|
|
198
|
+
const error = { message: 'Lock ID 50234 not found' }
|
|
199
|
+
expect(mockDevice.isTimeoutError(error)).toBe(false)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
describe('statusCode method', () => {
|
|
204
|
+
it('should handle standard HTTP status codes', async () => {
|
|
205
|
+
await mockDevice.statusCode('test action', { message: '400 Bad Request' })
|
|
206
|
+
|
|
207
|
+
expect(mockDevice.debugLog).toHaveBeenCalledWith('Bad Request, statusCode: 400 Bad Request')
|
|
208
|
+
expect(mockDevice.debugErrorLog).not.toHaveBeenCalled()
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('should handle AggregateError specifically', async () => {
|
|
212
|
+
await mockDevice.statusCode('refreshStatus', {
|
|
213
|
+
message: 'Multiple errors occurred',
|
|
214
|
+
constructor: { name: 'AggregateError' },
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
expect(mockDevice.debugErrorLog).toHaveBeenCalledWith('refreshStatus failed with AggregateError: Multiple errors occurred')
|
|
218
|
+
expect(mockDevice.debugLog).not.toHaveBeenCalled()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('should handle error with empty message', async () => {
|
|
222
|
+
await mockDevice.statusCode('refreshStatus', { message: '' })
|
|
223
|
+
|
|
224
|
+
expect(mockDevice.debugErrorLog).toHaveBeenCalledWith('refreshStatus failed with Object: ')
|
|
225
|
+
expect(mockDevice.debugLog).not.toHaveBeenCalled()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should handle error message without numeric status code', async () => {
|
|
229
|
+
await mockDevice.statusCode('refreshStatus', { message: 'Network connection failed' })
|
|
230
|
+
|
|
231
|
+
expect(mockDevice.debugErrorLog).toHaveBeenCalledWith('refreshStatus failed with Object: Network connection failed')
|
|
232
|
+
expect(mockDevice.debugLog).not.toHaveBeenCalled()
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('should handle 429 Too Many Requests status code', async () => {
|
|
236
|
+
await mockDevice.statusCode('test action', { message: '429 Too Many Requests' })
|
|
237
|
+
|
|
238
|
+
expect(mockDevice.debugLog).toHaveBeenCalledWith('Too Many Requests, exceeded the number of requests allowed for a given time window, statusCode: 429 Too Many Requests')
|
|
239
|
+
expect(mockDevice.debugErrorLog).not.toHaveBeenCalled()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should handle unknown numeric status codes', async () => {
|
|
243
|
+
await mockDevice.statusCode('test action', { message: '503 Service Unavailable' })
|
|
244
|
+
|
|
245
|
+
expect(mockDevice.debugLog).toHaveBeenCalledWith('Unknown statusCode: 503 Service Unavailable, Submit Bugs Here: https://tinyurl.com/AugustYaleBug')
|
|
246
|
+
expect(mockDevice.debugErrorLog).toHaveBeenCalledWith('failed test action, Error: [object Object]')
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
describe('updateCharacteristic', () => {
|
|
252
|
+
let device: MockDeviceWithCharacteristics
|
|
253
|
+
let mockService: Service
|
|
254
|
+
const CHARACTERISTIC = 'LockCurrentState'
|
|
255
|
+
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
device = new MockDeviceWithCharacteristics()
|
|
258
|
+
mockService = {
|
|
259
|
+
updateCharacteristic: vi.fn(),
|
|
260
|
+
} as unknown as Service
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should skip HomeKit update when CharacteristicValue is undefined', async () => {
|
|
264
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, undefined as any, 'LockCurrentState')
|
|
265
|
+
|
|
266
|
+
expect(mockService.updateCharacteristic).not.toHaveBeenCalled()
|
|
267
|
+
expect(device.debugLog).toHaveBeenCalledWith('LockCurrentState: undefined')
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
describe('initial state discovery (no prior context)', () => {
|
|
271
|
+
it('should push value to HomeKit on first update', async () => {
|
|
272
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 0, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
273
|
+
|
|
274
|
+
expect(mockService.updateCharacteristic).toHaveBeenCalledWith(CHARACTERISTIC, 0)
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('should NOT log info message on initial discovery', async () => {
|
|
278
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 0, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
279
|
+
|
|
280
|
+
expect(device.infoLog).not.toHaveBeenCalled()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should store the value in context for future comparisons', async () => {
|
|
284
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 0, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
285
|
+
|
|
286
|
+
expect(device.accessory.context.LockMechanismLockCurrentState).toBe(0)
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
describe('subsequent updates with unchanged value', () => {
|
|
291
|
+
it('should NOT push to HomeKit when value has not changed', async () => {
|
|
292
|
+
// Seed context as if a previous update occurred
|
|
293
|
+
device.accessory.context.LockMechanismLockCurrentState = 1
|
|
294
|
+
|
|
295
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 1, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
296
|
+
|
|
297
|
+
expect(mockService.updateCharacteristic).not.toHaveBeenCalled()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should NOT log info message when value has not changed', async () => {
|
|
301
|
+
device.accessory.context.LockMechanismLockCurrentState = 1
|
|
302
|
+
|
|
303
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 1, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
304
|
+
|
|
305
|
+
expect(device.infoLog).not.toHaveBeenCalled()
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
describe('genuine state change', () => {
|
|
310
|
+
it('should push to HomeKit when value changes', async () => {
|
|
311
|
+
device.accessory.context.LockMechanismLockCurrentState = 1
|
|
312
|
+
|
|
313
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 0, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
314
|
+
|
|
315
|
+
expect(mockService.updateCharacteristic).toHaveBeenCalledWith(CHARACTERISTIC, 0)
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it('should log "was Unlocked" when lock changes from secured to unsecured', async () => {
|
|
319
|
+
device.accessory.context.LockMechanismLockCurrentState = 1
|
|
320
|
+
|
|
321
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 0, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
322
|
+
|
|
323
|
+
expect(device.infoLog).toHaveBeenCalledWith('was Unlocked')
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('should log "was Locked" when lock changes from unsecured to secured', async () => {
|
|
327
|
+
device.accessory.context.LockMechanismLockCurrentState = 0
|
|
328
|
+
|
|
329
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 1, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
330
|
+
|
|
331
|
+
expect(device.infoLog).toHaveBeenCalledWith('was Locked')
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('should update context to new value', async () => {
|
|
335
|
+
device.accessory.context.LockMechanismLockCurrentState = 1
|
|
336
|
+
|
|
337
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 0, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
338
|
+
|
|
339
|
+
expect(device.accessory.context.LockMechanismLockCurrentState).toBe(0)
|
|
340
|
+
})
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
describe('contact sensor state changes', () => {
|
|
344
|
+
it('should log "was Closed" when door changes to closed', async () => {
|
|
345
|
+
device.accessory.context.ContactSensorContactSensorState = 1
|
|
346
|
+
|
|
347
|
+
await device.updateCharacteristic(mockService, 'ContactSensor', 'ContactSensorState', 0, 'ContactSensorState', 1, 'Opened', 'Closed')
|
|
348
|
+
|
|
349
|
+
expect(device.infoLog).toHaveBeenCalledWith('was Closed')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('should log "was Opened" when door changes to open', async () => {
|
|
353
|
+
device.accessory.context.ContactSensorContactSensorState = 0
|
|
354
|
+
|
|
355
|
+
await device.updateCharacteristic(mockService, 'ContactSensor', 'ContactSensorState', 1, 'ContactSensorState', 1, 'Opened', 'Closed')
|
|
356
|
+
|
|
357
|
+
expect(device.infoLog).toHaveBeenCalledWith('was Opened')
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
describe('updates without StatusMatch/StatusDoesNotMatch', () => {
|
|
362
|
+
it('should push to HomeKit but not log info when no status strings provided', async () => {
|
|
363
|
+
device.accessory.context.BatteryBatteryLevel = 90
|
|
364
|
+
|
|
365
|
+
await device.updateCharacteristic(mockService, 'Battery', 'BatteryLevel', 85, 'BatteryLevel')
|
|
366
|
+
|
|
367
|
+
expect(mockService.updateCharacteristic).toHaveBeenCalledWith('BatteryLevel', 85)
|
|
368
|
+
expect(device.infoLog).not.toHaveBeenCalled()
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
describe('startup simulation (seeded context matches API response)', () => {
|
|
373
|
+
it('should not push to HomeKit or log when seeded context matches first API value', async () => {
|
|
374
|
+
// Simulate constructor seeding: accessory.context.LockMechanismLockCurrentState ??= SECURED
|
|
375
|
+
device.accessory.context.LockMechanismLockCurrentState = 1
|
|
376
|
+
|
|
377
|
+
// First API response returns same state (SECURED = 1)
|
|
378
|
+
await device.updateCharacteristic(mockService, 'LockMechanism', CHARACTERISTIC, 1, 'LockCurrentState', 1, 'Locked', 'Unlocked')
|
|
379
|
+
|
|
380
|
+
expect(mockService.updateCharacteristic).not.toHaveBeenCalled()
|
|
381
|
+
expect(device.infoLog).not.toHaveBeenCalled()
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
})
|
|
@@ -10,7 +10,7 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
10
10
|
|
|
11
11
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { AugustPlatformHAP } from '../src/Platform.HAP.js'
|
|
14
14
|
|
|
15
15
|
// Mock the august-yale module
|
|
16
16
|
vi.mock('august-yale', () => {
|
|
@@ -37,7 +37,7 @@ vi.mock('node:fs', () => ({
|
|
|
37
37
|
}))
|
|
38
38
|
|
|
39
39
|
describe('augustPlatform', () => {
|
|
40
|
-
let platform:
|
|
40
|
+
let platform: AugustPlatformHAP
|
|
41
41
|
let mockApi: API
|
|
42
42
|
let mockLog: Logging
|
|
43
43
|
let mockConfig: AugustPlatformConfig
|
|
@@ -110,7 +110,7 @@ describe('augustPlatform', () => {
|
|
|
110
110
|
},
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
platform = new
|
|
113
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
114
114
|
|
|
115
115
|
// Access the private method for testing
|
|
116
116
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -137,7 +137,7 @@ describe('augustPlatform', () => {
|
|
|
137
137
|
},
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
platform = new
|
|
140
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
141
141
|
|
|
142
142
|
// Access the private method for testing
|
|
143
143
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -165,7 +165,7 @@ describe('augustPlatform', () => {
|
|
|
165
165
|
},
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
platform = new
|
|
168
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
169
169
|
|
|
170
170
|
// Access the private method for testing
|
|
171
171
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -192,7 +192,7 @@ describe('augustPlatform', () => {
|
|
|
192
192
|
},
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
platform = new
|
|
195
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
196
196
|
|
|
197
197
|
// Access the private method for testing
|
|
198
198
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -217,7 +217,7 @@ describe('augustPlatform', () => {
|
|
|
217
217
|
},
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
platform = new
|
|
220
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
221
221
|
|
|
222
222
|
// Access the private method for testing
|
|
223
223
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -245,7 +245,7 @@ describe('augustPlatform', () => {
|
|
|
245
245
|
},
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
platform = new
|
|
248
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
249
249
|
|
|
250
250
|
// Access the private method for testing
|
|
251
251
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -277,7 +277,7 @@ describe('augustPlatform', () => {
|
|
|
277
277
|
},
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
platform = new
|
|
280
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
281
281
|
|
|
282
282
|
// Test the public method
|
|
283
283
|
const normalizedCredentials = await platform.getNormalizedCredentials()
|
|
@@ -302,7 +302,7 @@ describe('augustPlatform', () => {
|
|
|
302
302
|
},
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
platform = new
|
|
305
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
306
306
|
|
|
307
307
|
// Access the private method for testing
|
|
308
308
|
const normalizeMethod = (platform as any).normalizeCredentialsForApi.bind(platform)
|
|
@@ -327,7 +327,7 @@ describe('augustPlatform', () => {
|
|
|
327
327
|
},
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
platform = new
|
|
330
|
+
platform = new AugustPlatformHAP(mockLog, config, mockApi)
|
|
331
331
|
|
|
332
332
|
// First call should normalize and cache
|
|
333
333
|
const firstCall = await platform.getNormalizedCredentials()
|
|
@@ -346,7 +346,7 @@ describe('augustPlatform', () => {
|
|
|
346
346
|
describe('discoverDevices', () => {
|
|
347
347
|
it('should handle 401 authentication errors and trigger re-authentication', async () => {
|
|
348
348
|
// Create platform instance
|
|
349
|
-
platform = new
|
|
349
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
350
350
|
|
|
351
351
|
// Mock August.details to throw 401 error
|
|
352
352
|
const August = await import('august-yale')
|
|
@@ -370,7 +370,7 @@ describe('augustPlatform', () => {
|
|
|
370
370
|
|
|
371
371
|
it('should handle "unauthorized" errors and trigger re-authentication', async () => {
|
|
372
372
|
// Create platform instance
|
|
373
|
-
platform = new
|
|
373
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
374
374
|
|
|
375
375
|
// Mock August.details to throw unauthorized error
|
|
376
376
|
const August = await import('august-yale')
|
|
@@ -391,7 +391,7 @@ describe('augustPlatform', () => {
|
|
|
391
391
|
|
|
392
392
|
it('should re-throw non-authentication errors', async () => {
|
|
393
393
|
// Create platform instance
|
|
394
|
-
platform = new
|
|
394
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
395
395
|
|
|
396
396
|
// Mock August.details to throw non-401 error
|
|
397
397
|
const August = await import('august-yale')
|
|
@@ -405,7 +405,7 @@ describe('augustPlatform', () => {
|
|
|
405
405
|
|
|
406
406
|
it('should process devices successfully when no errors occur', async () => {
|
|
407
407
|
// Create platform instance
|
|
408
|
-
platform = new
|
|
408
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
409
409
|
|
|
410
410
|
// Mock successful August.details response
|
|
411
411
|
const August = await import('august-yale')
|
|
@@ -440,7 +440,7 @@ describe('augustPlatform', () => {
|
|
|
440
440
|
},
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
platform = new
|
|
443
|
+
platform = new AugustPlatformHAP(mockLog, configWithExcludes, mockApi)
|
|
444
444
|
|
|
445
445
|
const August = await import('august-yale')
|
|
446
446
|
const mockDeviceIncluded = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
|
|
@@ -465,7 +465,7 @@ describe('augustPlatform', () => {
|
|
|
465
465
|
},
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
platform = new
|
|
468
|
+
platform = new AugustPlatformHAP(mockLog, configWithExcludes, mockApi)
|
|
469
469
|
|
|
470
470
|
const August = await import('august-yale')
|
|
471
471
|
const mockDeviceIncluded = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
|
|
@@ -492,7 +492,7 @@ describe('augustPlatform', () => {
|
|
|
492
492
|
// Make uuid.generate return predictable UUIDs based on input
|
|
493
493
|
vi.mocked(mockApi.hap.uuid.generate).mockImplementation((input: any) => `uuid-${input}`)
|
|
494
494
|
|
|
495
|
-
platform = new
|
|
495
|
+
platform = new AugustPlatformHAP(mockLog, configWithExcludes, mockApi)
|
|
496
496
|
|
|
497
497
|
// Simulate a cached accessory for the excluded lock
|
|
498
498
|
const mockAccessory = {
|
|
@@ -528,7 +528,7 @@ describe('augustPlatform', () => {
|
|
|
528
528
|
},
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
platform = new
|
|
531
|
+
platform = new AugustPlatformHAP(mockLog, configWithEmptyExcludes, mockApi)
|
|
532
532
|
|
|
533
533
|
const August = await import('august-yale')
|
|
534
534
|
const mockDeviceA = { lockId: 'lock-a', LockName: 'Lock A' } as any
|
|
@@ -554,7 +554,7 @@ describe('augustPlatform', () => {
|
|
|
554
554
|
},
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
-
platform = new
|
|
557
|
+
platform = new AugustPlatformHAP(mockLog, configWithBoth, mockApi)
|
|
558
558
|
|
|
559
559
|
const August = await import('august-yale')
|
|
560
560
|
const mockDeviceIncluded = { lockId: 'included-lock-id', LockName: 'Included Lock' } as any
|
|
@@ -585,7 +585,7 @@ describe('augustPlatform', () => {
|
|
|
585
585
|
|
|
586
586
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfigFile))
|
|
587
587
|
|
|
588
|
-
platform = new
|
|
588
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
589
589
|
|
|
590
590
|
// This should not throw an error when trying to set installId
|
|
591
591
|
await expect(platform.validated()).resolves.toBeUndefined()
|
|
@@ -605,7 +605,7 @@ describe('augustPlatform', () => {
|
|
|
605
605
|
|
|
606
606
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfigFile))
|
|
607
607
|
|
|
608
|
-
platform = new
|
|
608
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
609
609
|
|
|
610
610
|
// This should not throw an error when trying to set installId
|
|
611
611
|
await expect(platform.validated()).resolves.toBeUndefined()
|
|
@@ -625,7 +625,7 @@ describe('augustPlatform', () => {
|
|
|
625
625
|
|
|
626
626
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockConfigFile))
|
|
627
627
|
|
|
628
|
-
platform = new
|
|
628
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
629
629
|
|
|
630
630
|
await platform.validated()
|
|
631
631
|
|
|
@@ -33,7 +33,7 @@ import { readFileSync } from 'node:fs'
|
|
|
33
33
|
|
|
34
34
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import { AugustPlatformHAP } from '../src/Platform.HAP.js'
|
|
37
37
|
|
|
38
38
|
// Same mock conventions as the rest of the suite.
|
|
39
39
|
vi.mock('august-yale', () => {
|
|
@@ -57,7 +57,7 @@ vi.mock('node:fs', () => ({
|
|
|
57
57
|
// execute() runs the supplied fn against a fake "client" passed through
|
|
58
58
|
// from the test. Returns the underlying mock execute fn for assertions.
|
|
59
59
|
function installConnectivity(
|
|
60
|
-
platform:
|
|
60
|
+
platform: AugustPlatformHAP,
|
|
61
61
|
client: any,
|
|
62
62
|
state: 'healthy' | 'degraded' | 'offline' | 'recovering' = 'healthy',
|
|
63
63
|
) {
|
|
@@ -78,7 +78,7 @@ function installConnectivity(
|
|
|
78
78
|
// Helper: register N stub LockMechanism objects on the platform. Each
|
|
79
79
|
// has its own lockId and a vi.fn() applyRefresh. Returns the array of
|
|
80
80
|
// stubs in registration order.
|
|
81
|
-
function registerLocks(platform:
|
|
81
|
+
function registerLocks(platform: AugustPlatformHAP, count: number) {
|
|
82
82
|
const locks: any[] = []
|
|
83
83
|
for (let i = 0; i < count; i++) {
|
|
84
84
|
const lock = {
|
|
@@ -94,7 +94,7 @@ function registerLocks(platform: AugustPlatform, count: number) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
describe('augustPlatform.startPolling', () => {
|
|
97
|
-
let platform:
|
|
97
|
+
let platform: AugustPlatformHAP
|
|
98
98
|
let mockApi: API
|
|
99
99
|
let mockLog: Logging
|
|
100
100
|
let mockConfig: AugustPlatformConfig
|
|
@@ -137,7 +137,7 @@ describe('augustPlatform.startPolling', () => {
|
|
|
137
137
|
options: { logging: 'debug' },
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
platform = new
|
|
140
|
+
platform = new AugustPlatformHAP(mockLog, mockConfig, mockApi)
|
|
141
141
|
// Force a deterministic poll interval (1s) so tests can advance
|
|
142
142
|
// time precisely.
|
|
143
143
|
;(platform as any).platformRefreshRate = 1
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const child = require('node:child_process')
|
|
4
|
-
const fs = require('node:fs')
|
|
5
|
-
|
|
6
|
-
const bump = (process.argv[2] || 'patch').toLowerCase()
|
|
7
|
-
const allowed = new Set(['major', 'minor', 'patch'])
|
|
8
|
-
|
|
9
|
-
if (!allowed.has(bump)) {
|
|
10
|
-
console.error(`Invalid bump level '${bump}'. Use major, minor, or patch.`)
|
|
11
|
-
process.exit(1)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
|
|
15
|
-
|
|
16
|
-
function inc(v, level) {
|
|
17
|
-
const parts = v.split('-')[0].split('+')[0].split('.').map(Number)
|
|
18
|
-
if (level === 'major') {
|
|
19
|
-
parts[0] += 1
|
|
20
|
-
parts[1] = 0
|
|
21
|
-
parts[2] = 0
|
|
22
|
-
} else if (level === 'minor') {
|
|
23
|
-
parts[1] += 1
|
|
24
|
-
parts[2] = 0
|
|
25
|
-
} else {
|
|
26
|
-
parts[2] += 1
|
|
27
|
-
}
|
|
28
|
-
return parts.join('.')
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function latestStable(name) {
|
|
32
|
-
try {
|
|
33
|
-
const versions = JSON.parse(child.execSync(`npm info ${name} versions --json`).toString('utf8').trim())
|
|
34
|
-
const stable = (Array.isArray(versions) ? versions : [])
|
|
35
|
-
.filter(v => /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/.test(v) && !v.includes('-'))
|
|
36
|
-
stable.sort((a, b) => {
|
|
37
|
-
const ap = a.split('.').map(Number)
|
|
38
|
-
const bp = b.split('.').map(Number)
|
|
39
|
-
for (let i = 0; i < 3; i++) {
|
|
40
|
-
if (ap[i] !== bp[i]) return bp[i] - ap[i]
|
|
41
|
-
}
|
|
42
|
-
return 0
|
|
43
|
-
})
|
|
44
|
-
return stable[0] || ''
|
|
45
|
-
} catch {
|
|
46
|
-
return ''
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const base = latestStable(pkg.name) || pkg.version
|
|
51
|
-
process.stdout.write(inc(base, bump))
|