node-red-contrib-ccu 4.6.1 → 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 +88 -31
- package/README.md +16 -2
- package/nodes/ccu-connection.html +28 -7
- package/nodes/ccu-connection.js +412 -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/nodes/ccu-connection.js
CHANGED
|
@@ -17,10 +17,15 @@ 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
|
|
|
25
|
+
/** how long the XML-RPC callback server keeps an idle keep-alive connection open: longer than
|
|
26
|
+
* hmipserver's HTTP client pool (1200 s), so the pool never reuses a connection this side closed */
|
|
27
|
+
const KEEP_ALIVE_TIMEOUT_MS = 1300 * 1000;
|
|
28
|
+
|
|
24
29
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
25
30
|
|
|
26
31
|
// B-37: how long after our init the process's listDevices/newDevices still belong to it -
|
|
@@ -51,8 +56,20 @@ function execToCallback(promise, callback) {
|
|
|
51
56
|
);
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
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;
|
|
56
73
|
|
|
57
74
|
/**
|
|
58
75
|
* task 9: has a device description changed in a way that changes its paramsets?
|
|
@@ -163,6 +180,11 @@ module.exports = function (RED) {
|
|
|
163
180
|
switch (request.query.type) {
|
|
164
181
|
case 'ifaces': {
|
|
165
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
|
+
|
|
166
188
|
object[iface] = {
|
|
167
189
|
enabled: Boolean(config.ifaceTypes[iface].enabled),
|
|
168
190
|
connected: Boolean(config.ifaceStatus[iface]),
|
|
@@ -508,6 +530,8 @@ module.exports = function (RED) {
|
|
|
508
530
|
const pass = auth ? config.password : undefined;
|
|
509
531
|
|
|
510
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
|
|
511
535
|
ReGaHSS: {
|
|
512
536
|
conf: 'rega',
|
|
513
537
|
rpc: this.isLocal ? binrpc : xmlrpc,
|
|
@@ -519,7 +543,7 @@ module.exports = function (RED) {
|
|
|
519
543
|
'BidCos-RF': {
|
|
520
544
|
conf: 'bcrf',
|
|
521
545
|
rpc: this.isLocal || transport.bcrfBinRpc ? binrpc : xmlrpc,
|
|
522
|
-
port: this.isLocal ? 32001 : tls ? 42001 : 2001,
|
|
546
|
+
port: Number(config.bcrfPort) || (this.isLocal ? 32001 : tls ? 42001 : 2001),
|
|
523
547
|
protocol: this.isLocal || transport.bcrfBinRpc ? 'binrpc' : 'http',
|
|
524
548
|
auth,
|
|
525
549
|
user,
|
|
@@ -532,7 +556,7 @@ module.exports = function (RED) {
|
|
|
532
556
|
'BidCos-Wired': {
|
|
533
557
|
conf: 'bcwi',
|
|
534
558
|
rpc: this.isLocal ? binrpc : xmlrpc,
|
|
535
|
-
port: this.isLocal ? 32000 : tls ? 42000 : 2000,
|
|
559
|
+
port: Number(config.bcwiPort) || (this.isLocal ? 32000 : tls ? 42000 : 2000),
|
|
536
560
|
protocol: this.isLocal ? 'binrpc' : 'http',
|
|
537
561
|
auth,
|
|
538
562
|
user,
|
|
@@ -545,7 +569,7 @@ module.exports = function (RED) {
|
|
|
545
569
|
'HmIP-RF': {
|
|
546
570
|
conf: 'iprf',
|
|
547
571
|
rpc: xmlrpc,
|
|
548
|
-
port: this.isLocal ? 32010 : tls ? 42010 : 2010,
|
|
572
|
+
port: Number(config.iprfPort) || (this.isLocal ? 32010 : tls ? 42010 : 2010),
|
|
549
573
|
protocol: 'http',
|
|
550
574
|
auth,
|
|
551
575
|
user,
|
|
@@ -563,7 +587,7 @@ module.exports = function (RED) {
|
|
|
563
587
|
VirtualDevices: {
|
|
564
588
|
conf: 'virt',
|
|
565
589
|
rpc: xmlrpc,
|
|
566
|
-
port: this.isLocal ? 39292 : tls ? 49292 : 9292,
|
|
590
|
+
port: Number(config.virtPort) || (this.isLocal ? 39292 : tls ? 49292 : 9292),
|
|
567
591
|
path: 'groups',
|
|
568
592
|
protocol: 'http',
|
|
569
593
|
auth,
|
|
@@ -577,7 +601,7 @@ module.exports = function (RED) {
|
|
|
577
601
|
CUxD: {
|
|
578
602
|
conf: 'cuxd',
|
|
579
603
|
rpc: binrpc,
|
|
580
|
-
port: 8701,
|
|
604
|
+
port: Number(config.cuxdPort) || 8701,
|
|
581
605
|
protocol: 'binrpc',
|
|
582
606
|
init: true,
|
|
583
607
|
ping: true,
|
|
@@ -632,8 +656,10 @@ module.exports = function (RED) {
|
|
|
632
656
|
this.lastRealEvent = {};
|
|
633
657
|
this.ifaceStatus = {};
|
|
634
658
|
// task 11: interfaces whose init failed and is retried (InitRetry per iface),
|
|
635
|
-
// 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
|
|
636
661
|
this.initRetry = {};
|
|
662
|
+
this.startedAt = Date.now();
|
|
637
663
|
this.ifaceWaiting = {};
|
|
638
664
|
// B-37: per interface {inFlight, at, reinit} of our own init, and how long after it the
|
|
639
665
|
// process's listDevices/newDevices still answer it (see keptSubscription())
|
|
@@ -655,6 +681,17 @@ module.exports = function (RED) {
|
|
|
655
681
|
this.inSecure = inSecure;
|
|
656
682
|
this.metaPort = Number.parseInt(config.metaPort, 10) || (tls ? 443 : 80);
|
|
657
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 = [];
|
|
658
695
|
this.metaMode = false;
|
|
659
696
|
this.meta = null;
|
|
660
697
|
// #167: minutes between re-reads of channel names, rooms and
|
|
@@ -738,6 +775,12 @@ module.exports = function (RED) {
|
|
|
738
775
|
RED.settings.userDir || path.join(__dirname, '..'),
|
|
739
776
|
'ccu_values_' + this.host + '.json',
|
|
740
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
|
+
);
|
|
741
784
|
|
|
742
785
|
this.loadMetadata();
|
|
743
786
|
this.loadRegadata();
|
|
@@ -748,7 +791,7 @@ module.exports = function (RED) {
|
|
|
748
791
|
|
|
749
792
|
this.rega = new Rega({
|
|
750
793
|
host: this.host,
|
|
751
|
-
port: this.isLocal ? 8183 : tls ? 48181 : 8181,
|
|
794
|
+
port: Number(config.regaPort) || (this.isLocal ? 8183 : tls ? 48181 : 8181),
|
|
752
795
|
tls,
|
|
753
796
|
insecure: inSecure,
|
|
754
797
|
username: user,
|
|
@@ -800,7 +843,7 @@ module.exports = function (RED) {
|
|
|
800
843
|
|
|
801
844
|
/**
|
|
802
845
|
* B-31: the password and the token, from the credentials or - once - from the
|
|
803
|
-
* plain properties of a flow written before 4.
|
|
846
|
+
* plain properties of a flow written before 4.5.0.
|
|
804
847
|
* @param {object} config the node's configuration as deployed
|
|
805
848
|
* @returns {{password: string, metaToken: string}}
|
|
806
849
|
*/
|
|
@@ -963,7 +1006,10 @@ module.exports = function (RED) {
|
|
|
963
1006
|
const waitingChanged = Boolean(this.ifaceWaiting[iface]) !== waiting;
|
|
964
1007
|
if (this.ifaceStatus[iface] !== connected || waitingChanged) {
|
|
965
1008
|
if (this.ifaceStatus[iface] !== connected) {
|
|
966
|
-
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') {
|
|
967
1013
|
this.logger.info('Interface', iface, connected ? 'connected' : 'disconnected');
|
|
968
1014
|
} else if (waiting) {
|
|
969
1015
|
// task 11: the retry logs the one warn line, this stays quiet
|
|
@@ -973,7 +1019,9 @@ module.exports = function (RED) {
|
|
|
973
1019
|
'Interface',
|
|
974
1020
|
iface,
|
|
975
1021
|
connected
|
|
976
|
-
? this.
|
|
1022
|
+
? this.lite && this.lite.has(iface)
|
|
1023
|
+
? 'lite-rpc connected'
|
|
1024
|
+
: this.ifaceTypes[iface].protocol +
|
|
977
1025
|
' port ' +
|
|
978
1026
|
this.ifaceTypes[iface].port +
|
|
979
1027
|
' connected'
|
|
@@ -989,11 +1037,7 @@ module.exports = function (RED) {
|
|
|
989
1037
|
delete this.ifaceWaiting[iface];
|
|
990
1038
|
}
|
|
991
1039
|
|
|
992
|
-
|
|
993
|
-
if (typeof this.users[id].setStatus === 'function') {
|
|
994
|
-
this.users[id].setStatus({ifaceStatus: this.ifaceStatus, ifaceWaiting: this.ifaceWaiting});
|
|
995
|
-
}
|
|
996
|
-
});
|
|
1040
|
+
this.pushStatus();
|
|
997
1041
|
}
|
|
998
1042
|
}
|
|
999
1043
|
|
|
@@ -1007,6 +1051,7 @@ module.exports = function (RED) {
|
|
|
1007
1051
|
this.initRetry[iface] = new InitRetry({
|
|
1008
1052
|
iface,
|
|
1009
1053
|
logger: this.logger,
|
|
1054
|
+
startedAt: this.startedAt,
|
|
1010
1055
|
attempt: () => this.rpcInit(iface, {retry: true}),
|
|
1011
1056
|
onState: (state) => {
|
|
1012
1057
|
if (state === 'connected') {
|
|
@@ -1022,6 +1067,15 @@ module.exports = function (RED) {
|
|
|
1022
1067
|
return this.initRetry[iface];
|
|
1023
1068
|
}
|
|
1024
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
|
+
|
|
1025
1079
|
/**
|
|
1026
1080
|
* An init succeeded (the first one, a retry, or a re-init after a ping timeout).
|
|
1027
1081
|
* @param iface
|
|
@@ -1032,6 +1086,21 @@ module.exports = function (RED) {
|
|
|
1032
1086
|
} else {
|
|
1033
1087
|
this.setIfaceStatus(iface, true);
|
|
1034
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
|
+
});
|
|
1035
1104
|
}
|
|
1036
1105
|
|
|
1037
1106
|
/**
|
|
@@ -1262,6 +1331,7 @@ module.exports = function (RED) {
|
|
|
1262
1331
|
this.logger.debug('ccu-connection destructor');
|
|
1263
1332
|
this.closing = true;
|
|
1264
1333
|
this.stopInitRetries();
|
|
1334
|
+
this.stopLite();
|
|
1265
1335
|
this.stats(false);
|
|
1266
1336
|
|
|
1267
1337
|
this.logger.debug('clear regaPollTimeout');
|
|
@@ -1392,9 +1462,10 @@ module.exports = function (RED) {
|
|
|
1392
1462
|
/**
|
|
1393
1463
|
* B-28: an inconclusive detection (the box busy or still starting) used
|
|
1394
1464
|
* to leave the connection in ReGa mode for good - the only re-detection
|
|
1395
|
-
* ran from rare error paths, at most every five minutes. Ask again
|
|
1396
|
-
*
|
|
1397
|
-
* 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.
|
|
1398
1469
|
* @param {{inconclusive: boolean, reason?: string}} outcome
|
|
1399
1470
|
*/
|
|
1400
1471
|
scheduleMetaRedetect(outcome) {
|
|
@@ -1416,7 +1487,7 @@ module.exports = function (RED) {
|
|
|
1416
1487
|
return;
|
|
1417
1488
|
}
|
|
1418
1489
|
|
|
1419
|
-
const delay = retryDelay(this.metaRedetectAttempt);
|
|
1490
|
+
const delay = retryDelay((this.metaRedetectAttempt - 1) * RETRY_INTERVAL);
|
|
1420
1491
|
const line =
|
|
1421
1492
|
'meta api detection inconclusive (' +
|
|
1422
1493
|
outcome.reason +
|
|
@@ -1483,6 +1554,21 @@ module.exports = function (RED) {
|
|
|
1483
1554
|
this.closeClient('ReGaHSS');
|
|
1484
1555
|
}
|
|
1485
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
|
+
|
|
1486
1572
|
this.logger.info(
|
|
1487
1573
|
'openccu-lite detected (' +
|
|
1488
1574
|
(info.implementation || 'meta api') +
|
|
@@ -1525,12 +1611,29 @@ module.exports = function (RED) {
|
|
|
1525
1611
|
resolve();
|
|
1526
1612
|
},
|
|
1527
1613
|
onStatus: (connected) => {
|
|
1528
|
-
this.setIfaceStatus(
|
|
1614
|
+
this.setIfaceStatus(META_IFACE, connected);
|
|
1529
1615
|
},
|
|
1530
1616
|
onGone: () => {
|
|
1531
1617
|
this.metaMode = false;
|
|
1532
1618
|
this.meta = null;
|
|
1533
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();
|
|
1534
1637
|
this.logger.warn('this box no longer answers as openccu-lite - detecting again');
|
|
1535
1638
|
this.detectMetaOutcome().then((outcome) => {
|
|
1536
1639
|
if (outcome.info) {
|
|
@@ -2213,9 +2316,16 @@ module.exports = function (RED) {
|
|
|
2213
2316
|
* @param config
|
|
2214
2317
|
*/
|
|
2215
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;
|
|
2216
2322
|
Object.keys(this.ifaceTypes).forEach((iface) => {
|
|
2217
2323
|
const enabled = config[this.ifaceTypes[iface].conf + 'Enabled'];
|
|
2218
2324
|
this.ifaceTypes[iface].enabled = enabled;
|
|
2325
|
+
if (enabled && liteIfaces.includes(iface)) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2219
2329
|
if (enabled && iface === 'ReGaHSS' && this.metaMode) {
|
|
2220
2330
|
// B-27: openccu-lite has no ReGaHSS, nothing listens on 31999.
|
|
2221
2331
|
// A client would only reconnect forever (with binrpc < 4.3.0
|
|
@@ -2245,6 +2355,249 @@ module.exports = function (RED) {
|
|
|
2245
2355
|
}
|
|
2246
2356
|
});
|
|
2247
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
|
+
}
|
|
2248
2601
|
}
|
|
2249
2602
|
|
|
2250
2603
|
/**
|
|
@@ -2862,6 +3215,17 @@ module.exports = function (RED) {
|
|
|
2862
3215
|
this.logger.info(protocol === 'binrpc' ? 'binrpc' : 'xmlrpc', 'server listening on', url);
|
|
2863
3216
|
this.serverError[iface] = null;
|
|
2864
3217
|
});
|
|
3218
|
+
if (this.servers[protocol].httpServer) {
|
|
3219
|
+
// hmipserver delivers its events through a pooled HTTP keep-alive client whose idle
|
|
3220
|
+
// timeout is 1200 s, and sends the next event on a pooled connection without
|
|
3221
|
+
// checking it. Node closes an idle keep-alive connection after 5 s by default, so
|
|
3222
|
+
// at the idle boundary the two raced: the event went out onto a socket this side was
|
|
3223
|
+
// closing, hmipserver logged "Connection reset" and the event was lost, a few times a
|
|
3224
|
+
// day. The callback server keeps an idle connection longer than the client pool
|
|
3225
|
+
// does, so it is always the client that lets go.
|
|
3226
|
+
this.servers[protocol].httpServer.keepAliveTimeout = KEEP_ALIVE_TIMEOUT_MS;
|
|
3227
|
+
this.servers[protocol].httpServer.headersTimeout = KEEP_ALIVE_TIMEOUT_MS + 5000;
|
|
3228
|
+
}
|
|
2865
3229
|
|
|
2866
3230
|
// Todo binrpc module: emit error event on server object to eliminate the reach-in
|
|
2867
3231
|
const errorEmitter = protocol === 'binrpc' ? this.servers[protocol].server : this.servers[protocol];
|
|
@@ -4126,6 +4490,31 @@ module.exports = function (RED) {
|
|
|
4126
4490
|
*/
|
|
4127
4491
|
methodCall(iface, method, parameters, {quiet = false} = {}) {
|
|
4128
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
|
+
|
|
4129
4518
|
if (this.clients[iface]) {
|
|
4130
4519
|
this.logger.debug('rpc >', iface, method, JSON.stringify(parameters));
|
|
4131
4520
|
this.clients[iface].methodCall(method, parameters, (err, res) => {
|