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.
Files changed (49) hide show
  1. package/.prettierignore +3 -0
  2. package/CHANGELOG.md +140 -0
  3. package/HANDOFF.md +46 -10
  4. package/README.md +71 -0
  5. package/ROADMAP.md +130 -47
  6. package/nodes/ccu-alexa.html +3 -1
  7. package/nodes/ccu-connection.html +51 -0
  8. package/nodes/ccu-connection.js +323 -16
  9. package/nodes/ccu-display.js +29 -13
  10. package/nodes/ccu-get-value.html +3 -1
  11. package/nodes/ccu-mqtt.html +21 -4
  12. package/nodes/ccu-mqtt.js +126 -102
  13. package/nodes/ccu-poll.js +6 -0
  14. package/nodes/ccu-rpc-event.html +6 -0
  15. package/nodes/ccu-rpc-event.js +31 -12
  16. package/nodes/ccu-set-value.html +38 -0
  17. package/nodes/ccu-set-value.js +59 -235
  18. package/nodes/ccu-signal.html +55 -1
  19. package/nodes/ccu-signal.js +52 -9
  20. package/nodes/ccu-switch.html +17 -4
  21. package/nodes/ccu-switch.js +28 -4
  22. package/nodes/ccu-sysvar.html +40 -1
  23. package/nodes/ccu-sysvar.js +78 -8
  24. package/nodes/ccu-value.html +27 -1
  25. package/nodes/ccu-value.js +69 -19
  26. package/nodes/lib/cast.js +59 -14
  27. package/nodes/lib/channelfilter.js +136 -0
  28. package/nodes/lib/dynconfig.js +81 -0
  29. package/nodes/lib/localccu.js +81 -0
  30. package/nodes/lib/message.js +6 -2
  31. package/nodes/lib/metaprovider.js +716 -0
  32. package/nodes/lib/valuestatus.js +145 -0
  33. package/nodes/locales/de/ccu-display.html +17 -0
  34. package/nodes/locales/de/ccu-rpc-event.json +1 -0
  35. package/nodes/locales/de/ccu-sysvar.json +3 -2
  36. package/nodes/locales/de/ccu-value.json +2 -1
  37. package/nodes/locales/en-US/ccu-display.html +17 -0
  38. package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
  39. package/nodes/locales/en-US/ccu-switch.html +12 -0
  40. package/nodes/locales/en-US/ccu-sysvar.json +3 -2
  41. package/nodes/locales/en-US/ccu-value.json +2 -1
  42. package/package.json +1 -1
  43. package/patch-certainonly.py +51 -0
  44. package/roadmap-archive/B-17.md +106 -0
  45. package/roadmap-archive/B-2.md +69 -0
  46. package/roadmap-archive/B-3.md +57 -0
  47. package/roadmap-archive/B-4.md +44 -0
  48. package/roadmap-archive/B-5.md +57 -0
  49. 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};
@@ -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
- datapointEnum: description.ENUM,
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.ENUM ? description.ENUM[Number(payload)] : undefined,
85
+ valueEnum: enumList(description) ? enumList(description)[Number(payload)] : undefined,
82
86
  valueStable,
83
87
  rooms: ccu.channelRooms[channel] || [],
84
88
  room: