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.
@@ -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
- /** B-28: how often an inconclusive metadata api detection is repeated (1, 2, 4, 8 s, then every 15 s) */
55
- const META_REDETECT_ATTEMPTS = 20;
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.4.6.
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 === 'ReGaHSS') {
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.ifaceTypes[iface].protocol +
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
- Object.keys(this.users).forEach((id) => {
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 after
1396
- * 1, 2, 4, 8 s, then every 15 s, up to META_REDETECT_ATTEMPTS times;
1397
- * after that the five-minute rule of recheckMeta() applies.
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('ReGaHSS', connected);
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) => {