Skip to content

Commit bf10832

Browse files
Fix August API ETIMEDOUT errors after 24 hours with session refresh and retry logic (#166)
This PR addresses persistent ETIMEDOUT connection failures that occur after approximately 24 hours of plugin operation. The issue manifests as: ``` [August] [DEBUG] Lock: DoorName failed (refreshStatus) lockDetails, Error: {"errno":-110,"code":"ETIMEDOUT","syscall":"read"} [August] Lock: DoorName pushChanges: read ETIMEDOUT ``` ## Root Cause The August API invalidates sessions after extended periods, but the plugin continued using stale connections without attempting to re-establish them when timeouts occurred. ## Solution Implemented a minimal timeout detection and session refresh mechanism: 1. **Timeout Detection**: Added `isTimeoutError()` method to specifically identify ETIMEDOUT errors in network operations 2. **Session Refresh**: Added `refreshAugustSession()` method that calls `augustConfig.end()` to clear stale tokens, forcing re-authentication on the next API call 3. **Retry Logic**: Enhanced `refreshStatus()` and `pushChanges()` methods to automatically retry operations once after session refresh when timeouts are detected ## Technical Details The fix leverages the existing `august-yale` library's session management: - Uses `augustConfig.end()` to clear stale tokens - Allows automatic re-authentication on subsequent API calls - Implements single retry to avoid infinite loops - Preserves all existing error logging and debugging ## Testing - ✅ Build passes without compilation errors - ✅ All existing tests continue to pass - ✅ Linting requirements satisfied - ✅ No breaking changes to existing functionality The solution is minimal and surgical, adding only 64 lines of code focused specifically on timeout handling without modifying any existing behavior. Fixes #126. <!-- START COPILOT CODING AGENT TIPS --> --- 💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click [here](https://survey3.medallia.com/?EAHeSx-AP01bZqG0Ld9QLQ) to start the survey. --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: donavanbecker <[email protected]> Co-authored-by: Donavan Becker <[email protected]>
1 parent f55d1c9 commit bf10832

File tree

3 files changed

+64
-0
lines changed

3 files changed

+64
-0
lines changed

src/devices/device.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,13 @@ export abstract class deviceBase {
177177
}
178178
}
179179

180+
/**
181+
* Check if error is a network timeout that requires session refresh
182+
*/
183+
isTimeoutError(error: { message: string }): boolean {
184+
return Boolean(error.message && error.message.includes('ETIMEDOUT'))
185+
}
186+
180187
/**
181188
* Logging for Device
182189
*/

src/devices/lock.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,27 @@ export class LockMechanism extends deviceBase {
314314
} catch (e: any) {
315315
await this.statusCode('(refreshStatus) lockDetails', e)
316316
await this.errorLog(`(refreshStatus) lockDetails: ${e.message ?? e}`)
317+
318+
// Check if this is a timeout error and retry once after session refresh
319+
if (this.isTimeoutError(e)) {
320+
try {
321+
await this.debugLog('Timeout detected, refreshing August session and retrying...')
322+
await this.platform.refreshAugustSession()
323+
324+
// Retry the operation once
325+
if (this.platform.augustConfig && this.platform.augustConfig.details) {
326+
const lockDetails: any = await this.platform.augustConfig.details(this.device.lockId)
327+
await this.debugSuccessLog(`(refreshStatus retry) lockDetails: ${JSON.stringify(lockDetails)}`)
328+
// Update HomeKit
329+
this.lockDetails = lockDetails
330+
this.lockStatus = lockDetails.LockStatus
331+
await this.parseStatus()
332+
await this.updateHomeKitCharacteristics()
333+
}
334+
} catch (retryError: any) {
335+
await this.errorLog(`(refreshStatus retry) failed: ${retryError.message ?? retryError}`)
336+
}
337+
}
317338
}
318339
} else {
319340
await this.debugLog(`(refreshStatus) deviceRefreshRate: ${this.deviceRefreshRate}`)
@@ -358,6 +379,27 @@ export class LockMechanism extends deviceBase {
358379
} catch (e: any) {
359380
await this.statusCode('pushChanges', e)
360381
await this.debugLog(`pushChanges: ${e.message ?? e}`)
382+
383+
// Check if this is a timeout error and retry once after session refresh
384+
if (this.isTimeoutError(e)) {
385+
try {
386+
await this.debugLog('Timeout detected in pushChanges, refreshing August session and retrying...')
387+
await this.platform.refreshAugustSession()
388+
389+
// Retry the operation once
390+
if (this.LockMechanism && this.LockMechanism.LockTargetState !== this.LockMechanism.LockCurrentState) {
391+
if (this.LockMechanism.LockTargetState === this.hap.Characteristic.LockTargetState.UNSECURED) {
392+
await this.platform.augustConfig.unlock(this.device.lockId)
393+
} else {
394+
await this.platform.augustConfig.lock(this.device.lockId)
395+
}
396+
await this.successLog(`Retry: Sending request to August API: ${this.LockMechanism.LockTargetState === 1 ? 'Locked' : 'Unlocked'}`)
397+
await this.updateHomeKitCharacteristics()
398+
}
399+
} catch (retryError: any) {
400+
await this.errorLog(`(pushChanges retry) failed: ${retryError.message ?? retryError}`)
401+
}
402+
}
361403
}
362404
}
363405

src/platform.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,21 @@ export class AugustPlatform implements DynamicPlatformPlugin {
198198
}
199199
}
200200

201+
/**
202+
* Refresh August session by clearing current token
203+
*/
204+
async refreshAugustSession(): Promise<void> {
205+
try {
206+
if (this.augustConfig) {
207+
await this.debugLog('Refreshing August session due to timeout error')
208+
this.augustConfig.end() // Clear the current token to force re-authentication
209+
await this.debugLog('August session refreshed successfully')
210+
}
211+
} catch (e: any) {
212+
await this.errorLog(`Failed to refresh August session: ${e.message ?? e}`)
213+
}
214+
}
215+
201216
async pluginConfig() {
202217
const currentConfig = JSON.parse(readFileSync(this.api.user.configPath(), 'utf8'))
203218
// check the platforms section is an array before we do array things on it

0 commit comments

Comments
 (0)