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,145 @@
1
+ /* Node status line showing the last known value and how old it is
2
+ (B-5, issues #54 and #52).
3
+
4
+ The data was always there - the connection keeps value, ts and lc for
5
+ every datapoint and system variable - the nodes just never showed it,
6
+ and the sysvar node only showed anything at all when "emit value on
7
+ start" was enabled. The age is re-rendered on a timer so the line does
8
+ not silently become stale between events. */
9
+
10
+ /** Node-RED status lines are narrow - keep them readable. */
11
+ const MAX_TEXT = 45;
12
+
13
+ const MINUTE = 60;
14
+ const HOUR = 60 * MINUTE;
15
+ const DAY = 24 * HOUR;
16
+
17
+ /** How often the age is re-rendered, matched to the resolution shown. */
18
+ function refreshInterval(seconds) {
19
+ if (seconds < MINUTE) {
20
+ return 5000;
21
+ }
22
+
23
+ if (seconds < HOUR) {
24
+ return 30000;
25
+ }
26
+
27
+ return 300000;
28
+ }
29
+
30
+ /**
31
+ * Compact age of a timestamp, in the style Node-RED status lines use.
32
+ * @param {number} ts epoch milliseconds
33
+ * @param {number} now epoch milliseconds
34
+ * @returns {string} e.g. '5s', '12m', '3h', '2d'
35
+ */
36
+ function relativeTime(ts, now) {
37
+ const seconds = Math.max(0, Math.round((now - ts) / 1000));
38
+ if (seconds < MINUTE) {
39
+ return seconds + 's';
40
+ }
41
+
42
+ if (seconds < HOUR) {
43
+ return Math.floor(seconds / MINUTE) + 'm';
44
+ }
45
+
46
+ if (seconds < DAY) {
47
+ return Math.floor(seconds / HOUR) + 'h';
48
+ }
49
+
50
+ return Math.floor(seconds / DAY) + 'd';
51
+ }
52
+
53
+ /**
54
+ * The status text for a value with a timestamp.
55
+ * @param {*} value
56
+ * @param {number} [ts] epoch milliseconds; omitted renders the value alone
57
+ * @param {number} now epoch milliseconds
58
+ * @returns {string}
59
+ */
60
+ function formatValueStatus(value, ts, now) {
61
+ let text = String(value);
62
+ if (text.length > MAX_TEXT) {
63
+ text = text.slice(0, MAX_TEXT - 1) + '…';
64
+ }
65
+
66
+ if (!ts) {
67
+ return text;
68
+ }
69
+
70
+ return text + ' (' + relativeTime(ts, now) + ')';
71
+ }
72
+
73
+ /**
74
+ * Keeps a node's status line showing the last value and its age.
75
+ *
76
+ * @param {object} node the Node-RED node (needs .status())
77
+ * @param {object} [options]
78
+ * @param {Function} [options.now] clock, for tests
79
+ * @returns {{set: Function, clear: Function, stop: Function}}
80
+ */
81
+ function valueStatus(node, options) {
82
+ const now = (options && options.now) || Date.now;
83
+ let timer = null;
84
+ let current = null;
85
+
86
+ function render() {
87
+ if (!current) {
88
+ return;
89
+ }
90
+
91
+ node.status({
92
+ fill: 'green',
93
+ shape: 'ring',
94
+ text: formatValueStatus(current.value, current.ts, now()),
95
+ });
96
+ }
97
+
98
+ function schedule() {
99
+ if (timer) {
100
+ clearInterval(timer);
101
+ timer = null;
102
+ }
103
+
104
+ if (!current || !current.ts) {
105
+ return;
106
+ }
107
+
108
+ const interval = refreshInterval(Math.round((now() - current.ts) / 1000));
109
+ timer = setInterval(() => {
110
+ render();
111
+ // the resolution coarsens as the value ages - re-arm accordingly
112
+ schedule();
113
+ }, interval);
114
+ if (typeof timer.unref === 'function') {
115
+ timer.unref();
116
+ }
117
+ }
118
+
119
+ return {
120
+ /**
121
+ * @param {*} value
122
+ * @param {number} [ts] epoch milliseconds
123
+ */
124
+ set(value, ts) {
125
+ current = {value, ts};
126
+ render();
127
+ schedule();
128
+ },
129
+ clear() {
130
+ current = null;
131
+ if (timer) {
132
+ clearInterval(timer);
133
+ timer = null;
134
+ }
135
+ },
136
+ stop() {
137
+ if (timer) {
138
+ clearInterval(timer);
139
+ timer = null;
140
+ }
141
+ },
142
+ };
143
+ }
144
+
145
+ module.exports = {valueStatus, formatValueStatus, relativeTime};
@@ -4,4 +4,21 @@
4
4
  als Nachricht übergeben werden diese
5
5
  anstelle der Konfiguration genutzt.</p>
6
6
  <p>Mit <code>msg.disbleX</code> kann explizit gesetzt werden, dass keine Daten für die Zeile übertragen werden.</p>
7
+
8
+ <h3>Dynamische Konfiguration</h3>
9
+ <p><code>msg.config</code> ist ein Objekt, dessen Eigenschaften die Konfiguration dieses Nodes für genau diese Nachricht überschreiben. Die gespeicherte Konfiguration bleibt unverändert.</p>
10
+ <ul>
11
+ <li><code>iface</code></li>
12
+ <li><code>channel</code></li>
13
+ <li><code>channelType</code></li>
14
+ <li><code>sound</code></li>
15
+ <li><code>repeat</code></li>
16
+ <li><code>pause</code></li>
17
+ <li><code>signal</code></li>
18
+ <li><code>line1 ... line6</code></li>
19
+ <li><code>color1 ... color6</code></li>
20
+ <li><code>icon1 ... icon6</code></li>
21
+ <li><code>disable1 ... disable6</code></li>
22
+ </ul>
23
+ <p><code>msg.line1</code> usw. funktionieren unverändert weiter.</p>
7
24
  </script>
@@ -4,5 +4,6 @@
4
4
  "change": "Nur geänderte Werte ausgeben",
5
5
  "cache": "Beim Start letzten bekannten Wert ausgeben",
6
6
  "working": "Während WORKING keine Werte ausgeben",
7
+ "certainOnly": "Unsichere Werte verwerfen"
7
8
  }
8
9
  }
@@ -2,6 +2,7 @@
2
2
  "ccu-sysvar": {
3
3
  "tip": "Wird keine Variable ausgewählt kann der Variablen-Name über msg.topic übergeben werden.",
4
4
  "change": "Nur geänderte Werte ausgeben",
5
- "cache": "Beim Start aktuellen Wert ausgeben"
5
+ "cache": "Beim Start aktuellen Wert ausgeben",
6
+ "property": "Wert aus"
6
7
  }
7
- }
8
+ }
@@ -8,6 +8,7 @@
8
8
  "change": "Nur geänderte Werte ausgeben",
9
9
  "cache": "Beim Start letzten bekannten Wert ausgeben",
10
10
  "working": "Während WORKING keine Werte ausgeben",
11
- "queue": "Queue"
11
+ "queue": "Queue",
12
+ "certainOnly": "Unsichere Werte verwerfen"
12
13
  }
13
14
  }
@@ -3,4 +3,21 @@
3
3
  <p>If <code>msg.lineX</code>, <code>msg.iconX</code> or <code>msg.colorX</code>
4
4
  are set in the message these are used instead of the configuration.</p>
5
5
  <p> With <code>msg.disbleX</code> you can explicitly set that no data is transferred for the line.</p>
6
+
7
+ <h3>Dynamic configuration</h3>
8
+ <p><code>msg.config</code> is an object whose properties override this node's configuration for that one message. The stored configuration is never modified.</p>
9
+ <ul>
10
+ <li><code>iface</code></li>
11
+ <li><code>channel</code></li>
12
+ <li><code>channelType</code></li>
13
+ <li><code>sound</code></li>
14
+ <li><code>repeat</code></li>
15
+ <li><code>pause</code></li>
16
+ <li><code>signal</code></li>
17
+ <li><code>line1 ... line6</code></li>
18
+ <li><code>color1 ... color6</code></li>
19
+ <li><code>icon1 ... icon6</code></li>
20
+ <li><code>disable1 ... disable6</code></li>
21
+ </ul>
22
+ <p><code>msg.line1</code> and friends keep working unchanged.</p>
6
23
  </script>
@@ -3,6 +3,7 @@
3
3
  "tip": "Tip: ",
4
4
  "change": "Emit only changed values",
5
5
  "cache": "Emit cached value on start",
6
- "working": "Don't emit values while actuator is in WORKING state"
6
+ "working": "Don't emit values while actuator is in WORKING state",
7
+ "certainOnly": "Discard uncertain values"
7
8
  }
8
- }
9
+ }
@@ -0,0 +1,12 @@
1
+ <script type="text/html" data-help-name="ccu-switch">
2
+ <h3>Dynamic configuration</h3>
3
+ <p><code>msg.config</code> is an object whose properties override this node's configuration for that one message. The stored configuration is never modified.</p>
4
+ <ul>
5
+ <li><code>iface</code></li>
6
+ <li><code>channel</code></li>
7
+ <li><code>datapoint</code></li>
8
+ <li><code>datapointProperty</code></li>
9
+ <li><code>sysvar</code></li>
10
+ <li><code>sysvarProperty</code></li>
11
+ </ul>
12
+ </script>
@@ -2,6 +2,7 @@
2
2
  "ccu-sysvar": {
3
3
  "tip": "Omit the variable name to supply it via msg.payload",
4
4
  "change": "Emit changed values only",
5
- "cache": "Emit value on start"
5
+ "cache": "Emit value on start",
6
+ "property": "Value from"
6
7
  }
7
- }
8
+ }
@@ -8,6 +8,7 @@
8
8
  "change": "Emit changed values only",
9
9
  "cache": "Emit cached value on start",
10
10
  "working": "Don't emit values while actuator is in WORKING state",
11
- "queue": "Queue Commands"
11
+ "queue": "Queue Commands",
12
+ "certainOnly": "Discard uncertain values"
12
13
  }
13
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.2.0",
3
+ "version": "4.4.0",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [
@@ -0,0 +1,51 @@
1
+ import io
2
+ import json
3
+
4
+ # --- editors: certainOnly checkbox (#96) -----------------------------------
5
+ for name, default_anchor in (
6
+ ("ccu-value", " cache: {value: false},\n"),
7
+ ("ccu-rpc-event", " cache: {value: false},\n"),
8
+ ):
9
+ p = "nodes/%s.html" % name
10
+ s = io.open(p, encoding="utf-8").read()
11
+
12
+ assert default_anchor in s, "defaults anchor in " + p
13
+ s = s.replace(default_anchor, default_anchor + " certainOnly: {value: false},\n", 1)
14
+
15
+ old = """ <label class="ccu-checkbox">
16
+ <input type="checkbox" id="node-input-cache">
17
+ <span data-i18n="%s.cache"></span>
18
+ </label>
19
+ </div>
20
+ </div>""" % name
21
+ new = """ <label class="ccu-checkbox">
22
+ <input type="checkbox" id="node-input-cache">
23
+ <span data-i18n="%s.cache"></span>
24
+ </label>
25
+
26
+ <label class="ccu-checkbox">
27
+ <input type="checkbox" id="node-input-certainOnly">
28
+ <span data-i18n="%s.certainOnly"></span>
29
+ </label>
30
+ </div>
31
+ </div>""" % (name, name)
32
+ assert old in s, "checkbox block in " + p
33
+ s = s.replace(old, new, 1)
34
+
35
+ io.open(p, "w", encoding="utf-8", newline="\n").write(s)
36
+ print("patched", p)
37
+
38
+ # --- locales ---------------------------------------------------------------
39
+ LABELS = {
40
+ "de": "Unsichere Werte verwerfen",
41
+ "en-US": "Discard uncertain values",
42
+ }
43
+ for loc, label in LABELS.items():
44
+ for name in ("ccu-value", "ccu-rpc-event"):
45
+ p = "nodes/locales/%s/%s.json" % (loc, name)
46
+ d = json.load(io.open(p, encoding="utf-8"))
47
+ d[name]["certainOnly"] = label
48
+ io.open(p, "w", encoding="utf-8", newline="\n").write(
49
+ json.dumps(d, indent=2, ensure_ascii=False) + "\n"
50
+ )
51
+ print("locale", p)
@@ -0,0 +1,106 @@
1
+ # B-17 — openccu-lite (a CCU without ReGaHSS)
2
+
3
+ **✅ done 2026-09-06** (`4.4.0`).
4
+
5
+ [openccu-lite](https://github.com/hobbyquaker/openccu-lite) is a Homematic
6
+ CCU firmware that drops ReGaHSS. The interface processes are the ones we
7
+ already talk to (`rfd` BidCos-RF/BIN-RPC, `hs485d`, `hmipserver`
8
+ XML-RPC) — but nothing listens on 8181/8183, `rega.exe`/`tclrega.exe` do
9
+ not exist, HM-Script is never interpreted, and there are no ReGa ids.
10
+ Everything this package took from the ReGa apart from names, rooms and
11
+ functions has no replacement there.
12
+
13
+ What it offers instead is `occulited`'s **metadata API**, a small JSON
14
+ store served under `/api/meta/v1/` behind the box's lighttpd, with a
15
+ Server-Sent-Events change stream. Its porting kit (`docs/porting-from-rega.md`,
16
+ `docs/PORTING-PROMPT.md`, `docs/meta-api.md` and the `fixtures/`
17
+ conformance corpus in the openccu-lite repository) is what this item
18
+ was implemented against.
19
+
20
+ ## The rule the port follows
21
+
22
+ Keep the ReGa code path, add a provider, detect at runtime. On a CCU3,
23
+ RaspberryMatic or OpenCCU nothing changes: the same configuration, the
24
+ same messages, the same cache files.
25
+
26
+ ## What was built
27
+
28
+ - **`nodes/lib/metaprovider.js`** — the counterpart of the `getRega*()`
29
+ methods. `detect()` is the feature test (`GET /api/meta/v1/version`,
30
+ no credential, 404 on a CCU). `MetaProvider` loads `GET /snapshot`,
31
+ then follows `GET /events/sse?since=<revision>`: `object.updated` and
32
+ `object.deleted` are applied to the in-memory document, everything
33
+ that can rewrite member paths (`node.*`, `enum.*`, `import`, an
34
+ explicit `resync`, a gap in the revisions) re-reads the snapshot.
35
+ `buildNames()` is pure and derives exactly what the rest of the code
36
+ already reads — `channelNames`, `channelRooms`, `channelFunctions`
37
+ keyed by the bare address (the API's ref is `<interface>.<address>`),
38
+ plus the `rooms` / `functions` name lists the editor asks for.
39
+ - **Rooms and functions are a tree** there (`room/eg/wohnzimmer`), not a
40
+ flat list. They are flattened to the arrays the nodes have always
41
+ published, ancestors included and most specific first, so a channel in
42
+ _Wohnzimmer_ below _Erdgeschoss_ matches a room filter on either name
43
+ and `msg.room` is still the room the user means.
44
+ - **Detection in the startup path** of `ccu-connection.js`: when the
45
+ ReGaHSS interface is ticked, the node probes once before deciding
46
+ between `startMeta()` and `getRegaData()`. The provider probes again
47
+ on every reconnect; three failed probes in a row hand control back to
48
+ the detection (a restored backup or a firmware swap under a running
49
+ Node-RED), and on the ReGa side a failing `getRegaData()` /
50
+ `refreshRegaData()` re-probes at most every five minutes. So a box
51
+ that changes kind is picked up in both directions without a redeploy.
52
+ - **Credential**: on the box the read-only local token
53
+ (`/usr/local/etc/occulite/local-token`) is read automatically, off the
54
+ box an _openccu-lite token_ field takes one created on the box's Users
55
+ page. `401`/`403` degrades to addresses-only, is logged once, and is
56
+ retried — never fatal. An _openccu-lite port_ field covers boxes whose
57
+ web server is not on 80/443 (it is also what the integration test
58
+ uses).
59
+ - **The ReGa-only nodes stay**: `ccu-sysvar`, `ccu-program`,
60
+ `ccu-script` and `ccu-poll` remain in the palette and in flows, the
61
+ connection logs one line about them when it starts, and every message
62
+ they get is rejected with `… are not available on this box
63
+ (openccu-lite has no ReGaHSS)` instead of breaking the connection.
64
+ - **No new dependency.** `node:http`/`node:https` rather than `fetch`,
65
+ because the connection node already offers "ignore invalid TLS
66
+ certificates", which `fetch` cannot express per request without
67
+ pulling in undici — and a Node stream is the natural shape for SSE.
68
+
69
+ ## Tests
70
+
71
+ 16 unit tests in `test/unit/metaprovider.test.js` (`npm run test:pure`):
72
+ `buildNames()` over the openccu-lite conformance corpus, copied verbatim
73
+ into `test/fixtures/openccu-lite/` (`valid-house`, `valid-empty`,
74
+ `valid-depth-8`), plus a fake `/api/meta/v1` server built from
75
+ `node:http` for detection (openccu-lite / a CCU's 404 / a 200 that is
76
+ not the metadata API / nothing listening), the 401 path, an event →
77
+ "names changed" round trip, the re-snapshot triggers, reconnects and the
78
+ "not an openccu-lite any more" hand-back. The existing ReGa specs are
79
+ untouched.
80
+
81
+ Verified against a real `occulited` (built from the openccu-lite
82
+ repository, `--listen 127.0.0.1:2132`, the `valid-house` fixture
83
+ imported) with `node-red-node-test-helper` driving a real
84
+ `ccu-connection` node: names, rooms and functions match the box; a
85
+ `PATCH /objects/{ref}` rename, a room rename and a `node.moved` all
86
+ reach `ccu.channelNames` / `ccu.channelRooms` in under 150 ms; the four
87
+ ReGa-only nodes error per message; `createMessage()` produces the same
88
+ `channelName` / `rooms` / `room` / `functions` / `function` fields as on
89
+ a CCU.
90
+
91
+ ## Findings handed back to openccu-lite
92
+
93
+ 1. `occulited`'s SSE endpoint does not flush the response header until
94
+ it has an event or a heartbeat to write, so an HTTP client cannot
95
+ tell "connected" from "still connecting" for up to 30 s. A client
96
+ that applies its ordinary request timeout to the open stream tears it
97
+ down on every cycle; this implementation arms a separate idle timeout
98
+ after the socket connects and reports "connected" once the snapshot
99
+ is in, not when the stream opens.
100
+ 2. The porting prompt's example is `fetch`, which cannot do per-request
101
+ TLS verification settings in Node — worth a sentence in the kit,
102
+ since every integration that offers "ignore invalid certificates"
103
+ hits it.
104
+
105
+ Original item: openccu-lite port — names, rooms and functions from the
106
+ metadata API instead of ReGaHSS, ReGa path unchanged, runtime detection.
@@ -0,0 +1,69 @@
1
+ # B-2 — Dynamic node configuration via msg
2
+
3
+ **✅ done 2026-09-04** (`4.3.0`) — issues #172, #71, #103, #80, #56,
4
+ #148, #185, #133.
5
+
6
+ The request arrived from every direction and was deliberately designed
7
+ once rather than patched per node (which is why PR #173's auto-subscribe
8
+ was closed rather than merged).
9
+
10
+ ## The mechanism
11
+
12
+ `msg.config` is an object whose properties override the receiving node's
13
+ own configuration **for that message only**. `nodes/lib/dynconfig.js`
14
+ builds the effective configuration; the stored one is never touched.
15
+
16
+ Why a namespace rather than flat properties everywhere: an event message
17
+ already carries `channel`, `datapoint`, `deviceType`, `rooms` and friends,
18
+ so honouring those on the signal or display node would let any message
19
+ coming from an event node silently retarget it. `msg.ccu` was the obvious
20
+ namespace but is taken — it holds the CCU host name — hence `msg.config`.
21
+
22
+ Each node passes the list of keys it accepts; anything else in
23
+ `msg.config` is ignored, so a stray property cannot reach node internals.
24
+ The flat mechanism the value/get-value/set-value nodes always had (fill in
25
+ fields left empty in the dialog from `msg.channel`, `msg.rooms`, …) is
26
+ unchanged and keeps working; `msg.config` wins over it.
27
+
28
+ ## Per node
29
+
30
+ - **set-value** — the whole filter plus `force`. This is also the fix for
31
+ #133: `setValues()` wrote message properties into `this.config`, so the
32
+ `if (!config[key])` guard skipped every later message and the node
33
+ stayed pinned to the first message's targets. The whitelist/blacklist
34
+ are keyed by the effective filter now, so an unchanged filter keeps the
35
+ caching benefit and a changed one is re-evaluated. The 200-line inline
36
+ matching chain moved to `nodes/lib/channelfilter.js` (pure, 14 tests),
37
+ which also fixed the device-name filter requiring the _channel_ to have
38
+ a ReGa name while comparing the _device_ name.
39
+ - **value** — `iface`, `channel`, `datapoint`. A message without a
40
+ payload writes nothing (the 4.0.0 guard) and now retargets the output
41
+ subscription, which is what #172 asked for. For a node with its own
42
+ channel/datapoint configured this is a no-op unless `msg.config`
43
+ overrides them, so no existing flow changes behaviour.
44
+ - **switch** — the source selection (`iface`/`channel`/`datapoint`/
45
+ `datapointProperty`/`sysvar`/`sysvarProperty`), so one node serves a
46
+ whole subflow (#103).
47
+ - **sysvar** — `name`, plus a typed input for _where the value comes
48
+ from_ (msg/flow/global/env) instead of always `msg.payload` (#56).
49
+ - **signal** — every command field including `soundList`/`dimmerList`,
50
+ which also accept a plain array of values instead of the editor's
51
+ list-of-objects shape (#148). The existing `<field>Type` typed inputs
52
+ keep working; `msg.config` wins over them.
53
+ - **display** — channel and the EP-WM55 settings, joining the lines,
54
+ colours and icons that were already overridable.
55
+
56
+ ## Subflows (#158)
57
+
58
+ Node-RED substitutes `${PARAMETER}` in a node's string properties before
59
+ the node is constructed, so subflow parameters already worked at runtime —
60
+ but the editor's channel autocomplete cleared any value that was not a
61
+ known channel address, wiping the placeholder every time the dialog was
62
+ opened. Fixed in the value, get-value, switch, signal and alexa editors.
63
+
64
+ Not done: an explicit `env` type on the filter fields of the event nodes.
65
+ The `${PARAMETER}` route covers the use case and the filters are read once
66
+ at construction, so a typed input would need runtime evaluation for little
67
+ gain. Revisit if someone asks.
68
+
69
+ Original item: | B-2 | Dynamic node configuration via msg — recurring theme #172, #71, #103, #80, #56, plus #148 (SOUNDFILE_LIST via msg), #158 (subflow env config) and #133 — the latter confirmed still broken in 4.0.0: `setValues()` writes msg values into `config` permanently, so the first message's filters stick. Design properly rather than merging PR #173's auto-subscribe | tracker |
@@ -0,0 +1,57 @@
1
+ # B-3 — ccu-mqtt node fixes and additions
2
+
3
+ **✅ done 2026-09-04** (`4.3.0`) — issues #115, #22, #167, plus the three
4
+ findings from ROADMAP §8.2 that the hm2mqtt 3.0 rewrite had confirmed.
5
+
6
+ ## Fixed
7
+
8
+ - **The writeable check never fired.** Both `putParam` and `putParamset`
9
+ tested `!(description.OPERATIONS) && 2`, which is `(!OPERATIONS) && 2` —
10
+ always falsy, since `OPERATIONS` is a non-zero number for every real
11
+ parameter. Read-only parameters were written regardless. Now
12
+ `!(OPERATIONS & 2)`, and the write is skipped.
13
+ - **`putParamset` cast against the wrong description.** It passed
14
+ `paramsetDescription[filter.param]` for every parameter; `filter.param`
15
+ only exists on the `putParam` topic, so it was `undefined` there and
16
+ the whole paramset went out uncast (strings where the CCU wanted
17
+ numbers or booleans). Each value is cast against its own description.
18
+ - **Enum names never worked.** The CCU returns them in `VALUE_LIST`, but
19
+ `lib/cast.js` and `lib/message.js` read `ENUM`, which no interface
20
+ process sends: all 9494 ENUM-typed datapoints in the shipped
21
+ `paramsets.json` carry `VALUE_LIST` and none carries `ENUM`. So
22
+ `msg.datapointEnum` and `msg.valueEnum` had always been `undefined`,
23
+ and writing e.g. `AUTO-MODE` to an enum datapoint fell through to the
24
+ numeric cast and produced `0`. `enumList()` reads `VALUE_LIST` with
25
+ `ENUM` as a fallback for hand-written data.
26
+ - **The divergent cast copy is gone.** `ccu-mqtt.paramCast` differed from
27
+ `lib/cast.js` only in clamping to MIN/MAX (and in producing `NaN` for
28
+ unparseable input, which XML-RPC cannot encode). `castValue` gained a
29
+ `clamp` option; the mqtt node passes it, the value nodes do not (#74).
30
+ - The empty-name topic bug (`hm/status//STATE` for channels without a
31
+ ReGa name) was already fixed in 4.2.0.
32
+
33
+ ## Added
34
+
35
+ - **`get`** (#115): `hm/get/<channel>/<datapoint>` republishes the last
36
+ known value on its status topic. An empty datapoint republishes every
37
+ datapoint of the channel. Fresh user interest in April 2026.
38
+ - **`rpc`** (#22): the topic was already in the dispatcher's table, but
39
+ the class had no `rpc()` method, so `typeof this[command] === 'function'`
40
+ was false and the message was silently dropped; `topicOutputRpc` was
41
+ never read at all. That is why both fields sat commented out in the
42
+ editor since 2018 — exposing them would have created two settings that
43
+ do nothing. Both are visible now; the result or the error is published
44
+ on the response topic, and the never-working default topic loses its
45
+ meaningless `${command}` segment (migrated on dialog open). An empty
46
+ topic disables a command.
47
+ - **#167**: `getRegaData()` (channel names, rooms, functions, groups) ran
48
+ once, in the connection constructor; the recurring poll only refreshed
49
+ variables and programs. A function added in the CCU WebUI was therefore
50
+ invisible until the flow was redeployed. New connection option
51
+ _names/rooms refresh_ (minutes, default 15, 0 = off) re-reads channel
52
+ names, rooms and functions from the ReGa poll's tail.
53
+
54
+ `resolveChannel()` now holds the channel-name/address resolution the four
55
+ command handlers each had inline.
56
+
57
+ Original item: | B-3 | Fix the ccu-mqtt node bugs verified in hm2mqtt ROADMAP §1.3 (see §8.2); also #115 get/update method (fresh user interest Apr 2026), #22 missing RPC-topic GUI, #167 stale rooms/functions | hm2mqtt |
@@ -0,0 +1,44 @@
1
+ # B-4 — Local-CCU detection
2
+
3
+ **✅ done 2026-09-04** (`4.3.0`).
4
+
5
+ When Node-RED runs on the CCU itself (RedMatic), the interface processes
6
+ can be reached directly on their 3xxxx ports — BINRPC for BidCos and
7
+ ReGa, and without the hop through the CCU's lighttpd proxy. The
8
+ connection node picked that up from:
9
+
10
+ ```js
11
+ const rfdConf = fs.readFileSync('/etc/lighttpd/conf.d/proxy.conf').toString();
12
+ if (rfdConf.match(/"port"\s+=>\s+32001/)) {
13
+ this.isLocal = true;
14
+ }
15
+ ```
16
+
17
+ On current firmware that file is a single line:
18
+
19
+ ```
20
+ include "/var/etc/lighttpd_proxy.conf"
21
+ ```
22
+
23
+ which itself only contains `include "/etc/lighttpd/conf.d/proxy_normal.conf"` —
24
+ the `"port" => 32001` markers are two levels down the include chain.
25
+ So the regex has not matched in years and every local install silently
26
+ went through the proxy on 1999/2000/2001/2010/9292.
27
+
28
+ Measured on a lab CCU3 with firmware 3.89.8: `proxy.conf` is the bare
29
+ include above, while 31999, 32001, 32010 and 39292 are all listening.
30
+
31
+ ## Fix
32
+
33
+ `nodes/lib/localccu.js` asks the kernel instead of parsing lighttpd's
34
+ configuration: a listener on rfd's direct port (32001, or the ReGa's 31999) is exactly the condition that makes using the direct ports
35
+ correct, and it cannot drift when eQ-3 reorganises the config files
36
+ again. `/proc/net/tcp` and `/proc/net/tcp6` are plain files, so the check
37
+ stays synchronous — the interface table is built in the constructor —
38
+ and on a host without `/proc` (Windows, macOS) it simply reports "not
39
+ local", which is the safe answer.
40
+
41
+ 8 unit tests, including the real `/proc/net/tcp` line format from the
42
+ 3.89.8 lab box.
43
+
44
+ Original item: | B-4 | Fix/remove the dead `isLocal` detection (`/etc/lighttpd/conf.d/proxy.conf` grep, `ccu-connection.js:346` — file is a stub on firmware ≥ 3.87, so local installs silently go through the proxy) | hm2mqtt §15.4 |
@@ -0,0 +1,57 @@
1
+ # B-5 — Ping checks, sysvar node properties and status
2
+
3
+ **✅ done 2026-09-04** (`4.3.0`) — issues #44, #54, #52, #124, #166, #96,
4
+ #56, #183.
5
+
6
+ A collection of small connection- and node-level items that had all been
7
+ open since 2019/2020.
8
+
9
+ - **#44 — configurable ping checks.** The connection node had a _ping
10
+ timeout_ field but no way to switch the check off: entering 0 fell
11
+ through `Number.parseInt(config.rpcPingTimeout, 10) || 60` back to 60
12
+ seconds. New _RPC ping_ checkbox (default on). Off means no periodic
13
+ ping and no timeout-triggered re-init for that connection — the honest
14
+ reading of "deactivate ping checks", with the consequence documented in
15
+ the dialog.
16
+ - **#54 / #52 — last value and its age under the node.** The connection
17
+ keeps `value`, `ts` and `lc` for every datapoint and system variable;
18
+ the nodes simply never showed them, and the sysvar node showed nothing
19
+ at all unless "emit value on start" was enabled.
20
+ `nodes/lib/valuestatus.js` renders `<value> (3m)` and re-renders on a timer whose
21
+ interval follows the resolution shown, so the line does not go stale
22
+ between events. Used by the sysvar node (which also seeds it from the
23
+ connection's cache when "emit value on start" is off) and the rpc-event
24
+ node (which shows the topic that last fired).
25
+ - **#96 — filter uncertain values.** The `uncertain` flag was already in
26
+ every message; there was no way to filter on it. `uncertain` is a
27
+ subscription filter property now, exposed as a _discard uncertain
28
+ values_ checkbox on the value and rpc-event nodes. It is only put into
29
+ the filter when the option is on, so existing subscriptions are
30
+ unaffected, and it is evaluated before `callCallback()`'s attribute
31
+ loop, which may break early for a whitelisted datapoint.
32
+ - **#166 — immediate sysvar echo lost under concurrency.** `setVariable()`
33
+ triggers a ReGa poll so the new value comes back without waiting for
34
+ the 30 s interval, but only `if (!this.regaPollPending)`, and
35
+ `regaPoll()` itself no-ops while one is in flight. Writing several
36
+ variables at once therefore left all but the first invisible for up to
37
+ a poll interval — exactly the reported symptom ("works for one
38
+ variable, not for several"). The request is remembered and exactly one
39
+ more poll runs when the current one finishes.
40
+ - **#183 — RSSI conversion guarded.** `dp.value -= 256` ran for every
41
+ RSSI_DEVICE/RSSI_PEER value from the ReGa poll, so a value that was
42
+ already signed, or the 0 that means "never received", became -318 or
43
+ -256. Now only for values above 127, matching hm2mqtt's guard. (The
44
+ XML-RPC event path needs no conversion.)
45
+ - **#124 — re-read variable descriptions on poll** was already fixed in
46
+ 2020 (commit 453f2b2, v3.2.1) and the issue merely never closed; the
47
+ remaining gap (`info` not selectable as a property) is noted below.
48
+ - **#56** — the sysvar node's configurable source property landed with
49
+ B-2, which owns the dialog change.
50
+
51
+ ## Not done
52
+
53
+ - `info` (a variable's description) is still not selectable as a single
54
+ property in the get-value/sysvar nodes; it is reachable through the
55
+ whole message. Left for whoever touches those property lists next.
56
+
57
+ Original item: | B-5 | Configurable ping checks (#44), sysvar node property/status improvements (#54, #56, #52 last-event status, #124 re-read descriptions on poll, #166 immediate sysvar echo, #96 uncertain-filter option) | tracker |