node-red-contrib-ccu 4.6.2 → 4.7.1
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 +73 -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 +363 -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,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) => {
|