node-red-contrib-knx-ultimate 3.3.29 → 3.3.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -1
- package/nodes/hue-config.js +65 -58
- package/nodes/utils/HueEventStream.mjs +100 -0
- package/nodes/utils/hueEngine.mjs +213 -0
- package/package.json +3 -3
- package/nodes/utils/hueEngine.js +0 -256
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
-
**Version 3.3.
|
|
9
|
+
**Version 3.3.31** - April 2025<br/>
|
|
10
|
+
- **BREAKING CHANGE** **!!!!!!!**: node must be >=18.0.0 (needed before was >=16.0.0).**!!!!!!!** **BREAKING CHANGE**<br/>
|
|
11
|
+
- HUE: getting rid of the Eventsource package, due to sleepy connections not recognized.<br/>
|
|
12
|
+
|
|
13
|
+
**Version 3.3.30** - April 2025<br/>
|
|
10
14
|
- **BREAKING CHANGE** **!!!!!!!**: node must be >=18.0.0 (needed before was >=16.0.0).**!!!!!!!** **BREAKING CHANGE**<br/>
|
|
11
15
|
- HUE: fixed SSE Hue Bridge silent disconnection issue.<br/>
|
|
12
16
|
|
package/nodes/hue-config.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
/* eslint-disable no-inner-declarations */
|
|
6
6
|
/* eslint-disable max-len */
|
|
7
7
|
const cloneDeep = require("lodash/cloneDeep");
|
|
8
|
-
const
|
|
8
|
+
//const classHUE = require("./utils/hueEngine").classHUE;
|
|
9
9
|
const hueColorConverter = require("./utils/colorManipulators/hueColorConverter");
|
|
10
10
|
|
|
11
11
|
|
|
@@ -47,64 +47,67 @@ module.exports = (RED) => {
|
|
|
47
47
|
try {
|
|
48
48
|
if (node.hueManager !== undefined) await node.hueManager.close();
|
|
49
49
|
} catch (error) { /* empty */ }
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
50
|
+
|
|
51
|
+
(async () => {
|
|
52
|
+
try {
|
|
53
|
+
const { classHUE } = await import('./utils/hueEngine.mjs');
|
|
54
|
+
node.hueManager = new classHUE(node.host, node.credentials.username, node.credentials.clientkey, config.bridgeid, node.sysLogger);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
node.sysLogger?.error(`Errore hue-config: node.initHUEConnection: ${error.message}`);
|
|
57
|
+
throw (error)
|
|
58
|
+
}
|
|
59
|
+
node.hueManager.on("event", (_event) => {
|
|
60
|
+
node.nodeClients.forEach((_oClient) => {
|
|
61
|
+
const oClient = _oClient;
|
|
62
|
+
try {
|
|
63
|
+
if (oClient.handleSendHUE !== undefined) oClient.handleSendHUE(_event);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
node.sysLogger?.error(`Errore node.hueManager.on(event): ${error.message}`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
// Connected
|
|
70
|
+
node.hueManager.on("connected", () => {
|
|
71
|
+
if (node.linkStatus === "disconnected") {
|
|
72
|
+
// Start the timer to do initial read.
|
|
73
|
+
if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead);
|
|
74
|
+
node.timerDoInitialRead = setTimeout(() => {
|
|
75
|
+
(async () => {
|
|
76
|
+
try {
|
|
77
|
+
node.sysLogger?.info(`HTTP getting resource from HUE bridge : ${node.name}`);
|
|
78
|
+
await node.loadResourcesFromHUEBridge();
|
|
79
|
+
node.sysLogger?.info(`Total HUE resources count : ${node.hueAllResources.length}`);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
node.nodeClients.forEach((_oClient) => {
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
_oClient.setNodeStatusHue({
|
|
84
|
+
fill: "red",
|
|
85
|
+
shape: "ring",
|
|
86
|
+
text: "HUE",
|
|
87
|
+
payload: error.message,
|
|
88
|
+
});
|
|
89
|
+
}, 200);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
})();
|
|
93
|
+
}, 10000); // 17/02/2020 Do initial read of all nodes requesting initial read
|
|
64
94
|
}
|
|
65
95
|
});
|
|
66
|
-
});
|
|
67
|
-
// Connected
|
|
68
|
-
node.hueManager.on("connected", () => {
|
|
69
|
-
if (node.linkStatus === "disconnected") {
|
|
70
|
-
// Start the timer to do initial read.
|
|
71
|
-
if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead);
|
|
72
|
-
node.timerDoInitialRead = setTimeout(() => {
|
|
73
|
-
(async () => {
|
|
74
|
-
try {
|
|
75
|
-
node.sysLogger?.info(`HTTP getting resource from HUE bridge : ${node.name}`);
|
|
76
|
-
await node.loadResourcesFromHUEBridge();
|
|
77
|
-
node.sysLogger?.info(`Total HUE resources count : ${node.hueAllResources.length}`);
|
|
78
|
-
} catch (error) {
|
|
79
|
-
node.nodeClients.forEach((_oClient) => {
|
|
80
|
-
setTimeout(() => {
|
|
81
|
-
_oClient.setNodeStatusHue({
|
|
82
|
-
fill: "red",
|
|
83
|
-
shape: "ring",
|
|
84
|
-
text: "HUE",
|
|
85
|
-
payload: error.message,
|
|
86
|
-
});
|
|
87
|
-
}, 200);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
})();
|
|
91
|
-
}, 10000); // 17/02/2020 Do initial read of all nodes requesting initial read
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
node.hueManager.on("disconnected", () => {
|
|
98
|
+
node.nodeClients.forEach((_oClient) => {
|
|
99
|
+
_oClient.setNodeStatusHue({
|
|
100
|
+
fill: "red",
|
|
101
|
+
shape: "ring",
|
|
102
|
+
text: "HUE Disconnected",
|
|
103
|
+
payload: "",
|
|
104
|
+
});
|
|
102
105
|
});
|
|
103
106
|
});
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
107
|
+
try {
|
|
108
|
+
await node.hueManager.Connect();
|
|
109
|
+
} catch (error) { }
|
|
110
|
+
})();
|
|
108
111
|
|
|
109
112
|
};
|
|
110
113
|
|
|
@@ -122,12 +125,16 @@ module.exports = (RED) => {
|
|
|
122
125
|
}
|
|
123
126
|
await node.startWatchdogTimer();
|
|
124
127
|
})();
|
|
125
|
-
},
|
|
128
|
+
}, 60000);
|
|
126
129
|
};
|
|
127
130
|
|
|
128
|
-
(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
+
setTimeout(() => {
|
|
132
|
+
(async () => {
|
|
133
|
+
await node.initHUEConnection();
|
|
134
|
+
node.startWatchdogTimer();
|
|
135
|
+
})();
|
|
136
|
+
}, 5000);
|
|
137
|
+
|
|
131
138
|
|
|
132
139
|
// Functions called from the nodes ----------------------------------------------------------------
|
|
133
140
|
// Query the HUE Bridge to return the resources
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { fetch, Agent } from 'undici';
|
|
3
|
+
import { setTimeout as delay } from 'timers/promises';
|
|
4
|
+
|
|
5
|
+
class HueEventStream extends EventEmitter {
|
|
6
|
+
constructor(url, { headers = {}, reconnectInterval = 5000 } = {}) {
|
|
7
|
+
super();
|
|
8
|
+
this.url = url;
|
|
9
|
+
this.headers = headers;
|
|
10
|
+
this.reconnectInterval = reconnectInterval;
|
|
11
|
+
this.abortController = null;
|
|
12
|
+
this.agent = new Agent({
|
|
13
|
+
connect: {
|
|
14
|
+
rejectUnauthorized: false
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
this.connected = false;
|
|
18
|
+
this._start();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async _start() {
|
|
22
|
+
while (true) {
|
|
23
|
+
try {
|
|
24
|
+
this.abortController = new AbortController();
|
|
25
|
+
const res = await fetch(this.url, {
|
|
26
|
+
headers: this.headers,
|
|
27
|
+
dispatcher: this.agent,
|
|
28
|
+
signal: this.abortController.signal
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (res.status !== 200) {
|
|
32
|
+
this.emit('error', new Error(`Unexpected status: ${res.status}`));
|
|
33
|
+
await delay(this.reconnectInterval);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.emit('open');
|
|
38
|
+
this.connected = true;
|
|
39
|
+
|
|
40
|
+
let buffer = '';
|
|
41
|
+
for await (const chunk of res.body) {
|
|
42
|
+
buffer += chunk.toString();
|
|
43
|
+
|
|
44
|
+
const lines = buffer.split(/\r?\n\r?\n/);
|
|
45
|
+
buffer = lines.pop(); // l'ultima parte potrebbe essere incompleta
|
|
46
|
+
|
|
47
|
+
for (const block of lines) {
|
|
48
|
+
const event = this._parseEvent(block);
|
|
49
|
+
if (event) this.emit('message', event);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.emit('close');
|
|
54
|
+
this.connected = false;
|
|
55
|
+
await delay(this.reconnectInterval);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err.name !== 'AbortError') {
|
|
58
|
+
this.emit('error', err);
|
|
59
|
+
await delay(this.reconnectInterval);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_parseEvent(block) {
|
|
66
|
+
const lines = block.split(/\r?\n/);
|
|
67
|
+
let data = '';
|
|
68
|
+
let event = 'message';
|
|
69
|
+
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (line.startsWith('data:')) {
|
|
72
|
+
data += line.slice(5).trim();
|
|
73
|
+
} else if (line.startsWith('event:')) {
|
|
74
|
+
event = line.slice(6).trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
return {
|
|
80
|
+
type: event,
|
|
81
|
+
data: data ? JSON.parse(data) : null
|
|
82
|
+
};
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return {
|
|
85
|
+
type: event,
|
|
86
|
+
data: data || null
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
close() {
|
|
92
|
+
if (this.abortController) {
|
|
93
|
+
this.abortController.abort();
|
|
94
|
+
this.abortController = null;
|
|
95
|
+
}
|
|
96
|
+
this.connected = false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { HueEventStream };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { fetch, Agent } from 'undici';
|
|
3
|
+
import { setTimeout as pleaseWait } from 'timers/promises';
|
|
4
|
+
import * as http from './http.js';
|
|
5
|
+
|
|
6
|
+
class classHUE extends EventEmitter {
|
|
7
|
+
constructor(_hueBridgeIP, _username, _clientkey, _bridgeid, _sysLogger) {
|
|
8
|
+
super();
|
|
9
|
+
this.HUEBridgeConnectionStatus = "disconnected";
|
|
10
|
+
this.exitAllQueues = false;
|
|
11
|
+
this.hueBridgeIP = _hueBridgeIP;
|
|
12
|
+
this.username = _username;
|
|
13
|
+
this.clientkey = _clientkey;
|
|
14
|
+
this.bridgeid = _bridgeid;
|
|
15
|
+
this.commandQueue = [];
|
|
16
|
+
this.sysLogger = _sysLogger;
|
|
17
|
+
this.timerCheckConnected = null;
|
|
18
|
+
this.restartSSECounter = 0;
|
|
19
|
+
this.handleQueue();
|
|
20
|
+
this.eventStreamAbort = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Connect = async () => {
|
|
24
|
+
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
25
|
+
if (this.eventStreamAbort) this.eventStreamAbort.abort();
|
|
26
|
+
|
|
27
|
+
this.hueApiV2 = http.use({
|
|
28
|
+
key: this.username,
|
|
29
|
+
prefix: `https://${this.hueBridgeIP}/clip/v2`
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const agent = new Agent({
|
|
33
|
+
connect: { rejectUnauthorized: false }
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const headers = {
|
|
37
|
+
'hue-application-key': this.username,
|
|
38
|
+
'Accept': 'text/event-stream',
|
|
39
|
+
'Cache-control': 'no-cache'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.eventStreamAbort = new AbortController();
|
|
43
|
+
|
|
44
|
+
const url = `https://${this.hueBridgeIP}/eventstream/clip/v2`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(url, {
|
|
48
|
+
headers,
|
|
49
|
+
dispatcher: agent,
|
|
50
|
+
signal: this.eventStreamAbort.signal
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (res.status !== 200) {
|
|
54
|
+
this.emit('error', new Error(`Status ${res.status}`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.emit("connected");
|
|
59
|
+
this.HUEBridgeConnectionStatus = "connected";
|
|
60
|
+
this.sysLogger?.info(`classHUE: connected to SSE`);
|
|
61
|
+
|
|
62
|
+
this.timerCheckConnected = setInterval(() => {
|
|
63
|
+
(async () => {
|
|
64
|
+
try {
|
|
65
|
+
this.restartSSECounter += 1;
|
|
66
|
+
if (this.restartSSECounter >= 2) {
|
|
67
|
+
this.sysLogger?.debug(`Restarted SSE Client, per sicurezza, altrimenti potrebbe addormentarsi`);
|
|
68
|
+
this.restartSSECounter = 0;
|
|
69
|
+
this.eventStreamAbort.abort();
|
|
70
|
+
await this.Connect();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const jReturn = await this.hueApiV2.get('/resource/bridge');
|
|
74
|
+
if (!Array.isArray(jReturn) || jReturn.length < 1) throw new Error("Bridge not found");
|
|
75
|
+
this.HUEBridgeConnectionStatus = "connected";
|
|
76
|
+
} catch (error) {
|
|
77
|
+
this.sysLogger?.error(`Ping ERROR: ${error.message}`);
|
|
78
|
+
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
79
|
+
this.commandQueue = [];
|
|
80
|
+
try { await this.close(); } catch (error) { }
|
|
81
|
+
this.restartSSECounter = 0;
|
|
82
|
+
this.emit("disconnected");
|
|
83
|
+
}
|
|
84
|
+
})();
|
|
85
|
+
}, 12000);
|
|
86
|
+
|
|
87
|
+
let buffer = '';
|
|
88
|
+
const textDecoder = new TextDecoder();
|
|
89
|
+
for await (const chunk of res.body) {
|
|
90
|
+
buffer += textDecoder.decode(chunk, { stream: true });
|
|
91
|
+
let parts = buffer.split(/\r?\n\r?\n/);
|
|
92
|
+
if (parts.length > 1) {
|
|
93
|
+
buffer = parts.pop();
|
|
94
|
+
} else {
|
|
95
|
+
buffer = parts[0];
|
|
96
|
+
parts = [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const block of parts) {
|
|
100
|
+
const parsed = this._parseEvent(block);
|
|
101
|
+
if (parsed?.data && Array.isArray(parsed.data)) {
|
|
102
|
+
parsed.data.forEach(ev => {
|
|
103
|
+
for (let index = 0; index < ev.data.length; index++) {
|
|
104
|
+
const element = ev.data[index];
|
|
105
|
+
this.emit("event", element)
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err.name !== 'AbortError' && this.sysLogger) {
|
|
113
|
+
this.sysLogger.error(`EventStream error: ${err.message}`)
|
|
114
|
+
this.commandQueue = [];
|
|
115
|
+
try { await this.close(); } catch (error) { }
|
|
116
|
+
this.restartSSECounter = 0;
|
|
117
|
+
this.emit("disconnected");
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
_parseEvent = (block) => {
|
|
123
|
+
const lines = block.split(/\r?\n/);
|
|
124
|
+
let data = '';
|
|
125
|
+
let event = 'message';
|
|
126
|
+
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
if (line.startsWith('data:')) {
|
|
129
|
+
data += line.slice(5).trim();
|
|
130
|
+
} else if (line.startsWith('event:')) {
|
|
131
|
+
event = line.slice(6).trim();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
return {
|
|
137
|
+
type: event,
|
|
138
|
+
data: data ? JSON.parse(data) : null
|
|
139
|
+
};
|
|
140
|
+
} catch (e) {
|
|
141
|
+
return {
|
|
142
|
+
type: event,
|
|
143
|
+
data: data || null
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
processQueueItem = async () => {
|
|
149
|
+
try {
|
|
150
|
+
const jRet = this.commandQueue.pop();
|
|
151
|
+
switch (jRet._operation) {
|
|
152
|
+
case "setLight":
|
|
153
|
+
await this.hueApiV2.put(`/resource/light/${jRet._lightID}`, jRet._state);
|
|
154
|
+
break;
|
|
155
|
+
case "setGroupedLight":
|
|
156
|
+
await this.hueApiV2.put(`/resource/grouped_light/${jRet._lightID}`, jRet._state);
|
|
157
|
+
break;
|
|
158
|
+
case "setScene":
|
|
159
|
+
await this.hueApiV2.put(`/resource/scene/${jRet._lightID}`, jRet._state);
|
|
160
|
+
break;
|
|
161
|
+
case "stopScene":
|
|
162
|
+
const allResources = await this.hueApiV2.get("/resource");
|
|
163
|
+
const jScene = allResources.find((res) => res.id === jRet._lightID);
|
|
164
|
+
const linkedLight = allResources.find((res) => res.id === jScene.group.rid).children || [];
|
|
165
|
+
linkedLight.forEach((light) => {
|
|
166
|
+
this.writeHueQueueAdd(light.rid, jRet._state, "setLight");
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
default:
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
this.sysLogger?.error(`processQueueItem: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
handleQueue = async () => {
|
|
178
|
+
do {
|
|
179
|
+
if (this.commandQueue && this.commandQueue.length > 0) {
|
|
180
|
+
try {
|
|
181
|
+
await this.processQueueItem();
|
|
182
|
+
} catch (error) { }
|
|
183
|
+
}
|
|
184
|
+
await pleaseWait(150);
|
|
185
|
+
} while (!this.exitAllQueues);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
writeHueQueueAdd = async (_lightID, _state, _operation) => {
|
|
189
|
+
this.commandQueue.unshift({ _lightID, _state, _operation });
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
deleteHueQueue = async (_lightID) => {
|
|
193
|
+
this.commandQueue = this.commandQueue.filter((el) => el._lightID !== _lightID);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
close = async () =>
|
|
197
|
+
new Promise((resolve, reject) => {
|
|
198
|
+
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
199
|
+
try {
|
|
200
|
+
this.exitAllQueues = true;
|
|
201
|
+
this.restartSSECounter = 0;
|
|
202
|
+
try {
|
|
203
|
+
if (this.eventStreamAbort) this.eventStreamAbort.abort();
|
|
204
|
+
} catch (error) { }
|
|
205
|
+
this.HUEBridgeConnectionStatus = "disconnected";
|
|
206
|
+
resolve(true);
|
|
207
|
+
} catch (error) {
|
|
208
|
+
reject(error);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export { classHUE };
|
package/package.json
CHANGED
|
@@ -3,22 +3,22 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=18.0.0"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.3.
|
|
6
|
+
"version": "3.3.31",
|
|
7
7
|
"description": "Control your KNX 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.",
|
|
8
8
|
"dependencies": {
|
|
9
9
|
"binary-parser": "2.2.1",
|
|
10
10
|
"crypto-js": "4.2.0",
|
|
11
11
|
"dns-sync": "0.2.1",
|
|
12
|
-
"eventsource": "2.0.2",
|
|
13
12
|
"js-yaml": "4.1.0",
|
|
14
13
|
"knxultimate": "4.1.1",
|
|
15
14
|
"lodash": "4.17.21",
|
|
16
|
-
"node-color-log": "12.0.1",
|
|
17
15
|
"mkdirp": "3.0.1",
|
|
16
|
+
"node-color-log": "12.0.1",
|
|
18
17
|
"node-hue-api": "5.0.0-beta.16",
|
|
19
18
|
"path": "0.12.7",
|
|
20
19
|
"ping": "0.4.4",
|
|
21
20
|
"simple-get": "4.0.1",
|
|
21
|
+
"undici": "^7.8.0",
|
|
22
22
|
"xml2js": "0.6.0"
|
|
23
23
|
},
|
|
24
24
|
"node-red": {
|
package/nodes/utils/hueEngine.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
|
-
const { EventEmitter } = require("events");
|
|
3
|
-
const EventSource = require("eventsource");
|
|
4
|
-
const http = require("./http");
|
|
5
|
-
const { setTimeout: pleaseWait } = require('timers/promises');
|
|
6
|
-
//const { forEach } = require("lodash");
|
|
7
|
-
// Configura il rate limiter
|
|
8
|
-
//const limiter = new RateLimiter({ tokensPerInterval: 1, interval: 150 }); // HUE telegram interval
|
|
9
|
-
|
|
10
|
-
class classHUE extends EventEmitter {
|
|
11
|
-
|
|
12
|
-
constructor(_hueBridgeIP, _username, _clientkey, _bridgeid, _sysLogger) {
|
|
13
|
-
super();
|
|
14
|
-
this.HUEBridgeConnectionStatus = "disconnected";
|
|
15
|
-
this.exitAllQueues = false;
|
|
16
|
-
this.hueBridgeIP = _hueBridgeIP;
|
|
17
|
-
this.username = _username;
|
|
18
|
-
this.clientkey = _clientkey;
|
|
19
|
-
this.bridgeid = _bridgeid;
|
|
20
|
-
this.commandQueue = [];
|
|
21
|
-
// eslint-disable-next-line max-len
|
|
22
|
-
this.sysLogger = _sysLogger;
|
|
23
|
-
this.timerCheckConnected = null;
|
|
24
|
-
this.restartSSECounter = 0; // To auto reset the SSE Connection
|
|
25
|
-
this.handleQueue();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
Connect = () => {
|
|
29
|
-
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
30
|
-
|
|
31
|
-
const options = {
|
|
32
|
-
headers: {
|
|
33
|
-
"hue-application-key": this.username,
|
|
34
|
-
pragma: "no-cache",
|
|
35
|
-
"cache-control": "no-cache,no-store, must-revalidate",
|
|
36
|
-
},
|
|
37
|
-
https: {
|
|
38
|
-
rejectUnauthorized: false,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Init the http to use the username and bridge ip
|
|
43
|
-
this.hueApiV2 = http.use({
|
|
44
|
-
key: this.username,
|
|
45
|
-
prefix: `https://${this.hueBridgeIP}/clip/v2`
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
if (this.es !== null && this.es !== undefined) {
|
|
50
|
-
(async () => {
|
|
51
|
-
await this.close();
|
|
52
|
-
this.exitAllQueues = false;
|
|
53
|
-
})();
|
|
54
|
-
}
|
|
55
|
-
} catch (error) {
|
|
56
|
-
/* empty */
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
this.es = new EventSource(`https://${this.hueBridgeIP}/eventstream/clip/v2`, options);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error(`hueEngine: ew EventSource: ${error.message}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.es.onmessage = (event) => {
|
|
67
|
-
try {
|
|
68
|
-
if (event && event.type === "message" && event.data) {
|
|
69
|
-
const data = JSON.parse(event.data);
|
|
70
|
-
data.forEach((element) => {
|
|
71
|
-
if (element.type === "update") {
|
|
72
|
-
element.data.forEach((ev) => {
|
|
73
|
-
this.emit("event", ev);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null)
|
|
80
|
-
this.sysLogger.error(`KNXUltimatehueEngine: classHUE: this.es.onmessage: ${error.message}`);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
this.es.onopen = () => {
|
|
85
|
-
// if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error('KNXUltimatehueEngine: classHUE: SSE-Connected')
|
|
86
|
-
this.emit("connected");
|
|
87
|
-
this.HUEBridgeConnectionStatus = "connected";
|
|
88
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.info(`KNXUltimatehueEngine: classHUE: this.es.onopen: connected`);
|
|
89
|
-
// Check wether the hue bridge is connected or not
|
|
90
|
-
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
91
|
-
this.timerCheckConnected = setInterval(() => {
|
|
92
|
-
(async () => {
|
|
93
|
-
try {
|
|
94
|
-
this.restartSSECounter += 1;
|
|
95
|
-
if (this.restartSSECounter >= 6) {
|
|
96
|
-
// Restart SSE client, due to silent disconnection affecting the SSE server
|
|
97
|
-
this.restartSSECounter = 0;
|
|
98
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug(`KNXUltimatehueEngine: classHUE:this.timerCheckConnected = setInterval: reconnection to the eventsource`);
|
|
99
|
-
this.es.close();
|
|
100
|
-
this.es = new EventSource(`https://${this.hueBridgeIP}/eventstream/clip/v2`, options);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug(`KNXUltimatehueEngine: classHUE: Pinging...`);
|
|
104
|
-
const jReturn = await this.hueApiV2.get('/resource/bridge');
|
|
105
|
-
if (!Array.isArray(jReturn) || jReturn.length < 1) throw new Error("jReturn: not an array or array empty")
|
|
106
|
-
this.HUEBridgeConnectionStatus = "connected";
|
|
107
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.debug(`KNXUltimatehueEngine: classHUE: Ping OK`);
|
|
108
|
-
|
|
109
|
-
} catch (error) {
|
|
110
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error(`KNXUltimatehueEngine: classHUE: Ping ERROR: ${error.message}`);
|
|
111
|
-
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
112
|
-
this.commandQueue = [];
|
|
113
|
-
try {
|
|
114
|
-
await this.close();
|
|
115
|
-
} catch (error) { }
|
|
116
|
-
this.restartSSECounter = 0;
|
|
117
|
-
this.emit("disconnected");
|
|
118
|
-
}
|
|
119
|
-
})();
|
|
120
|
-
}, 120000);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
this.es.onerror = (error) => {
|
|
124
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error(`KNXUltimatehueEngine: classHUE: this.es.onopen: ${error.message}`);
|
|
125
|
-
// (async () => {
|
|
126
|
-
// await this.close();
|
|
127
|
-
// if (this.HUEBridgeConnectionStatus === 'connected') this.emit('disconnected');
|
|
128
|
-
// })();
|
|
129
|
-
|
|
130
|
-
// 29/08/2023 NON riattivare, perchè alla disconnessione, va in loop e consuma tutto il pool di risorse.
|
|
131
|
-
// try {
|
|
132
|
-
// this.es.close();
|
|
133
|
-
// this.es = null;
|
|
134
|
-
// if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error(`KNXUltimatehueEngine: classHUE: request.on(error): ${error.message}`);
|
|
135
|
-
// } catch (err) { /* empty */ }
|
|
136
|
-
// this.Connect();
|
|
137
|
-
// // this.emit('error', error)
|
|
138
|
-
// };
|
|
139
|
-
};
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Process single item in the queue
|
|
143
|
-
processQueueItem = async () => {
|
|
144
|
-
try {
|
|
145
|
-
const jRet = this.commandQueue.pop();
|
|
146
|
-
// jRet is ({ _lightID, _state, _operation });;
|
|
147
|
-
switch (jRet._operation) {
|
|
148
|
-
case "setLight":
|
|
149
|
-
// It can be a light or a grouped light
|
|
150
|
-
try {
|
|
151
|
-
const ok = await this.hueApiV2.put(`/resource/light/${jRet._lightID}`, jRet._state);
|
|
152
|
-
} catch (error) {
|
|
153
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) {
|
|
154
|
-
this.sysLogger.error(`KNXUltimatehueEngine: classHUE: processQueueItem: setLight light: ${error.message}.`);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
break;
|
|
158
|
-
case "setGroupedLight":
|
|
159
|
-
try {
|
|
160
|
-
await this.hueApiV2.put(`/resource/grouped_light/${jRet._lightID}`, jRet._state);
|
|
161
|
-
} catch (error) {
|
|
162
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null)
|
|
163
|
-
this.sysLogger.info(`KNXUltimatehueEngine: classHUE: processQueueItem: setLight grouped_light: ${error.message}`);
|
|
164
|
-
}
|
|
165
|
-
break;
|
|
166
|
-
case "setScene":
|
|
167
|
-
try {
|
|
168
|
-
const sceneID = jRet._lightID;
|
|
169
|
-
await this.hueApiV2.put(`/resource/scene/${sceneID}`, jRet._state);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null)
|
|
172
|
-
this.sysLogger.info(`KNXUltimatehueEngine: classHUE: processQueueItem: setScene: ${error.message}`);
|
|
173
|
-
}
|
|
174
|
-
break;
|
|
175
|
-
case "stopScene":
|
|
176
|
-
try {
|
|
177
|
-
const allResources = await this.hueApiV2.get("/resource");
|
|
178
|
-
const sceneID = jRet._lightID;
|
|
179
|
-
const jScene = allResources.find((res) => res.id === sceneID) || "";
|
|
180
|
-
const linkedLight = allResources.find((res) => res.id === jScene.group.rid).children || "";
|
|
181
|
-
linkedLight.forEach((light) => {
|
|
182
|
-
this.writeHueQueueAdd(light.rid, jRet._state, "setLight");
|
|
183
|
-
});
|
|
184
|
-
} catch (error) {
|
|
185
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error(`KNXUltimatehueEngine: classHUE: processQueueItem: stopScene: ${error.message}`);
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
188
|
-
default:
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (this.sysLogger !== undefined && this.sysLogger !== null) this.sysLogger.error(`KNXUltimatehueEngine: classHUE: processQueueItem: ${error.trace}`);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// // Handle the send queue
|
|
197
|
-
// // ######################################
|
|
198
|
-
handleQueue = async () => {
|
|
199
|
-
// Verifica se è possibile eseguire una nuova richiesta
|
|
200
|
-
do {
|
|
201
|
-
if (this.commandQueue !== undefined && this.commandQueue.length > 0) {
|
|
202
|
-
//if (remainingRequests >= 0) {
|
|
203
|
-
// OK, i can send
|
|
204
|
-
//console.log("\x1b[32m Messaggio. remainingRequests=" + remainingRequests + "\x1b[0m " + new Date().toTimeString(), this.commandQueue.length, "remainingRequests " + remainingRequests);
|
|
205
|
-
try {
|
|
206
|
-
await this.processQueueItem();
|
|
207
|
-
} catch (error) {
|
|
208
|
-
}
|
|
209
|
-
//} else {
|
|
210
|
-
// Limit reached, skip this round.
|
|
211
|
-
//console.log("\x1b[41m HO DETTO SPETA. remainingRequests=" + remainingRequests + "\x1b[0m " + new Date().toTimeString(), this.commandQueue.length, "remainingRequests " + remainingRequests);
|
|
212
|
-
//}
|
|
213
|
-
}
|
|
214
|
-
await pleaseWait(150);
|
|
215
|
-
} while (!this.exitAllQueues);
|
|
216
|
-
//console.log("\x1b[42m End processing commandQueue \x1b[0m " + new Date().toTimeString(), this.commandQueue.length);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
writeHueQueueAdd = async (_lightID, _state, _operation) => {
|
|
221
|
-
// Add the new item
|
|
222
|
-
this.commandQueue.unshift({ _lightID, _state, _operation });
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Clears all items fo _lightID from the HUE sending queue. Useful to clear unwanted dimming commands
|
|
227
|
-
* @param {string} _lightID HUE Light ID
|
|
228
|
-
* @returns {}
|
|
229
|
-
*/
|
|
230
|
-
deleteHueQueue = async (_lightID) => {
|
|
231
|
-
// Add the new item
|
|
232
|
-
this.commandQueue = this.commandQueue.filter((el) => el._lightID !== _lightID);
|
|
233
|
-
};
|
|
234
|
-
// ######################################
|
|
235
|
-
|
|
236
|
-
close = async () =>
|
|
237
|
-
new Promise((resolve, reject) => {
|
|
238
|
-
if (this.timerCheckConnected !== null) clearInterval(this.timerCheckConnected);
|
|
239
|
-
try {
|
|
240
|
-
this.exitAllQueues = true;
|
|
241
|
-
this.restartSSECounter = 0;
|
|
242
|
-
setTimeout(() => {
|
|
243
|
-
try {
|
|
244
|
-
if (this.es !== null && this.es !== undefined) this.es.close();
|
|
245
|
-
if (this.es !== null && this.es !== undefined) this.es.removeEventListener();
|
|
246
|
-
} catch (error) { }
|
|
247
|
-
this.es = null;
|
|
248
|
-
this.HUEBridgeConnectionStatus = "disconnected";
|
|
249
|
-
resolve(true);
|
|
250
|
-
}, 2000);
|
|
251
|
-
} catch (error) {
|
|
252
|
-
reject(error);
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
module.exports.classHUE = classHUE;
|