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