UNPKG

homebridge-ecobee-status

Version:

Homebridge plugin to control Ecobee thermostat Home/Away/Sleep status through HomeKit security system interface

154 lines 7.04 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NetworkRetry = void 0; // network-retry.ts const axios_1 = __importDefault(require("axios")); class NetworkRetry { constructor(options = {}) { this.consecutiveFailures = 0; this.lastSuccessTime = Date.now(); this.totalWindowMs = (options.totalWindowSeconds || 270) * 1000; // Default 4.5 minutes to allow for overhead this.maxAttempts = options.maxAttempts || 8; this.initialDelay = options.initialDelay || 15000; // 15 seconds this.maxDelay = options.maxDelay || 60000; // 1 minute this.backoffFactor = options.backoffFactor || 2; this.retryableErrors = options.retryableErrors || [ 'ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', ]; } // Public method for external error checking isRetryableNetworkError(error) { return this.isRetryableError(error); } isRetryableError(error) { var _a, _b; if (axios_1.default.isAxiosError(error)) { const axiosError = error; // Check if it's a network error if (axiosError.code && this.retryableErrors.includes(axiosError.code)) { return true; } // Check if it's a 5xx server error if (((_a = axiosError.response) === null || _a === void 0 ? void 0 : _a.status) && axiosError.response.status >= 500) { return true; } // Check if it's a 429 rate limit error if (((_b = axiosError.response) === null || _b === void 0 ? void 0 : _b.status) === 429) { return true; } // Check if it's a DNS resolution error if (axiosError.code === 'EAI_AGAIN') { return true; } // Check if it's a timeout error if (axiosError.code === 'ETIMEDOUT') { return true; } } return false; } formatErrorMessage(error) { var _a; if (axios_1.default.isAxiosError(error)) { const axiosError = error; let message = axiosError.message; if (axiosError.code === 'EAI_AGAIN') { message = 'DNS resolution failed - network may be temporarily unavailable'; } else if (axiosError.code === 'ETIMEDOUT') { message = 'Connection timed out - network may be temporarily unavailable'; } if ((_a = axiosError.response) === null || _a === void 0 ? void 0 : _a.data) { const data = typeof axiosError.response.data === 'string' ? axiosError.response.data : JSON.stringify(axiosError.response.data); message += ` (${data})`; } return message; } return error instanceof Error ? error.message : String(error); } calculateDelay(attempt, error) { var _a; // Check for rate limit headers first if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 429) { // Check for Retry-After header const retryAfter = error.response.headers['retry-after']; if (retryAfter) { // If it's a number, it's seconds; if it's a date, parse it const retryAfterMs = isNaN(Number(retryAfter)) ? new Date(retryAfter).getTime() - Date.now() : Number(retryAfter) * 1000; // Ensure it's within reasonable bounds return Math.min(Math.max(retryAfterMs, this.initialDelay), 5 * 60 * 1000); // Max 5 minutes } // For 429 without Retry-After, use longer delays const baseDelay = this.initialDelay * Math.pow(2, attempt + 2); // More aggressive backoff const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1); return Math.min(baseDelay + jitter, this.maxDelay * 2); // Allow longer delays for rate limits } // Original calculation for other errors const baseDelay = this.initialDelay * Math.pow(this.backoffFactor, attempt); // Add jitter (±25% of base delay) const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1); const delay = baseDelay + jitter; // Cap at max delay return Math.min(delay, this.maxDelay); } shouldReduceVerbosity() { const timeSinceSuccess = Date.now() - this.lastSuccessTime; return this.consecutiveFailures > 3 || timeSinceSuccess > 5 * 60 * 1000; } async execute(operation, logger, context = 'operation') { var _a; const startTime = Date.now(); let lastError; for (let attempt = 0; attempt < this.maxAttempts; attempt++) { try { const result = await operation(); this.consecutiveFailures = 0; this.lastSuccessTime = Date.now(); return result; } catch (error) { this.consecutiveFailures++; lastError = error instanceof Error ? error : new Error(String(error)); const timeElapsed = Date.now() - startTime; const timeRemaining = this.totalWindowMs - timeElapsed; if (!this.isRetryableError(error) || timeRemaining <= 0 || attempt === this.maxAttempts - 1) { throw error; } const delay = this.calculateDelay(attempt, error); if (logger) { const errorMessage = this.formatErrorMessage(error); // Special handling for rate limit errors if (axios_1.default.isAxiosError(error) && ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 429) { logger.warn(`${context} rate limited. Waiting ${Math.round(delay / 1000)}s before retry...`); } else if (this.shouldReduceVerbosity()) { if (attempt === 0) { logger.warn(`${context} failed: ${errorMessage}. Will retry in background.`); } } else { logger.warn(`${context} failed (attempt ${attempt + 1}/${this.maxAttempts}): ${errorMessage}. ` + `Retrying in ${Math.round(delay / 1000)}s...`); } } await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } } exports.NetworkRetry = NetworkRetry; //# sourceMappingURL=network-retry.js.map