node-red-contrib-ccu 4.6.2 → 4.7.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 +51 -0
- package/README.md +16 -2
- package/nodes/ccu-connection.html +28 -7
- package/nodes/ccu-connection.js +397 -23
- package/nodes/lib/initretry.js +72 -17
- package/nodes/lib/lite.js +309 -0
- package/nodes/locales/de/ccu-connection.html +3 -1
- package/nodes/locales/en-US/ccu-connection.html +3 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,57 @@ Notable changes to node-red-contrib-ccu. Format follows
|
|
|
5
5
|
user-visible symptom and the cause, not the commit list (the release notes
|
|
6
6
|
append commits automatically).
|
|
7
7
|
|
|
8
|
+
## 4.7.0 (2026-10-27)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **On openccu-lite the interfaces are reached through the system's lite-rpc**
|
|
13
|
+
(with [occulite-client](https://www.npmjs.com/package/occulite-client))
|
|
14
|
+
instead of XML-RPC/BIN-RPC clients and callback servers of this node's own -
|
|
15
|
+
the requests over the system's web port, the events from a stream the
|
|
16
|
+
connection opens, replayed after a break and re-read after a restart of the
|
|
17
|
+
system; one subscriber at the interface processes serves every program on
|
|
18
|
+
the system, and nothing registers with `init`. The credential is the
|
|
19
|
+
openccu-lite token of the connection node, or on the system itself
|
|
20
|
+
RedMatic's own token, which the system mints for it. A new setting
|
|
21
|
+
_openccu-lite interfaces_ in the connection node chooses: _auto_ (the
|
|
22
|
+
default; the CCU way on a CCU), _openccu-lite_ (lite-rpc only) or _CCU_ (the
|
|
23
|
+
direct connection with callbacks, on openccu-lite too). An existing
|
|
24
|
+
connection is moved to lite-rpc once, at its first start on openccu-lite,
|
|
25
|
+
and the log says so; a choice made in the node is never changed by a later
|
|
26
|
+
version. The value cache is filled from the system's state store at deploy,
|
|
27
|
+
so nodes with _cache_ get a value on openccu-lite as well. CUxD and CCU-Jack
|
|
28
|
+
keep their own connections. On a CCU, RaspberryMatic and OpenCCU nothing
|
|
29
|
+
changes.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **An interface process that is not up yet is tried every 3 s for the first
|
|
34
|
+
90 s, then every 15 s.** The 1, 2, 4, 8 s steps and the 15 s after them
|
|
35
|
+
missed hmipserver's ready moment on a booting system by up to 15 s (measured
|
|
36
|
+
on an openccu-lite Pi 4: hmipserver answers 50-55 s after the boot, the next
|
|
37
|
+
attempt came at 61 s). The same holds after a process went away. When one
|
|
38
|
+
interface appears, the others still waiting are tried at once, and the log
|
|
39
|
+
line that reports the connection says how long after the start (or after the
|
|
40
|
+
outage) it came. The metadata API's re-detection on openccu-lite follows the
|
|
41
|
+
same schedule.
|
|
42
|
+
- **The port of each interface can be set in the connection node**, beside its
|
|
43
|
+
checkbox: blank is the CCU's port for the transport as before (2001/42001 and
|
|
44
|
+
so on, the 3xxxx ports on the CCU itself), a number replaces it - for a CCU
|
|
45
|
+
behind port forwarding, or a simulator on another port.
|
|
46
|
+
- The test suite runs hm-simulator 1.4 on free ports, so it runs beside a CCU
|
|
47
|
+
or another suite, and drives the simulator's scenario API: devices paired
|
|
48
|
+
and removed while Node-RED runs, a ramp-time write, a service message, an
|
|
49
|
+
interface process that restarts and forgets its clients, TLS with basic
|
|
50
|
+
auth.
|
|
51
|
+
- **On openccu-lite the metadata stream reports as `Metadata`, not as
|
|
52
|
+
"Interface ReGaHSS".** The system has no ReGaHSS; the connection borrowed
|
|
53
|
+
that slot for the stream's state, so the log said "Interface ReGaHSS
|
|
54
|
+
connected" at every start and reconnect, right next to "this box has no
|
|
55
|
+
ReGaHSS". The log now says "Metadata stream connected", the nodes' status
|
|
56
|
+
counts the stream under that name, and the editors' interface lists do not
|
|
57
|
+
offer it.
|
|
58
|
+
|
|
8
59
|
## 4.6.2 (2026-10-23)
|
|
9
60
|
|
|
10
61
|
### Fixed
|
package/README.md
CHANGED
|
@@ -36,8 +36,22 @@ BINRPC/XMLRPC works unchanged. Device, channel, room and function names come fro
|
|
|
36
36
|
ReGaHSS; the connection node detects this at start (`GET /api/meta/v1/version`), nothing has to be configured, and
|
|
37
37
|
`msg.channelName`, `msg.rooms`, `msg.functions` and the room/function filters keep their shape.
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
-
|
|
39
|
+
Since 4.7.0 the interface processes are reached through the system's **lite-rpc** as well, with
|
|
40
|
+
[occulite-client](https://www.npmjs.com/package/occulite-client): the requests over the system's web port, the events
|
|
41
|
+
from a stream the connection opens (replayed after a break, re-read after a restart of the system), one subscriber at
|
|
42
|
+
the processes for every program on the system, no callback server and no `init` of this node's own; the value cache is
|
|
43
|
+
filled from the system's state store at deploy. The connection node's **openccu-lite interfaces** setting chooses
|
|
44
|
+
_auto_ (the default: lite-rpc on openccu-lite, the CCU way on a CCU), _openccu-lite_ (lite-rpc only) or _CCU_ (the
|
|
45
|
+
direct connection with callbacks, on openccu-lite too); an existing connection is moved to lite-rpc once at its first
|
|
46
|
+
start on openccu-lite, the log says so, and a choice made in the node is never changed by a later version. CUxD and
|
|
47
|
+
CCU-Jack keep their own connections.
|
|
48
|
+
|
|
49
|
+
- Node-RED **on the system** (RedMatic from its addon catalogue): lite-rpc with RedMatic's own token, which the system
|
|
50
|
+
mints for it from RedMatic's manifest (RedMatic 9.13 or later); the names come with the system's read-only token.
|
|
51
|
+
Nothing to configure. An older RedMatic has no such token and connects the CCU way, the log says so.
|
|
52
|
+
- Node-RED **elsewhere**: an API token of the system goes into the connection node's **openccu-lite token** field -
|
|
53
|
+
for lite-rpc it needs the `rpc:*` scope of what the flow does (`rpc:read` for events and reads, `rpc:operate` for
|
|
54
|
+
`setValue`, `rpc:configure` for paramsets and links, `rpc:admin` for the rest; the rpc node may call anything).
|
|
41
55
|
- **System variables, programs and HM-Script do not exist there.** The `ccu-sysvar`, `ccu-program`, `ccu-script` and
|
|
42
56
|
`ccu-poll` nodes stay in the flows and answer every message with an error.
|
|
43
57
|
|
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
cuxdEnabled: {value: false},
|
|
14
14
|
jackEnabled: {value: false},
|
|
15
15
|
jackPort: {value: 2121},
|
|
16
|
+
regaPort: {value: ''},
|
|
17
|
+
bcrfPort: {value: ''},
|
|
18
|
+
bcwiPort: {value: ''},
|
|
19
|
+
iprfPort: {value: ''},
|
|
20
|
+
virtPort: {value: ''},
|
|
21
|
+
cuxdPort: {value: ''},
|
|
16
22
|
|
|
17
23
|
regaPoll: {value: true},
|
|
18
24
|
regaInterval: {value: 30},
|
|
@@ -31,6 +37,7 @@
|
|
|
31
37
|
username: {value: ''},
|
|
32
38
|
|
|
33
39
|
metaPort: {value: ''},
|
|
40
|
+
connectionType: {value: 'auto'},
|
|
34
41
|
|
|
35
42
|
queueTimeout: {value: 5000, required: true},
|
|
36
43
|
queuePause: {value: 250, required: true},
|
|
@@ -223,12 +230,18 @@
|
|
|
223
230
|
<div class="form-row">
|
|
224
231
|
<label for="node-input-rfdEnabled"><i class="icon-tag"></i> Interfaces</label>
|
|
225
232
|
<div style="width: 70%; display: inline-block; vertical-align: text-top;">
|
|
226
|
-
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-regaEnabled"> ReGaHSS</label
|
|
227
|
-
<
|
|
228
|
-
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-
|
|
229
|
-
<
|
|
230
|
-
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-
|
|
231
|
-
<
|
|
233
|
+
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-regaEnabled"> ReGaHSS</label>
|
|
234
|
+
<input type="text" id="node-config-input-regaPort" class="ccu-port" style="width: 70px;" placeholder="8181"><br>
|
|
235
|
+
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-bcrfEnabled"> BidCos-RF</label>
|
|
236
|
+
<input type="text" id="node-config-input-bcrfPort" class="ccu-port" style="width: 70px;" placeholder="2001"><br>
|
|
237
|
+
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-bcwiEnabled"> BidCos-Wired</label>
|
|
238
|
+
<input type="text" id="node-config-input-bcwiPort" class="ccu-port" style="width: 70px;" placeholder="2000"><br>
|
|
239
|
+
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-iprfEnabled"> HmIP-RF</label>
|
|
240
|
+
<input type="text" id="node-config-input-iprfPort" class="ccu-port" style="width: 70px;" placeholder="2010"><br>
|
|
241
|
+
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-virtEnabled"> VirtualDevices</label>
|
|
242
|
+
<input type="text" id="node-config-input-virtPort" class="ccu-port" style="width: 70px;" placeholder="9292"><br>
|
|
243
|
+
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-cuxdEnabled"> CUxD</label>
|
|
244
|
+
<input type="text" id="node-config-input-cuxdPort" class="ccu-port" style="width: 70px;" placeholder="8701"><br>
|
|
232
245
|
<label class="ccu-checkbox"><input type="checkbox" id="node-config-input-jackEnabled"> CCU-Jack</label>
|
|
233
246
|
<input type="text" id="node-config-input-jackPort" style="width: 70px;" placeholder="2121">
|
|
234
247
|
</div>
|
|
@@ -300,10 +313,18 @@
|
|
|
300
313
|
<label for="node-config-input-password"><i class="icon-tag"></i> Password</label>
|
|
301
314
|
<input type="password" id="node-config-input-password">
|
|
302
315
|
</div>
|
|
316
|
+
<div class="form-row">
|
|
317
|
+
<label for="node-config-input-connectionType"><i class="icon-tag"></i> openccu-lite interfaces</label>
|
|
318
|
+
<select id="node-config-input-connectionType" style="width: 70%;">
|
|
319
|
+
<option value="auto">auto: through the system's lite-rpc on openccu-lite (the default), the CCU way elsewhere</option>
|
|
320
|
+
<option value="openccu-lite">openccu-lite: lite-rpc only</option>
|
|
321
|
+
<option value="ccu">CCU: directly, with callbacks (XML-RPC/BIN-RPC) - on openccu-lite too</option>
|
|
322
|
+
</select>
|
|
323
|
+
</div>
|
|
303
324
|
<div class="form-row">
|
|
304
325
|
<label for="node-config-input-metaToken"><i class="icon-tag"></i> openccu-lite token</label>
|
|
305
326
|
<input type="password" id="node-config-input-metaToken" placeholder="olt_...">
|
|
306
|
-
<small>Only for openccu-lite (a CCU ignores this). Leave empty when Node-RED runs on the
|
|
327
|
+
<small>Only for openccu-lite (a CCU ignores this). Leave empty when Node-RED runs on the system itself (RedMatic).</small>
|
|
307
328
|
</div>
|
|
308
329
|
<div class="form-row">
|
|
309
330
|
<label for="node-config-input-metaPort"><i class="icon-tag"></i> openccu-lite port</label>
|
package/nodes/ccu-connection.js
CHANGED
|
@@ -17,7 +17,8 @@ const nextport = require('./lib/nextport.js');
|
|
|
17
17
|
const hmDiscover = require('./lib/discover.js');
|
|
18
18
|
const {Rega} = require('homematic-rega'); // ES module - require(esm) needs Node >= 20.19 / >= 22.12
|
|
19
19
|
const metaProvider = require('./lib/metaprovider.js');
|
|
20
|
-
const {InitRetry, retryDelay} = require('./lib/initretry.js');
|
|
20
|
+
const {InitRetry, retryDelay, RETRY_INTERVAL} = require('./lib/initretry.js');
|
|
21
|
+
const lite = require('./lib/lite.js');
|
|
21
22
|
const xmlrpc = require('homematic-xmlrpc');
|
|
22
23
|
const binrpc = require('binrpc');
|
|
23
24
|
|
|
@@ -55,8 +56,20 @@ function execToCallback(promise, callback) {
|
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/**
|
|
59
|
-
|
|
59
|
+
/**
|
|
60
|
+
* task 16: the pseudo-interface that carries the metadata stream's state on openccu-lite in
|
|
61
|
+
* ifaceTypes/ifaceStatus, where the ReGaHSS slot was borrowed before - the nodes' status logic
|
|
62
|
+
* works on those tables, and the system has no ReGaHSS to report. Never an RPC interface: no
|
|
63
|
+
* client, no init, not offered in the editors' interface lists (pseudo).
|
|
64
|
+
*/
|
|
65
|
+
const META_IFACE = 'Metadata';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* B-28: how often an inconclusive metadata api detection is repeated on the init retry's schedule
|
|
69
|
+
* (task 17: every 3 s for 90 s, then every 15 s - 41 attempts reach the same 255 s the 1/2/4/8/15 s
|
|
70
|
+
* steps covered in 20)
|
|
71
|
+
*/
|
|
72
|
+
const META_REDETECT_ATTEMPTS = 41;
|
|
60
73
|
|
|
61
74
|
/**
|
|
62
75
|
* task 9: has a device description changed in a way that changes its paramsets?
|
|
@@ -167,6 +180,11 @@ module.exports = function (RED) {
|
|
|
167
180
|
switch (request.query.type) {
|
|
168
181
|
case 'ifaces': {
|
|
169
182
|
Object.keys(config.ifaceTypes).forEach((iface) => {
|
|
183
|
+
if (config.ifaceTypes[iface].pseudo) {
|
|
184
|
+
// task 16: the metadata stream is not an interface a node could address
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
170
188
|
object[iface] = {
|
|
171
189
|
enabled: Boolean(config.ifaceTypes[iface].enabled),
|
|
172
190
|
connected: Boolean(config.ifaceStatus[iface]),
|
|
@@ -512,6 +530,8 @@ module.exports = function (RED) {
|
|
|
512
530
|
const pass = auth ? config.password : undefined;
|
|
513
531
|
|
|
514
532
|
this.ifaceTypes = {
|
|
533
|
+
// task 15: a port set in the dialog wins over the default of the transport (a CCU
|
|
534
|
+
// behind port forwarding, hm-simulator on a free port); blank is the default
|
|
515
535
|
ReGaHSS: {
|
|
516
536
|
conf: 'rega',
|
|
517
537
|
rpc: this.isLocal ? binrpc : xmlrpc,
|
|
@@ -523,7 +543,7 @@ module.exports = function (RED) {
|
|
|
523
543
|
'BidCos-RF': {
|
|
524
544
|
conf: 'bcrf',
|
|
525
545
|
rpc: this.isLocal || transport.bcrfBinRpc ? binrpc : xmlrpc,
|
|
526
|
-
port: this.isLocal ? 32001 : tls ? 42001 : 2001,
|
|
546
|
+
port: Number(config.bcrfPort) || (this.isLocal ? 32001 : tls ? 42001 : 2001),
|
|
527
547
|
protocol: this.isLocal || transport.bcrfBinRpc ? 'binrpc' : 'http',
|
|
528
548
|
auth,
|
|
529
549
|
user,
|
|
@@ -536,7 +556,7 @@ module.exports = function (RED) {
|
|
|
536
556
|
'BidCos-Wired': {
|
|
537
557
|
conf: 'bcwi',
|
|
538
558
|
rpc: this.isLocal ? binrpc : xmlrpc,
|
|
539
|
-
port: this.isLocal ? 32000 : tls ? 42000 : 2000,
|
|
559
|
+
port: Number(config.bcwiPort) || (this.isLocal ? 32000 : tls ? 42000 : 2000),
|
|
540
560
|
protocol: this.isLocal ? 'binrpc' : 'http',
|
|
541
561
|
auth,
|
|
542
562
|
user,
|
|
@@ -549,7 +569,7 @@ module.exports = function (RED) {
|
|
|
549
569
|
'HmIP-RF': {
|
|
550
570
|
conf: 'iprf',
|
|
551
571
|
rpc: xmlrpc,
|
|
552
|
-
port: this.isLocal ? 32010 : tls ? 42010 : 2010,
|
|
572
|
+
port: Number(config.iprfPort) || (this.isLocal ? 32010 : tls ? 42010 : 2010),
|
|
553
573
|
protocol: 'http',
|
|
554
574
|
auth,
|
|
555
575
|
user,
|
|
@@ -567,7 +587,7 @@ module.exports = function (RED) {
|
|
|
567
587
|
VirtualDevices: {
|
|
568
588
|
conf: 'virt',
|
|
569
589
|
rpc: xmlrpc,
|
|
570
|
-
port: this.isLocal ? 39292 : tls ? 49292 : 9292,
|
|
590
|
+
port: Number(config.virtPort) || (this.isLocal ? 39292 : tls ? 49292 : 9292),
|
|
571
591
|
path: 'groups',
|
|
572
592
|
protocol: 'http',
|
|
573
593
|
auth,
|
|
@@ -581,7 +601,7 @@ module.exports = function (RED) {
|
|
|
581
601
|
CUxD: {
|
|
582
602
|
conf: 'cuxd',
|
|
583
603
|
rpc: binrpc,
|
|
584
|
-
port: 8701,
|
|
604
|
+
port: Number(config.cuxdPort) || 8701,
|
|
585
605
|
protocol: 'binrpc',
|
|
586
606
|
init: true,
|
|
587
607
|
ping: true,
|
|
@@ -636,8 +656,10 @@ module.exports = function (RED) {
|
|
|
636
656
|
this.lastRealEvent = {};
|
|
637
657
|
this.ifaceStatus = {};
|
|
638
658
|
// task 11: interfaces whose init failed and is retried (InitRetry per iface),
|
|
639
|
-
// and which of them are waiting for an unreachable process
|
|
659
|
+
// and which of them are waiting for an unreachable process; task 17: when this
|
|
660
|
+
// node started, for the retry's "connected … after the start" line
|
|
640
661
|
this.initRetry = {};
|
|
662
|
+
this.startedAt = Date.now();
|
|
641
663
|
this.ifaceWaiting = {};
|
|
642
664
|
// B-37: per interface {inFlight, at, reinit} of our own init, and how long after it the
|
|
643
665
|
// process's listDevices/newDevices still answer it (see keptSubscription())
|
|
@@ -659,6 +681,17 @@ module.exports = function (RED) {
|
|
|
659
681
|
this.inSecure = inSecure;
|
|
660
682
|
this.metaPort = Number.parseInt(config.metaPort, 10) || (tls ? 443 : 80);
|
|
661
683
|
this.metaToken = config.metaToken || metaProvider.readLocalToken();
|
|
684
|
+
// task 14: the way to the interfaces on openccu-lite - `auto` (the default) goes through
|
|
685
|
+
// the system's lite-rpc where the system is openccu-lite and a credential allows it,
|
|
686
|
+
// `openccu-lite` insists on it, `ccu` keeps the direct connection with callbacks there
|
|
687
|
+
// too. The credential for lite-rpc: the node's token, else the addon's own (RedMatic's,
|
|
688
|
+
// minted by the system); the local token reads names only and is no credential here.
|
|
689
|
+
this.connectionType = ['ccu', 'openccu-lite'].includes(config.connectionType)
|
|
690
|
+
? config.connectionType
|
|
691
|
+
: 'auto';
|
|
692
|
+
this.liteToken = config.metaToken || lite.readAddonToken();
|
|
693
|
+
this.lite = null;
|
|
694
|
+
this.liteIfaces = [];
|
|
662
695
|
this.metaMode = false;
|
|
663
696
|
this.meta = null;
|
|
664
697
|
// #167: minutes between re-reads of channel names, rooms and
|
|
@@ -742,6 +775,12 @@ module.exports = function (RED) {
|
|
|
742
775
|
RED.settings.userDir || path.join(__dirname, '..'),
|
|
743
776
|
'ccu_values_' + this.host + '.json',
|
|
744
777
|
);
|
|
778
|
+
// task 14: written once, when a connection of type `auto` first went through lite-rpc -
|
|
779
|
+
// the one-time move is said in the log once, not at every start
|
|
780
|
+
this.liteMarkerFile = path.join(
|
|
781
|
+
RED.settings.userDir || path.join(__dirname, '..'),
|
|
782
|
+
'ccu_lite_' + this.host + '.json',
|
|
783
|
+
);
|
|
745
784
|
|
|
746
785
|
this.loadMetadata();
|
|
747
786
|
this.loadRegadata();
|
|
@@ -752,7 +791,7 @@ module.exports = function (RED) {
|
|
|
752
791
|
|
|
753
792
|
this.rega = new Rega({
|
|
754
793
|
host: this.host,
|
|
755
|
-
port: this.isLocal ? 8183 : tls ? 48181 : 8181,
|
|
794
|
+
port: Number(config.regaPort) || (this.isLocal ? 8183 : tls ? 48181 : 8181),
|
|
756
795
|
tls,
|
|
757
796
|
insecure: inSecure,
|
|
758
797
|
username: user,
|
|
@@ -804,7 +843,7 @@ module.exports = function (RED) {
|
|
|
804
843
|
|
|
805
844
|
/**
|
|
806
845
|
* B-31: the password and the token, from the credentials or - once - from the
|
|
807
|
-
* plain properties of a flow written before 4.
|
|
846
|
+
* plain properties of a flow written before 4.5.0.
|
|
808
847
|
* @param {object} config the node's configuration as deployed
|
|
809
848
|
* @returns {{password: string, metaToken: string}}
|
|
810
849
|
*/
|
|
@@ -967,7 +1006,10 @@ module.exports = function (RED) {
|
|
|
967
1006
|
const waitingChanged = Boolean(this.ifaceWaiting[iface]) !== waiting;
|
|
968
1007
|
if (this.ifaceStatus[iface] !== connected || waitingChanged) {
|
|
969
1008
|
if (this.ifaceStatus[iface] !== connected) {
|
|
970
|
-
if (iface
|
|
1009
|
+
if (this.ifaceTypes[iface].pseudo) {
|
|
1010
|
+
// task 16: the stream of the system's metadata api, not an interface process
|
|
1011
|
+
this.logger.info(iface, 'stream', connected ? 'connected' : 'disconnected');
|
|
1012
|
+
} else if (iface === 'ReGaHSS') {
|
|
971
1013
|
this.logger.info('Interface', iface, connected ? 'connected' : 'disconnected');
|
|
972
1014
|
} else if (waiting) {
|
|
973
1015
|
// task 11: the retry logs the one warn line, this stays quiet
|
|
@@ -977,7 +1019,9 @@ module.exports = function (RED) {
|
|
|
977
1019
|
'Interface',
|
|
978
1020
|
iface,
|
|
979
1021
|
connected
|
|
980
|
-
? this.
|
|
1022
|
+
? this.lite && this.lite.has(iface)
|
|
1023
|
+
? 'lite-rpc connected'
|
|
1024
|
+
: this.ifaceTypes[iface].protocol +
|
|
981
1025
|
' port ' +
|
|
982
1026
|
this.ifaceTypes[iface].port +
|
|
983
1027
|
' connected'
|
|
@@ -993,11 +1037,7 @@ module.exports = function (RED) {
|
|
|
993
1037
|
delete this.ifaceWaiting[iface];
|
|
994
1038
|
}
|
|
995
1039
|
|
|
996
|
-
|
|
997
|
-
if (typeof this.users[id].setStatus === 'function') {
|
|
998
|
-
this.users[id].setStatus({ifaceStatus: this.ifaceStatus, ifaceWaiting: this.ifaceWaiting});
|
|
999
|
-
}
|
|
1000
|
-
});
|
|
1040
|
+
this.pushStatus();
|
|
1001
1041
|
}
|
|
1002
1042
|
}
|
|
1003
1043
|
|
|
@@ -1011,6 +1051,7 @@ module.exports = function (RED) {
|
|
|
1011
1051
|
this.initRetry[iface] = new InitRetry({
|
|
1012
1052
|
iface,
|
|
1013
1053
|
logger: this.logger,
|
|
1054
|
+
startedAt: this.startedAt,
|
|
1014
1055
|
attempt: () => this.rpcInit(iface, {retry: true}),
|
|
1015
1056
|
onState: (state) => {
|
|
1016
1057
|
if (state === 'connected') {
|
|
@@ -1026,6 +1067,15 @@ module.exports = function (RED) {
|
|
|
1026
1067
|
return this.initRetry[iface];
|
|
1027
1068
|
}
|
|
1028
1069
|
|
|
1070
|
+
/** the interface tables as they are now, to every node of this connection */
|
|
1071
|
+
pushStatus() {
|
|
1072
|
+
Object.keys(this.users).forEach((id) => {
|
|
1073
|
+
if (typeof this.users[id].setStatus === 'function') {
|
|
1074
|
+
this.users[id].setStatus({ifaceStatus: this.ifaceStatus, ifaceWaiting: this.ifaceWaiting});
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1029
1079
|
/**
|
|
1030
1080
|
* An init succeeded (the first one, a retry, or a re-init after a ping timeout).
|
|
1031
1081
|
* @param iface
|
|
@@ -1036,6 +1086,21 @@ module.exports = function (RED) {
|
|
|
1036
1086
|
} else {
|
|
1037
1087
|
this.setIfaceStatus(iface, true);
|
|
1038
1088
|
}
|
|
1089
|
+
|
|
1090
|
+
this.retryMissingNow(iface);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* task 17: an interface appeared - every other one whose init retry is waiting on its timer
|
|
1095
|
+
* is tried at once (rfd and hmipserver come up close together on a booting system).
|
|
1096
|
+
* @param {string} iface the one that appeared
|
|
1097
|
+
*/
|
|
1098
|
+
retryMissingNow(iface) {
|
|
1099
|
+
Object.keys(this.initRetry).forEach((other) => {
|
|
1100
|
+
if (other !== iface) {
|
|
1101
|
+
this.initRetry[other].poke();
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1039
1104
|
}
|
|
1040
1105
|
|
|
1041
1106
|
/**
|
|
@@ -1266,6 +1331,7 @@ module.exports = function (RED) {
|
|
|
1266
1331
|
this.logger.debug('ccu-connection destructor');
|
|
1267
1332
|
this.closing = true;
|
|
1268
1333
|
this.stopInitRetries();
|
|
1334
|
+
this.stopLite();
|
|
1269
1335
|
this.stats(false);
|
|
1270
1336
|
|
|
1271
1337
|
this.logger.debug('clear regaPollTimeout');
|
|
@@ -1396,9 +1462,10 @@ module.exports = function (RED) {
|
|
|
1396
1462
|
/**
|
|
1397
1463
|
* B-28: an inconclusive detection (the box busy or still starting) used
|
|
1398
1464
|
* to leave the connection in ReGa mode for good - the only re-detection
|
|
1399
|
-
* ran from rare error paths, at most every five minutes. Ask again
|
|
1400
|
-
*
|
|
1401
|
-
* after that the five-minute rule of
|
|
1465
|
+
* ran from rare error paths, at most every five minutes. Ask again on the
|
|
1466
|
+
* init retry's schedule (every 3 s for 90 s, then every 15 s - task 17), up
|
|
1467
|
+
* to META_REDETECT_ATTEMPTS times; after that the five-minute rule of
|
|
1468
|
+
* recheckMeta() applies.
|
|
1402
1469
|
* @param {{inconclusive: boolean, reason?: string}} outcome
|
|
1403
1470
|
*/
|
|
1404
1471
|
scheduleMetaRedetect(outcome) {
|
|
@@ -1420,7 +1487,7 @@ module.exports = function (RED) {
|
|
|
1420
1487
|
return;
|
|
1421
1488
|
}
|
|
1422
1489
|
|
|
1423
|
-
const delay = retryDelay(this.metaRedetectAttempt);
|
|
1490
|
+
const delay = retryDelay((this.metaRedetectAttempt - 1) * RETRY_INTERVAL);
|
|
1424
1491
|
const line =
|
|
1425
1492
|
'meta api detection inconclusive (' +
|
|
1426
1493
|
outcome.reason +
|
|
@@ -1487,6 +1554,21 @@ module.exports = function (RED) {
|
|
|
1487
1554
|
this.closeClient('ReGaHSS');
|
|
1488
1555
|
}
|
|
1489
1556
|
|
|
1557
|
+
// task 16: the metadata stream's state is the Metadata pseudo-interface's, not the
|
|
1558
|
+
// ReGaHSS slot's - the system has none, and "Interface ReGaHSS connected" misled
|
|
1559
|
+
this.ifaceTypes[META_IFACE] = {
|
|
1560
|
+
conf: 'meta',
|
|
1561
|
+
pseudo: true,
|
|
1562
|
+
init: false,
|
|
1563
|
+
ping: false,
|
|
1564
|
+
protocol: this.tlsEnabled ? 'https' : 'http',
|
|
1565
|
+
port: this.metaPort,
|
|
1566
|
+
enabled: true,
|
|
1567
|
+
};
|
|
1568
|
+
delete this.ifaceStatus.ReGaHSS;
|
|
1569
|
+
delete this.ifaceWaiting.ReGaHSS;
|
|
1570
|
+
this.ifaceStatus[META_IFACE] = null;
|
|
1571
|
+
|
|
1490
1572
|
this.logger.info(
|
|
1491
1573
|
'openccu-lite detected (' +
|
|
1492
1574
|
(info.implementation || 'meta api') +
|
|
@@ -1529,12 +1611,29 @@ module.exports = function (RED) {
|
|
|
1529
1611
|
resolve();
|
|
1530
1612
|
},
|
|
1531
1613
|
onStatus: (connected) => {
|
|
1532
|
-
this.setIfaceStatus(
|
|
1614
|
+
this.setIfaceStatus(META_IFACE, connected);
|
|
1533
1615
|
},
|
|
1534
1616
|
onGone: () => {
|
|
1535
1617
|
this.metaMode = false;
|
|
1536
1618
|
this.meta = null;
|
|
1537
1619
|
this.lastMetaProbe = 0;
|
|
1620
|
+
if (this.lite) {
|
|
1621
|
+
// task 14: lite-rpc went with the system's answer; the CCU way needs a deploy
|
|
1622
|
+
this.stopLite();
|
|
1623
|
+
this.liteIfaces.forEach((iface) => this.setIfaceStatus(iface, false));
|
|
1624
|
+
this.logger.warn(
|
|
1625
|
+
'lite-rpc closed: the system no longer answers as openccu-lite - deploy again to reach the interfaces the CCU way',
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// task 16: the ReGaHSS slot is the interface again
|
|
1630
|
+
delete this.ifaceTypes[META_IFACE];
|
|
1631
|
+
delete this.ifaceStatus[META_IFACE];
|
|
1632
|
+
if (this.ifaceTypes.ReGaHSS.enabled) {
|
|
1633
|
+
this.ifaceStatus.ReGaHSS = null;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
this.pushStatus();
|
|
1538
1637
|
this.logger.warn('this box no longer answers as openccu-lite - detecting again');
|
|
1539
1638
|
this.detectMetaOutcome().then((outcome) => {
|
|
1540
1639
|
if (outcome.info) {
|
|
@@ -2217,9 +2316,16 @@ module.exports = function (RED) {
|
|
|
2217
2316
|
* @param config
|
|
2218
2317
|
*/
|
|
2219
2318
|
initIfaces(config) {
|
|
2319
|
+
// task 14: the interfaces that go through lite-rpc get no client and no init of their own
|
|
2320
|
+
const liteIfaces = this.liteInterfaces(config);
|
|
2321
|
+
this.liteIfaces = liteIfaces;
|
|
2220
2322
|
Object.keys(this.ifaceTypes).forEach((iface) => {
|
|
2221
2323
|
const enabled = config[this.ifaceTypes[iface].conf + 'Enabled'];
|
|
2222
2324
|
this.ifaceTypes[iface].enabled = enabled;
|
|
2325
|
+
if (enabled && liteIfaces.includes(iface)) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2223
2329
|
if (enabled && iface === 'ReGaHSS' && this.metaMode) {
|
|
2224
2330
|
// B-27: openccu-lite has no ReGaHSS, nothing listens on 31999.
|
|
2225
2331
|
// A client would only reconnect forever (with binrpc < 4.3.0
|
|
@@ -2249,6 +2355,249 @@ module.exports = function (RED) {
|
|
|
2249
2355
|
}
|
|
2250
2356
|
});
|
|
2251
2357
|
this.logger.info('Interfaces:', this.enabledIfaces.join(', '));
|
|
2358
|
+
if (liteIfaces.length > 0) {
|
|
2359
|
+
this.startLite(liteIfaces);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
/**
|
|
2364
|
+
* task 14: which of the enabled interfaces go through the system's lite-rpc - the ones
|
|
2365
|
+
* lite-rpc carries (not CUxD, not CCU-Jack), on an openccu-lite system, with a credential,
|
|
2366
|
+
* unless the connection type says `ccu`.
|
|
2367
|
+
* @param {object} config the node's config
|
|
2368
|
+
* @returns {string[]}
|
|
2369
|
+
*/
|
|
2370
|
+
liteInterfaces(config) {
|
|
2371
|
+
const enabled = lite.LITE_INTERFACES.filter((iface) => config[this.ifaceTypes[iface].conf + 'Enabled']);
|
|
2372
|
+
if (this.connectionType === 'ccu') {
|
|
2373
|
+
if (this.metaMode) {
|
|
2374
|
+
this.logger.info(
|
|
2375
|
+
'openccu-lite: the direct connection with callbacks by choice (connection type CCU) - the interfaces are reached on their own ports',
|
|
2376
|
+
);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
return [];
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
if (!this.metaMode) {
|
|
2383
|
+
if (this.connectionType === 'openccu-lite') {
|
|
2384
|
+
this.logger.warn(
|
|
2385
|
+
'connection type openccu-lite, but the system did not answer as openccu-lite - the interfaces are reached the CCU way until it does',
|
|
2386
|
+
);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
return [];
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
if (!this.liteToken) {
|
|
2393
|
+
this.logger.info(
|
|
2394
|
+
'openccu-lite without a credential for lite-rpc (no token in the connection node, no addon token in ' +
|
|
2395
|
+
lite.ADDON_TOKEN_FILE +
|
|
2396
|
+
') - the interfaces are reached directly, with callbacks',
|
|
2397
|
+
);
|
|
2398
|
+
return [];
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
if (enabled.length === 0) {
|
|
2402
|
+
return [];
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
return enabled;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
/**
|
|
2409
|
+
* task 14: the init id the callback table expects for an interface - the same shape rpcInit
|
|
2410
|
+
* registers, so getIfaceFromIdInit() recognises the stream's calls.
|
|
2411
|
+
* @param {string} iface
|
|
2412
|
+
* @returns {string}
|
|
2413
|
+
*/
|
|
2414
|
+
liteInitId(iface) {
|
|
2415
|
+
const hash = base62(crypto.createHash('sha1').update(this.initUrl(iface)).digest()).slice(0, 6);
|
|
2416
|
+
return 'nr_' + hash + '_' + iface;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
/**
|
|
2420
|
+
* task 14: the interfaces through the system's lite-rpc with occulite-client - one client,
|
|
2421
|
+
* the stream's events into the same rpcMethods table the callback servers feed, the
|
|
2422
|
+
* requests through methodCall. The one-time move of an `auto` connection is said once
|
|
2423
|
+
* (the marker file); `ccu` in the connection node is never moved.
|
|
2424
|
+
* @param {string[]} ifaces
|
|
2425
|
+
*/
|
|
2426
|
+
startLite(ifaces) {
|
|
2427
|
+
if (this.connectionType === 'auto') {
|
|
2428
|
+
let marker = null;
|
|
2429
|
+
try {
|
|
2430
|
+
marker = JSON.parse(fs.readFileSync(this.liteMarkerFile, 'utf8'));
|
|
2431
|
+
} catch {
|
|
2432
|
+
marker = null;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
if (!marker) {
|
|
2436
|
+
this.logger.info(
|
|
2437
|
+
'openccu-lite: ' +
|
|
2438
|
+
ifaces.join(', ') +
|
|
2439
|
+
" go through the system's lite-rpc from now on, the default on openccu-lite - moved once; connection type CCU in the connection node keeps the direct connection with callbacks, and is never changed again",
|
|
2440
|
+
);
|
|
2441
|
+
try {
|
|
2442
|
+
fs.writeFileSync(
|
|
2443
|
+
this.liteMarkerFile,
|
|
2444
|
+
JSON.stringify({migrated: new Date().toISOString(), host: this.host}),
|
|
2445
|
+
);
|
|
2446
|
+
} catch (error) {
|
|
2447
|
+
this.logger.warn('cannot write ' + this.liteMarkerFile + ': ' + error.message);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
const url = (this.tlsEnabled ? 'https' : 'http') + '://' + this.host + ':' + this.metaPort;
|
|
2453
|
+
const running = (iface) => this.lite && this.lite.running(iface) !== false;
|
|
2454
|
+
this.lite = new lite.LiteConnection({
|
|
2455
|
+
url,
|
|
2456
|
+
token: this.liteToken,
|
|
2457
|
+
insecure: Boolean(this.inSecure),
|
|
2458
|
+
interfaces: ifaces,
|
|
2459
|
+
initId: (iface) => this.liteInitId(iface),
|
|
2460
|
+
logger: this.logger,
|
|
2461
|
+
createClient: this.liteCreateClient,
|
|
2462
|
+
onCallback: (method, err, parameters, callback) => {
|
|
2463
|
+
if (err) {
|
|
2464
|
+
this.logger.error('rpc <', 'lite', method, err);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
this.logger.debug('rpc <', 'lite', method, JSON.stringify(parameters));
|
|
2468
|
+
const handler = method === 'event' ? 'eventSingle' : method;
|
|
2469
|
+
if (isIterable(parameters)) {
|
|
2470
|
+
this.callRpcMethod(handler, err, parameters, callback);
|
|
2471
|
+
} else {
|
|
2472
|
+
this.logger.error(
|
|
2473
|
+
'rpc <',
|
|
2474
|
+
'lite',
|
|
2475
|
+
'method',
|
|
2476
|
+
method,
|
|
2477
|
+
'params not iterable',
|
|
2478
|
+
JSON.stringify(parameters),
|
|
2479
|
+
);
|
|
2480
|
+
callback(null, this.rpcDefaultAnswer(method));
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
onInterface: (iface, state) => {
|
|
2484
|
+
if (!ifaces.includes(iface) || this.closing) {
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
this.logger.debug('lite-rpc interface', iface, state);
|
|
2489
|
+
if (state === 'up' || state === 'added' || state === 'restarted') {
|
|
2490
|
+
this.lastEvent[iface] = now();
|
|
2491
|
+
this.setIfaceStatus(iface, true);
|
|
2492
|
+
if (state === 'restarted' || state === 'added') {
|
|
2493
|
+
this.getDevices(iface).catch(() => {});
|
|
2494
|
+
}
|
|
2495
|
+
} else {
|
|
2496
|
+
this.setIfaceStatus(iface, false, true);
|
|
2497
|
+
}
|
|
2498
|
+
},
|
|
2499
|
+
onStatus: (state) => {
|
|
2500
|
+
if (this.closing) {
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
this.logger.debug('lite-rpc client', state);
|
|
2505
|
+
if (state === 'ready') {
|
|
2506
|
+
ifaces.forEach((iface) => this.setIfaceStatus(iface, running(iface), !running(iface)));
|
|
2507
|
+
} else if (state === 'degraded' || state === 'offline' || state === 'connecting') {
|
|
2508
|
+
ifaces.forEach((iface) => this.setIfaceStatus(iface, false, true));
|
|
2509
|
+
} else if (state === 'closed') {
|
|
2510
|
+
ifaces.forEach((iface) => this.setIfaceStatus(iface, false));
|
|
2511
|
+
}
|
|
2512
|
+
},
|
|
2513
|
+
onValues: (values) => this.applyLiteValues(values),
|
|
2514
|
+
});
|
|
2515
|
+
this.logger.info(
|
|
2516
|
+
'lite-rpc: ' + ifaces.join(', ') + ' through ' + url + ' (occulite-client), no callback server',
|
|
2517
|
+
);
|
|
2518
|
+
ifaces.forEach((iface) => this.setIfaceStatus(iface, false, true));
|
|
2519
|
+
this.lite
|
|
2520
|
+
.start()
|
|
2521
|
+
.then(() => {
|
|
2522
|
+
if (this.closing || !this.lite) {
|
|
2523
|
+
return;
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const up = ifaces.filter((iface) => running(iface));
|
|
2527
|
+
this.logger.info('lite-rpc connected: ' + (up.join(', ') || 'no interface running'));
|
|
2528
|
+
return Promise.all(
|
|
2529
|
+
up.map((iface) =>
|
|
2530
|
+
this.getDevices(iface)
|
|
2531
|
+
.then(() => {
|
|
2532
|
+
if (['BidCos-RF', 'BidCos-Wired', 'HmIP-RF'].includes(iface)) {
|
|
2533
|
+
return this.methodCall(iface, 'getLinks', [], {quiet: true})
|
|
2534
|
+
.then((res) => {
|
|
2535
|
+
this.links[iface] = res;
|
|
2536
|
+
})
|
|
2537
|
+
.catch(() => {});
|
|
2538
|
+
}
|
|
2539
|
+
})
|
|
2540
|
+
.catch((error) => {
|
|
2541
|
+
this.logger.warn('lite-rpc ' + iface + ' listDevices: ' + error.message);
|
|
2542
|
+
}),
|
|
2543
|
+
),
|
|
2544
|
+
);
|
|
2545
|
+
})
|
|
2546
|
+
.catch((error) => {
|
|
2547
|
+
if (this.closing) {
|
|
2548
|
+
return;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
this.logger.warn('lite-rpc: ' + (error.message || error) + ' - the client keeps trying');
|
|
2552
|
+
ifaces.forEach((iface) => this.setIfaceStatus(iface, false, true));
|
|
2553
|
+
});
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2556
|
+
/**
|
|
2557
|
+
* task 14: the state store's picture into the values cache at connect (what a CCU's ReGa
|
|
2558
|
+
* getValues gave): the nodes with "cache" get a value at deploy on openccu-lite as well.
|
|
2559
|
+
* @param {Map<string, {value: *, ts: number, lc?: number, confirmed: boolean}>} values box.values
|
|
2560
|
+
*/
|
|
2561
|
+
applyLiteValues(values) {
|
|
2562
|
+
if (!values || typeof values.forEach !== 'function') {
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
let count = 0;
|
|
2567
|
+
values.forEach((entry, ref) => {
|
|
2568
|
+
const [iface, channel, datapoint] = String(ref).split('.');
|
|
2569
|
+
if (!this.liteIfaces.includes(iface) || !channel || !datapoint) {
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
const ts = entry.ts || 0;
|
|
2574
|
+
const message = this.createMessage(iface, channel, datapoint, entry.value, {
|
|
2575
|
+
cache: true,
|
|
2576
|
+
change: false,
|
|
2577
|
+
working: false,
|
|
2578
|
+
uncertain: entry.confirmed === false,
|
|
2579
|
+
ts,
|
|
2580
|
+
lc: entry.lc || ts,
|
|
2581
|
+
});
|
|
2582
|
+
this.values[message.datapointName] = message;
|
|
2583
|
+
count += 1;
|
|
2584
|
+
if (!datapoint.startsWith('PRESS_')) {
|
|
2585
|
+
this.callCallbacks(message);
|
|
2586
|
+
}
|
|
2587
|
+
});
|
|
2588
|
+
this.logger.debug('lite-rpc: ' + count + ' values from the state store');
|
|
2589
|
+
this.cachedValuesReceived = true;
|
|
2590
|
+
this.saveValues();
|
|
2591
|
+
this.scheduleValuesPrune();
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
/** task 14: close the lite-rpc client, if any */
|
|
2595
|
+
stopLite() {
|
|
2596
|
+
if (this.lite) {
|
|
2597
|
+
this.logger.debug('lite-rpc client closing');
|
|
2598
|
+
this.lite.close();
|
|
2599
|
+
this.lite = null;
|
|
2600
|
+
}
|
|
2252
2601
|
}
|
|
2253
2602
|
|
|
2254
2603
|
/**
|
|
@@ -4141,6 +4490,31 @@ module.exports = function (RED) {
|
|
|
4141
4490
|
*/
|
|
4142
4491
|
methodCall(iface, method, parameters, {quiet = false} = {}) {
|
|
4143
4492
|
return new Promise((resolve, reject) => {
|
|
4493
|
+
if (this.lite && this.lite.has(iface)) {
|
|
4494
|
+
// task 14: through the system's lite-rpc; a fault keeps its code, a scope the
|
|
4495
|
+
// token lacks is said as such - the interface stays connected either way
|
|
4496
|
+
this.logger.debug('rpc >', iface, method, JSON.stringify(parameters));
|
|
4497
|
+
this.lite.call(iface, method, parameters).then(
|
|
4498
|
+
(res) => {
|
|
4499
|
+
this.logger.debug(' <', iface, method, JSON.stringify(res));
|
|
4500
|
+
resolve(res);
|
|
4501
|
+
},
|
|
4502
|
+
(error) => {
|
|
4503
|
+
this.logger[quiet ? 'debug' : 'error'](
|
|
4504
|
+
' <',
|
|
4505
|
+
iface,
|
|
4506
|
+
method,
|
|
4507
|
+
JSON.stringify(parameters),
|
|
4508
|
+
error.faultCode === undefined
|
|
4509
|
+
? error.message
|
|
4510
|
+
: 'fault ' + error.faultCode + ' ' + error.faultString,
|
|
4511
|
+
);
|
|
4512
|
+
reject(error);
|
|
4513
|
+
},
|
|
4514
|
+
);
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4144
4518
|
if (this.clients[iface]) {
|
|
4145
4519
|
this.logger.debug('rpc >', iface, method, JSON.stringify(parameters));
|
|
4146
4520
|
this.clients[iface].methodCall(method, parameters, (err, res) => {
|
package/nodes/lib/initretry.js
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
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
|
-
*
|
|
8
|
+
* own retry (task 12) until it succeeds or the node is closed: every 3 s for
|
|
9
|
+
* the first 90 s after the interface was first missed (the start, or the
|
|
10
|
+
* moment a connected process went away), then every 15 s (task 17 - the
|
|
11
|
+
* 1/2/4/8/15 s steps of before hit hmipserver's ready moment, ~50-55 s after
|
|
12
|
+
* an openccu-lite boot, only at their 61 s tick). When one interface appears
|
|
13
|
+
* the others that are waiting are tried at once (poke): rfd and hmipserver
|
|
14
|
+
* come up close together.
|
|
10
15
|
*
|
|
11
16
|
* While the process is unreachable the interface is "waiting": one line, then
|
|
12
17
|
* debug. Before the interface was ever connected, a refused connection is
|
|
@@ -17,20 +22,22 @@
|
|
|
17
22
|
* distinct fault.
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
|
-
const
|
|
25
|
+
const RETRY_INTERVAL = 3000;
|
|
26
|
+
const RETRY_WINDOW = 90000;
|
|
21
27
|
const RETRY_MAX_DELAY = 15000;
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
|
-
* The delay before the next attempt.
|
|
25
|
-
* @param {number}
|
|
26
|
-
* @returns {number} milliseconds
|
|
30
|
+
* The delay before the next attempt (task 17).
|
|
31
|
+
* @param {number} elapsed milliseconds since the interface was first missed
|
|
32
|
+
* @returns {number} milliseconds: 3 s within the first 90 s, then 15 s
|
|
27
33
|
*/
|
|
28
|
-
function retryDelay(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
34
|
+
function retryDelay(elapsed) {
|
|
35
|
+
return elapsed < RETRY_WINDOW ? RETRY_INTERVAL : RETRY_MAX_DELAY;
|
|
36
|
+
}
|
|
32
37
|
|
|
33
|
-
|
|
38
|
+
/** @param {number} ms @returns {string} seconds with one decimal, e.g. "54.0 s" */
|
|
39
|
+
function seconds(ms) {
|
|
40
|
+
return (Math.max(0, ms) / 1000).toFixed(1) + ' s';
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
/**
|
|
@@ -92,14 +99,19 @@ class InitRetry {
|
|
|
92
99
|
* @param {object} options.logger {debug, info, warn, error}
|
|
93
100
|
* @param {function(string)} options.onState 'waiting' | 'failed' | 'connected'
|
|
94
101
|
* @param {object} [options.timers] {setTimeout, clearTimeout}, for tests
|
|
102
|
+
* @param {function(): number} [options.now] the clock, for tests
|
|
103
|
+
* @param {number} [options.startedAt] when the program (the connection node) started, for the log line
|
|
95
104
|
*/
|
|
96
|
-
constructor({iface, attempt, logger, onState, timers}) {
|
|
105
|
+
constructor({iface, attempt, logger, onState, timers, now, startedAt}) {
|
|
97
106
|
this.iface = iface;
|
|
98
107
|
this.attempt = attempt;
|
|
99
108
|
this.logger = logger;
|
|
100
109
|
this.onState = onState || (() => {});
|
|
101
110
|
this.timers = timers || {setTimeout, clearTimeout};
|
|
111
|
+
this.now = now || Date.now;
|
|
112
|
+
this.startedAt = startedAt === undefined ? this.now() : startedAt;
|
|
102
113
|
this.timer = null;
|
|
114
|
+
this.running = false;
|
|
103
115
|
this.stopped = false;
|
|
104
116
|
this.everConnected = false;
|
|
105
117
|
this.reset();
|
|
@@ -109,6 +121,8 @@ class InitRetry {
|
|
|
109
121
|
this.failures = 0;
|
|
110
122
|
this.warned = false;
|
|
111
123
|
this.faults = new Set();
|
|
124
|
+
// task 17: when the process was first missed - the 90 s of 3 s steps count from here
|
|
125
|
+
this.missedAt = null;
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
/** true while a retry is scheduled */
|
|
@@ -125,8 +139,12 @@ class InitRetry {
|
|
|
125
139
|
return;
|
|
126
140
|
}
|
|
127
141
|
|
|
142
|
+
if (this.failures === 0) {
|
|
143
|
+
this.missedAt = this.now();
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
this.failures += 1;
|
|
129
|
-
const delay = retryDelay(this.
|
|
147
|
+
const delay = retryDelay(this.now() - this.missedAt);
|
|
130
148
|
const seconds = delay / 1000;
|
|
131
149
|
const reason = describeError(error);
|
|
132
150
|
if (isFault(error)) {
|
|
@@ -177,14 +195,38 @@ class InitRetry {
|
|
|
177
195
|
return Promise.resolve();
|
|
178
196
|
}
|
|
179
197
|
|
|
198
|
+
this.running = true;
|
|
180
199
|
return Promise.resolve()
|
|
181
200
|
.then(() => this.attempt())
|
|
182
201
|
.then(
|
|
183
|
-
() =>
|
|
184
|
-
|
|
202
|
+
() => {
|
|
203
|
+
this.running = false;
|
|
204
|
+
this.succeeded();
|
|
205
|
+
},
|
|
206
|
+
(error) => {
|
|
207
|
+
this.running = false;
|
|
208
|
+
this.failed(error);
|
|
209
|
+
},
|
|
185
210
|
);
|
|
186
211
|
}
|
|
187
212
|
|
|
213
|
+
/**
|
|
214
|
+
* task 17: another interface just appeared - try this one now instead of at its timer
|
|
215
|
+
* (rfd and hmipserver come up close together). Nothing while an attempt is running or
|
|
216
|
+
* none is scheduled.
|
|
217
|
+
* @returns {boolean} whether an attempt was started
|
|
218
|
+
*/
|
|
219
|
+
poke() {
|
|
220
|
+
if (this.stopped || this.running || this.timer === null) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
this.clear();
|
|
225
|
+
this.logger.debug(this.iface + ': another interface appeared, trying now');
|
|
226
|
+
this.run();
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
188
230
|
/** an init attempt succeeded */
|
|
189
231
|
succeeded() {
|
|
190
232
|
if (this.stopped) {
|
|
@@ -193,7 +235,11 @@ class InitRetry {
|
|
|
193
235
|
|
|
194
236
|
this.clear();
|
|
195
237
|
if (this.failures > 0) {
|
|
196
|
-
|
|
238
|
+
// task 17: one line with the time, so a log shows how close the start got to the process's ready moment
|
|
239
|
+
const since = this.everConnected
|
|
240
|
+
? seconds(this.now() - this.missedAt) + ' after it went missing'
|
|
241
|
+
: seconds(this.now() - this.startedAt) + ' after the start';
|
|
242
|
+
this.logger.info(this.iface + ' connected after ' + (this.failures + 1) + ' attempts, ' + since);
|
|
197
243
|
}
|
|
198
244
|
|
|
199
245
|
this.everConnected = true;
|
|
@@ -215,4 +261,13 @@ class InitRetry {
|
|
|
215
261
|
}
|
|
216
262
|
}
|
|
217
263
|
|
|
218
|
-
module.exports = {
|
|
264
|
+
module.exports = {
|
|
265
|
+
InitRetry,
|
|
266
|
+
retryDelay,
|
|
267
|
+
isFault,
|
|
268
|
+
isRefused,
|
|
269
|
+
describeError,
|
|
270
|
+
RETRY_INTERVAL,
|
|
271
|
+
RETRY_WINDOW,
|
|
272
|
+
RETRY_MAX_DELAY,
|
|
273
|
+
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* task 14: the interfaces of an openccu-lite system through its lite-rpc, with occulite-client,
|
|
3
|
+
* instead of XML-RPC/BIN-RPC clients and callback servers of our own.
|
|
4
|
+
*
|
|
5
|
+
* One client per connection node: the requests go to /api/rpc/v1 with the credential, the events
|
|
6
|
+
* come from the stream the client opens (resumed after a break, re-read after a restart of the
|
|
7
|
+
* system), and `callbackEvents` turns them into the very calls a callback server would make -
|
|
8
|
+
* event, system.multicall, newDevices, deleteDevices, updateDevice, replaceDevice, readdedDevice
|
|
9
|
+
* with the same argument lists - so the connection's rpcMethods table feeds from it unchanged.
|
|
10
|
+
* No init, no ping, no callback ports for these interfaces. CUxD and CCU-Jack are not carried by
|
|
11
|
+
* lite-rpc and keep their own clients.
|
|
12
|
+
*
|
|
13
|
+
* The credential: the openccu-lite token of the connection node, else the addon's own token the
|
|
14
|
+
* system mints for RedMatic from its manifest (/run/occulite/addon-tokens/redmatic.api). The local
|
|
15
|
+
* token (meta:read alone) is no credential for lite-rpc.
|
|
16
|
+
*
|
|
17
|
+
* occulite-client is ESM; it is loaded here, lazily, so a CCU user never loads it and a Node too
|
|
18
|
+
* old for require(esm) gets a message instead of a crash (B-32's lesson).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
|
|
23
|
+
const LITE_INTERFACES = ['BidCos-RF', 'BidCos-Wired', 'HmIP-RF', 'VirtualDevices'];
|
|
24
|
+
const ADDON_TOKEN_FILE = '/run/occulite/addon-tokens/redmatic.api';
|
|
25
|
+
|
|
26
|
+
/** the stream's events as the callback server's methods, see callbackEvents() */
|
|
27
|
+
const CALLBACK_METHODS = [
|
|
28
|
+
'event',
|
|
29
|
+
'system.multicall',
|
|
30
|
+
'newDevices',
|
|
31
|
+
'deleteDevices',
|
|
32
|
+
'updateDevice',
|
|
33
|
+
'replaceDevice',
|
|
34
|
+
'readdedDevice',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
let modules = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* occulite-client, loaded once. Throws with a readable message where it cannot be loaded.
|
|
41
|
+
* @returns {{OccuLite: Function, callbackEvents: Function, nodeTransport: Function}}
|
|
42
|
+
*/
|
|
43
|
+
function load() {
|
|
44
|
+
if (!modules) {
|
|
45
|
+
try {
|
|
46
|
+
const {OccuLite} = require('occulite-client');
|
|
47
|
+
const {callbackEvents} = require('occulite-client/callbacks');
|
|
48
|
+
const {nodeTransport} = require('occulite-client/node');
|
|
49
|
+
modules = {OccuLite, callbackEvents, nodeTransport};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
'occulite-client cannot be loaded (' +
|
|
53
|
+
(error.message || error) +
|
|
54
|
+
'): lite-rpc needs Node.js 20.19, 22.12 or newer; the direct connection is used instead',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return modules;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The addon's own token on an openccu-lite system (RedMatic's, minted from its manifest), or
|
|
64
|
+
* undefined where there is none.
|
|
65
|
+
* @param {string} [file] the token file, for tests
|
|
66
|
+
* @returns {string|undefined}
|
|
67
|
+
*/
|
|
68
|
+
function readAddonToken(file = module.exports.ADDON_TOKEN_FILE) {
|
|
69
|
+
try {
|
|
70
|
+
const token = fs.readFileSync(file, 'utf8').trim();
|
|
71
|
+
return token === '' ? undefined : token;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Whether an error of the client is an RPC fault of the interface process (the process answered).
|
|
79
|
+
* @param {*} error
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function isFault(error) {
|
|
83
|
+
return Boolean(error) && error.kind === 'rpc-fault' && error.faultCode !== undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The client's error as the connection reports it: a fault keeps its code and string, a refused
|
|
88
|
+
* scope says which, everything else its kind.
|
|
89
|
+
* @param {*} error
|
|
90
|
+
* @returns {Error}
|
|
91
|
+
*/
|
|
92
|
+
function translateError(error) {
|
|
93
|
+
if (isFault(error)) {
|
|
94
|
+
const fault = new Error(error.faultString || error.message);
|
|
95
|
+
fault.faultCode = error.faultCode;
|
|
96
|
+
fault.faultString = error.faultString || error.message;
|
|
97
|
+
return fault;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error && error.kind === 'forbidden') {
|
|
101
|
+
const refused = new Error(
|
|
102
|
+
'the openccu-lite token may not call this method' + (error.scope ? ' (needs ' + error.scope + ')' : ''),
|
|
103
|
+
);
|
|
104
|
+
refused.kind = 'forbidden';
|
|
105
|
+
refused.scope = error.scope;
|
|
106
|
+
return refused;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (error && error.kind) {
|
|
110
|
+
const translated = new Error(error.message || error.kind);
|
|
111
|
+
translated.kind = error.kind;
|
|
112
|
+
return translated;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const RPC_TIERS = ['rpc:operate', 'rpc:configure', 'rpc:admin'];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* occulite-client B-3: the client decides "names only" from the literal `rpc:read` in
|
|
122
|
+
* GET /api/auth/v1/state, but a token made with a higher tier (rpc:admin, say) carries that tier
|
|
123
|
+
* alone - the system includes the lower ones. The transport spells `rpc:read` out for the client
|
|
124
|
+
* where a tier implies it. Goes once the package fixes it.
|
|
125
|
+
* @param {object} transport occulite-client's Transport
|
|
126
|
+
* @returns {object} the same transport
|
|
127
|
+
*/
|
|
128
|
+
function tierAwareTransport(transport) {
|
|
129
|
+
return {
|
|
130
|
+
request: (method, url, headers, body, signal) =>
|
|
131
|
+
transport.request(method, url, headers, body, signal).then((response) => {
|
|
132
|
+
if (response.status !== 200 || !/\/api\/auth\/v1\/state(\?|$)/.test(String(url))) {
|
|
133
|
+
return response;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let parsed;
|
|
137
|
+
try {
|
|
138
|
+
parsed = JSON.parse(response.body);
|
|
139
|
+
} catch {
|
|
140
|
+
return response;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const scopes = Array.isArray(parsed && parsed.scopes) ? parsed.scopes : [];
|
|
144
|
+
if (scopes.includes('rpc:read') || !scopes.some((scope) => RPC_TIERS.includes(scope))) {
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {...response, body: JSON.stringify({...parsed, scopes: [...scopes, 'rpc:read']})};
|
|
149
|
+
}),
|
|
150
|
+
stream: (url, headers, signal) => transport.stream(url, headers, signal),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
class LiteConnection {
|
|
155
|
+
/**
|
|
156
|
+
* @param {object} options
|
|
157
|
+
* @param {string} options.url the system, scheme and authority
|
|
158
|
+
* @param {string} options.token the credential
|
|
159
|
+
* @param {boolean} [options.insecure] accept any certificate
|
|
160
|
+
* @param {string[]} options.interfaces the interfaces to follow (the enabled ones lite-rpc carries)
|
|
161
|
+
* @param {function(string): string} options.initId the init id the connection's callback table expects per interface
|
|
162
|
+
* @param {function(string, *, Array, Function)} options.onCallback (method, err, params, answer): a callback server's call
|
|
163
|
+
* @param {function(string, string)} options.onInterface (iface, state): up | down | restarted | added | removed
|
|
164
|
+
* @param {function(string)} options.onStatus the client's state: connecting | ready | degraded | offline | closed
|
|
165
|
+
* @param {function(Map)} [options.onValues] box.values once ready (the state store's picture)
|
|
166
|
+
* @param {object} options.logger {debug, info, warn, error}
|
|
167
|
+
* @param {number} [options.timeout] per request, ms
|
|
168
|
+
* @param {Function} [options.createClient] for tests, in place of new OccuLite()
|
|
169
|
+
*/
|
|
170
|
+
constructor(options) {
|
|
171
|
+
this.options = options;
|
|
172
|
+
this.interfaces = [...options.interfaces];
|
|
173
|
+
this.logger = options.logger;
|
|
174
|
+
this.box = null;
|
|
175
|
+
this.server = null;
|
|
176
|
+
this.closed = false;
|
|
177
|
+
this.ready = false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {string} iface
|
|
182
|
+
* @returns {boolean} whether this interface goes through lite-rpc
|
|
183
|
+
*/
|
|
184
|
+
has(iface) {
|
|
185
|
+
return this.interfaces.includes(iface);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Opens the client. Resolves once the first picture is loaded, rejects with the client's
|
|
190
|
+
* reason (unauthenticated, forbidden, unreachable, timeout ...); the stream keeps trying by
|
|
191
|
+
* itself afterwards.
|
|
192
|
+
* @returns {Promise<void>}
|
|
193
|
+
*/
|
|
194
|
+
start() {
|
|
195
|
+
const {OccuLite, callbackEvents, nodeTransport} = load();
|
|
196
|
+
const debug = (message) => this.logger.debug('occulite-client: ' + message);
|
|
197
|
+
const clientOptions = {
|
|
198
|
+
url: this.options.url,
|
|
199
|
+
auth: {token: this.options.token},
|
|
200
|
+
transport: tierAwareTransport(nodeTransport(this.options.insecure ? {insecure: true} : {})),
|
|
201
|
+
interfaces: this.interfaces,
|
|
202
|
+
values: true,
|
|
203
|
+
meta: false,
|
|
204
|
+
devices: true,
|
|
205
|
+
timeout: this.options.timeout || 30000,
|
|
206
|
+
log: {debug, info: debug, warn: debug, error: debug},
|
|
207
|
+
};
|
|
208
|
+
const box = this.options.createClient ? this.options.createClient(clientOptions) : new OccuLite(clientOptions);
|
|
209
|
+
this.box = box;
|
|
210
|
+
box.on('status', (event) => {
|
|
211
|
+
if (this.closed || this.box !== box) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const state = event && event.state ? event.state : event;
|
|
216
|
+
this.ready = state === 'ready';
|
|
217
|
+
this.options.onStatus(state);
|
|
218
|
+
});
|
|
219
|
+
box.on('interface', (event) => {
|
|
220
|
+
if (!this.closed && this.box === box) {
|
|
221
|
+
this.options.onInterface(event.interface, event.state);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
box.on('resync', () => {
|
|
225
|
+
if (!this.closed && this.box === box && this.options.onValues) {
|
|
226
|
+
this.options.onValues(box.values);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
// the stream's events as a callback server's calls, into the connection's own table
|
|
230
|
+
this.server = callbackEvents(box, {initId: this.options.initId});
|
|
231
|
+
CALLBACK_METHODS.forEach((method) => {
|
|
232
|
+
this.server.on(method, (err, parameters, callback) => {
|
|
233
|
+
if (this.closed || this.box !== box) {
|
|
234
|
+
callback(null, '');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
this.options.onCallback(method, err, parameters, callback);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return box.ready().then(() => {
|
|
243
|
+
if (this.closed || this.box !== box) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
this.ready = true;
|
|
248
|
+
if (this.options.onValues) {
|
|
249
|
+
this.options.onValues(box.values);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Any method of an interface process through lite-rpc; the credential's tier decides.
|
|
256
|
+
* @param {string} iface
|
|
257
|
+
* @param {string} method
|
|
258
|
+
* @param {Array} parameters
|
|
259
|
+
* @returns {Promise<*>}
|
|
260
|
+
*/
|
|
261
|
+
call(iface, method, parameters) {
|
|
262
|
+
if (!this.box) {
|
|
263
|
+
return Promise.reject(new Error('lite-rpc client not started'));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return this.box.call(iface, method, parameters || []).then(
|
|
267
|
+
(result) => (result === null || result === undefined ? '' : result),
|
|
268
|
+
(error) => {
|
|
269
|
+
throw translateError(error);
|
|
270
|
+
},
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Whether the system says this interface runs (the client's live list).
|
|
276
|
+
* @param {string} iface
|
|
277
|
+
* @returns {boolean|undefined} undefined while the list is not known
|
|
278
|
+
*/
|
|
279
|
+
running(iface) {
|
|
280
|
+
if (!this.box || !Array.isArray(this.box.interfaces)) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const entry = this.box.interfaces.find((info) => info.name === iface);
|
|
285
|
+
return entry ? entry.running !== false : false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Closes the stream; nothing is de-registered, because nothing was registered. */
|
|
289
|
+
close() {
|
|
290
|
+
this.closed = true;
|
|
291
|
+
this.ready = false;
|
|
292
|
+
if (this.box) {
|
|
293
|
+
this.box.close();
|
|
294
|
+
this.box = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = {
|
|
300
|
+
LiteConnection,
|
|
301
|
+
LITE_INTERFACES,
|
|
302
|
+
ADDON_TOKEN_FILE,
|
|
303
|
+
CALLBACK_METHODS,
|
|
304
|
+
readAddonToken,
|
|
305
|
+
tierAwareTransport,
|
|
306
|
+
translateError,
|
|
307
|
+
isFault,
|
|
308
|
+
load,
|
|
309
|
+
};
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
<li>VirtualDevices: Heizungsgruppen, in CCU eingebundene Systeme wie Osram Lightify</li>
|
|
34
34
|
<li>CUxD</li>
|
|
35
35
|
<li>CCU-Jack: XMLRPC-Schnittstelle des <a href="https://github.com/mdzio/ccu-jack" target="_blank">CCU-Jack</a> Addons (Port konfigurierbar, Standard 2121 bzw. 2122 mit TLS)</li>
|
|
36
|
+
<li>Das Portfeld neben jeder Schnittstelle: leer ist der Port der CCU für den Transport (2001/42001 usw., die 3xxxx-Ports auf der CCU selbst); eine Zahl ersetzt ihn, etwa für eine CCU hinter einer Portweiterleitung oder einen Simulator auf einem anderen Port.</li>
|
|
36
37
|
</ul>
|
|
37
38
|
|
|
38
39
|
</li>
|
|
@@ -65,6 +66,7 @@ Weiteres unter <a target="_blank" href="https://nodered.org/docs/user-guide/cont
|
|
|
65
66
|
</ul>
|
|
66
67
|
<ul>
|
|
67
68
|
<li><b>openccu-lite Port</b>: Nur nötig, wenn der Webserver des Systems nicht auf Port 80 bzw. 443 (TLS) läuft.</li>
|
|
69
|
+
<li><b>openccu-lite Schnittstellen</b>: Wie die Schnittstellenprozesse auf einem openccu-lite-System erreicht werden. <i>auto</i> (Voreinstellung) geht dort über das <b>lite-rpc</b> des Systems - die Aufrufe über seinen Webport, die Ereignisse aus einem Stream, den dieser Node öffnet; ein Abonnent an den Prozessen für alle Programme auf dem System, die Ereignisse werden nach einer Unterbrechung nachgeliefert, kein eigener Callback-Server und kein eigenes <code>init</code> - und auf einer CCU, RaspberryMatic oder OpenCCU den CCU-Weg. Das Zugangsmerkmal ist das openccu-lite-Token oben, auf dem System selbst das eigene Token von RedMatic, das das System dafür erzeugt; ohne eines wird die direkte Verbindung benutzt, das Log sagt es. <i>CCU</i> behält auch auf openccu-lite die direkte Verbindung mit Rückrufen (CUxD geht immer so; lite-rpc trägt es nicht). Ein Flow aus einer Version vor 4.7.0 wird beim ersten Start auf openccu-lite einmal auf lite-rpc umgestellt - das Log sagt es - und eine hier getroffene Wahl ändert keine spätere Version mehr. Die Nodes sysvar, program, script und poll bleiben auf openccu-lite so oder so ohne Funktion.</li>
|
|
68
70
|
</ul>
|
|
69
71
|
<ul>
|
|
70
72
|
<li><b>Device data / Re-read device data</b>: Liest die Geräteliste jeder verbundenen Schnittstelle neu ein.
|
|
@@ -91,7 +93,7 @@ Weiteres unter <a target="_blank" href="https://nodered.org/docs/user-guide/cont
|
|
|
91
93
|
solange die CCU ihr selbst ausgestelltes Zertifikat verwendet.</li>
|
|
92
94
|
<li>Username: Ein Benutzer der sich an der CCU anmelden darf.</li>
|
|
93
95
|
<li>Password: Das Passwort des Benutzers. Es wird in den Node-RED-Credentials gespeichert
|
|
94
|
-
(verschlüsselt in <code>flows_cred.json</code>), nicht im Flow. Ein Flow aus einer Version vor 4.
|
|
96
|
+
(verschlüsselt in <code>flows_cred.json</code>), nicht im Flow. Ein Flow aus einer Version vor 4.5.0,
|
|
95
97
|
der es noch im Klartext enthält, wird beim ersten Start übernommen; der nächste Deploy entfernt es aus
|
|
96
98
|
<code>flows.json</code>.</li>
|
|
97
99
|
</ul>
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
<li>VirtualDevices: heating groups and systems the CCU integrates, such as Osram Lightify</li>
|
|
19
19
|
<li>CUxD</li>
|
|
20
20
|
<li>CCU-Jack: the XML-RPC interface of the <a href="https://github.com/mdzio/ccu-jack" target="_blank">CCU-Jack</a> addon (port configurable, 2121 by default, 2122 with TLS)</li>
|
|
21
|
+
<li>The port field beside each interface: blank is the CCU's port for the transport (2001/42001 and so on, the 3xxxx ports on the CCU itself); a number replaces it, for a CCU behind port forwarding or a simulator on another port.</li>
|
|
21
22
|
</ul>
|
|
22
23
|
</li>
|
|
23
24
|
<li><b>RPC ping</b> and <b>RPC ping timeout (s)</b>: How often the connection to the interface processes is checked with a ping, and after how many seconds without an answer it subscribes again. Off means no pings and no timeout-triggered re-init for this connection.</li>
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
<li><b>Context Store</b>: The Node-RED context store the states of all devices and variables are kept in. See <a target="_blank" href="https://nodered.org/docs/user-guide/context">Working with context</a> in the Node-RED documentation.</li>
|
|
28
29
|
<li><b>openccu-lite token</b>: Only for <a href="https://github.com/hobbyquaker/openccu-lite" target="_blank">openccu-lite</a>, a CCU firmware without ReGaHSS. Names, rooms and functions come from the system's metadata API there, which needs a token made under <i>Users</i> on the system. When Node-RED runs on the system itself (RedMatic) the field stays empty: the local token is read automatically. The token is kept, like the password, in Node-RED's credentials (encrypted in <code>flows_cred.json</code>, never in the flow, never in an export). A CCU, RaspberryMatic or OpenCCU ignores the field. openccu-lite has no system variables, programs or HM-Script; those nodes stay in the flow and report an error per message.</li>
|
|
29
30
|
<li><b>openccu-lite port</b>: Only needed when the system's web server is not on port 80 (443 with TLS).</li>
|
|
31
|
+
<li><b>openccu-lite interfaces</b>: How the interface processes are reached on an openccu-lite system. <i>auto</i> (the default) goes through the system's <b>lite-rpc</b> there - the requests over its web port, the events from a stream this node opens, one subscriber at the processes for every program on the system, the events replayed after a break, no callback server and no <code>init</code> of its own - and the CCU way on a CCU, RaspberryMatic or OpenCCU. The credential is the openccu-lite token above, or on the system itself RedMatic's own token, which the system mints for it; without one the direct connection is used and the log says so. <i>CCU</i> keeps the direct connection with callbacks on openccu-lite as well (CUxD always goes that way; lite-rpc does not carry it). A flow from before 4.7.0 is moved to lite-rpc once at its first start on openccu-lite - the log says so - and a choice made here is never changed by a later version. The sysvar, program, script and poll nodes stay unavailable on openccu-lite either way.</li>
|
|
30
32
|
<li><b>Device data / Re-read device data</b>: Reads the device list of every connected interface again. New and changed devices (a channel whose type changed, say) get their datapoints fetched anew, removed ones leave the table. A device the CCU reports with a changed type or version is re-read without the button as well. The button needs a deployed connection.</li>
|
|
31
33
|
<li><b>Authentication</b>: Encryption and login for a CCU on the network (Node-RED not on the CCU). Both belong to the CCU's web server, which fronts the interface processes: on a CCU3 TLS and authentication are switched on in the firewall settings, on openccu-lite under <i>Remote access</i> (classic RPC). When Node-RED runs on the system itself (RedMatic; address <code>localhost</code> / <code>127.0.0.1</code>) the processes are reached directly on the loopback, BidCos over BIN-RPC, without TLS and without a login; these settings are then not used.
|
|
32
34
|
<ul>
|
|
@@ -34,7 +36,7 @@
|
|
|
34
36
|
<li>TLS/SSL: Transport encryption. The interfaces are reached over the CCU's TLS ports (42000, 42001, 42010, 49292, ReGaHSS 48181, CCU-Jack 2122), openccu-lite's metadata API over 443. BIN-RPC has no TLS: with TLS on, BidCos-RF always goes over XML-RPC.</li>
|
|
35
37
|
<li>Ignore invalid TLS Certificates: Node-RED accepts an untrusted certificate too. Needed as long as the CCU uses its self-signed certificate.</li>
|
|
36
38
|
<li>Username: A user allowed to log in to the CCU.</li>
|
|
37
|
-
<li>Password: The user's password. It is kept in Node-RED's credentials (encrypted in <code>flows_cred.json</code>), not in the flow. A flow from a version before 4.
|
|
39
|
+
<li>Password: The user's password. It is kept in Node-RED's credentials (encrypted in <code>flows_cred.json</code>), not in the flow. A flow from a version before 4.5.0 that still carries it in plain text is taken over at the first start; the next deploy removes it from <code>flows.json</code>.</li>
|
|
38
40
|
</ul>
|
|
39
41
|
The other direction stays unencrypted: the CCU sends its events to the XML-RPC or BIN-RPC server Node-RED opens for it (<i>Listen Address</i>, <i>BINRPC</i> and <i>XMLRPC listening port</i>) without TLS and without a login - it cannot log in there or verify our certificate. Those ports should be open in the Node-RED host's firewall for the CCU only.</li>
|
|
40
42
|
</ul>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-ccu",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "none",
|
|
6
6
|
"keywords": [
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"binrpc": "^4.3.0",
|
|
54
54
|
"homematic-rega": "^2.0.1",
|
|
55
55
|
"homematic-xmlrpc": "^2.0.0",
|
|
56
|
-
"mqtt-wildcard": "^3.0.9"
|
|
56
|
+
"mqtt-wildcard": "^3.0.9",
|
|
57
|
+
"occulite-client": "^0.3.0"
|
|
57
58
|
},
|
|
58
59
|
"deprecated": false,
|
|
59
60
|
"description": "Node-RED Nodes for the Homematic CCU",
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"eslint-config-prettier": "^10.1.8",
|
|
65
66
|
"eslint-plugin-html": "^8.2.0",
|
|
66
67
|
"globals": "^16.5.0",
|
|
67
|
-
"hm-simulator": "^1.
|
|
68
|
+
"hm-simulator": "^1.4.0",
|
|
68
69
|
"mocha": "^11.7.1",
|
|
69
70
|
"node-red": "^4.1.0",
|
|
70
71
|
"node-red-node-test-helper": "^0.3.6",
|