node-red-contrib-ccu 4.4.5 → 4.5.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/CHANGELOG.md +150 -0
- package/README.md +22 -0
- package/nodes/ccu-alexa.html +12 -14
- package/nodes/ccu-connection.html +63 -95
- package/nodes/ccu-connection.js +812 -115
- package/nodes/ccu-get-value.html +18 -15
- package/nodes/ccu-get-value.js +16 -0
- package/nodes/ccu-homeassistant.html +0 -48
- package/nodes/ccu-mqtt.html +0 -8
- package/nodes/ccu-poll.html +0 -17
- package/nodes/ccu-program.html +0 -53
- package/nodes/ccu-rpc-event.html +0 -34
- package/nodes/ccu-rpc.html +0 -64
- package/nodes/ccu-script.html +0 -8
- package/nodes/ccu-set-value.html +0 -62
- package/nodes/ccu-set-value.js +6 -11
- package/nodes/ccu-signal.html +0 -64
- package/nodes/ccu-switch.html +12 -24
- package/nodes/ccu-sysvar.html +0 -64
- package/nodes/ccu-value.html +23 -95
- package/nodes/ccu-value.js +8 -2
- package/nodes/lib/initretry.js +11 -0
- package/nodes/lib/metaprovider.js +47 -19
- package/nodes/lib/party.js +103 -0
- package/nodes/lib/topic.js +22 -1
- package/nodes/lib/transport.js +80 -0
- package/nodes/locales/de/ccu-alexa.html +4 -0
- package/nodes/locales/de/ccu-connection.html +103 -0
- package/nodes/locales/de/ccu-get-value.html +16 -0
- package/nodes/locales/de/ccu-homeassistant.html +23 -0
- package/nodes/locales/de/ccu-mqtt.html +21 -0
- package/nodes/locales/de/ccu-poll.html +8 -0
- package/nodes/locales/de/ccu-program.html +26 -0
- package/nodes/locales/de/ccu-rpc-event.html +32 -0
- package/nodes/locales/de/ccu-rpc.html +31 -0
- package/nodes/locales/de/ccu-script.html +13 -0
- package/nodes/locales/de/ccu-set-value.html +46 -0
- package/nodes/locales/de/ccu-signal.html +29 -0
- package/nodes/locales/de/ccu-switch.html +18 -0
- package/nodes/locales/de/ccu-sysvar.html +43 -0
- package/nodes/locales/de/ccu-value.html +98 -0
- package/nodes/locales/de/ccu-value.json +1 -0
- package/nodes/locales/en-US/ccu-alexa.html +4 -0
- package/nodes/locales/en-US/ccu-connection.html +41 -0
- package/nodes/locales/en-US/ccu-get-value.html +13 -0
- package/nodes/locales/en-US/ccu-homeassistant.html +23 -0
- package/nodes/locales/en-US/ccu-mqtt.html +21 -0
- package/nodes/locales/en-US/ccu-poll.html +7 -0
- package/nodes/locales/en-US/ccu-program.html +25 -0
- package/nodes/locales/en-US/ccu-rpc-event.html +31 -0
- package/nodes/locales/en-US/ccu-rpc.html +30 -0
- package/nodes/locales/en-US/ccu-script.html +13 -0
- package/nodes/locales/en-US/ccu-set-value.html +45 -0
- package/nodes/locales/en-US/ccu-signal.html +29 -0
- package/nodes/locales/en-US/ccu-switch.html +6 -0
- package/nodes/locales/en-US/ccu-sysvar.html +42 -0
- package/nodes/locales/en-US/ccu-value.html +81 -0
- package/nodes/locales/en-US/ccu-value.json +1 -0
- package/package.json +1 -1
package/nodes/ccu-sysvar.html
CHANGED
|
@@ -137,67 +137,3 @@
|
|
|
137
137
|
</div>
|
|
138
138
|
<div class="form-tips" data-i18n="ccu-sysvar.tip"></div>
|
|
139
139
|
</script>
|
|
140
|
-
|
|
141
|
-
<script type="text/x-red" data-help-name="ccu-sysvar" lang="de-DE">
|
|
142
|
-
<p>Setzen und lesen von ReGaHSS Systemvariablen.</p>
|
|
143
|
-
<h3>Inputs</h3>
|
|
144
|
-
<dl class="message-properties">
|
|
145
|
-
<dt>payload
|
|
146
|
-
<span class="property-type">*</span>
|
|
147
|
-
</dt>
|
|
148
|
-
<dd>
|
|
149
|
-
Der Wert der der Systemvariablen zugewiesen werden soll.
|
|
150
|
-
</dd>
|
|
151
|
-
</dl>
|
|
152
|
-
<h3>Outputs</h3>
|
|
153
|
-
<ol class="node-ports">
|
|
154
|
-
<li>Output
|
|
155
|
-
|
|
156
|
-
<code>msg</code> Beispiel:<br>
|
|
157
|
-
<pre><code>
|
|
158
|
-
{
|
|
159
|
-
"ccu": "localhost",
|
|
160
|
-
"iface": "ReGaHSS",
|
|
161
|
-
"type": "SYSVAR",
|
|
162
|
-
"name": "Anwesenheit",
|
|
163
|
-
"payload": false,
|
|
164
|
-
"value": false,
|
|
165
|
-
"valueType": "boolean",
|
|
166
|
-
"valueEnum": "nicht anwesend",
|
|
167
|
-
"ts": 1525197999611,
|
|
168
|
-
"lc": 1525197999611,
|
|
169
|
-
"enum": ["nicht anwesend", "anwesend"],
|
|
170
|
-
"id": 950,
|
|
171
|
-
"topic": "ReGaHSS/Anwesenheit"
|
|
172
|
-
}
|
|
173
|
-
</code></pre>
|
|
174
|
-
</li>
|
|
175
|
-
</ol>
|
|
176
|
-
|
|
177
|
-
<h3>Dynamische Konfiguration</h3>
|
|
178
|
-
<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>
|
|
179
|
-
<ul>
|
|
180
|
-
<li><code>name</code></li>
|
|
181
|
-
</ul>
|
|
182
|
-
<p>Woher der zu schreibende Wert kommt, stellt das Feld <em>Wert aus</em> ein (msg/flow/global/env) — er muss nicht mehr in <code>msg.payload</code> stehen.</p>
|
|
183
|
-
</script>
|
|
184
|
-
|
|
185
|
-
<script type="text/x-red" data-help-name="ccu-sysvar" lang="en-US">
|
|
186
|
-
<p>Set and read ReGaHSS system variables.</p>
|
|
187
|
-
<h3>Inputs</h3>
|
|
188
|
-
<dl class="message-properties">
|
|
189
|
-
<dt>payload
|
|
190
|
-
<span class="property-type">*</span>
|
|
191
|
-
</dt>
|
|
192
|
-
<dd>
|
|
193
|
-
The value that should be assigned to the variable.
|
|
194
|
-
</dd>
|
|
195
|
-
</dl>
|
|
196
|
-
|
|
197
|
-
<h3>Dynamic configuration</h3>
|
|
198
|
-
<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>
|
|
199
|
-
<ul>
|
|
200
|
-
<li><code>name</code></li>
|
|
201
|
-
</ul>
|
|
202
|
-
<p>The <em>Value from</em> field selects where the value to write comes from (msg/flow/global/env); it no longer has to be <code>msg.payload</code>.</p>
|
|
203
|
-
</script>
|
package/nodes/ccu-value.html
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
cache: {value: false},
|
|
18
18
|
certainOnly: {value: false},
|
|
19
19
|
queue: {value: false},
|
|
20
|
+
force: {value: false},
|
|
20
21
|
on: {value: ''},
|
|
21
22
|
onType: {value: 'undefined'},
|
|
22
23
|
ramp: {value: ''},
|
|
@@ -57,6 +58,7 @@
|
|
|
57
58
|
let ifacesLoaded = false;
|
|
58
59
|
let ifacesPending = false;
|
|
59
60
|
let ifaceLoadRetries = 5;
|
|
61
|
+
let channelLoadRetries = 30;
|
|
60
62
|
|
|
61
63
|
function loadIfaces(iface, cb) {
|
|
62
64
|
if (ifacesPending) {
|
|
@@ -100,7 +102,16 @@
|
|
|
100
102
|
|
|
101
103
|
const nodeId = $nodeInputCcuConfig.val();
|
|
102
104
|
const url = 'ccu?config=' + nodeId + '&type=channels&iface=' + $nodeInputIface.val();
|
|
103
|
-
$.getJSON(url, d => {
|
|
105
|
+
$.getJSON(url, (d, _status, xhr) => {
|
|
106
|
+
// task 9: an empty list while the connection is still initialising is "not yet",
|
|
107
|
+
// not "nothing" - keep what is configured, say so, and ask again
|
|
108
|
+
if (Object.keys(d).length === 0 && xhr.getResponseHeader('X-CCU-Loading') && channelLoadRetries-- > 0) {
|
|
109
|
+
$nodeInputChannel.attr('placeholder', 'loading device data ...');
|
|
110
|
+
setTimeout(loadConfig, 2000);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
$nodeInputChannel.removeAttr('placeholder');
|
|
104
115
|
data = d;
|
|
105
116
|
autocompleteChannel();
|
|
106
117
|
autocompleteDatapoint();
|
|
@@ -179,7 +190,7 @@
|
|
|
179
190
|
|
|
180
191
|
// ${ENV} placeholders are substituted by Node-RED before the node is
|
|
181
192
|
// constructed (subflow parameters, #158) - do not clear them
|
|
182
|
-
if (!/^\$\{[^}]+\}$/.test($nodeInputChannel.val()) && !data[$nodeInputChannel.val().split(' ')[0]]) {
|
|
193
|
+
if (Object.keys(data).length > 0 && !/^\$\{[^}]+\}$/.test($nodeInputChannel.val()) && !data[$nodeInputChannel.val().split(' ')[0]]) {
|
|
183
194
|
$nodeInputChannel.val('');
|
|
184
195
|
$nodeInputDatapoint.val('');
|
|
185
196
|
}
|
|
@@ -466,6 +477,16 @@
|
|
|
466
477
|
</div>
|
|
467
478
|
</div>
|
|
468
479
|
|
|
480
|
+
<div class="form-row">
|
|
481
|
+
<label for="node-input-force"><i class="icon-bookmark"></i> </label>
|
|
482
|
+
<div style="width: 70%; display: inline-block; vertical-align: text-top;">
|
|
483
|
+
<label class="ccu-checkbox" for="node-input-force">
|
|
484
|
+
<input type="checkbox" id="node-input-force">
|
|
485
|
+
<span data-i18n="ccu-value.force"></span>
|
|
486
|
+
</label>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
469
490
|
<div class="form-row">
|
|
470
491
|
<label for="node-input-change"><i class="icon-bookmark"></i> </label>
|
|
471
492
|
<div style="width: 70%; display: inline-block; vertical-align: text-top;">
|
|
@@ -518,96 +539,3 @@
|
|
|
518
539
|
<div id="config-lookup-tree"></div>
|
|
519
540
|
</div>
|
|
520
541
|
</script>
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
<script type="text/x-red" data-help-name="ccu-value" lang="de-DE">
|
|
525
|
-
<p>Geräte steuern und/oder Events von einem Gerät empfangen.</p>
|
|
526
|
-
<h3>Konfiguration</h3>
|
|
527
|
-
<ul>
|
|
528
|
-
<li><b>CCU:</b> Die zu verwendende CCU-Konfiguration.</li>
|
|
529
|
-
<li><b>Topic:</b> <code>msg.topic</code></li>
|
|
530
|
-
<li><b>Interface</b>: Hier wird das Interface gewählt, über welches das gewüschnte Geräte angebunden ist.</li>
|
|
531
|
-
<li><b>Channel</b>: Der Kanal dessen Datenpunkte verwendet werden sollen. Er besteht aus der Serien- sowie der Kanalnummer. Eine Autovervollständigung hilft bei der Auswahl anhand der Klarnamen.</li>
|
|
532
|
-
<li><b>Datapoint</b>: Der Datenpunkt des Kanals, auch hier wird über eine Autovervollständigung die Auswahl vereinfacht.</li>
|
|
533
|
-
<li><b>ON_TIME</b>:
|
|
534
|
-
Zeit in Sekunden nach der ein Schaltaktor/Dimmer nach dem anschalten wieder ausgeschaltet werden soll.<br>
|
|
535
|
-
Dabei ist:
|
|
536
|
-
<ul>
|
|
537
|
-
<li><code>undefined</code> Keine automatische Ausschaltung</li>
|
|
538
|
-
<li><code>number</code> Eine feste Verzögerung in Sekunden</li>
|
|
539
|
-
<li><code>global</code>, <code>flow</code> Die Verzögerung in Sekunden wird aus einer Context-Variablen übernommen</li>
|
|
540
|
-
<li><code>msg</code> Die Verzögerung in Sekunden wird kann über eine msg property übergeben werden</li>
|
|
541
|
-
</ul>
|
|
542
|
-
|
|
543
|
-
</li>
|
|
544
|
-
<li><b>set Mode</b>:
|
|
545
|
-
Eine Erweiterung für Geräte, die sowohl den rx_mode BURST als auch WAKEUP unterstützen wie z.B.:
|
|
546
|
-
Batterie-Schaltaktoren oder Rauchmelder mit Sirenenfunktion. Mit dem Parameter kann in diesem Fall angegeben
|
|
547
|
-
werden, ob der übergebene Wert über BURST oder WAKEUP übertragen werden soll.
|
|
548
|
-
<ul>
|
|
549
|
-
<li><b>WAKEUP</b> - Überträgt den Wert sobald sich der entsprechende Empfänger bei der Zentrale meldet.</li>
|
|
550
|
-
<li><b>BURST</b> - Versucht alle BURST-Empfänger für die Übertragung aufzuwecken was sich negativ auf den DutyCycle und die Batterielaufzeit aller Burst-Empfänger auswirkt.</li>
|
|
551
|
-
</ul>
|
|
552
|
-
</li>
|
|
553
|
-
<li><b>Flags</b>:
|
|
554
|
-
<ul>
|
|
555
|
-
<li><b>Nur geänderte Werte ausgeben</b> - Ein Event wird nur dann erzeugt, wenn sich der Wert seit dem letztn Event verändert hat.</li>
|
|
556
|
-
<li><b>Während WORKING keine Werte ausgeben</b> - Es gibt Aktoren, z.B. Dimmer oder Rolladen, die während einer Rampe (Öffnen/Schließen eines Rollladen, Dimmen einer Lampe) fortwährend den aktuellen Zustand ausgeben. Um nur den End-Status auszugeben kann diese Option benutzt werden. Gebräuchlich auch bei Verwendung mit Dashboard-Nodes um "springende" Slider zu verhindern.</li>
|
|
557
|
-
<li><b>Beim Start letzten bekannten Wert ausgeben</b> - Wenn Node-RED gestartet wird, dann wird der letzte Wert (sofern in der ReGaHSS bekannt) ausgegeben. Z.B. um beim Start einen korrekten Wert an das Dashboard zu übergeben. <b>Achtung:</b> </li>
|
|
558
|
-
</ul>
|
|
559
|
-
|
|
560
|
-
</li>
|
|
561
|
-
</ul>
|
|
562
|
-
|
|
563
|
-
<h3>Input</h3>
|
|
564
|
-
<p>
|
|
565
|
-
Für den Input des Value Node wird <code>msg.payload</code> als der Wert verwendet der in den Datenpunkt geschrieben wird.
|
|
566
|
-
|
|
567
|
-
Die Bestimmung des Datenpunkts sowie des Kanals und der Schnittstellen können hier dynamisch sein. Dazu werden die Eingabefelder leer gelassen und über die eingehende <code>msg</code> gesetzt.
|
|
568
|
-
</p>
|
|
569
|
-
<b>Option 1</b>: Interface, Channel und Datapoint können in der msg als Properties angegeben werden:
|
|
570
|
-
<code>
|
|
571
|
-
<pre>{
|
|
572
|
-
"interface": "BidCos-RF",
|
|
573
|
-
"channel": "OEQ1868878:1",
|
|
574
|
-
"datapoint": "STATE",
|
|
575
|
-
"payload": true
|
|
576
|
-
}</pre>
|
|
577
|
-
</code>
|
|
578
|
-
|
|
579
|
-
<b>Option 2</b>: Der Datenpunkt kann über <code>msg.topic</code> angegeben werden wobei Interface, Channel und Datapoint mit Punkt getrennt werden:
|
|
580
|
-
<code>
|
|
581
|
-
<pre>{
|
|
582
|
-
"topic": "BidCos-RF.OEQ1868878:1.STATE",
|
|
583
|
-
"payload": true
|
|
584
|
-
}</pre>
|
|
585
|
-
</code>
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
<h3>Output</h3>
|
|
589
|
-
<p>Der Value Node gibt nur Events aus, wenn Interface, Channel und Datapoint gesetzt sind.</p>
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
<h3>Dynamische Konfiguration</h3>
|
|
593
|
-
<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>
|
|
594
|
-
<ul>
|
|
595
|
-
<li><code>iface</code></li>
|
|
596
|
-
<li><code>channel</code></li>
|
|
597
|
-
<li><code>datapoint</code></li>
|
|
598
|
-
</ul>
|
|
599
|
-
<p>Eine Nachricht ohne <code>payload</code> schreibt nichts, richtet aber die Ausgabe-Seite des Nodes neu aus.</p>
|
|
600
|
-
</script>
|
|
601
|
-
|
|
602
|
-
<script type="text/x-red" data-help-name="ccu-value" lang="en-US">
|
|
603
|
-
<p>Set and read channel datapoints.</p>
|
|
604
|
-
|
|
605
|
-
<h3>Dynamic configuration</h3>
|
|
606
|
-
<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>
|
|
607
|
-
<ul>
|
|
608
|
-
<li><code>iface</code></li>
|
|
609
|
-
<li><code>channel</code></li>
|
|
610
|
-
<li><code>datapoint</code></li>
|
|
611
|
-
</ul>
|
|
612
|
-
<p>A message without a <code>payload</code> writes nothing but retargets the node's output side.</p>
|
|
613
|
-
</script>
|
package/nodes/ccu-value.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
const {parseTopicAddress} = require('./lib/topic.js');
|
|
1
2
|
const path = require('path');
|
|
2
3
|
|
|
3
4
|
const statusHelper = require(path.join(__dirname, '/lib/status.js'));
|
|
4
5
|
const {effectiveConfig, configCacheKey} = require(path.join(__dirname, '/lib/dynconfig.js'));
|
|
5
6
|
|
|
6
7
|
/** What a message may supply or override (B-2, #172). */
|
|
7
|
-
const DYNAMIC_KEYS = ['iface', 'channel', 'datapoint'];
|
|
8
|
+
const DYNAMIC_KEYS = ['iface', 'channel', 'datapoint', 'force'];
|
|
8
9
|
|
|
9
10
|
module.exports = function (RED) {
|
|
10
11
|
class CcuValue {
|
|
@@ -31,7 +32,9 @@ module.exports = function (RED) {
|
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
this.on('input', (message, send, done) => {
|
|
34
|
-
|
|
35
|
+
// task 6 (#39): the address may come as `iface.channel.datapoint` or in the
|
|
36
|
+
// shape the nodes emit, `${CCU}/${Interface}/${channel}/${datapoint}`
|
|
37
|
+
const {iface: tIface, channel: tChannel, datapoint: tDatapoint} = parseTopicAddress(message.topic);
|
|
35
38
|
// msg.config wins over the node's own configuration; the flat
|
|
36
39
|
// msg.iface/channel/datapoint chain below is unchanged
|
|
37
40
|
const {config: over} = effectiveConfig({}, message, DYNAMIC_KEYS);
|
|
@@ -46,6 +49,8 @@ module.exports = function (RED) {
|
|
|
46
49
|
''
|
|
47
50
|
).split(' ')[0];
|
|
48
51
|
const datapoint = over.datapoint || config.datapoint || message.datapoint || tDatapoint;
|
|
52
|
+
// B-19: force writes through the queue's dedupe (as set-value has it)
|
|
53
|
+
const force = typeof over.force === 'undefined' ? Boolean(config.force) : Boolean(over.force);
|
|
49
54
|
|
|
50
55
|
if (!iface) {
|
|
51
56
|
this.error('interface undefined');
|
|
@@ -119,6 +124,7 @@ module.exports = function (RED) {
|
|
|
119
124
|
datapoint,
|
|
120
125
|
message.payload,
|
|
121
126
|
config.burst,
|
|
127
|
+
force,
|
|
122
128
|
)
|
|
123
129
|
.then(() => {
|
|
124
130
|
done();
|
package/nodes/lib/initretry.js
CHANGED
|
@@ -70,6 +70,17 @@ function describeError(error) {
|
|
|
70
70
|
return (error.syscall ? error.syscall + ' ' : 'connect ') + error.code;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// task 5 (#27): the CCU's web server answers a 401 (authentication on, the
|
|
74
|
+
// credentials missing or wrong) with an HTML page, and the XML-RPC client
|
|
75
|
+
// reports only the parser's complaint about it - say what that means
|
|
76
|
+
if (/^Unknown XML-RPC tag '(HTML|HEAD|META|BODY|TITLE|!DOCTYPE)/i.test(String(error.message || ''))) {
|
|
77
|
+
return (
|
|
78
|
+
'the CCU answered a web page instead of XML-RPC (' +
|
|
79
|
+
error.message +
|
|
80
|
+
') - authentication is on and the credentials are missing or wrong, or this is not an RPC port'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
return error.message || String(error);
|
|
74
85
|
}
|
|
75
86
|
|
|
@@ -281,38 +281,65 @@ function readBody(res) {
|
|
|
281
281
|
});
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
/** lighttpd answers these while the api behind it is not up yet - the box is starting, nothing is decided */
|
|
285
|
+
const INCONCLUSIVE_STATUS = new Set([502, 503, 504]);
|
|
286
|
+
|
|
284
287
|
/**
|
|
285
|
-
* Feature detection: `GET /api/meta/v1/version` answers on
|
|
286
|
-
* needs no credential; a CCU / RaspberryMatic / OpenCCU
|
|
287
|
-
*
|
|
288
|
+
* Feature detection with its outcome: `GET /api/meta/v1/version` answers on
|
|
289
|
+
* openccu-lite and needs no credential; a CCU / RaspberryMatic / OpenCCU
|
|
290
|
+
* answers 404 or HTML - that is a conclusive "no". A timeout, a refused
|
|
291
|
+
* connection or a 502/503/504 decides nothing (B-28: a busy or still starting
|
|
292
|
+
* box), so the caller can ask again instead of settling for the ReGa.
|
|
293
|
+
* Never rejects.
|
|
288
294
|
* @param {object} options host, port, tls, insecure, timeout, logger
|
|
289
|
-
* @returns {Promise<object|null
|
|
295
|
+
* @returns {Promise<{info: object|null, inconclusive: boolean, reason?: string}>}
|
|
290
296
|
*/
|
|
291
|
-
async function
|
|
297
|
+
async function detectOutcome(options) {
|
|
292
298
|
const {logger = SILENT} = options;
|
|
299
|
+
let status;
|
|
300
|
+
let body;
|
|
293
301
|
try {
|
|
294
|
-
const
|
|
302
|
+
const answer = await request({
|
|
295
303
|
...options,
|
|
296
304
|
path: API_PATH + '/version',
|
|
297
305
|
token: undefined,
|
|
298
306
|
timeout: options.timeout || 5000,
|
|
299
307
|
});
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
logger.debug('meta api detection: status ' + status);
|
|
303
|
-
return null;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const info = JSON.parse(body);
|
|
307
|
-
if (!info || info.api !== 'meta' || typeof info.version !== 'number') {
|
|
308
|
-
return null;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return info;
|
|
308
|
+
status = answer.status;
|
|
309
|
+
body = await readBody(answer.res);
|
|
312
310
|
} catch (error) {
|
|
313
311
|
logger.debug('meta api detection: ' + error.message);
|
|
314
|
-
return null;
|
|
312
|
+
return {info: null, inconclusive: true, reason: error.message};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (status !== 200) {
|
|
316
|
+
logger.debug('meta api detection: status ' + status);
|
|
317
|
+
return {info: null, inconclusive: INCONCLUSIVE_STATUS.has(status), reason: 'status ' + status};
|
|
315
318
|
}
|
|
319
|
+
|
|
320
|
+
let info = null;
|
|
321
|
+
try {
|
|
322
|
+
info = JSON.parse(body);
|
|
323
|
+
} catch {
|
|
324
|
+
info = null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!info || info.api !== 'meta' || typeof info.version !== 'number') {
|
|
328
|
+
return {info: null, inconclusive: false, reason: 'not the metadata api'};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {info, inconclusive: false};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Feature detection: the version document of an openccu-lite, or null for
|
|
336
|
+
* anything else (a CCU, an unreachable box). See detectOutcome() for the
|
|
337
|
+
* difference between "no" and "not now".
|
|
338
|
+
* @param {object} options host, port, tls, insecure, timeout, logger
|
|
339
|
+
* @returns {Promise<object|null>} the version document, or null
|
|
340
|
+
*/
|
|
341
|
+
async function detect(options) {
|
|
342
|
+
return (await detectOutcome(options)).info;
|
|
316
343
|
}
|
|
317
344
|
|
|
318
345
|
/**
|
|
@@ -708,6 +735,7 @@ module.exports = {
|
|
|
708
735
|
MetaProvider,
|
|
709
736
|
buildNames,
|
|
710
737
|
detect,
|
|
738
|
+
detectOutcome,
|
|
711
739
|
flattenEnum,
|
|
712
740
|
memberNames,
|
|
713
741
|
readLocalToken,
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* Party mode on HmIP thermostats (task 2, #156, #161).
|
|
2
|
+
|
|
3
|
+
An HmIP HEATING_CLIMATECONTROL_TRANSCEIVER channel carries PARTY_TIME_START,
|
|
4
|
+
PARTY_TIME_END and PARTY_SET_POINT_TEMPERATURE (and the read-only
|
|
5
|
+
PARTY_MODE). The device takes the three only together: a setValue of one
|
|
6
|
+
of them resets the other two - PARTY_TIME_START alone puts PARTY_TIME_END
|
|
7
|
+
back to `1999_11_30 00:00` (#156). The CCU's WebUI writes them as one
|
|
8
|
+
putParamset on VALUES, and so do we: a write to one of the three is merged
|
|
9
|
+
with the current values of the other two into one putParamset.
|
|
10
|
+
|
|
11
|
+
The times are strings in the device's own format, `YYYY_MM_DD HH:MM` in
|
|
12
|
+
local time (minutes in steps of five - the device rounds). A Date, an ISO
|
|
13
|
+
string or epoch milliseconds are formatted for it; a string already in the
|
|
14
|
+
format passes through.
|
|
15
|
+
|
|
16
|
+
The older HM thermostats (PARTY_MODE_SUBMIT, the PARTY_START_ and PARTY_STOP_ fields)
|
|
17
|
+
are another protocol and not covered here. Pure. */
|
|
18
|
+
|
|
19
|
+
const PARTY_DATAPOINTS = ['PARTY_TIME_START', 'PARTY_TIME_END', 'PARTY_SET_POINT_TEMPERATURE'];
|
|
20
|
+
const PARTY_TIMES = ['PARTY_TIME_START', 'PARTY_TIME_END'];
|
|
21
|
+
const TIME_FORMAT = /^\d{4}_\d{2}_\d{2} \d{2}:\d{2}$/;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Is this a write the device only takes together with the other two?
|
|
25
|
+
* @param {string} datapoint
|
|
26
|
+
* @param {object} [valuesDescription] the channel's VALUES paramset description
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isPartyWrite(datapoint, valuesDescription) {
|
|
30
|
+
return (
|
|
31
|
+
PARTY_DATAPOINTS.includes(datapoint) &&
|
|
32
|
+
Boolean(valuesDescription) &&
|
|
33
|
+
PARTY_DATAPOINTS.every((name) => Boolean(valuesDescription[name]))
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The device's time string for a value: a Date, epoch milliseconds, an ISO
|
|
39
|
+
* (or Date-parsable) string, or a string already in the format.
|
|
40
|
+
* @param {*} value
|
|
41
|
+
* @returns {string} the formatted time, or the value as a string when it cannot be read as a time
|
|
42
|
+
*/
|
|
43
|
+
function formatPartyTime(value) {
|
|
44
|
+
if (typeof value === 'string' && TIME_FORMAT.test(value)) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let date;
|
|
49
|
+
if (value instanceof Date) {
|
|
50
|
+
date = value;
|
|
51
|
+
} else if (typeof value === 'number') {
|
|
52
|
+
date = new Date(value);
|
|
53
|
+
} else if (typeof value === 'string' && value.trim() !== '') {
|
|
54
|
+
date = new Date(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!date || Number.isNaN(date.getTime())) {
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
62
|
+
return (
|
|
63
|
+
date.getFullYear() +
|
|
64
|
+
'_' +
|
|
65
|
+
pad(date.getMonth() + 1) +
|
|
66
|
+
'_' +
|
|
67
|
+
pad(date.getDate()) +
|
|
68
|
+
' ' +
|
|
69
|
+
pad(date.getHours()) +
|
|
70
|
+
':' +
|
|
71
|
+
pad(date.getMinutes())
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The complete VALUES paramset for one party write: the written datapoint
|
|
77
|
+
* plus the current values of the other two.
|
|
78
|
+
* @param {string} datapoint the one being written
|
|
79
|
+
* @param {*} value its new value
|
|
80
|
+
* @param {object} current the other datapoints' current values by name (undefined when unknown)
|
|
81
|
+
* @returns {{values: object, missing: string[]}} the merged three, and the names no current value was known for
|
|
82
|
+
*/
|
|
83
|
+
function mergePartyValues(datapoint, value, current) {
|
|
84
|
+
const values = {};
|
|
85
|
+
const missing = [];
|
|
86
|
+
for (const name of PARTY_DATAPOINTS) {
|
|
87
|
+
let v = name === datapoint ? value : current && current[name];
|
|
88
|
+
if (v === undefined || v === null) {
|
|
89
|
+
missing.push(name);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (PARTY_TIMES.includes(name)) {
|
|
94
|
+
v = formatPartyTime(v);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
values[name] = v;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {values, missing};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {PARTY_DATAPOINTS, isPartyWrite, formatPartyTime, mergePartyValues};
|
package/nodes/lib/topic.js
CHANGED
|
@@ -35,4 +35,25 @@ function topicReplace(topic, message) {
|
|
|
35
35
|
return topic;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/**
|
|
39
|
+
* The datapoint address in an incoming msg.topic (task 6, #39). Two shapes:
|
|
40
|
+
* the value node's old input form `iface.channel.datapoint` (dots), and the
|
|
41
|
+
* shape the nodes emit by default, `${CCU}/${Interface}/${channel}/${datapoint}`
|
|
42
|
+
* or without the CCU part - with slashes, the last three segments count, so
|
|
43
|
+
* an event's topic can be fed straight back into a value node.
|
|
44
|
+
* @param {string} topic
|
|
45
|
+
* @returns {{iface: string|undefined, channel: string|undefined, datapoint: string|undefined}}
|
|
46
|
+
*/
|
|
47
|
+
function parseTopicAddress(topic) {
|
|
48
|
+
const text = typeof topic === 'string' ? topic : '';
|
|
49
|
+
if (text.includes('/')) {
|
|
50
|
+
const parts = text.split('/').filter((part) => part !== '');
|
|
51
|
+
const [iface, channel, datapoint] = parts.slice(-3);
|
|
52
|
+
return parts.length >= 3 ? {iface, channel, datapoint} : {};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const [iface, channel, datapoint] = text.split('.');
|
|
56
|
+
return {iface, channel, datapoint};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {topicReplace, parseTopicAddress};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* TLS and authentication for the connections to the CCU (task 5, #27).
|
|
2
|
+
|
|
3
|
+
Both belong to the CCU's web server, which fronts the interface processes
|
|
4
|
+
for the LAN: XML-RPC on 2000/2001/2010/9292 in plain text and on
|
|
5
|
+
42000/42001/42010/49292 over TLS, the ReGaHSS on 8181 and 48181 - a CCU3
|
|
6
|
+
switches them on in its firewall settings, openccu-lite on its remote
|
|
7
|
+
access page ("classic RPC"). The processes themselves listen on the
|
|
8
|
+
loopback (31999, 32000, 32001, 32010, 39292) with neither. Hence:
|
|
9
|
+
|
|
10
|
+
- a local connection (Node-RED on the CCU) reaches the processes directly,
|
|
11
|
+
so TLS and credentials are not used whatever the dialog says: https to a
|
|
12
|
+
plain port or credentials in a BIN-RPC frame only break the connection;
|
|
13
|
+
- BIN-RPC has neither. With TLS on, BidCos-RF goes XML-RPC over TLS even
|
|
14
|
+
when BIN-RPC was asked for (the hidden `bcrfBinRpc` property); with
|
|
15
|
+
authentication and without TLS the option stays, and a warning says the
|
|
16
|
+
CCU will refuse it;
|
|
17
|
+
- CUxD (BIN-RPC on 8701) has neither, on any connection;
|
|
18
|
+
- the callback server Node-RED runs for the CCU's events stays plain: the
|
|
19
|
+
CCU presents no credentials and cannot verify our certificate.
|
|
20
|
+
|
|
21
|
+
Pure: the connection node feeds it the config and logs the notes. */
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {object} Transport
|
|
25
|
+
* @property {boolean} tls the XML-RPC and ReGa clients use TLS (the 4xxxx ports)
|
|
26
|
+
* @property {boolean} auth the clients send the username and password (HTTP basic)
|
|
27
|
+
* @property {boolean} inSecure invalid certificates are accepted (only with tls)
|
|
28
|
+
* @property {boolean} bcrfBinRpc BidCos-RF goes over BIN-RPC on a remote connection
|
|
29
|
+
* @property {Array<{level: 'info'|'warn', message: string}>} notes what to log once
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {object} config
|
|
34
|
+
* @param {boolean} config.isLocal Node-RED runs on the CCU (lib/localccu.js)
|
|
35
|
+
* @param {boolean} [config.tls]
|
|
36
|
+
* @param {boolean} [config.authentication]
|
|
37
|
+
* @param {boolean} [config.inSecure]
|
|
38
|
+
* @param {boolean} [config.bcrfBinRpc]
|
|
39
|
+
* @returns {Transport}
|
|
40
|
+
*/
|
|
41
|
+
function resolveTransport({isLocal, tls, authentication, inSecure, bcrfBinRpc}) {
|
|
42
|
+
const notes = [];
|
|
43
|
+
|
|
44
|
+
if (isLocal) {
|
|
45
|
+
if (tls || authentication) {
|
|
46
|
+
notes.push({
|
|
47
|
+
level: 'info',
|
|
48
|
+
message:
|
|
49
|
+
'local connection: TLS and authentication are not used, the interface processes are reached directly',
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {tls: false, auth: false, inSecure: false, bcrfBinRpc: false, notes};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const useTls = Boolean(tls);
|
|
57
|
+
const useAuth = Boolean(authentication);
|
|
58
|
+
let binRpc = Boolean(bcrfBinRpc);
|
|
59
|
+
|
|
60
|
+
if (binRpc && useTls) {
|
|
61
|
+
binRpc = false;
|
|
62
|
+
notes.push({
|
|
63
|
+
level: 'warn',
|
|
64
|
+
message: 'BidCos-RF: BIN-RPC has no TLS - using XML-RPC over TLS (port 42001) instead',
|
|
65
|
+
});
|
|
66
|
+
} else if (binRpc && useAuth) {
|
|
67
|
+
notes.push({
|
|
68
|
+
level: 'warn',
|
|
69
|
+
message: 'BidCos-RF: BIN-RPC carries no credentials - the CCU refuses it while authentication is on',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (useAuth && !useTls) {
|
|
74
|
+
notes.push({level: 'warn', message: 'authentication without TLS: the credentials are sent in plain text'});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {tls: useTls, auth: useAuth, inSecure: useTls && Boolean(inSecure), bcrfBinRpc: binRpc, notes};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {resolveTransport};
|