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,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>
|
|
@@ -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
|
+
}
|
|
@@ -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>
|
package/package.json
CHANGED
|
@@ -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 |
|