UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control and ETS group address importer. Easy to use and highly configurable.

214 lines (191 loc) 6.58 kB
import { EventEmitter } from 'events'; import { fetch, Agent } from 'undici'; import { setTimeout as pleaseWait } from 'timers/promises'; import * as http from './http.js'; class classHUE extends EventEmitter { constructor(_hueBridgeIP, _username, _clientkey, _bridgeid, _sysLogger) { super(); this.HUEBridgeConnectionStatus = "disconnected"; this.exitAllQueues = false; this.hueBridgeIP = _hueBridgeIP; this.username = _username; this.clientkey = _clientkey; this.bridgeid = _bridgeid; this.commandQueue = []; this.sysLogger = _sysLogger; this.timerCheckConnected = null; this.restartSSECounter = 0; this.handleQueue(); this.eventStreamAbort = null; } Connect = async () => { if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected); if (this.eventStreamAbort) this.eventStreamAbort.abort(); this.hueApiV2 = http.use({ key: this.username, prefix: `https://${this.hueBridgeIP}/clip/v2` }); const agent = new Agent({ connect: { rejectUnauthorized: false } }); const headers = { 'hue-application-key': this.username, 'Accept': 'text/event-stream', 'Cache-control': 'no-cache' }; this.eventStreamAbort = new AbortController(); const url = `https://${this.hueBridgeIP}/eventstream/clip/v2`; try { const res = await fetch(url, { headers, dispatcher: agent, signal: this.eventStreamAbort.signal }); if (res.status !== 200) { this.emit('error', new Error(`Status ${res.status}`)); return; } this.emit("connected"); this.HUEBridgeConnectionStatus = "connected"; this.sysLogger?.info(`classHUE: connected to SSE`); this.timerCheckConnected = setInterval(() => { (async () => { try { this.restartSSECounter += 1; if (this.restartSSECounter >= 4) { this.sysLogger?.debug(`Restarted SSE Client, per sicurezza, altrimenti potrebbe addormentarsi`); this.restartSSECounter = 0; this.eventStreamAbort.abort(); await this.Connect(); return; } const jReturn = await this.hueApiV2.get('/resource/bridge'); if (!Array.isArray(jReturn) || jReturn.length < 1) throw new Error("Bridge not found"); this.HUEBridgeConnectionStatus = "connected"; } catch (error) { this.sysLogger?.error(`Ping ERROR: ${error.message}`); if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected); this.commandQueue = []; try { await this.close(); } catch (error) { } this.restartSSECounter = 0; this.emit("disconnected"); } })(); }, 120000); let buffer = ''; const textDecoder = new TextDecoder(); for await (const chunk of res.body) { buffer += textDecoder.decode(chunk, { stream: true }); let parts = buffer.split(/\r?\n\r?\n/); if (parts.length > 1) { buffer = parts.pop(); } else { buffer = parts[0]; parts = []; } for (const block of parts) { const parsed = this._parseEvent(block); if (parsed?.data && Array.isArray(parsed.data)) { parsed.data.forEach(ev => { for (let index = 0; index < ev.data.length; index++) { const element = ev.data[index]; this.emit("event", element) } }); } } } } catch (err) { if (err.name !== 'AbortError' && this.sysLogger) { this.sysLogger.error(`EventStream error: ${err.message}`) this.commandQueue = []; try { await this.close(); } catch (error) { } this.restartSSECounter = 0; this.emit("disconnected"); }; } }; _parseEvent = (block) => { const lines = block.split(/\r?\n/); let data = ''; let event = 'message'; for (const line of lines) { if (line.startsWith('data:')) { data += line.slice(5).trim(); } else if (line.startsWith('event:')) { event = line.slice(6).trim(); } } try { return { type: event, data: data ? JSON.parse(data) : null }; } catch (e) { return { type: event, data: data || null }; } }; processQueueItem = async () => { try { const jRet = this.commandQueue.pop(); switch (jRet._operation) { case "setLight": await this.hueApiV2.put(`/resource/light/${jRet._lightID}`, jRet._state); break; case "setGroupedLight": await this.hueApiV2.put(`/resource/grouped_light/${jRet._lightID}`, jRet._state); break; case "setScene": await this.hueApiV2.put(`/resource/scene/${jRet._lightID}`, jRet._state); break; case "stopScene": const allResources = await this.hueApiV2.get("/resource"); const jScene = allResources.find((res) => res.id === jRet._lightID); const linkedLight = allResources.find((res) => res.id === jScene.group.rid).children || []; linkedLight.forEach((light) => { this.writeHueQueueAdd(light.rid, jRet._state, "setLight"); }); break; default: break; } } catch (error) { this.sysLogger?.error(`processQueueItem: ${error.message}`); } }; handleQueue = async () => { do { if (this.commandQueue && this.commandQueue.length > 0) { try { await this.processQueueItem(); } catch (error) { } } await pleaseWait(150); } while (!this.exitAllQueues); }; writeHueQueueAdd = async (_lightID, _state, _operation) => { this.commandQueue.unshift({ _lightID, _state, _operation }); }; deleteHueQueue = async (_lightID) => { this.commandQueue = this.commandQueue.filter((el) => el._lightID !== _lightID); }; close = async () => new Promise((resolve, reject) => { if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected); try { this.exitAllQueues = true; this.restartSSECounter = 0; try { if (this.eventStreamAbort) this.eventStreamAbort.abort(); } catch (error) { } this.HUEBridgeConnectionStatus = "disconnected"; resolve(true); } catch (error) { reject(error); } }); } export { classHUE };