homebridge-lg-thinq-ceiling-fan
Version:
A Homebridge plugin for controlling LG ceiling fans via the LG ThinQ platform with working speed control
681 lines (680 loc) • 34.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LGApi = void 0;
const axios_1 = __importDefault(require("axios"));
const luxon_1 = require("luxon");
const crypto = __importStar(require("crypto"));
const qs = __importStar(require("querystring"));
const settings_1 = require("./settings");
class LGApi {
constructor(country = settings_1.LG_API_CONSTANTS.DEFAULT_COUNTRY, language = settings_1.LG_API_CONSTANTS.DEFAULT_LANGUAGE) {
this.authData = null;
this.gatewayInfo = null;
this.tokenObtainedAt = null;
this.lastApiError = null;
this.consecutiveErrors = 0;
this.circuitBreakerOpen = false;
this.country = country;
this.language = language;
this.axiosInstance = axios_1.default.create({
timeout: 30000,
});
}
generateRandomString(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return result;
}
createSignature(message, secret) {
return crypto.createHmac('sha1', secret).update(message).digest('base64');
}
async getGatewayInfo() {
try {
const headers = {
'x-api-key': 'VGhpblEyLjAgU0VSVklDRQ==',
'x-thinq-app-ver': '3.6.1200',
'x-thinq-app-type': 'NUTS',
'x-thinq-app-level': 'PRD',
'x-thinq-app-os': 'ANDROID',
'x-thinq-app-logintype': 'LGE',
'x-service-code': 'SVC202',
'x-country-code': this.country,
'x-language-code': this.language,
'x-service-phase': 'OP',
'x-origin': 'app-native',
'x-model-name': 'samsung/SM-G930L',
'x-os-version': 'AOS/7.1.2',
'x-app-version': 'LG ThinQ/3.6.12110',
'x-message-id': this.generateRandomString(22),
'user-agent': 'okhttp/3.14.9',
'x-client-id': 'c713ea8e50f657534ff8b9d373dfebfc2ed70b88285c26b8ade49868c0b164d9',
};
const response = await this.axiosInstance.get(settings_1.LG_API_CONSTANTS.GATEWAY_URL, {
headers: headers,
});
if (response.data?.result) {
this.gatewayInfo = {
countryCode: this.country,
languageCode: this.language,
thinq2Uri: response.data.result.thinq2Uri,
empUri: response.data.result.empTermsUri,
empSpxUri: response.data.result.empSpxUri,
thinq1Uri: response.data.result.thinq1Uri,
};
return this.gatewayInfo;
}
else {
const errorMsg = response.data?.lgedmRoot?.returnMsg || 'Unknown error';
const errorCode = response.data?.resultCode || 'Unknown';
throw new Error(`Gateway request failed: ${errorMsg} (Code: ${errorCode})`);
}
}
catch (error) {
throw new Error(`Failed to get gateway info: ${error}`);
}
}
async getUserNumber() {
try {
const timestamp = luxon_1.DateTime.utc().toRFC2822();
const signature = this.createSignature(`/users/profile\n${timestamp}`, settings_1.LG_API_CONSTANTS.OAUTH_SECRET_KEY);
const headers = {
'Accept': 'application/json',
'Authorization': 'Bearer ' + this.authData.accessToken,
'X-Lge-Svccode': 'SVC202',
'X-Application-Key': settings_1.LG_API_CONSTANTS.APPLICATION_KEY,
'lgemp-x-app-key': settings_1.LG_API_CONSTANTS.CLIENT_ID,
'X-Device-Type': 'M01',
'X-Device-Platform': 'ADR',
'x-lge-oauth-date': timestamp,
'x-lge-oauth-signature': signature,
};
const response = await this.axiosInstance.get(`${this.authData.lgeapiUrl}users/profile`, {
headers: headers,
});
if (response.data?.status === 2) {
throw new Error(response.data.message || 'Failed to get user profile');
}
return response.data.account.userNo;
}
catch (error) {
throw new Error(`Failed to get user number: ${error}`);
}
}
async authenticateWithToken(refreshToken) {
try {
if (!this.gatewayInfo) {
await this.getGatewayInfo();
}
const tokenData = {
grant_type: 'refresh_token',
refresh_token: refreshToken,
};
const timestamp = luxon_1.DateTime.utc().toRFC2822();
const requestUrl = '/oauth/1.0/oauth2/token?' + qs.stringify(tokenData);
const headers = {
'x-lge-app-os': 'ADR',
'x-lge-appkey': settings_1.LG_API_CONSTANTS.CLIENT_ID,
'x-lge-oauth-signature': this.createSignature(`${requestUrl}\n${timestamp}`, settings_1.LG_API_CONSTANTS.OAUTH_SECRET_KEY),
'x-lge-oauth-date': timestamp,
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
};
const lgeapiUrl = `https://${this.country.toLowerCase()}.lgeapi.com/`;
const response = await this.axiosInstance.post(`${lgeapiUrl}oauth/1.0/oauth2/token`, qs.stringify(tokenData), { headers });
if (response.data?.access_token) {
this.authData = {
accessToken: response.data.access_token,
refreshToken: response.data.refresh_token || refreshToken,
userId: '',
countryCode: this.country,
languageCode: this.language,
lgeapiUrl: response.data.oauth2_backend_url ? decodeURIComponent(response.data.oauth2_backend_url) : lgeapiUrl,
};
try {
this.authData.userId = await this.getUserNumber();
}
catch (error) {
}
this.tokenObtainedAt = new Date();
return this.authData;
}
else {
throw new Error('Authentication failed: Invalid response');
}
}
catch (error) {
if (error.response?.status === 415) {
throw new Error('Token authentication failed: Unsupported Media Type - check request format');
}
else if (error.response?.status === 401) {
throw new Error('Token authentication failed: Invalid or expired refresh token');
}
else {
throw new Error(`Token authentication failed: ${error}`);
}
}
}
async authenticateWithCredentials(username, password) {
try {
if (!this.gatewayInfo) {
await this.getGatewayInfo();
}
if (!this.gatewayInfo || !this.gatewayInfo.empSpxUri || !this.gatewayInfo.empUri) {
throw new Error('Gateway information incomplete - missing required URIs');
}
const hashedPassword = crypto.createHash('sha512').update(password).digest('hex');
const defaultEmpHeaders = {
'Accept': 'application/json',
'X-Application-Key': settings_1.LG_API_CONSTANTS.APPLICATION_KEY,
'X-Client-App-Key': settings_1.LG_API_CONSTANTS.CLIENT_ID,
'X-Lge-Svccode': 'SVC709',
'X-Device-Type': 'M01',
'X-Device-Platform': 'ADR',
'X-Device-Language-Type': 'IETF',
'X-Device-Publish-Flag': 'Y',
'X-Device-Country': this.country,
'X-Device-Language': this.language,
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
'Access-Control-Allow-Origin': '*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
};
const preLoginData = {
'user_auth2': hashedPassword,
'log_param': 'login request / user_id : ' + username + ' / third_party : null / svc_list : SVC202,SVC710 / 3rd_service : ',
};
const preLoginResponse = await this.axiosInstance.post(this.gatewayInfo.empSpxUri + '/preLogin', qs.stringify(preLoginData), { headers: defaultEmpHeaders });
if (!preLoginResponse.data?.signature || !preLoginResponse.data?.tStamp) {
throw new Error('PreLogin failed - missing signature or timestamp');
}
const loginHeaders = {
...defaultEmpHeaders,
'X-Signature': preLoginResponse.data.signature,
'X-Timestamp': preLoginResponse.data.tStamp,
};
const loginData = {
'user_auth2': preLoginResponse.data.encrypted_pw,
'password_hash_prameter_flag': 'Y',
'svc_list': 'SVC202,SVC710',
};
const loginUrl = `${this.gatewayInfo.empUri}/emp/v2.0/account/session/${encodeURIComponent(username)}`;
const loginResponse = await this.axiosInstance.post(loginUrl, qs.stringify(loginData), { headers: loginHeaders });
if (!loginResponse.data?.account) {
throw new Error('Invalid credentials or account not found');
}
const account = loginResponse.data.account;
if (!this.gatewayInfo.empSpxUri) {
throw new Error('EMP SPX URI not available');
}
const empSearchKeyUrl = this.gatewayInfo.empSpxUri + '/searchKey?key_name=OAUTH_SECRETKEY&sever_type=OP';
const secretKeyResponse = await this.axiosInstance.get(empSearchKeyUrl);
const secretKey = secretKeyResponse.data.returnData;
const empOAuthData = {
account_type: account.userIDType,
client_id: settings_1.LG_API_CONSTANTS.CLIENT_ID,
country_code: account.country,
redirect_uri: 'lgaccount.lgsmartthinq:/',
response_type: 'code',
state: '12345',
username: account.userID,
};
const empOAuthUrl = new URL('https://emp-oauth.lgecloud.com/emp/oauth2/authorize/empsession?' + qs.stringify(empOAuthData));
const timestamp = luxon_1.DateTime.utc().toRFC2822();
const signature = this.createSignature(`${empOAuthUrl.pathname}${empOAuthUrl.search}\n${timestamp}`, secretKey);
const empOAuthHeaders = {
'lgemp-x-app-key': settings_1.LG_API_CONSTANTS.OAUTH_CLIENT_KEY,
'lgemp-x-date': timestamp,
'lgemp-x-session-key': account.loginSessionID,
'lgemp-x-signature': signature,
'Accept': 'application/json',
'X-Device-Type': 'M01',
'X-Device-Platform': 'ADR',
'Content-Type': 'application/x-www-form-urlencoded',
'Access-Control-Allow-Origin': '*',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ' +
'(KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.44',
};
const authorizeResponse = await this.axiosInstance.get(empOAuthUrl.href, {
headers: empOAuthHeaders,
});
if (authorizeResponse.data.status !== 1) {
throw new Error('Authorization failed: ' + (authorizeResponse.data.message || 'Unknown error'));
}
const redirectUri = new URL(authorizeResponse.data.redirect_uri);
const authCode = redirectUri.searchParams.get('code');
const oauthBackendUrl = redirectUri.searchParams.get('oauth2_backend_url');
if (!authCode || !oauthBackendUrl) {
throw new Error('Failed to extract authorization code or backend URL');
}
const tokenData = {
code: authCode,
grant_type: 'authorization_code',
redirect_uri: empOAuthData.redirect_uri,
};
const requestUrl = '/oauth/1.0/oauth2/token?' + qs.stringify(tokenData);
const tokenResponse = await this.axiosInstance.post(oauthBackendUrl + 'oauth/1.0/oauth2/token', qs.stringify(tokenData), {
headers: {
'x-lge-app-os': 'ADR',
'x-lge-appkey': settings_1.LG_API_CONSTANTS.CLIENT_ID,
'x-lge-oauth-signature': this.createSignature(`${requestUrl}\n${timestamp}`, settings_1.LG_API_CONSTANTS.OAUTH_SECRET_KEY),
'x-lge-oauth-date': timestamp,
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
},
});
if (!tokenResponse.data?.access_token) {
throw new Error('Failed to obtain access token');
}
this.authData = {
accessToken: tokenResponse.data.access_token,
refreshToken: tokenResponse.data.refresh_token,
userId: '',
countryCode: this.country,
languageCode: this.language,
lgeapiUrl: decodeURIComponent(tokenResponse.data.oauth2_backend_url),
};
this.authData.userId = await this.getUserNumber();
this.tokenObtainedAt = new Date();
return this.authData;
}
catch (error) {
throw new Error(`Credential authentication failed: ${error}`);
}
}
async getDevices() {
try {
if (!this.authData || !this.gatewayInfo) {
throw new Error('Not authenticated');
}
const homesResponse = await this.axiosInstance.get(`${this.gatewayInfo.thinq2Uri}/service/homes`, {
headers: {
'x-api-key': 'VGhpblEyLjAgU0VSVklDRQ==',
'x-thinq-app-ver': '3.6.1200',
'x-thinq-app-type': 'NUTS',
'x-thinq-app-level': 'PRD',
'x-thinq-app-os': 'ANDROID',
'x-thinq-app-logintype': 'LGE',
'x-service-code': 'SVC202',
'x-country-code': this.authData.countryCode,
'x-language-code': this.authData.languageCode,
'x-service-phase': 'OP',
'x-origin': 'app-native',
'x-model-name': 'samsung/SM-G930L',
'x-os-version': 'AOS/7.1.2',
'x-app-version': 'LG ThinQ/3.6.12110',
'x-message-id': this.generateRandomString(22),
'user-agent': 'okhttp/3.14.9',
'x-emp-token': this.authData.accessToken,
'x-user-no': this.authData.userId,
'x-client-id': settings_1.LG_API_CONSTANTS.CLIENT_ID,
},
});
const homes = homesResponse.data?.result?.item || [];
const allDevices = [];
for (const home of homes) {
const devicesResponse = await this.axiosInstance.get(`${this.gatewayInfo.thinq2Uri}/service/homes/${home.homeId}`, {
headers: {
'x-api-key': 'VGhpblEyLjAgU0VSVklDRQ==',
'x-thinq-app-ver': '3.6.1200',
'x-thinq-app-type': 'NUTS',
'x-thinq-app-level': 'PRD',
'x-thinq-app-os': 'ANDROID',
'x-thinq-app-logintype': 'LGE',
'x-service-code': 'SVC202',
'x-country-code': this.authData.countryCode,
'x-language-code': this.authData.languageCode,
'x-service-phase': 'OP',
'x-origin': 'app-native',
'x-model-name': 'samsung/SM-G930L',
'x-os-version': 'AOS/7.1.2',
'x-app-version': 'LG ThinQ/3.6.12110',
'x-message-id': this.generateRandomString(22),
'user-agent': 'okhttp/3.14.9',
'x-emp-token': this.authData.accessToken,
'x-user-no': this.authData.userId,
'x-client-id': settings_1.LG_API_CONSTANTS.CLIENT_ID,
},
});
const homeDevices = devicesResponse.data?.result?.devices || [];
allDevices.push(...homeDevices);
}
return allDevices;
}
catch (error) {
throw new Error(`Failed to get devices: ${error}`);
}
}
async getDeviceStatus(deviceId) {
try {
if (!this.authData || !this.gatewayInfo) {
throw new Error('Not authenticated');
}
console.log(`[LG API DEBUG] Getting device status for device: ${deviceId}`);
console.log(`[LG API DEBUG] Using auth token: ${this.authData.accessToken ?
this.authData.accessToken.substring(0, 20) + '...' : 'null'}`);
console.log(`[LG API DEBUG] Request URL: ${this.gatewayInfo.thinq2Uri}/service/devices/${deviceId}`);
const response = await this.axiosInstance.get(`${this.gatewayInfo.thinq2Uri}/service/devices/${deviceId}`, {
headers: {
'x-api-key': 'VGhpblEyLjAgU0VSVklDRQ==',
'x-thinq-app-ver': '3.6.1200',
'x-thinq-app-type': 'NUTS',
'x-thinq-app-level': 'PRD',
'x-thinq-app-os': 'ANDROID',
'x-thinq-app-logintype': 'LGE',
'x-service-code': 'SVC202',
'x-country-code': this.authData.countryCode,
'x-language-code': this.authData.languageCode,
'x-service-phase': 'OP',
'x-origin': 'app-native',
'x-model-name': 'samsung/SM-G930L',
'x-os-version': 'AOS/7.1.2',
'x-app-version': 'LG ThinQ/3.6.12110',
'x-message-id': this.generateRandomString(22),
'user-agent': 'okhttp/3.14.9',
'x-emp-token': this.authData.accessToken,
'x-user-no': this.authData.userId,
'x-client-id': settings_1.LG_API_CONSTANTS.CLIENT_ID,
},
});
console.log(`[LG API DEBUG] Device status response status: ${response.status}`);
console.log('[LG API DEBUG] Device status response data:', JSON.stringify(response.data, null, 2));
return response.data?.result || {};
}
catch (error) {
console.error(`[LG API ERROR] Device status request failed for device ${deviceId}:`);
console.error(`[LG API ERROR] Status: ${error.response?.status || 'unknown'}`);
console.error(`[LG API ERROR] Status Text: ${error.response?.statusText || 'unknown'}`);
console.error('[LG API ERROR] Response Headers:', error.response?.headers || 'none');
console.error('[LG API ERROR] Response Data:', JSON.stringify(error.response?.data, null, 2) || 'none');
console.error(`[LG API ERROR] Request URL: ${this.gatewayInfo?.thinq2Uri}/service/devices/${deviceId}`);
console.error(`[LG API ERROR] Auth token exists: ${!!this.authData?.accessToken}`);
console.error(`[LG API ERROR] User ID exists: ${!!this.authData?.userId}`);
if (error.response?.status === 400) {
throw new Error(`Bad Request (400): ${JSON.stringify(error.response.data) ||
'Invalid request format or parameters'}`);
}
else if (error.response?.status === 401) {
throw new Error(`Unauthorized (401): ${JSON.stringify(error.response.data) ||
'Authentication token expired or invalid'}`);
}
else if (error.response?.status === 403) {
throw new Error(`Forbidden (403): ${JSON.stringify(error.response.data) ||
'Access denied to device'}`);
}
else if (error.response?.status === 404) {
throw new Error(`Not Found (404): Device ${deviceId} not found`);
}
else if (error.response?.status === 429) {
throw new Error('Rate Limited (429): Too many requests, please wait before retrying');
}
else if (error.response?.status >= 500) {
throw new Error(`Server Error (${error.response.status}): LG API server issue - ${JSON.stringify(error.response.data) || 'Internal server error'}`);
}
throw new Error(`Failed to get device status: ${error.message || error}`);
}
}
async sendCommand(deviceId, command) {
try {
if (!this.authData || !this.gatewayInfo) {
throw new Error('Not authenticated');
}
const requestData = {
ctrlKey: 'basicCtrl',
command: 'Set',
dataKey: command.dataKey,
dataValue: command.dataValue,
};
console.log(`[LG API DEBUG] Sending command to device: ${deviceId}`);
console.log('[LG API DEBUG] Command data:', JSON.stringify(requestData, null, 2));
console.log(`[LG API DEBUG] Using auth token: ${this.authData.accessToken ?
this.authData.accessToken.substring(0, 20) + '...' : 'null'}`);
console.log(`[LG API DEBUG] Request URL: ${this.gatewayInfo.thinq2Uri}/service/devices/${deviceId}/control-sync`);
const response = await this.axiosInstance.post(`${this.gatewayInfo.thinq2Uri}/service/devices/${deviceId}/control-sync`, requestData, {
headers: {
'x-api-key': 'VGhpblEyLjAgU0VSVklDRQ==',
'x-thinq-app-ver': '3.6.1200',
'x-thinq-app-type': 'NUTS',
'x-thinq-app-level': 'PRD',
'x-thinq-app-os': 'ANDROID',
'x-thinq-app-logintype': 'LGE',
'x-service-code': 'SVC202',
'x-country-code': this.authData.countryCode,
'x-language-code': this.authData.languageCode,
'x-service-phase': 'OP',
'x-origin': 'app-native',
'x-model-name': 'samsung/SM-G930L',
'x-os-version': 'AOS/7.1.2',
'x-app-version': 'LG ThinQ/3.6.12110',
'x-message-id': this.generateRandomString(22),
'user-agent': 'okhttp/3.14.9',
'x-emp-token': this.authData.accessToken,
'x-user-no': this.authData.userId,
'x-client-id': settings_1.LG_API_CONSTANTS.CLIENT_ID,
'Content-Type': 'application/json',
},
});
console.log(`[LG API DEBUG] Command response status: ${response.status}`);
console.log('[LG API DEBUG] Command response data:', JSON.stringify(response.data, null, 2));
return response.data?.result || {};
}
catch (error) {
console.error(`[LG API ERROR] Command request failed for device ${deviceId}:`);
console.error('[LG API ERROR] Command:', JSON.stringify(command, null, 2));
console.error(`[LG API ERROR] Status: ${error.response?.status || 'unknown'}`);
console.error(`[LG API ERROR] Status Text: ${error.response?.statusText || 'unknown'}`);
console.error('[LG API ERROR] Response Headers:', error.response?.headers || 'none');
console.error('[LG API ERROR] Response Data:', JSON.stringify(error.response?.data, null, 2) || 'none');
console.error(`[LG API ERROR] Request URL: ${this.gatewayInfo?.thinq2Uri}/service/devices/${deviceId}/control-sync`);
console.error(`[LG API ERROR] Auth token exists: ${!!this.authData?.accessToken}`);
console.error(`[LG API ERROR] User ID exists: ${!!this.authData?.userId}`);
if (error.response?.status === 400) {
throw new Error(`Bad Request (400): ${JSON.stringify(error.response.data) ||
'Invalid command format or parameters'}`);
}
else if (error.response?.status === 401) {
throw new Error(`Unauthorized (401): ${JSON.stringify(error.response.data) ||
'Authentication token expired or invalid'}`);
}
else if (error.response?.status === 403) {
throw new Error(`Forbidden (403): ${JSON.stringify(error.response.data) ||
'Access denied to device'}`);
}
else if (error.response?.status === 404) {
throw new Error(`Not Found (404): Device ${deviceId} not found or endpoint not available`);
}
else if (error.response?.status === 429) {
throw new Error('Rate Limited (429): Too many requests, please wait before retrying');
}
else if (error.response?.status >= 500) {
throw new Error(`Server Error (${error.response.status}): LG API server issue - ${JSON.stringify(error.response.data) || 'Internal server error'}`);
}
throw new Error(`Failed to send command: ${error.message || error}`);
}
}
async refreshAccessToken() {
if (!this.authData) {
throw new Error('No authentication data available');
}
try {
console.log('[LG API DEBUG] Refreshing access token');
const updatedAuth = await this.authenticateWithToken(this.authData.refreshToken);
this.authData = updatedAuth;
this.tokenObtainedAt = new Date();
console.log('[LG API DEBUG] Access token refreshed successfully');
}
catch (error) {
console.error('[LG API ERROR] Failed to refresh access token:', error.message);
throw new Error(`Failed to refresh access token: ${error.message}`);
}
}
async autoRefreshWithCredentials(username, password) {
try {
console.log('[LG API DEBUG] Starting auto-refresh process');
if (this.authData?.refreshToken) {
try {
console.log('[LG API DEBUG] Attempting refresh token renewal');
await this.refreshAccessToken();
console.log('[LG API DEBUG] Refresh token renewal successful');
return this.authData;
}
catch (error) {
console.warn(`[LG API WARN] Refresh token renewal failed: ${error.message}, falling back to credentials`);
}
}
else {
console.log('[LG API DEBUG] No refresh token available, using credentials directly');
}
console.log('[LG API DEBUG] Re-authenticating with username/password');
const newAuth = await this.authenticateWithCredentials(username, password);
this.authData = newAuth;
console.log('[LG API DEBUG] Re-authentication successful');
console.log(`[LG API DEBUG] New token expires: ${newAuth.accessToken ? 'exists' : 'missing'}`);
return newAuth;
}
catch (error) {
console.error('[LG API ERROR] Auto-refresh completely failed:', error.message);
throw new Error(`Auto-refresh failed: ${error.message}`);
}
}
isTokenExpired() {
if (!this.tokenObtainedAt) {
return true;
}
const tokenExpiresInMinutes = 30;
const currentTime = new Date();
const tokenObtainedTime = new Date(this.tokenObtainedAt);
const tokenAge = Math.floor((currentTime.getTime() - tokenObtainedTime.getTime()) / 60000);
return tokenAge > tokenExpiresInMinutes;
}
isCircuitBreakerOpen() {
if (!this.circuitBreakerOpen) {
return false;
}
if (this.lastApiError && (new Date().getTime() - this.lastApiError.getTime()) > 5 * 60 * 1000) {
console.log('[LG API DEBUG] Circuit breaker reset after 5 minutes');
this.circuitBreakerOpen = false;
this.consecutiveErrors = 0;
return false;
}
return true;
}
recordApiSuccess() {
this.consecutiveErrors = 0;
this.circuitBreakerOpen = false;
this.lastApiError = null;
}
recordApiError() {
this.lastApiError = new Date();
this.consecutiveErrors++;
if (this.consecutiveErrors >= 5) {
console.log(`[LG API WARN] Circuit breaker opened due to ${this.consecutiveErrors} consecutive errors`);
this.circuitBreakerOpen = true;
}
}
async executeWithRetry(apiCall, username, password) {
if (this.isCircuitBreakerOpen()) {
throw new Error('Circuit breaker is open due to consecutive API failures. Service will retry automatically in a few minutes.');
}
try {
console.log('[LG API DEBUG] Executing API call with retry capability');
console.log(`[LG API DEBUG] Auth token available: ${!!this.authData?.accessToken}`);
console.log(`[LG API DEBUG] Refresh token available: ${!!this.authData?.refreshToken}`);
console.log(`[LG API DEBUG] Credentials available for fallback: ${!!(username && password)}`);
console.log(`[LG API DEBUG] Consecutive errors: ${this.consecutiveErrors}`);
const result = await apiCall();
this.recordApiSuccess();
return result;
}
catch (error) {
this.recordApiError();
const status = error.response?.status;
console.error(`[LG API ERROR] API call failed with status: ${status}`);
console.error('[LG API ERROR] Error details:', JSON.stringify(error.response?.data, null, 2));
console.error(`[LG API ERROR] Consecutive errors: ${this.consecutiveErrors}`);
if (status === 401 || status === 403 || status === 400) {
console.log(`[LG API DEBUG] Authentication-related error detected (${status}), attempting token refresh`);
if (username && password) {
try {
console.log('[LG API DEBUG] Attempting auto-refresh with credentials');
await this.autoRefreshWithCredentials(username, password);
console.log('[LG API DEBUG] Auto-refresh successful, retrying original API call');
const result = await apiCall();
this.recordApiSuccess();
return result;
}
catch (refreshError) {
console.error('[LG API ERROR] Auto-refresh failed:', refreshError.message);
throw new Error(`API call failed and auto-refresh failed: ${refreshError.message}`);
}
}
else {
console.error('[LG API ERROR] No credentials available for auto-refresh');
if (status === 400) {
throw new Error('Bad Request (400): This may indicate expired authentication or invalid request format. ' +
`Consider restarting Homebridge to re-authenticate. Original error: ${error.message}`);
}
throw new Error(`Authentication error (${status}) and no credentials available for auto-refresh. ` +
`Original error: ${error.message}`);
}
}
throw error;
}
}
isAuthenticated() {
const hasAuth = this.authData !== null && this.authData.accessToken !== '';
const tokenValid = !this.isTokenExpired();
console.log(`[LG API DEBUG] Authentication check: hasAuth=${hasAuth}, tokenValid=${tokenValid}`);
if (this.tokenObtainedAt) {
const tokenAge = Math.floor((new Date().getTime() - this.tokenObtainedAt.getTime()) / 60000);
console.log(`[LG API DEBUG] Token age: ${tokenAge} minutes`);
}
return hasAuth && tokenValid;
}
async validateAuthentication() {
if (!this.authData || !this.authData.accessToken) {
console.log('[LG API DEBUG] No authentication data available');
return false;
}
if (this.isTokenExpired()) {
console.log('[LG API DEBUG] Token appears to be expired, validation suggests refresh needed');
return false;
}
console.log('[LG API DEBUG] Authentication appears valid');
return true;
}
getAuthData() {
return this.authData;
}
}
exports.LGApi = LGApi;