node-red-contrib-ccu 4.4.4 → 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 +175 -0
- package/README.md +35 -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 +46 -9
- package/nodes/lib/localccu.js +21 -3
- 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
|
@@ -5,16 +5,20 @@
|
|
|
5
5
|
* addon started before the interfaces, a daemon restart) used to leave the
|
|
6
6
|
* interface without events: rpcCheckInit only runs with cached devices and
|
|
7
7
|
* then waits pingTimeout (600 s for HmIP-RF). Now a failed init schedules its
|
|
8
|
-
* own retry,
|
|
9
|
-
* node is closed.
|
|
8
|
+
* own retry, 1 s, 2 s, 4 s, 8 s and then every 15 s (task 12), until it
|
|
9
|
+
* succeeds or the node is closed.
|
|
10
10
|
*
|
|
11
|
-
* While the process is unreachable the interface is "waiting": one
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* While the process is unreachable the interface is "waiting": one line, then
|
|
12
|
+
* debug. Before the interface was ever connected, a refused connection is
|
|
13
|
+
* info - the process is still starting (openccu-lite's early start starts
|
|
14
|
+
* Node-RED before rfd and hmipserver) - and every other transport error, or a
|
|
15
|
+
* refusal after it had been connected, is a warning. A fault answer (the
|
|
16
|
+
* process is up and refuses the init) stays an error, logged once per
|
|
17
|
+
* distinct fault.
|
|
14
18
|
*/
|
|
15
19
|
|
|
16
|
-
const RETRY_DELAYS = [2000, 4000, 8000
|
|
17
|
-
const RETRY_MAX_DELAY =
|
|
20
|
+
const RETRY_DELAYS = [1000, 2000, 4000, 8000];
|
|
21
|
+
const RETRY_MAX_DELAY = 15000;
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* The delay before the next attempt.
|
|
@@ -39,6 +43,15 @@ function isFault(error) {
|
|
|
39
43
|
return Boolean(error) && typeof error === 'object' && error.faultCode !== undefined;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Whether nothing listens on the port (yet): the process is not started.
|
|
48
|
+
* @param {*} error
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
function isRefused(error) {
|
|
52
|
+
return Boolean(error) && (error.code === 'ECONNREFUSED' || /ECONNREFUSED/.test(String(error.message || '')));
|
|
53
|
+
}
|
|
54
|
+
|
|
42
55
|
/**
|
|
43
56
|
* A short reason for the log line, e.g. "connect ECONNREFUSED".
|
|
44
57
|
* @param {*} error
|
|
@@ -57,6 +70,17 @@ function describeError(error) {
|
|
|
57
70
|
return (error.syscall ? error.syscall + ' ' : 'connect ') + error.code;
|
|
58
71
|
}
|
|
59
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
|
+
|
|
60
84
|
return error.message || String(error);
|
|
61
85
|
}
|
|
62
86
|
|
|
@@ -77,6 +101,7 @@ class InitRetry {
|
|
|
77
101
|
this.timers = timers || {setTimeout, clearTimeout};
|
|
78
102
|
this.timer = null;
|
|
79
103
|
this.stopped = false;
|
|
104
|
+
this.everConnected = false;
|
|
80
105
|
this.reset();
|
|
81
106
|
}
|
|
82
107
|
|
|
@@ -122,7 +147,18 @@ class InitRetry {
|
|
|
122
147
|
);
|
|
123
148
|
} else {
|
|
124
149
|
this.warned = true;
|
|
125
|
-
|
|
150
|
+
if (!this.everConnected && isRefused(error)) {
|
|
151
|
+
this.logger.info(
|
|
152
|
+
this.iface +
|
|
153
|
+
' not listening yet (' +
|
|
154
|
+
reason +
|
|
155
|
+
'), waiting for it - retrying in ' +
|
|
156
|
+
seconds +
|
|
157
|
+
' s',
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
this.logger.warn(this.iface + ' not reachable yet (' + reason + '), retrying');
|
|
161
|
+
}
|
|
126
162
|
}
|
|
127
163
|
|
|
128
164
|
this.onState('waiting');
|
|
@@ -160,6 +196,7 @@ class InitRetry {
|
|
|
160
196
|
this.logger.info(this.iface + ' connected after ' + (this.failures + 1) + ' attempts');
|
|
161
197
|
}
|
|
162
198
|
|
|
199
|
+
this.everConnected = true;
|
|
163
200
|
this.reset();
|
|
164
201
|
this.onState('connected');
|
|
165
202
|
}
|
|
@@ -178,4 +215,4 @@ class InitRetry {
|
|
|
178
215
|
}
|
|
179
216
|
}
|
|
180
217
|
|
|
181
|
-
module.exports = {InitRetry, retryDelay, isFault, describeError, RETRY_DELAYS, RETRY_MAX_DELAY};
|
|
218
|
+
module.exports = {InitRetry, retryDelay, isFault, isRefused, describeError, RETRY_DELAYS, RETRY_MAX_DELAY};
|
package/nodes/lib/localccu.js
CHANGED
|
@@ -17,7 +17,15 @@
|
|
|
17
17
|
Rather than chasing lighttpd's include chain, ask the kernel: a
|
|
18
18
|
listener on the direct port is exactly the condition that makes using
|
|
19
19
|
it correct. /proc/net/tcp is a plain file, so this stays synchronous -
|
|
20
|
-
the interface table is built in the connection node's constructor.
|
|
20
|
+
the interface table is built in the connection node's constructor.
|
|
21
|
+
|
|
22
|
+
A listener alone misses a start before rfd: openccu-lite starts an addon
|
|
23
|
+
that copes with it before the interface processes (its early start), so
|
|
24
|
+
the ports are not open yet and the connection went through the proxy for
|
|
25
|
+
good. The CCU's own interface list, /etc/config/InterfacesList.xml, is
|
|
26
|
+
there from the first second on every CCU firmware (CCU3, OpenCCU,
|
|
27
|
+
openccu-lite), so a loopback host with that file is the CCU we run on as
|
|
28
|
+
well. */
|
|
21
29
|
|
|
22
30
|
const fs = require('fs');
|
|
23
31
|
|
|
@@ -26,6 +34,9 @@ const LOCAL_PROBE_PORTS = [32001, 31999];
|
|
|
26
34
|
|
|
27
35
|
const PROC_NET_TCP = ['/proc/net/tcp', '/proc/net/tcp6'];
|
|
28
36
|
|
|
37
|
+
/** The CCU firmware's list of its interface processes. */
|
|
38
|
+
const INTERFACES_LIST = '/etc/config/InterfacesList.xml';
|
|
39
|
+
|
|
29
40
|
/** /proc/net/tcp connection state 0A = TCP_LISTEN. */
|
|
30
41
|
const TCP_LISTEN = '0A';
|
|
31
42
|
|
|
@@ -67,6 +78,7 @@ function listeningPorts(files) {
|
|
|
67
78
|
* @param {string} host the configured CCU host
|
|
68
79
|
* @param {object} [options]
|
|
69
80
|
* @param {string[]} [options.files] /proc files to read, for tests
|
|
81
|
+
* @param {string} [options.interfacesList] the CCU's interface list, for tests
|
|
70
82
|
* @returns {boolean}
|
|
71
83
|
*/
|
|
72
84
|
function isLocalCcu(host, options) {
|
|
@@ -75,7 +87,13 @@ function isLocalCcu(host, options) {
|
|
|
75
87
|
}
|
|
76
88
|
|
|
77
89
|
const ports = listeningPorts(options && options.files);
|
|
78
|
-
|
|
90
|
+
if (LOCAL_PROBE_PORTS.some((port) => ports.has(port))) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// the interface processes are not up yet (a start before them): the
|
|
95
|
+
// firmware's interface list still says this is the CCU
|
|
96
|
+
return fs.existsSync((options && options.interfacesList) || INTERFACES_LIST);
|
|
79
97
|
}
|
|
80
98
|
|
|
81
|
-
module.exports = {isLocalCcu, listeningPorts, LOCAL_PROBE_PORTS};
|
|
99
|
+
module.exports = {isLocalCcu, listeningPorts, LOCAL_PROBE_PORTS, INTERFACES_LIST};
|
|
@@ -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};
|