UNPKG

homebridge-econet-rheem

Version:

Homebridge plugin for control of Rheem and Ruud thermostats and water heaters

190 lines 7.81 kB
import axios, { isAxiosError } from 'axios'; import https from 'https'; import tls from 'tls'; import { DeviceAuth, UserAuth } from './auth.js'; import { EquipmentType } from './enums.js'; import { strings } from '../i18n/i18n.js'; import { CLEARBLADE_CERT_INTERMEDIATE, CLEARBLADE_CERT_ROOT, CLEARBLADE_HOST, CLEARBLADE_KEY, CLEARBLADE_SECRET } from '../homebridge/settings.js'; import { DELAYS, MINUTE, SECOND } from '../tools/time.js'; const BASE_HEADERS = { 'ClearBlade-SystemKey': CLEARBLADE_KEY, 'ClearBlade-SystemSecret': CLEARBLADE_SECRET, 'Content-Type': 'application/json; charset=UTF-8', }; const BASE_URL_V1 = `https://${CLEARBLADE_HOST}/api/v/1`; const BASE_URL_V2 = `https://${CLEARBLADE_HOST}/api/v/2`; const AUTH_USER_URL = `${BASE_URL_V1}/user/auth`; const AUTH_DEVICE_URL = `${BASE_URL_V2}/devices/${CLEARBLADE_KEY}/auth`; const LOCATIONS_URL = `${BASE_URL_V1}/code/${CLEARBLADE_KEY}/getUserDataForApp`; const HTTP_TIMEOUT = 10 * SECOND; const HTTP_RETRY_CODES = [ 'ERR_NETWORK', // General network error in Axios 'ETIMEDOUT', // Request timed out 'ECONNREFUSED', // Connection refused by server '429', // Too Many Requests (rate limit) '500', // Internal Server Error '502', // Bad Gateway '503', // Service Unavailable '504', // Gateway Timeout ]; export class EconetApi { dependency; log; userAuth; retryIndex = 0; httpsAgent = new https.Agent({ ca: [CLEARBLADE_CERT_ROOT, CLEARBLADE_CERT_INTERMEDIATE, ...tls.rootCertificates] }); static instance; constructor(dependency) { this.dependency = dependency; this.log = dependency.log; } static async connect(dependency) { const api = new EconetApi(dependency); EconetApi.instance = api; api.userAuth = UserAuth.load(dependency.email); let shouldContinue = true; if (!api.userAuth) { shouldContinue = await api.authenticateUser(); } if (!shouldContinue) { return; } const equipmentsData = await api.getLocations(); if (!equipmentsData) { return; } for (const equipmentData of equipmentsData.values()) { const device = dependency.devices.find((device) => device.serialNumber === equipmentData.serial_number); if (device !== undefined && DeviceAuth.load(device.serialNumber, dependency.email) === undefined) { await api.authenticateDevice(device.serialNumber, device.deviceName, device.activeKey); } } return equipmentsData; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async httpRequest(caller, // eslint-disable-next-line @typescript-eslint/no-explicit-any data, url, ...parameters) { parameters.forEach(param => { url = url.replace('%s', param ?? ''); }); let config; if (this.userAuth?.token) { const headers = { ...BASE_HEADERS, 'ClearBlade-UserToken': this.userAuth?.token }; config = { headers: headers, timeout: HTTP_TIMEOUT, httpsAgent: this.httpsAgent }; } else { config = { headers: BASE_HEADERS, timeout: HTTP_TIMEOUT, httpsAgent: this.httpsAgent }; } try { let res; if (data) { res = await axios.post(url, data, config); } else { res = await axios.get(url, config); } if (!res.data) { this.log.warning(caller, JSON.stringify(res.data)); throw new Error(strings.http.noDataReceived); } this.log.ifDebug(`${caller}() —`, `${url.substring(BASE_URL_V1.length + 1)}${data ? ` ${JSON.stringify(data)}` : ''}`, `\n${JSON.stringify(res.data)}`); this.retryIndex = 0; return res.data; } catch (err) { return this.retryHTTPIfPossible(err, caller, () => this.httpRequest(caller, data, url, ...parameters)); } } // eslint-disable-next-line @typescript-eslint/no-explicit-any async retryHTTPIfPossible(err, caller, retry) { if (!isAxiosError(err)) { this.log.warning(err.message); return undefined; } const errorCode = err.code || err.response?.status?.toString() || 'UNKNOWN'; if (!HTTP_RETRY_CODES.includes(errorCode) || this.retryIndex >= DELAYS.length) { this.log.warning(err.message); return undefined; } const retryDelay = DELAYS[Math.min(this.retryIndex, DELAYS.length - 1)]; if (retryDelay <= MINUTE) { this.log.ifDebug(strings.http.retryInSeconds, retryDelay / SECOND); } else { this.log.ifDebug(strings.http.retryInMinutes, retryDelay / MINUTE); } await new Promise(resolve => setTimeout(resolve, retryDelay)); this.retryIndex += 1; return await retry(); } static async authenticateUser() { await EconetApi.instance?.authenticateUser(); } async authenticateUser() { const data = { email: this.dependency.email, password: this.dependency.password }; const tokenData = await this.httpRequest(this.authenticateUser.name, data, AUTH_USER_URL); if (!tokenData) { return false; } if (tokenData.options.success === false) { this.log.warning(tokenData.options.message); return false; } this.userAuth = new UserAuth(tokenData); this.userAuth.save(this.dependency.email); if (!this.dependency.disableLogging) { this.log.always(strings.http.authSuccess); } return true; } async authenticateDevice(serialNumber, deviceName, activeKey) { const data = { deviceName, activeKey }; const tokenData = await this.httpRequest(this.authenticateDevice.name, data, AUTH_DEVICE_URL); if (!tokenData) { return false; } DeviceAuth.save(serialNumber, tokenData, this.dependency.email); return true; } async getLocations() { const data = { 'resource': 'friedrich' }; const locationsData = await this.httpRequest(this.getLocations.name, data, LOCATIONS_URL); if (!locationsData) { return; } const equipmentsData = []; for (const location of locationsData.results.locations) { for (const equipmentData of location.equiptments) { if (equipmentData.device_type === undefined) { continue; } if (!this.validateEquipmentData(equipmentData)) { continue; } equipmentsData.push(equipmentData); if (equipmentData.device_type !== EquipmentType.THERMOSTAT || !Array.isArray(equipmentData.zoning_devices)) { continue; } equipmentData.zoning_devices.forEach(zoningEquipmentData => { if (this.validateEquipmentData(zoningEquipmentData)) { equipmentsData.push(zoningEquipmentData); } }); } } return equipmentsData; } validateEquipmentData(equipmentData) { if (!Object.values(EquipmentType).includes(equipmentData.device_type)) { this.log.warning(strings.equipment.unsupported, equipmentData.device_type); return false; } if (equipmentData.serial_number === undefined) { this.log.warning(strings.equipment.missingSerial, equipmentData.device_type); return false; } return true; } } //# sourceMappingURL=http.js.map