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, ETS group address importer, and KNX routing between interfaces. Easy to use and highly configurable.

177 lines (152 loc) 4.34 kB
import { EventEmitter } from 'events' import { Agent as HTTPSAgent, request as httpsRequest } from 'node:https' import { URL } from 'node:url' import { TextDecoder } from 'node:util' import { setTimeout as delay } from 'timers/promises' const createAbortError = () => { const err = new Error('The operation was aborted') err.name = 'AbortError' return err } const openEventStream = (url, { headers = {}, agent, signal } = {}) => { return new Promise((resolve, reject) => { let response = null let requestInstance = null let settled = false const safeResolve = (value) => { if (settled) return settled = true resolve(value) } const safeReject = (error) => { if (settled) return settled = true reject(error) } const cleanup = () => { if (signal) signal.removeEventListener('abort', onAbort) } const onAbort = () => { const abortErr = createAbortError() if (response && typeof response.destroy === 'function') { response.destroy(abortErr) } else if (requestInstance && typeof requestInstance.destroy === 'function') { requestInstance.destroy(abortErr) } } const target = new URL(url) const options = { protocol: target.protocol, hostname: target.hostname, port: target.port || (target.protocol === 'https:' ? 443 : 80), path: `${target.pathname}${target.search}`, method: 'GET', headers, agent } requestInstance = httpsRequest(options, (res) => { response = res res.once('close', cleanup) res.once('error', cleanup) safeResolve(res) }) requestInstance.on('error', (error) => { cleanup() safeReject(error) }) requestInstance.end() if (signal) { if (signal.aborted) { onAbort() cleanup() safeReject(createAbortError()) return } signal.addEventListener('abort', onAbort) } }) } class HueEventStream extends EventEmitter { constructor (url, { headers = {}, reconnectInterval = 5000 } = {}) { super() this.url = url this.headers = headers this.reconnectInterval = reconnectInterval this.abortController = null this.agent = new HTTPSAgent({ rejectUnauthorized: false }) this.connected = false this._start() } async _start () { while (true) { try { this.abortController = new AbortController() const res = await openEventStream(this.url, { headers: this.headers, agent: this.agent, signal: this.abortController.signal }) if (res.statusCode !== 200) { res.resume?.() this.emit('error', new Error(`Unexpected status: ${res.statusCode}`)) await delay(this.reconnectInterval) continue } this.emit('open') this.connected = true let buffer = '' const decoder = new TextDecoder() for await (const chunk of res) { buffer += decoder.decode(chunk, { stream: true }) const lines = buffer.split(/\r?\n\r?\n/) buffer = lines.pop() // l'ultima parte potrebbe essere incompleta for (const block of lines) { const event = this._parseEvent(block) if (event) this.emit('message', event) } } this.emit('close') this.connected = false await delay(this.reconnectInterval) } catch (err) { if (err.name !== 'AbortError') { this.emit('error', err) await delay(this.reconnectInterval) } } } } _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 } } } close () { if (this.abortController) { this.abortController.abort() this.abortController = null } this.connected = false } } export { HueEventStream }