|
|
class GyverHub extends EventEmitter {
|
|
|
/** @type {Config} */
|
|
|
config;
|
|
|
/** @type {Connection[]} */
|
|
|
#connections = [];
|
|
|
/** @type {Device[]} */
|
|
|
#devices = [];
|
|
|
_ws;
|
|
|
|
|
|
static api_v = 1;
|
|
|
|
|
|
constructor() {
|
|
|
super();
|
|
|
this.config = new Config();
|
|
|
this.config.set('hub', 'prefix', 'MyDevices');
|
|
|
this.config.set('hub', 'client_id', Math.round(Math.random() * 0xffffffff).toString(16));
|
|
|
}
|
|
|
|
|
|
addConnection(connClass) {
|
|
|
for (const connection of this.#connections)
|
|
|
if (connection instanceof connClass)
|
|
|
return connection;
|
|
|
|
|
|
const conn = new connClass(this);
|
|
|
conn.addEventListener('statechange', e => {
|
|
|
this.dispatchEvent(new ConnectionStateChangeEvent('connectionstatechange', e.connection, e.state));
|
|
|
this.dispatchEvent(new ConnectionStateChangeEvent('connectionstatechange.' + conn.name, e.connection, e.state));
|
|
|
if (conn.isConnected()) conn.discover();
|
|
|
});
|
|
|
this.#connections.push(conn);
|
|
|
|
|
|
if (conn.name === 'HTTP') {
|
|
|
this._ws = this.addConnection(WebSocketConnection);
|
|
|
}
|
|
|
|
|
|
return conn;
|
|
|
}
|
|
|
|
|
|
get mqtt() {
|
|
|
for (const connection of this.#connections) {
|
|
|
if (connection instanceof MQTTConnection)
|
|
|
return connection;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
get bt() {
|
|
|
for (const connection of this.#connections) {
|
|
|
if (connection instanceof BLEConnection)
|
|
|
return connection;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
get serial() {
|
|
|
for (const connection of this.#connections) {
|
|
|
if (connection instanceof SerialConnection)
|
|
|
return connection;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
get tg() {
|
|
|
for (const connection of this.#connections) {
|
|
|
if (connection instanceof TelegramConnection)
|
|
|
return connection;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
get http() {
|
|
|
for (const connection of this.#connections) {
|
|
|
if (connection instanceof HTTPConnection)
|
|
|
return connection;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* ID клиента (хаба).
|
|
|
* @type {string}
|
|
|
*/
|
|
|
get clientId() {
|
|
|
return this.config.get('hub', 'client_id');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Текущий префикс устройств.
|
|
|
* @type {string}
|
|
|
*/
|
|
|
get prefix() {
|
|
|
return this.config.get('hub', 'prefix');
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Получить список всех известных префиксов (текущий и префиксы устройств), без дубликатов.
|
|
|
* @returns {string[]}
|
|
|
*/
|
|
|
getAllPrefixes() {
|
|
|
const list = [this.prefix];
|
|
|
for (const dev_info of Object.values(this.config.get('devices') ?? {}))
|
|
|
if (dev_info.prefix && !list.includes(dev_info.prefix))
|
|
|
list.push(dev_info.prefix);
|
|
|
|
|
|
return list;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Initialize connections.
|
|
|
*/
|
|
|
async begin() {
|
|
|
for (const connection of this.#connections) {
|
|
|
await connection.begin();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//#region Device communication
|
|
|
|
|
|
/**
|
|
|
* Discover all known devices by all active connnections.
|
|
|
*/
|
|
|
async discover() {
|
|
|
for (let dev of this.#devices) {
|
|
|
dev.active_connections.length = 0;
|
|
|
}
|
|
|
|
|
|
for (const connection of this.#connections) {
|
|
|
connection.discover();
|
|
|
}
|
|
|
|
|
|
this._checkDiscoverEnd();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Search for new devices on all active connections.
|
|
|
*/
|
|
|
async search() {
|
|
|
for (const connection of this.#connections) {
|
|
|
connection.search();
|
|
|
}
|
|
|
|
|
|
this._checkDiscoverEnd();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Check if hub currently allows discover.
|
|
|
* @returns {boolean}
|
|
|
*/
|
|
|
#isDiscovering() {
|
|
|
return this.#connections.some(conn => conn.isDiscovering());
|
|
|
}
|
|
|
|
|
|
_checkDiscoverEnd() {
|
|
|
if (!this.#isDiscovering()) this.dispatchEvent(new Event('discoverfinished'));
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
//#region Device list management
|
|
|
|
|
|
/**
|
|
|
* Получить объект устройства по ID.
|
|
|
* @param {string} id
|
|
|
* @returns {Device | null}
|
|
|
*/
|
|
|
dev(id) {
|
|
|
if (!id) return null;
|
|
|
for (let d of this.#devices) {
|
|
|
if (d.info.id == id) return d;
|
|
|
}
|
|
|
if (this.config.get('devices', id, 'id') === id) {
|
|
|
const device = new Device(this, id);
|
|
|
this.#devices.push(device);
|
|
|
this.dispatchEvent(new DeviceEvent('devicecreated', device));
|
|
|
return device;
|
|
|
}
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Получить список ID устройств.
|
|
|
* @returns {string[]}
|
|
|
*/
|
|
|
getDeviceIds() {
|
|
|
const ids = this.#devices.map(d => d.info.id);
|
|
|
const cfg = this.config.get('devices');
|
|
|
if (cfg)
|
|
|
for (const i of Object.keys(cfg))
|
|
|
if (!ids.includes(i))
|
|
|
ids.push(i);
|
|
|
return ids;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Add or update device info.
|
|
|
* @param {object} data
|
|
|
* @param {Connection | undefined} conn
|
|
|
*/
|
|
|
addDevice(data, conn = undefined) {
|
|
|
let device = this.dev(data.id);
|
|
|
|
|
|
if (device) { // exists
|
|
|
let infoChanged = false;
|
|
|
for (const key in data) {
|
|
|
if (device.info[key] !== data[key]) {
|
|
|
device.info[key] = data[key];
|
|
|
infoChanged = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (conn) device.addConnection(conn);
|
|
|
if (infoChanged) this.dispatchEvent(new DeviceEvent('deviceinfochanged', device));
|
|
|
|
|
|
} else { // not exists
|
|
|
if (!data.prefix) data.prefix = this.prefix;
|
|
|
device = new Device(this, data.id);
|
|
|
|
|
|
for (const key in data) {
|
|
|
device.info[key] = data[key];
|
|
|
}
|
|
|
|
|
|
if (conn) device.addConnection(conn);
|
|
|
this.#devices.push(device);
|
|
|
this.dispatchEvent(new DeviceEvent('devicecreated', device));
|
|
|
this.dispatchEvent(new DeviceEvent('deviceadded', device));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Move device in list
|
|
|
* @param {string} id
|
|
|
* @param {number} dir
|
|
|
*/
|
|
|
moveDevice(id, dir) {
|
|
|
if (this.#devices.length == 1) return;
|
|
|
let idx = 0;
|
|
|
for (let d of this.#devices) {
|
|
|
if (d.info.id == id) break;
|
|
|
idx++;
|
|
|
}
|
|
|
if (dir == 1 ? idx <= this.#devices.length - 2 : idx >= 1) {
|
|
|
let b = this.#devices[idx];
|
|
|
this.#devices[idx] = this.#devices[idx + dir];
|
|
|
this.#devices[idx + dir] = b;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Remove device from hub
|
|
|
* @param {string} id
|
|
|
*/
|
|
|
deleteDevice(id) {
|
|
|
this.config.delete('devices', id);
|
|
|
for (const i in this.#devices) {
|
|
|
if (this.#devices[i].info.id === id) {
|
|
|
this.#devices.splice(i, 1);
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//#endregion
|
|
|
|
|
|
async _parsePacket(conn, data, ip = null, port = null) {
|
|
|
if (!data || !data.length) return;
|
|
|
|
|
|
data = data.trim()
|
|
|
.replaceAll("#{", "{")
|
|
|
.replaceAll("}#", "}")
|
|
|
.replaceAll(/([^\\])\\([^\"\\nrt])/ig, "$1\\\\$2")
|
|
|
.replaceAll(/\t/ig, "\\t")
|
|
|
.replaceAll(/\n/ig, "\\n")
|
|
|
.replaceAll(/\r/ig, "\\r");
|
|
|
|
|
|
for (const code in HubCodes) {
|
|
|
const re = new RegExp(`(#${Number(code).toString(16)})([:,\\]\\}])`, "ig");
|
|
|
data = data.replaceAll(re, `"${HubCodes[code]}"$2`);
|
|
|
}
|
|
|
|
|
|
const re = /(#[0-9a-f][0-9a-f])([:,\]\}])/ig;
|
|
|
if (data.match(re)) {
|
|
|
this.onHubError('Device has newer API version. Update App!');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
data = JSON.parse(data);
|
|
|
} catch (e) {
|
|
|
console.log('Wrong packet (JSON): ' + e + ' in: ' + data);
|
|
|
// this.onHubError('Wrong packet (JSON)');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!data.id) return this.onHubError('Wrong packet (ID)');
|
|
|
if (data.client && this.clientId != data.client) return;
|
|
|
const type = data.type;
|
|
|
delete data.type;
|
|
|
|
|
|
console.log('[IN]', type, data);
|
|
|
|
|
|
if (type == 'discover') {
|
|
|
if (!this.#isDiscovering()) {
|
|
|
console.log('Device not added (not discovering):', data);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (conn instanceof HTTPConnection) {
|
|
|
data.ip = ip;
|
|
|
data.http_port = port;
|
|
|
}
|
|
|
this.addDevice(data, conn);
|
|
|
}
|
|
|
|
|
|
const device = this.dev(data.id);
|
|
|
if (device) {
|
|
|
device.addConnection(conn);
|
|
|
await device._parse(type, data);
|
|
|
}
|
|
|
return device;
|
|
|
}
|
|
|
}; |