// fibaro-api.ts import superagent from 'superagent'; import Throttle from 'superagent-throttle'; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; import * as constants from './constants'; declare const Buffer; // Throttle for GET requests // fix HC2 - 503-error (devices > 100) const throttle = new Throttle({ active: true, rate: constants.DEFAULT_THROTTLE_RATE, ratePer: constants.DEFAULT_THROTTLE_RATE_PER, concurrent: constants.DEFAULT_THROTTLE_CONCURRENT, }); export class FibaroClient { private baseUrl: string; private auth: string; private adminAuth: string; private https: boolean; private ca: Buffer | null; public status: boolean; constructor( baseUrl: string, username: string, password: string, log: (message: string, error?: Error) => void, adminUsername?: string, adminPassword?: string, ) { this.baseUrl = baseUrl; this.auth = this.createAuthString(username, password); this.adminAuth = adminUsername ? this.createAuthString(adminUsername, adminPassword!) : ''; this.https = baseUrl?.includes('https:') ?? false; this.ca = this.loadCertificate(log); this.status = this.https && !this.ca ? false : true; if (this.https && !this.ca) { log('Put a valid ca.cer file in config.json folder.'); } } private createAuthString(username: string, password: string): string { return 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'); } private loadCertificate(log: (message: string, error?: Error) => void): Buffer | null { if (!this.baseUrl || !this.https) { return null; } const certPaths = [ ...constants.CERTIFICATE_PATHS, path.resolve( process.env.UIX_CONFIG_PATH?.replace(/\/config\.json$/, '') || path.join(os.homedir(), '.homebridge'), constants.CERTIFICATE_FILE_NAME, ), ]; for (const certPath of certPaths) { try { if (fs.existsSync(certPath)) { return fs.readFileSync(certPath); } } catch (e) { log(`Error reading ${constants.CERTIFICATE_FILE_NAME} from ${certPath}:`, e as Error); } } return null; } composeURL(service) { return this.baseUrl + service; } async genericGet(service) { const url = this.composeURL(service); let request = superagent .get(url) .use(throttle.plugin()) .set('Authorization', this.auth) .set('accept', constants.HTTP_ACCEPT_HEADER); if (this.https) { request = request.ca(this.ca as Buffer); } return request; } genericPost(service, body) { const url = this.composeURL(service); let request = superagent .post(url) .use(throttle.plugin()) .send(body) .set('Authorization', this.auth) .set('accept', constants.HTTP_ACCEPT_HEADER); if (this.https) { request = request.ca(this.ca as Buffer); } return request.catch(error => { if (error.message === 'Request discarded due to concurrent limit') { // Silently discard the request return Promise.resolve({ discarded: true }); } throw error; // Re-throw other errors }); } genericPut(service, body) { const url = this.composeURL(service); let request = superagent .put(url) .use(throttle.plugin()) .send(body) .set('Authorization', this.auth) .set('accept', constants.HTTP_ACCEPT_HEADER); if (this.https) { request = request.ca(this.ca as Buffer); } return request.catch(error => { if (error.message === 'Request discarded due to concurrent limit') { // Silently discard the request return Promise.resolve({ discarded: true }); } throw error; // Re-throw other errors }); } genericAdminPut(service, body) { const url = this.composeURL(service); let request = superagent .put(url) .use(throttle.plugin()) .send(body) .set('Authorization', this.adminAuth) .set('accept', constants.HTTP_ACCEPT_HEADER); if (this.https) { request = request.ca(this.ca as Buffer); } return request; } getInfo() { return this.genericGet(constants.API_URL_INFO); } getScenes() { return this.genericGet(constants.API_URL_SCENES); } getClimateZones() { return this.genericGet(constants.API_URL_CLIMATE + '?detailed=false'); } getClimateZone(ID) { return this.genericGet(constants.API_URL_CLIMATE + '/' + ID); } getHeatingZones() { return this.genericGet(constants.API_URL_HEATING); } getHeatingZone(ID) { return this.genericGet(constants.API_URL_HEATING + '/' + ID); } setClimateZoneHandTemperature(ID, mode, temperature, timestamp) { const body = { 'properties': { 'handMode': mode, 'handTimestamp': timestamp, }, }; switch (mode) { case 'Heat': body.properties['handSetPointHeating'] = temperature; break; case 'Cool': body.properties['handSetPointCooling'] = temperature; break; } return this.genericPut(constants.API_URL_CLIMATE + '/' + ID, body); } setHeatingZoneHandTemperature(ID, temperature, timestamp) { const body = { 'properties': { 'handTemperature': temperature, 'handTimestamp': timestamp, }, }; return this.genericPut(constants.API_URL_HEATING + '/' + ID, body); } getRooms() { return this.genericGet(constants.API_URL_ROOMS); } getDevices() { return this.genericGet(constants.API_URL_DEVICES); } getDeviceProperties(ID) { return this.genericGet(constants.API_URL_DEVICES + '/' + ID); } refreshStates(lastPoll) { return this.genericGet(constants.API_URL_REFRESH_STATES + '?last=' + lastPoll); } executeDeviceAction(ID, action, param) { const body = param !== null ? { 'args': param, } : {}; return this.genericPost(constants.API_URL_DEVICES + '/' + ID + '/action/' + action, body); } executeScene(ID, useOldApi) { const body = {}; return this.genericPost(constants.API_URL_SCENES + '/' + ID + (useOldApi ? '/action/start' : '/execute'), body); } getGlobalVariable(globalVariableID) { return this.genericGet(constants.API_URL_GLOBAL_VARIABLES + '/' + globalVariableID); } setGlobalVariable(globalVariableID, value) { const body = value !== null ? { 'value': value, 'invokeScenes': true, } : null; return this.genericAdminPut(constants.API_URL_GLOBAL_VARIABLES + '/' + globalVariableID, body); } }