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