import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import type { AxiosError } from 'axios'; import https from 'https'; import tls from 'tls'; import crypto from 'crypto'; import { DaikinConnectionException } from './exceptions.js'; export interface RetryConfig { maxAttempts: number; retryDelay: number; retryCondition?: (error: AxiosError) => boolean; } const DEFAULT_RETRY_CONFIG: RetryConfig = { maxAttempts: 3, retryDelay: 200, retryCondition: (error: AxiosError) => { return ( error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT' || error.code === 'ECONNREFUSED' || (error.response?.status ?? 0) >= 500 ); }, }; export class HttpClient { private client: AxiosInstance; private retryConfig: RetryConfig; constructor(baseURL: string, timeout = 30000, retryConfig?: Partial, httpsRejectUnauthorized = true) { this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; const isHttps = baseURL.startsWith('https://'); const config: AxiosRequestConfig = { baseURL, timeout, validateStatus: () => true, }; if (isHttps) { const httpsAgentOptions: https.AgentOptions = { rejectUnauthorized: httpsRejectUnauthorized, }; if (!httpsRejectUnauthorized) { // Enable legacy server connect and set cipher level // This is needed for older Daikin BRP072C devices try { (httpsAgentOptions as Record).secureOptions = crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT; } catch { // Ignore if constants not available } try { (httpsAgentOptions as Record).ciphers = 'DEFAULT@SECLEVEL=0'; } catch { // Ignore if cipher setting not available } } config.httpsAgent = new https.Agent(httpsAgentOptions); } this.client = axios.create(config); } setHeader(key: string, value: string): void { this.client.defaults.headers.common[key] = value; } removeHeader(key: string): void { delete this.client.defaults.headers.common[key]; } async get(url: string, config?: AxiosRequestConfig): Promise { return this.executeWithRetry(() => this.client.get(url, config)); } async post( url: string, data?: unknown, config?: AxiosRequestConfig, ): Promise { return this.executeWithRetry(() => this.client.post(url, data, config), ); } private async executeWithRetry( requestFn: () => Promise<{ data: T }>, attempt = 1, ): Promise { try { const response = await requestFn(); return response.data; } catch (error) { const axiosError = error as AxiosError; if ( attempt < this.retryConfig.maxAttempts && this.retryConfig.retryCondition?.(axiosError) ) { await this.delay(this.retryConfig.retryDelay * attempt); return this.executeWithRetry(requestFn, attempt + 1); } if (axiosError.response?.status === 403) { throw new DaikinConnectionException( `HTTP 403 Forbidden for ${axiosError.config?.url}`, ); } if (axiosError.response?.status === 404) { return {} as T; } throw new DaikinConnectionException( axiosError.message || 'Unknown error occurred', ); } } private delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } } export function createHttpClient( baseURL: string, timeout?: number, retryConfig?: Partial, httpsRejectUnauthorized = true, ): HttpClient { return new HttpClient(baseURL, timeout, retryConfig, httpsRejectUnauthorized); }