node-red-contrib-ccu 4.2.0 → 4.4.0
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/.prettierignore +3 -0
- package/CHANGELOG.md +140 -0
- package/HANDOFF.md +46 -10
- package/README.md +71 -0
- package/ROADMAP.md +130 -47
- package/nodes/ccu-alexa.html +3 -1
- package/nodes/ccu-connection.html +51 -0
- package/nodes/ccu-connection.js +323 -16
- package/nodes/ccu-display.js +29 -13
- package/nodes/ccu-get-value.html +3 -1
- package/nodes/ccu-mqtt.html +21 -4
- package/nodes/ccu-mqtt.js +126 -102
- package/nodes/ccu-poll.js +6 -0
- package/nodes/ccu-rpc-event.html +6 -0
- package/nodes/ccu-rpc-event.js +31 -12
- package/nodes/ccu-set-value.html +38 -0
- package/nodes/ccu-set-value.js +59 -235
- package/nodes/ccu-signal.html +55 -1
- package/nodes/ccu-signal.js +52 -9
- package/nodes/ccu-switch.html +17 -4
- package/nodes/ccu-switch.js +28 -4
- package/nodes/ccu-sysvar.html +40 -1
- package/nodes/ccu-sysvar.js +78 -8
- package/nodes/ccu-value.html +27 -1
- package/nodes/ccu-value.js +69 -19
- package/nodes/lib/cast.js +59 -14
- package/nodes/lib/channelfilter.js +136 -0
- package/nodes/lib/dynconfig.js +81 -0
- package/nodes/lib/localccu.js +81 -0
- package/nodes/lib/message.js +6 -2
- package/nodes/lib/metaprovider.js +716 -0
- package/nodes/lib/valuestatus.js +145 -0
- package/nodes/locales/de/ccu-display.html +17 -0
- package/nodes/locales/de/ccu-rpc-event.json +1 -0
- package/nodes/locales/de/ccu-sysvar.json +3 -2
- package/nodes/locales/de/ccu-value.json +2 -1
- package/nodes/locales/en-US/ccu-display.html +17 -0
- package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
- package/nodes/locales/en-US/ccu-switch.html +12 -0
- package/nodes/locales/en-US/ccu-sysvar.json +3 -2
- package/nodes/locales/en-US/ccu-value.json +2 -1
- package/package.json +1 -1
- package/patch-certainonly.py +51 -0
- package/roadmap-archive/B-17.md +106 -0
- package/roadmap-archive/B-2.md +69 -0
- package/roadmap-archive/B-3.md +57 -0
- package/roadmap-archive/B-4.md +44 -0
- package/roadmap-archive/B-5.md +57 -0
- package/roadmap-archive/README.md +14 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* Per-message dynamic node configuration (B-2, issues #172 #71 #103 #80
|
|
2
|
+
#56 #148 #133).
|
|
3
|
+
|
|
4
|
+
Two mechanisms, in increasing precedence:
|
|
5
|
+
|
|
6
|
+
1. Flat properties (`msg.channel`, `msg.rooms`, ...). Long-standing
|
|
7
|
+
behaviour of ccu-set-value/ccu-value/ccu-get-value: they fill in
|
|
8
|
+
configuration the node itself leaves empty. Kept as-is because
|
|
9
|
+
messages coming straight from an event node carry exactly these
|
|
10
|
+
names, which is what makes it convenient - and, for the same
|
|
11
|
+
reason, only ever used for the nodes that already did it.
|
|
12
|
+
2. `msg.config`, an object of overrides. Always wins, works for every
|
|
13
|
+
configurable field including lists, and cannot collide with the
|
|
14
|
+
properties an event message carries (`msg.ccu` is the CCU host, so
|
|
15
|
+
it is not available as a namespace). This is what makes the
|
|
16
|
+
settings of the signal/display/switch/sysvar nodes reachable from
|
|
17
|
+
a flow.
|
|
18
|
+
|
|
19
|
+
The old inline implementation in ccu-set-value wrote message
|
|
20
|
+
properties into the node's own `config` object, so only the *first*
|
|
21
|
+
message ever configured the node: every later message hit the
|
|
22
|
+
`if (!config[key])` guard and was ignored (#133). Nothing here mutates
|
|
23
|
+
the stored configuration. */
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Build the effective configuration for one message.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} config the node's stored configuration (never mutated)
|
|
29
|
+
* @param {object} message the incoming message
|
|
30
|
+
* @param {string[]} keys the keys that may be overridden; anything else
|
|
31
|
+
* in `msg.config` is ignored, so a stray property cannot reach
|
|
32
|
+
* node internals
|
|
33
|
+
* @param {object} [options]
|
|
34
|
+
* @param {boolean} [options.flat=false] also fill empty keys from
|
|
35
|
+
* top-level message properties (mechanism 1)
|
|
36
|
+
* @returns {{config: object, dynamic: boolean}} effective config plus
|
|
37
|
+
* whether anything actually came from the message
|
|
38
|
+
*/
|
|
39
|
+
function effectiveConfig(config, message, keys, options) {
|
|
40
|
+
const effective = {...config};
|
|
41
|
+
let dynamic = false;
|
|
42
|
+
|
|
43
|
+
if (!message || typeof message !== 'object') {
|
|
44
|
+
return {config: effective, dynamic};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options && options.flat) {
|
|
48
|
+
keys.forEach((key) => {
|
|
49
|
+
if (!effective[key] && key in message) {
|
|
50
|
+
effective[key] = message[key];
|
|
51
|
+
dynamic = true;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const overrides = message.config;
|
|
57
|
+
if (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) {
|
|
58
|
+
keys.forEach((key) => {
|
|
59
|
+
if (key in overrides) {
|
|
60
|
+
effective[key] = overrides[key];
|
|
61
|
+
dynamic = true;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {config: effective, dynamic};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A stable identity for a set of configuration keys, used to decide
|
|
71
|
+
* whether a cache derived from them is still valid. Two effective configs
|
|
72
|
+
* that produce the same key select the same devices/datapoints.
|
|
73
|
+
* @param {object} config
|
|
74
|
+
* @param {string[]} keys
|
|
75
|
+
* @returns {string}
|
|
76
|
+
*/
|
|
77
|
+
function configCacheKey(config, keys) {
|
|
78
|
+
return keys.map((key) => JSON.stringify(config[key] === undefined ? null : config[key])).join(' ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {effectiveConfig, configCacheKey};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* Detecting "Node-RED runs on the CCU itself" (B-4).
|
|
2
|
+
|
|
3
|
+
When it does, the interface processes can be reached directly on their
|
|
4
|
+
3xxxx ports (BINRPC where available) instead of going through the CCU's
|
|
5
|
+
lighttpd proxy on 1999/2000/2001/2010/9292 - fewer hops, and BINRPC
|
|
6
|
+
instead of XML-RPC for BidCos.
|
|
7
|
+
|
|
8
|
+
The old test grepped /etc/lighttpd/conf.d/proxy.conf for
|
|
9
|
+
`"port" => 32001`. On current firmware that file is a one-line
|
|
10
|
+
`include "/var/etc/lighttpd_proxy.conf"`, which itself only includes
|
|
11
|
+
proxy_normal.conf - the port markers are two levels down, so the regex
|
|
12
|
+
has not matched since (verified on CCU3 firmware 3.89.8, where 31999,
|
|
13
|
+
32001, 32010 and 39292 are all listening while proxy.conf says nothing
|
|
14
|
+
about them). Every local install has silently been running through the
|
|
15
|
+
proxy.
|
|
16
|
+
|
|
17
|
+
Rather than chasing lighttpd's include chain, ask the kernel: a
|
|
18
|
+
listener on the direct port is exactly the condition that makes using
|
|
19
|
+
it correct. /proc/net/tcp is a plain file, so this stays synchronous -
|
|
20
|
+
the interface table is built in the connection node's constructor. */
|
|
21
|
+
|
|
22
|
+
const fs = require('fs');
|
|
23
|
+
|
|
24
|
+
/** rfd's direct BINRPC port on firmware >= 3.41; the marker for "on the CCU". */
|
|
25
|
+
const LOCAL_PROBE_PORTS = [32001, 31999];
|
|
26
|
+
|
|
27
|
+
const PROC_NET_TCP = ['/proc/net/tcp', '/proc/net/tcp6'];
|
|
28
|
+
|
|
29
|
+
/** /proc/net/tcp connection state 0A = TCP_LISTEN. */
|
|
30
|
+
const TCP_LISTEN = '0A';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The TCP ports something is listening on, from /proc/net/tcp{,6}.
|
|
34
|
+
* @param {string[]} [files] override, for tests
|
|
35
|
+
* @returns {Set<number>} empty when /proc is unavailable (non-Linux)
|
|
36
|
+
*/
|
|
37
|
+
function listeningPorts(files) {
|
|
38
|
+
const ports = new Set();
|
|
39
|
+
|
|
40
|
+
(files || PROC_NET_TCP).forEach((file) => {
|
|
41
|
+
let content;
|
|
42
|
+
try {
|
|
43
|
+
content = fs.readFileSync(file, 'utf8');
|
|
44
|
+
} catch {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
content.split('\n').forEach((line) => {
|
|
49
|
+
// sl local_address rem_address st ...
|
|
50
|
+
const columns = line.trim().split(/\s+/);
|
|
51
|
+
if (columns.length < 4 || columns[3] !== TCP_LISTEN) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const port = Number.parseInt(String(columns[1]).split(':')[1], 16);
|
|
56
|
+
if (Number.isInteger(port)) {
|
|
57
|
+
ports.add(port);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return ports;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Are we talking to a CCU we are running on, with the direct ports open?
|
|
67
|
+
* @param {string} host the configured CCU host
|
|
68
|
+
* @param {object} [options]
|
|
69
|
+
* @param {string[]} [options.files] /proc files to read, for tests
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
function isLocalCcu(host, options) {
|
|
73
|
+
if (!host || !(String(host).startsWith('127.') || host === 'localhost' || host === '::1')) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ports = listeningPorts(options && options.files);
|
|
78
|
+
return LOCAL_PROBE_PORTS.some((port) => ports.has(port));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {isLocalCcu, listeningPorts, LOCAL_PROBE_PORTS};
|
package/nodes/lib/message.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* @param {object} [additions] merged into the message (cache, working, uncertain, ts, lc, ...)
|
|
19
19
|
* @returns {object} the message
|
|
20
20
|
*/
|
|
21
|
+
const {enumList} = require('./cast.js');
|
|
22
|
+
|
|
21
23
|
function createMessage(ccu, iface, channel, datapoint, payload, additions) {
|
|
22
24
|
const datapointName = iface + '.' + channel + '.' + datapoint;
|
|
23
25
|
if (!ccu.values[datapointName]) {
|
|
@@ -73,12 +75,14 @@ function createMessage(ccu, iface, channel, datapoint, payload, additions) {
|
|
|
73
75
|
datapointType: description.TYPE,
|
|
74
76
|
datapointMin: description.MIN,
|
|
75
77
|
datapointMax: description.MAX,
|
|
76
|
-
|
|
78
|
+
// the CCU sends the names in VALUE_LIST; reading description.ENUM
|
|
79
|
+
// meant both of these were always undefined (B-3, ROADMAP 8.2)
|
|
80
|
+
datapointEnum: enumList(description),
|
|
77
81
|
datapointDefault: description.DEFAULT,
|
|
78
82
|
datapointControl: description.CONTROL,
|
|
79
83
|
value: payload,
|
|
80
84
|
valuePrevious: ccu.values[datapointName].value,
|
|
81
|
-
valueEnum: description
|
|
85
|
+
valueEnum: enumList(description) ? enumList(description)[Number(payload)] : undefined,
|
|
82
86
|
valueStable,
|
|
83
87
|
rooms: ccu.channelRooms[channel] || [],
|
|
84
88
|
room:
|