homebridge-ecobee-status
Version:
Homebridge plugin to control Ecobee thermostat Home/Away/Sleep status through HomeKit security system interface
154 lines • 7.04 kB
JavaScript
;
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