homebridge-fibaro-home-center
Version:
Homebridge plugin for Fibaro Home Center (2, 2 Lite, 3, 3 Lite, Yubii Home).
250 lines (209 loc) • 6.59 kB
text/typescript
// 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);
}
}