homebridge-freeathome-local-kdljns
Version:
Platform Plugin to manage free@home accessories via HomeBridge using local API
361 lines (360 loc) • 16.2 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SystemAccessPoint = void 0;
const axios_1 = __importDefault(require("axios"));
const compare_versions_1 = __importDefault(require("compare-versions"));
const Logger_1 = require("freeathome-api/dist/lib/Logger");
const GuardedClient_1 = require("./GuardedClient");
const MessageBuilder_1 = require("./MessageBuilder");
const https = require('https');
class SystemAccessPoint {
constructor(configuration, subscriber, logger) {
this.online = false;
this.keepAliveMessageId = 1;
this.pingTimeoutSeconds = 10000;
this.keepAliveTimer = null;
this.pingTimeout = null;
this.deviceData = {};
this.subscribed = false;
this.logger = new Logger_1.ConsoleLogger();
/**
* protocols, we need some smarter way in the future (enableTLS: true in config)
*/
this._protocol1 = 'wss://';
this._protocol2 = 'https://';
/**
* ports will be set automagically (hopefully)
*/
this._port = '';
/**
* the API entry path
*/
this._path2api = '/fhapi/v1/api';
/**
* default UUID - we will read the "real" uuid from config.json
*/
this._uuid = '00000000-0000-0000-0000-000000000000';
/**
* minimal version to use the local API
*/
this._minversionAP = '2.6.0';
this.configuration = configuration;
this.subscriber = subscriber;
if (logger !== undefined && logger !== null) {
this.logger = logger;
}
// ignore self signed certs at instance level
this.axios = axios_1.default.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
}
createClient() {
return __awaiter(this, void 0, void 0, function* () {
this.settings = yield this.getSettings();
let user;
for (let tempUser of this.settings.users) {
if (tempUser.name == this.configuration.username) {
user = tempUser;
break;
}
}
if (user === undefined) {
this.logger.error('The user does not exist in the System Access Point\'s configuration');
throw new Error(`User ${this.configuration.username} does not exist`);
}
this.user = user;
let username = user.jid.split('@')[0];
/**
* private readonly _protocol1 = 'wss://'
* private _port = ''
* private readonly _path2api = '/fhapi/v1/api'
*/
this.client = new GuardedClient_1.GuardedClient(this.subscriber, {
service: this._protocol1 + this.configuration.hostname + ((this._port != '') ? ':' + this._port : '') + this._path2api + '/ws',
from: this.configuration.hostname,
resource: 'freeathome-api',
username: username,
password: this.configuration.password
}, this.logger);
this.messageBuilder = new MessageBuilder_1.MessageBuilder(username);
this.registerHandlers();
});
}
getSettings() {
return __awaiter(this, void 0, void 0, function* () {
let response = yield this.axios.get(this._protocol2 + this.configuration.hostname + '/settings.json');
if (response.status != 200) {
this.logger.error("Unexpected status code from System Access Point while retrieving settings.json.");
throw new Error("Unexpected status code from System Access Point while retrieving settings.json.");
}
if (!('flags' in response.data) || !('version' in response.data.flags)) {
this.logger.error("Flags key does not exist in settings.json.");
throw new Error("Flags key does not exist in settings.json.");
}
if (!('users' in response.data || !Array.isArray(response.data.users))) {
this.logger.error("Users key does not exist in settings.json.");
throw new Error("Users key does not exist in settings.json.");
}
return response.data;
});
}
getDeviceConfiguration() {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
let _restpath = '/rest/configuration';
let bwaToken = this.client.getBWAToken();
try {
let response = yield this.axios.get(this._protocol2 + this.configuration.hostname + this._path2api + _restpath, {
headers: { 'Authorization': 'Basic ' + bwaToken }
});
if (response.status != 200) {
this.logger.error("Unexpected status code from System Access Point while retrieving " + _restpath);
throw new Error("Unexpected status code from System Access Point while retrieving " + _restpath);
}
/**
* retrieving the uuid from device config
*/
this._uuid = (_a = Object.keys(response.data)[0]) !== null && _a !== void 0 ? _a : this._uuid;
this.deviceData = (_b = response.data[this._uuid]) === null || _b === void 0 ? void 0 : _b.devices;
this.subscriber.broadcastMessage({ result: response.data, type: 'subscribed' });
return response.data;
}
catch (e) {
this.logger.error("Unexpected status code from System Access Point while retrieving " + _restpath + "\n" + e.toString());
return null;
}
});
}
/**
*
* onopen: (event: WebSocket.OpenEvent) => void;
* onerror: (event: WebSocket.ErrorEvent) => void;
* onclose: (event: WebSocket.CloseEvent) => void;
* onmessage: (event: WebSocket.MessageEvent) => void;
*/
registerHandlers() {
if (this.client === undefined) {
throw new Error("Unknown error occurred! this.client undefined.");
}
this.client.on('error', err => {
this.logger.error(err.toString());
this.subscriber.broadcastMessage({
type: "error",
result: err
});
});
this.client.on('close', () => {
this.logger.log('Access Point has gone offline');
this.online = false;
this.subscribed = false;
this.subscriber.broadcastMessage({
'type': 'subscribed',
'result': false
});
this.disableKeepAliveMessages();
});
this.client.guardedOn('message', (stanza) => __awaiter(this, void 0, void 0, function* () {
var _a;
this.logger.debug('Received stanza:', JSON.parse(stanza));
let astanza = (_a = JSON.parse(stanza)[this._uuid]) !== null && _a !== void 0 ? _a : null;
this.heartBeat();
if (astanza.datapoints) {
this.handleEvent(astanza);
}
}));
this.client.on('open', (address) => __awaiter(this, void 0, void 0, function* () {
let connectedAs = 'Local API Websocket';
this.logger.log("Connected as " + connectedAs);
this.connectedAs = connectedAs;
this.logger.log("Retrieving configuration...");
let deviceData = this.getDeviceConfiguration();
}));
this.client.on('ping', ping => {
this.heartBeat();
this.logger.debug('WS Ping:', ping);
});
// Debug
this.client.on('status', status => {
this.logger.debug('Received new status:', status);
});
this.client.on('input', input => {
this.logger.debug('Received new input data:', input);
});
this.client.on('output', output => {
this.logger.debug('Received new output data:', output);
});
}
/**
* heartbeat to detect if WS still send messages od my be death
*/
heartBeat() {
return __awaiter(this, void 0, void 0, function* () {
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
}
let self = this;
this.logger.debug("*** heartBeat " + this.pingTimeoutSeconds);
// Use `WebSocket#terminate()`, which immediately destroys the connection,
// instead of `WebSocket#close()`, which waits for the close timer.
// Delay should be equal to the interval at which your server
// sends out pings plus a conservative assumption of the latency.
this.pingTimeout = setTimeout(() => {
//self.disconnect()
}, this.pingTimeoutSeconds);
});
}
handleEvent(stanza) {
this.logger.debug("handleEvent: ");
this.logger.debug(JSON.stringify(stanza));
for (const [key, value] of Object.entries(stanza.datapoints)) {
if (key) {
let telegram = key + '/' + value;
//this.logger.log('* ' + telegram)
this.applyIncrementalUpdate(telegram.split('/'));
}
}
}
unwrapEventData(item) {
return "";
}
sendMessage(message, value) {
return __awaiter(this, void 0, void 0, function* () {
// await this.client!.send(message)
let bwaToken = this.client.getBWAToken();
try {
let response = yield this.axios.put(this._protocol2 + this.configuration.hostname + this._path2api + '/rest/datapoint/' + this._uuid + '/' + message, value, {
headers: { 'Authorization': 'Basic ' + bwaToken }
});
if (response.status != 200) {
this.logger.error("Unexpected status code from System Access Point while PUT ");
throw new Error("Unexpected status code from System Access Point while PUT ");
}
}
catch (e) {
this.logger.error("Unexpected status code from System Access Point while PUT ");
}
});
}
/**
* create and connect WS client
*/
connect() {
return __awaiter(this, void 0, void 0, function* () {
yield this.createClient();
if ((0, compare_versions_1.default)(this.settings.flags.version, this._minversionAP) < 0) {
throw Error('Your System Access Point\'s firmware must be at least ' + this._minversionAP);
}
try {
yield this.client.start();
//this.sendKeepAliveMessages()
this.heartBeat();
}
catch (e) {
this.logger.error('Could not connect to System Access Point', e.toString());
throw Error("Could not connect to System Access Point");
}
});
}
disconnect() {
return __awaiter(this, void 0, void 0, function* () {
this.logger.log("Disconnecting from the System Access Point...");
yield this.client.stop();
});
}
sendKeepAliveMessage() {
return __awaiter(this, void 0, void 0, function* () {
//await this.sendMessage(this.keepAliveMessageId++)
});
}
sendKeepAliveMessages() {
this.keepAliveTimer = setInterval(() => this.sendKeepAliveMessage(), 15000);
}
disableKeepAliveMessages() {
if (this.keepAliveTimer !== null) {
clearInterval(this.keepAliveTimer);
this.keepAliveTimer = null;
}
}
applyIncrementalUpdate(update) {
if (update == null || !(update instanceof Object)) {
throw new Error("Invalid Incremental Update");
}
let upd = Array();
if (update.length == 4) {
const serialNo = update[0];
const channelNo = update[1];
const datapointNo = update[2];
const value = update[3];
upd[serialNo] = Array();
if (!(serialNo in this.deviceData)) {
this.deviceData[serialNo] = {
serialNumber: serialNo,
channels: {}
};
}
else {
upd[serialNo]['deviceId'] = this.deviceData[serialNo]['deviceId'];
upd[serialNo]['typeName'] = this.deviceData[serialNo]['typeName'];
}
if (channelNo != null) {
if (!(channelNo in this.deviceData[serialNo]['channels'])) {
this.deviceData[serialNo]['channels'][channelNo] = {
datapoints: {}
};
}
if (datapointNo != null) {
if (this.deviceData[serialNo]['channels'][channelNo]['datapoints'] != null) {
// remote API can be removed here
this.deviceData[serialNo]['channels'][channelNo]['datapoints'][datapointNo] = value;
}
else {
let channelKey = '';
if (this.deviceData[serialNo]['channels'][channelNo]['outputs'][datapointNo] != null) {
channelKey = 'outputs';
}
else if (this.deviceData[serialNo]['channels'][channelNo]['inputs'][datapointNo] != null) {
channelKey = 'inputs';
}
// local API inputs & outputs - datapointNo csn oeitherr be in inputs or outputs
upd[serialNo]['channels'] = [];
upd[serialNo]['channels'][channelNo] = [];
upd[serialNo]['channels'][channelNo][channelKey] = [];
upd[serialNo]['channels'][channelNo][channelKey][datapointNo] = this.deviceData[serialNo]['channels'][channelNo][channelKey][datapointNo];
upd[serialNo]['channels'][channelNo][channelKey][datapointNo].value = value;
}
// we need this in BuschJaegerApPlatform.prototype.processUpdate = function(actuators)
upd[serialNo]['serial'] = serialNo;
this.logger.debug("Updated Datapoint: " + serialNo + '/' + channelNo + '/' + datapointNo + '/' + value);
}
this.subscriber.broadcastMessage({ result: upd, type: 'update' });
}
}
}
setDatapoint(serialNo, channel, datapoint, value) {
return __awaiter(this, void 0, void 0, function* () {
yield this.sendMessage(this.messageBuilder.buildSetDatapointMessage(serialNo, channel, datapoint, value), value);
this.logger.log("Set Datapoint: " + serialNo + '/' + channel + '/' + datapoint + '/' + value);
});
}
getDeviceData() {
if (Object.entries(this.deviceData).length === 0 && this.deviceData.constructor === Object) {
throw new Error("Device Data was requested before we have initialized it");
}
return this.deviceData;
}
}
exports.SystemAccessPoint = SystemAccessPoint;