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.
@@ -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
- /** B-28: how often an inconclusive metadata api detection is repeated (1, 2, 4, 8 s, then every 15 s) */
59
- 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;
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.4.6.
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 === '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') {
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.ifaceTypes[iface].protocol +
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
- Object.keys(this.users).forEach((id) => {
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 after
1400
- * 1, 2, 4, 8 s, then every 15 s, up to META_REDETECT_ATTEMPTS times;
1401
- * 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.
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('ReGaHSS', connected);
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) => {