You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

316 lines
7.5 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;
}
};