UNPKG

homebridge-ecobee-status

Version:

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

157 lines 7.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthTokenManager = void 0; const moment_1 = __importDefault(require("moment")); const axios_1 = __importDefault(require("axios")); const querystring_1 = __importDefault(require("querystring")); const config_1 = require("./config"); const network_retry_1 = require("./network-retry"); class AuthTokenManager { constructor(platform) { this.platform = platform; this.ECOBEE_API_KEY = 'LvHbdQIXI5zoGoZW2uyWk2Ejfb1vtQWq'; this.TOKEN_REFRESH_BUFFER = 300; // Refresh 5 minutes before expiration this.authToken = ''; this.refreshToken = ''; this.expiration = (0, moment_1.default)(); this.refreshInProgress = false; this.lastRefreshAttempt = (0, moment_1.default)(0); this.MIN_REFRESH_INTERVAL = 60; // Increase from 30 to 60 seconds this.RATE_LIMIT_BACKOFF = 5 * 60 * 1000; // 5 minutes for rate limit errors this.refreshToken = platform.config.refreshToken; this.networkRetry = new network_retry_1.NetworkRetry({ totalWindowSeconds: this.TOKEN_REFRESH_BUFFER - 30, maxAttempts: 5, // Reduce attempts for rate limiting initialDelay: 30000, // Start with 30 seconds for auth endpoints maxDelay: 120000, // Cap at 2 minutes between retries backoffFactor: 2, }); } static configureForPlatform(platform) { if (!AuthTokenManager.instance) { AuthTokenManager.instance = new AuthTokenManager(platform); } } static getInstance() { return AuthTokenManager.instance; } isExpired() { // Check if we're rate limited if (this.rateLimitedUntil && (0, moment_1.default)().isBefore(this.rateLimitedUntil)) { return false; // Don't attempt refresh while rate limited } return this.authToken === '' || (0, moment_1.default)().add(this.TOKEN_REFRESH_BUFFER, 'seconds').isAfter(this.expiration); } clearBackgroundRefresh() { if (this.backgroundRefreshTimeout) { clearTimeout(this.backgroundRefreshTimeout); this.backgroundRefreshTimeout = undefined; } } scheduleBackgroundRefresh(delayMs) { this.clearBackgroundRefresh(); this.backgroundRefreshTimeout = setTimeout(() => { this.renewAuthToken() .catch(error => { // Only log if it's not a known network error if (!this.networkRetry.isRetryableNetworkError(error)) { this.platform.log.error('Background token refresh failed:', error); } }); }, delayMs); } async renewAuthToken() { var _a, _b; // Check if we're in a rate limit backoff period if (this.rateLimitedUntil && (0, moment_1.default)().isBefore(this.rateLimitedUntil)) { const waitTime = this.rateLimitedUntil.diff((0, moment_1.default)(), 'seconds'); this.platform.log.warn(`Rate limited. Waiting ${waitTime}s before attempting token refresh.`); return this.authToken; // Return existing token if still valid } // Prevent multiple simultaneous refresh attempts if (this.refreshInProgress) { this.platform.log.debug('Token refresh already in progress, waiting...'); while (this.refreshInProgress) { await new Promise(resolve => setTimeout(resolve, 100)); } return this.authToken; } // Prevent too frequent refresh attempts const timeSinceLastAttempt = (0, moment_1.default)().diff(this.lastRefreshAttempt, 'seconds'); if (timeSinceLastAttempt < this.MIN_REFRESH_INTERVAL) { this.platform.log.debug(`Skipping refresh, last attempt was ${timeSinceLastAttempt}s ago`); return this.authToken; } this.refreshInProgress = true; this.lastRefreshAttempt = (0, moment_1.default)(); this.clearBackgroundRefresh(); try { if (!this.refreshToken) { throw new Error('No refresh token in config file'); } const oldRefreshToken = this.refreshToken; const authData = await this.networkRetry.execute(async () => { const response = await axios_1.default.post('https://api.ecobee.com/token', querystring_1.default.stringify({ grant_type: 'refresh_token', code: oldRefreshToken, client_id: this.ECOBEE_API_KEY, }), { headers: { 'User-Agent': 'homebridge-ecobee-status/2.x', } }); return response.data; }, this.platform.log, 'Token refresh'); const loadedAuthToken = authData.access_token; const loadedExpiresIn = authData.expires_in; const loadedUpdatedRefreshToken = (_a = authData.refresh_token) !== null && _a !== void 0 ? _a : oldRefreshToken; // Validate the received tokens if (!loadedAuthToken || !loadedExpiresIn) { throw new Error('Invalid token data received from Ecobee API'); } this.authToken = loadedAuthToken; this.refreshToken = loadedUpdatedRefreshToken; this.expiration = (0, moment_1.default)().add(loadedExpiresIn, 'seconds'); // Schedule next refresh before token expires const nextRefreshIn = (loadedExpiresIn - this.TOKEN_REFRESH_BUFFER) * 1000; this.scheduleBackgroundRefresh(nextRefreshIn); // Update config file with new refresh token if (oldRefreshToken !== loadedUpdatedRefreshToken) { const updated = (0, config_1.updateHomebridgeConfig)(this.platform.api, (currentConfig) => { return currentConfig.replace(oldRefreshToken, loadedUpdatedRefreshToken); }); if (updated) { this.platform.log.debug('Updated refresh token in config'); } } // Clear rate limit on success this.rateLimitedUntil = undefined; return loadedAuthToken; } catch (error) { // Handle rate limit errors specifically if (axios_1.default.isAxiosError(error) && ((_b = error.response) === null || _b === void 0 ? void 0 : _b.status) === 429) { // Set rate limit backoff this.rateLimitedUntil = (0, moment_1.default)().add(this.RATE_LIMIT_BACKOFF, 'milliseconds'); this.platform.log.error(`Token refresh rate limited. Will retry after ${this.rateLimitedUntil.format('HH:mm:ss')}`); // Schedule retry after rate limit period this.scheduleBackgroundRefresh(this.RATE_LIMIT_BACKOFF); } else if (!this.networkRetry.isRetryableNetworkError(error)) { this.platform.log.warn('Error refreshing token:', error); // Schedule a normal retry this.scheduleBackgroundRefresh(60000); // 1 minute } return undefined; } finally { this.refreshInProgress = false; } } } exports.AuthTokenManager = AuthTokenManager; //# sourceMappingURL=auth-token-refresh.js.map