node-red-contrib-ccu 4.4.4 → 4.5.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 +175 -0
- package/README.md +35 -0
- package/nodes/ccu-alexa.html +12 -14
- package/nodes/ccu-connection.html +63 -95
- package/nodes/ccu-connection.js +812 -115
- package/nodes/ccu-get-value.html +18 -15
- package/nodes/ccu-get-value.js +16 -0
- package/nodes/ccu-homeassistant.html +0 -48
- package/nodes/ccu-mqtt.html +0 -8
- package/nodes/ccu-poll.html +0 -17
- package/nodes/ccu-program.html +0 -53
- package/nodes/ccu-rpc-event.html +0 -34
- package/nodes/ccu-rpc.html +0 -64
- package/nodes/ccu-script.html +0 -8
- package/nodes/ccu-set-value.html +0 -62
- package/nodes/ccu-set-value.js +6 -11
- package/nodes/ccu-signal.html +0 -64
- package/nodes/ccu-switch.html +12 -24
- package/nodes/ccu-sysvar.html +0 -64
- package/nodes/ccu-value.html +23 -95
- package/nodes/ccu-value.js +8 -2
- package/nodes/lib/initretry.js +46 -9
- package/nodes/lib/localccu.js +21 -3
- package/nodes/lib/metaprovider.js +47 -19
- package/nodes/lib/party.js +103 -0
- package/nodes/lib/topic.js +22 -1
- package/nodes/lib/transport.js +80 -0
- package/nodes/locales/de/ccu-alexa.html +4 -0
- package/nodes/locales/de/ccu-connection.html +103 -0
- package/nodes/locales/de/ccu-get-value.html +16 -0
- package/nodes/locales/de/ccu-homeassistant.html +23 -0
- package/nodes/locales/de/ccu-mqtt.html +21 -0
- package/nodes/locales/de/ccu-poll.html +8 -0
- package/nodes/locales/de/ccu-program.html +26 -0
- package/nodes/locales/de/ccu-rpc-event.html +32 -0
- package/nodes/locales/de/ccu-rpc.html +31 -0
- package/nodes/locales/de/ccu-script.html +13 -0
- package/nodes/locales/de/ccu-set-value.html +46 -0
- package/nodes/locales/de/ccu-signal.html +29 -0
- package/nodes/locales/de/ccu-switch.html +18 -0
- package/nodes/locales/de/ccu-sysvar.html +43 -0
- package/nodes/locales/de/ccu-value.html +98 -0
- package/nodes/locales/de/ccu-value.json +1 -0
- package/nodes/locales/en-US/ccu-alexa.html +4 -0
- package/nodes/locales/en-US/ccu-connection.html +41 -0
- package/nodes/locales/en-US/ccu-get-value.html +13 -0
- package/nodes/locales/en-US/ccu-homeassistant.html +23 -0
- package/nodes/locales/en-US/ccu-mqtt.html +21 -0
- package/nodes/locales/en-US/ccu-poll.html +7 -0
- package/nodes/locales/en-US/ccu-program.html +25 -0
- package/nodes/locales/en-US/ccu-rpc-event.html +31 -0
- package/nodes/locales/en-US/ccu-rpc.html +30 -0
- package/nodes/locales/en-US/ccu-script.html +13 -0
- package/nodes/locales/en-US/ccu-set-value.html +45 -0
- package/nodes/locales/en-US/ccu-signal.html +29 -0
- package/nodes/locales/en-US/ccu-switch.html +6 -0
- package/nodes/locales/en-US/ccu-sysvar.html +42 -0
- package/nodes/locales/en-US/ccu-value.html +81 -0
- package/nodes/locales/en-US/ccu-value.json +1 -0
- package/package.json +1 -1
package/nodes/ccu-connection.js
CHANGED
|
@@ -7,15 +7,17 @@ const crypto = require('crypto');
|
|
|
7
7
|
const base62 = require('./lib/base62.js').toBase62;
|
|
8
8
|
const {castValue, castSysvar} = require('./lib/cast.js');
|
|
9
9
|
const {combinedParameterValue} = require('./lib/combined.js');
|
|
10
|
+
const {isPartyWrite, mergePartyValues, PARTY_DATAPOINTS} = require('./lib/party.js');
|
|
10
11
|
const {createMessage} = require('./lib/message.js');
|
|
11
12
|
const {topicReplace} = require('./lib/topic.js');
|
|
12
13
|
const {isLocalCcu} = require('./lib/localccu.js');
|
|
14
|
+
const {resolveTransport} = require('./lib/transport.js');
|
|
13
15
|
const {bestMatch} = require('./lib/similarity.js');
|
|
14
16
|
const nextport = require('./lib/nextport.js');
|
|
15
17
|
const hmDiscover = require('./lib/discover.js');
|
|
16
18
|
const {Rega} = require('homematic-rega'); // ES module - require(esm) needs Node >= 20.19 / >= 22.12
|
|
17
19
|
const metaProvider = require('./lib/metaprovider.js');
|
|
18
|
-
const {InitRetry} = require('./lib/initretry.js');
|
|
20
|
+
const {InitRetry, retryDelay} = require('./lib/initretry.js');
|
|
19
21
|
const xmlrpc = require('homematic-xmlrpc');
|
|
20
22
|
const binrpc = require('binrpc');
|
|
21
23
|
|
|
@@ -44,6 +46,24 @@ function execToCallback(promise, callback) {
|
|
|
44
46
|
);
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
/** B-28: how often an inconclusive metadata api detection is repeated (1, 2, 4, 8 s, then every 15 s) */
|
|
50
|
+
const META_REDETECT_ATTEMPTS = 20;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* task 9: has a device description changed in a way that changes its paramsets?
|
|
54
|
+
* (the paramset key is TYPE/FIRMWARE/VERSION of the device and the channel TYPE)
|
|
55
|
+
* @param {object} known the cached description
|
|
56
|
+
* @param {object} device the one the process reports
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
function deviceChanged(known, device) {
|
|
60
|
+
return ['TYPE', 'VERSION', 'FIRMWARE', 'PARENT_TYPE'].some(
|
|
61
|
+
(key) =>
|
|
62
|
+
(known[key] === undefined ? '' : String(known[key])) !==
|
|
63
|
+
(device[key] === undefined ? '' : String(device[key])),
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
47
67
|
function isIterable(object) {
|
|
48
68
|
return object != null && typeof object[Symbol.iterator] === 'function' && typeof object.forEach === 'function';
|
|
49
69
|
}
|
|
@@ -85,6 +105,42 @@ module.exports = function (RED) {
|
|
|
85
105
|
});
|
|
86
106
|
ccu.network.listen.push('0.0.0.0');
|
|
87
107
|
|
|
108
|
+
/**
|
|
109
|
+
* task 9: the editor's channel pickers ask while the connection may still be
|
|
110
|
+
* initialising - an empty answer then means "not yet", not "nothing". The
|
|
111
|
+
* header lets the dialog show "loading" and ask again instead of emptying
|
|
112
|
+
* the picker (and the configured channel with it).
|
|
113
|
+
* @param {object} config the connection node
|
|
114
|
+
* @param {string} [iface] the interface asked for, or every enabled one
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function deviceDataLoading(config, iface) {
|
|
118
|
+
const ifaces = iface ? [iface] : Object.keys(config.ifaceTypes).filter((i) => config.ifaceTypes[i].enabled);
|
|
119
|
+
const connecting = ifaces.some(
|
|
120
|
+
(i) =>
|
|
121
|
+
config.ifaceTypes[i] &&
|
|
122
|
+
config.ifaceTypes[i].init &&
|
|
123
|
+
config.ifaceTypes[i].enabled &&
|
|
124
|
+
!config.ifaceStatus[i],
|
|
125
|
+
);
|
|
126
|
+
const fetching = Boolean(config.paramsetPending) || (config.paramsetQueue && config.paramsetQueue.length > 0);
|
|
127
|
+
return connecting || fetching;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// task 9: "re-read device data" from the connection node's dialog
|
|
131
|
+
RED.httpAdmin.post('/ccu/:id/reread', RED.auth.needsPermission('ccu.write'), (request, res) => {
|
|
132
|
+
const config = RED.nodes.getNode(request.params.id);
|
|
133
|
+
if (!config || typeof config.rereadDevices !== 'function') {
|
|
134
|
+
res.status(404).send(JSON.stringify({error: 'connection not deployed'}));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
config
|
|
139
|
+
.rereadDevices()
|
|
140
|
+
.then((summary) => res.status(200).send(JSON.stringify(summary)))
|
|
141
|
+
.catch((error) => res.status(500).send(JSON.stringify({error: error.message})));
|
|
142
|
+
});
|
|
143
|
+
|
|
88
144
|
RED.httpAdmin.get('/ccu', RED.auth.needsPermission('ccu.read'), (request, res) => {
|
|
89
145
|
if (request.query.config && request.query.config !== '_ADD_') {
|
|
90
146
|
const config = RED.nodes.getNode(request.query.config);
|
|
@@ -95,6 +151,10 @@ module.exports = function (RED) {
|
|
|
95
151
|
|
|
96
152
|
const object = {};
|
|
97
153
|
|
|
154
|
+
if (['channels', 'tree'].includes(request.query.type) && deviceDataLoading(config, request.query.iface)) {
|
|
155
|
+
res.set('X-CCU-Loading', '1');
|
|
156
|
+
}
|
|
157
|
+
|
|
98
158
|
switch (request.query.type) {
|
|
99
159
|
case 'ifaces': {
|
|
100
160
|
Object.keys(config.ifaceTypes).forEach((iface) => {
|
|
@@ -407,6 +467,13 @@ module.exports = function (RED) {
|
|
|
407
467
|
constructor(config) {
|
|
408
468
|
RED.nodes.createNode(this, config);
|
|
409
469
|
|
|
470
|
+
// B-31: the CCU password and the openccu-lite token come from Node-RED's
|
|
471
|
+
// credentials. A flow written by an older version still carries them as
|
|
472
|
+
// plain properties: use them once and move them into the credentials, so
|
|
473
|
+
// the next deploy drops them from flows.json.
|
|
474
|
+
const secrets = this.migrateSecrets(config);
|
|
475
|
+
config = {...config, password: secrets.password, metaToken: secrets.metaToken};
|
|
476
|
+
|
|
410
477
|
this.logger.debug('ccu-connection', config.host);
|
|
411
478
|
|
|
412
479
|
this.checkDuplicateConfig(config);
|
|
@@ -420,6 +487,21 @@ module.exports = function (RED) {
|
|
|
420
487
|
this.logger.info('local connection on ccu >= v3.41 detected');
|
|
421
488
|
}
|
|
422
489
|
|
|
490
|
+
// task 5 (#27): TLS and credentials belong to the CCU's web server, which
|
|
491
|
+
// fronts the interface processes for the LAN; a local connection reaches
|
|
492
|
+
// them directly and uses neither, BIN-RPC has neither (lib/transport.js)
|
|
493
|
+
const transport = resolveTransport({
|
|
494
|
+
isLocal: this.isLocal,
|
|
495
|
+
tls: config.tls,
|
|
496
|
+
authentication: config.authentication,
|
|
497
|
+
inSecure: config.inSecure,
|
|
498
|
+
bcrfBinRpc: config.bcrfBinRpc,
|
|
499
|
+
});
|
|
500
|
+
transport.notes.forEach((note) => this.logger[note.level](note.message));
|
|
501
|
+
const {tls, auth, inSecure} = transport;
|
|
502
|
+
const user = auth ? config.username : undefined;
|
|
503
|
+
const pass = auth ? config.password : undefined;
|
|
504
|
+
|
|
423
505
|
this.ifaceTypes = {
|
|
424
506
|
ReGaHSS: {
|
|
425
507
|
conf: 'rega',
|
|
@@ -431,55 +513,59 @@ module.exports = function (RED) {
|
|
|
431
513
|
},
|
|
432
514
|
'BidCos-RF': {
|
|
433
515
|
conf: 'bcrf',
|
|
434
|
-
rpc: this.isLocal ||
|
|
435
|
-
port: this.isLocal ? 32001 :
|
|
436
|
-
protocol: this.isLocal ||
|
|
437
|
-
auth
|
|
438
|
-
user
|
|
439
|
-
pass
|
|
440
|
-
tls
|
|
441
|
-
inSecure
|
|
516
|
+
rpc: this.isLocal || transport.bcrfBinRpc ? binrpc : xmlrpc,
|
|
517
|
+
port: this.isLocal ? 32001 : tls ? 42001 : 2001,
|
|
518
|
+
protocol: this.isLocal || transport.bcrfBinRpc ? 'binrpc' : 'http',
|
|
519
|
+
auth,
|
|
520
|
+
user,
|
|
521
|
+
pass,
|
|
522
|
+
tls,
|
|
523
|
+
inSecure,
|
|
442
524
|
init: true,
|
|
443
525
|
ping: true,
|
|
444
526
|
},
|
|
445
527
|
'BidCos-Wired': {
|
|
446
528
|
conf: 'bcwi',
|
|
447
529
|
rpc: this.isLocal ? binrpc : xmlrpc,
|
|
448
|
-
port: this.isLocal ? 32000 :
|
|
530
|
+
port: this.isLocal ? 32000 : tls ? 42000 : 2000,
|
|
449
531
|
protocol: this.isLocal ? 'binrpc' : 'http',
|
|
450
|
-
auth
|
|
451
|
-
user
|
|
452
|
-
pass
|
|
453
|
-
tls
|
|
454
|
-
inSecure
|
|
532
|
+
auth,
|
|
533
|
+
user,
|
|
534
|
+
pass,
|
|
535
|
+
tls,
|
|
536
|
+
inSecure,
|
|
455
537
|
init: true,
|
|
456
538
|
ping: true,
|
|
457
539
|
},
|
|
458
540
|
'HmIP-RF': {
|
|
459
541
|
conf: 'iprf',
|
|
460
542
|
rpc: xmlrpc,
|
|
461
|
-
port: this.isLocal ? 32010 :
|
|
543
|
+
port: this.isLocal ? 32010 : tls ? 42010 : 2010,
|
|
462
544
|
protocol: 'http',
|
|
463
|
-
auth
|
|
464
|
-
user
|
|
465
|
-
pass
|
|
466
|
-
tls
|
|
467
|
-
inSecure
|
|
545
|
+
auth,
|
|
546
|
+
user,
|
|
547
|
+
pass,
|
|
548
|
+
tls,
|
|
549
|
+
inSecure,
|
|
468
550
|
init: true,
|
|
469
551
|
ping: true, // Todo https://github.com/eq-3/occu/issues/42 - should be fixed, but isn't
|
|
470
552
|
pingTimeout: 600, // Overwrites ccu-connection config
|
|
553
|
+
// B-29: hmipserver forgets its clients when it restarts and does not tell them;
|
|
554
|
+
// a ping every 30 s without an event, and init again when no PONG comes within 10 s
|
|
555
|
+
pingInterval: 30,
|
|
556
|
+
pongTimeout: 10,
|
|
471
557
|
},
|
|
472
558
|
VirtualDevices: {
|
|
473
559
|
conf: 'virt',
|
|
474
560
|
rpc: xmlrpc,
|
|
475
|
-
port: this.isLocal ? 39292 :
|
|
561
|
+
port: this.isLocal ? 39292 : tls ? 49292 : 9292,
|
|
476
562
|
path: 'groups',
|
|
477
563
|
protocol: 'http',
|
|
478
|
-
auth
|
|
479
|
-
user
|
|
480
|
-
pass
|
|
481
|
-
tls
|
|
482
|
-
inSecure
|
|
564
|
+
auth,
|
|
565
|
+
user,
|
|
566
|
+
pass,
|
|
567
|
+
tls,
|
|
568
|
+
inSecure,
|
|
483
569
|
init: true,
|
|
484
570
|
ping: false, // Todo ?
|
|
485
571
|
},
|
|
@@ -494,14 +580,14 @@ module.exports = function (RED) {
|
|
|
494
580
|
'CCU-Jack': {
|
|
495
581
|
conf: 'jack',
|
|
496
582
|
rpc: xmlrpc,
|
|
497
|
-
port: Number(config.jackPort) || (
|
|
583
|
+
port: Number(config.jackPort) || (tls ? 2122 : 2121),
|
|
498
584
|
path: 'RPC3',
|
|
499
585
|
protocol: 'http',
|
|
500
|
-
auth
|
|
501
|
-
user
|
|
502
|
-
pass
|
|
503
|
-
tls
|
|
504
|
-
inSecure
|
|
586
|
+
auth,
|
|
587
|
+
user,
|
|
588
|
+
pass,
|
|
589
|
+
tls,
|
|
590
|
+
inSecure,
|
|
505
591
|
init: true,
|
|
506
592
|
ping: false,
|
|
507
593
|
},
|
|
@@ -536,6 +622,9 @@ module.exports = function (RED) {
|
|
|
536
622
|
// reconnecting after a CCU restart is then up to the CCU.
|
|
537
623
|
this.rpcPingEnabled = config.rpcPing === undefined ? true : Boolean(config.rpcPing);
|
|
538
624
|
this.rpcPingTimer = {};
|
|
625
|
+
// B-29: the liveness ping per interface, and when an event (PONG included) last arrived
|
|
626
|
+
this.liveness = {};
|
|
627
|
+
this.lastRealEvent = {};
|
|
539
628
|
this.ifaceStatus = {};
|
|
540
629
|
// task 11: interfaces whose init failed and is retried (InitRetry per iface),
|
|
541
630
|
// and which of them are waiting for an unreachable process
|
|
@@ -553,9 +642,9 @@ module.exports = function (RED) {
|
|
|
553
642
|
// B-17 openccu-lite: the metadata api sits behind the box's
|
|
554
643
|
// lighttpd, i.e. on the plain http(s) port. The field exists for
|
|
555
644
|
// reverse proxies and tests, it is empty in every normal install.
|
|
556
|
-
this.tlsEnabled =
|
|
557
|
-
this.inSecure =
|
|
558
|
-
this.metaPort = Number.parseInt(config.metaPort, 10) || (
|
|
645
|
+
this.tlsEnabled = tls;
|
|
646
|
+
this.inSecure = inSecure;
|
|
647
|
+
this.metaPort = Number.parseInt(config.metaPort, 10) || (tls ? 443 : 80);
|
|
559
648
|
this.metaToken = config.metaToken || metaProvider.readLocalToken();
|
|
560
649
|
this.metaMode = false;
|
|
561
650
|
this.meta = null;
|
|
@@ -612,6 +701,9 @@ module.exports = function (RED) {
|
|
|
612
701
|
this.setValueThrottle = 500;
|
|
613
702
|
this.setValueTimers = {};
|
|
614
703
|
this.setValueCache = {};
|
|
704
|
+
// B-19: the value this connection last wrote to a datapoint, so the queued
|
|
705
|
+
// write's dedupe can tell the echo of its own write from another source
|
|
706
|
+
this.lastWrite = {};
|
|
615
707
|
this.setValueQueue = [];
|
|
616
708
|
|
|
617
709
|
this.lastEvent = {};
|
|
@@ -639,11 +731,11 @@ module.exports = function (RED) {
|
|
|
639
731
|
|
|
640
732
|
this.rega = new Rega({
|
|
641
733
|
host: this.host,
|
|
642
|
-
port: this.isLocal ? 8183 :
|
|
643
|
-
tls
|
|
644
|
-
insecure:
|
|
645
|
-
username:
|
|
646
|
-
password:
|
|
734
|
+
port: this.isLocal ? 8183 : tls ? 48181 : 8181,
|
|
735
|
+
tls,
|
|
736
|
+
insecure: inSecure,
|
|
737
|
+
username: user,
|
|
738
|
+
password: pass,
|
|
647
739
|
});
|
|
648
740
|
|
|
649
741
|
this.enabledIfaces = [];
|
|
@@ -661,12 +753,15 @@ module.exports = function (RED) {
|
|
|
661
753
|
// metadata api. It does not on a CCU/RaspberryMatic/OpenCCU,
|
|
662
754
|
// where everything below runs exactly as before.
|
|
663
755
|
this.lastMetaProbe = now();
|
|
664
|
-
this.
|
|
665
|
-
.then((
|
|
666
|
-
if (info) {
|
|
667
|
-
return this.startMeta(info);
|
|
756
|
+
this.detectMetaOutcome()
|
|
757
|
+
.then((outcome) => {
|
|
758
|
+
if (outcome.info) {
|
|
759
|
+
return this.startMeta(outcome.info);
|
|
668
760
|
}
|
|
669
761
|
|
|
762
|
+
// B-28: a timeout or a refused connection decides nothing -
|
|
763
|
+
// the ReGa path starts, and the detection is repeated
|
|
764
|
+
this.scheduleMetaRedetect(outcome);
|
|
670
765
|
return this.getRegaData().then(() => {
|
|
671
766
|
this.regaPoll();
|
|
672
767
|
});
|
|
@@ -686,6 +781,41 @@ module.exports = function (RED) {
|
|
|
686
781
|
this.on('close', this.destructor);
|
|
687
782
|
}
|
|
688
783
|
|
|
784
|
+
/**
|
|
785
|
+
* B-31: the password and the token, from the credentials or - once - from the
|
|
786
|
+
* plain properties of a flow written before 4.4.6.
|
|
787
|
+
* @param {object} config the node's configuration as deployed
|
|
788
|
+
* @returns {{password: string, metaToken: string}}
|
|
789
|
+
*/
|
|
790
|
+
migrateSecrets(config) {
|
|
791
|
+
const credentials = this.credentials || {};
|
|
792
|
+
const result = {
|
|
793
|
+
password: credentials.password || config.password || '',
|
|
794
|
+
metaToken: credentials.metaToken || config.metaToken || '',
|
|
795
|
+
};
|
|
796
|
+
const plain = ['password', 'metaToken'].filter((key) => config[key] && !credentials[key]);
|
|
797
|
+
if (plain.length > 0) {
|
|
798
|
+
const moved = {...credentials};
|
|
799
|
+
plain.forEach((key) => {
|
|
800
|
+
moved[key] = config[key];
|
|
801
|
+
});
|
|
802
|
+
if (typeof RED.nodes.addCredentials === 'function') {
|
|
803
|
+
RED.nodes.addCredentials(this.id, moved);
|
|
804
|
+
this.warn(
|
|
805
|
+
'the ' +
|
|
806
|
+
plain.join(' and ') +
|
|
807
|
+
' of this connection came from the flow in plain text - moved into the credentials, the next deploy removes ' +
|
|
808
|
+
(plain.length > 1 ? 'them' : 'it') +
|
|
809
|
+
' from flows.json',
|
|
810
|
+
);
|
|
811
|
+
} else {
|
|
812
|
+
this.warn('the ' + plain.join(' and ') + ' of this connection is stored in the flow in plain text');
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
return result;
|
|
817
|
+
}
|
|
818
|
+
|
|
689
819
|
get logger() {
|
|
690
820
|
return {
|
|
691
821
|
trace: (...args) => {
|
|
@@ -807,6 +937,11 @@ module.exports = function (RED) {
|
|
|
807
937
|
* @param connected
|
|
808
938
|
*/
|
|
809
939
|
setIfaceStatus(iface, connected, waiting = false) {
|
|
940
|
+
if (!iface || !this.ifaceTypes[iface]) {
|
|
941
|
+
// B-32: never a status line for something that is not an interface of ours
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
|
|
810
945
|
waiting = Boolean(waiting) && !connected;
|
|
811
946
|
const waitingChanged = Boolean(this.ifaceWaiting[iface]) !== waiting;
|
|
812
947
|
if (this.ifaceStatus[iface] !== connected || waitingChanged) {
|
|
@@ -1115,6 +1250,8 @@ module.exports = function (RED) {
|
|
|
1115
1250
|
this.logger.debug('clear regaPollTimeout');
|
|
1116
1251
|
this.cancelRegaPoll = true;
|
|
1117
1252
|
clearTimeout(this.regaPollTimeout);
|
|
1253
|
+
clearTimeout(this.metaRedetectTimeout);
|
|
1254
|
+
this.metaRedetectTimeout = null;
|
|
1118
1255
|
|
|
1119
1256
|
if (this.meta) {
|
|
1120
1257
|
this.logger.debug('stop metadata event stream');
|
|
@@ -1126,6 +1263,7 @@ module.exports = function (RED) {
|
|
|
1126
1263
|
this.logger.debug('clear rpcPingTimer', iface);
|
|
1127
1264
|
clearTimeout(this.rpcPingTimer[iface]);
|
|
1128
1265
|
});
|
|
1266
|
+
Object.keys(this.liveness).forEach((iface) => this.stopLiveness(iface));
|
|
1129
1267
|
|
|
1130
1268
|
this.saveRegadata();
|
|
1131
1269
|
this.saveValues();
|
|
@@ -1214,7 +1352,17 @@ module.exports = function (RED) {
|
|
|
1214
1352
|
* @returns {Promise<object|null>}
|
|
1215
1353
|
*/
|
|
1216
1354
|
detectMeta() {
|
|
1217
|
-
return
|
|
1355
|
+
return this.detectMetaOutcome().then((outcome) => outcome.info);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* B-28: the detection with its outcome - `info` for an openccu-lite,
|
|
1360
|
+
* `inconclusive` when nothing was decided (timeout, refused connection,
|
|
1361
|
+
* the web server up but the api behind it not yet).
|
|
1362
|
+
* @returns {Promise<{info: object|null, inconclusive: boolean, reason?: string}>}
|
|
1363
|
+
*/
|
|
1364
|
+
detectMetaOutcome() {
|
|
1365
|
+
return metaProvider.detectOutcome({
|
|
1218
1366
|
host: this.host,
|
|
1219
1367
|
port: this.metaPort,
|
|
1220
1368
|
tls: Boolean(this.tlsEnabled),
|
|
@@ -1223,6 +1371,81 @@ module.exports = function (RED) {
|
|
|
1223
1371
|
});
|
|
1224
1372
|
}
|
|
1225
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* B-28: an inconclusive detection (the box busy or still starting) used
|
|
1376
|
+
* to leave the connection in ReGa mode for good - the only re-detection
|
|
1377
|
+
* ran from rare error paths, at most every five minutes. Ask again after
|
|
1378
|
+
* 1, 2, 4, 8 s, then every 15 s, up to META_REDETECT_ATTEMPTS times;
|
|
1379
|
+
* after that the five-minute rule of recheckMeta() applies.
|
|
1380
|
+
* @param {{inconclusive: boolean, reason?: string}} outcome
|
|
1381
|
+
*/
|
|
1382
|
+
scheduleMetaRedetect(outcome) {
|
|
1383
|
+
if (!outcome || !outcome.inconclusive || this.metaMode || this.cancelRegaPoll) {
|
|
1384
|
+
this.metaRedetectAttempt = 0;
|
|
1385
|
+
return;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
this.metaRedetectAttempt = (this.metaRedetectAttempt || 0) + 1;
|
|
1389
|
+
if (this.metaRedetectAttempt > META_REDETECT_ATTEMPTS) {
|
|
1390
|
+
this.logger.info(
|
|
1391
|
+
'meta api detection still inconclusive (' +
|
|
1392
|
+
outcome.reason +
|
|
1393
|
+
') after ' +
|
|
1394
|
+
META_REDETECT_ATTEMPTS +
|
|
1395
|
+
' attempts - staying with the ReGaHSS, detecting again every five minutes',
|
|
1396
|
+
);
|
|
1397
|
+
this.metaRedetectAttempt = 0;
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
const delay = retryDelay(this.metaRedetectAttempt);
|
|
1402
|
+
const line =
|
|
1403
|
+
'meta api detection inconclusive (' +
|
|
1404
|
+
outcome.reason +
|
|
1405
|
+
') - names come from the ReGaHSS until the box answers, detecting again in ' +
|
|
1406
|
+
delay / 1000 +
|
|
1407
|
+
' s';
|
|
1408
|
+
if (this.metaRedetectAttempt === 1) {
|
|
1409
|
+
this.logger.info(line);
|
|
1410
|
+
} else {
|
|
1411
|
+
this.logger.debug(line);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
clearTimeout(this.metaRedetectTimeout);
|
|
1415
|
+
this.metaRedetectTimeout = setTimeout(() => this.redetectMeta(), delay);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** B-28: one scheduled re-detection. */
|
|
1419
|
+
redetectMeta() {
|
|
1420
|
+
this.metaRedetectTimeout = null;
|
|
1421
|
+
if (this.metaMode || this.cancelRegaPoll) {
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
this.lastMetaProbe = now();
|
|
1426
|
+
this.detectMetaOutcome().then((outcome) => {
|
|
1427
|
+
if (this.metaMode || this.cancelRegaPoll) {
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (outcome.info) {
|
|
1432
|
+
this.logger.info('meta api detected after ' + (this.metaRedetectAttempt + 1) + ' attempts');
|
|
1433
|
+
this.metaRedetectAttempt = 0;
|
|
1434
|
+
return this.startMeta(outcome.info);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
if (outcome.inconclusive) {
|
|
1438
|
+
this.scheduleMetaRedetect(outcome);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
this.metaRedetectAttempt = 0;
|
|
1443
|
+
this.logger.info(
|
|
1444
|
+
'not an openccu-lite (' + outcome.reason + ') - names, rooms and functions come from the ReGaHSS',
|
|
1445
|
+
);
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1226
1449
|
/**
|
|
1227
1450
|
* Take names, rooms and functions from the openccu-lite metadata api
|
|
1228
1451
|
* instead of from the ReGaHSS. Resolves as soon as the first snapshot
|
|
@@ -1291,11 +1514,14 @@ module.exports = function (RED) {
|
|
|
1291
1514
|
this.meta = null;
|
|
1292
1515
|
this.lastMetaProbe = 0;
|
|
1293
1516
|
this.logger.warn('this box no longer answers as openccu-lite - detecting again');
|
|
1294
|
-
this.
|
|
1295
|
-
if (
|
|
1296
|
-
return this.startMeta(
|
|
1517
|
+
this.detectMetaOutcome().then((outcome) => {
|
|
1518
|
+
if (outcome.info) {
|
|
1519
|
+
return this.startMeta(outcome.info);
|
|
1297
1520
|
}
|
|
1298
1521
|
|
|
1522
|
+
// B-28: an unreachable box is not a CCU yet - keep asking
|
|
1523
|
+
this.scheduleMetaRedetect(outcome);
|
|
1524
|
+
|
|
1299
1525
|
if (this.ifaceTypes.ReGaHSS.enabled && !this.clients.ReGaHSS) {
|
|
1300
1526
|
this.createClient('ReGaHSS').then(() => this.setIfaceStatus('ReGaHSS', true));
|
|
1301
1527
|
}
|
|
@@ -1341,7 +1567,7 @@ module.exports = function (RED) {
|
|
|
1341
1567
|
* picked up without a redeploy.
|
|
1342
1568
|
*/
|
|
1343
1569
|
recheckMeta() {
|
|
1344
|
-
if (this.metaMode || this.cancelRegaPoll || this.metaRecheckPending) {
|
|
1570
|
+
if (this.metaMode || this.cancelRegaPoll || this.metaRecheckPending || this.metaRedetectTimeout) {
|
|
1345
1571
|
return;
|
|
1346
1572
|
}
|
|
1347
1573
|
|
|
@@ -1663,9 +1889,16 @@ module.exports = function (RED) {
|
|
|
1663
1889
|
this.regaPollPending = true;
|
|
1664
1890
|
clearTimeout(this.regaPollTimeout);
|
|
1665
1891
|
this.getRegaVariables()
|
|
1666
|
-
.catch((error) =>
|
|
1892
|
+
.catch((error) => {
|
|
1893
|
+
this.logger.error('getRegaVariables', error);
|
|
1894
|
+
// B-28: the poll is what fails on a box without ReGaHSS
|
|
1895
|
+
this.recheckMeta();
|
|
1896
|
+
})
|
|
1667
1897
|
.then(() => this.getRegaPrograms())
|
|
1668
|
-
.catch((error) =>
|
|
1898
|
+
.catch((error) => {
|
|
1899
|
+
this.logger.error('getRegaPrograms', error);
|
|
1900
|
+
this.recheckMeta();
|
|
1901
|
+
})
|
|
1669
1902
|
.finally(() => {
|
|
1670
1903
|
if (this.regaInterval && this.regaPollEnabled && !this.cancelRegaPoll) {
|
|
1671
1904
|
//this.logger.trace('rega next poll in', this.regaInterval, 'seconds');
|
|
@@ -2108,6 +2341,7 @@ module.exports = function (RED) {
|
|
|
2108
2341
|
// a failed one is retried by rpcInitFailed (task 11)
|
|
2109
2342
|
if (this.ifaceTypes[iface].ping && !this.closing) {
|
|
2110
2343
|
this.rpcCheckInit(iface);
|
|
2344
|
+
this.startLiveness(iface);
|
|
2111
2345
|
}
|
|
2112
2346
|
|
|
2113
2347
|
if (iface === 'CUxD') {
|
|
@@ -2163,32 +2397,237 @@ module.exports = function (RED) {
|
|
|
2163
2397
|
}
|
|
2164
2398
|
|
|
2165
2399
|
const knownDevices = [];
|
|
2166
|
-
|
|
2400
|
+
const summary = {total: 0, added: 0, changed: 0, removed: 0};
|
|
2167
2401
|
devices.forEach((device) => {
|
|
2402
|
+
if (!device || typeof device.ADDRESS !== 'string' || !device.TYPE) {
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2168
2406
|
knownDevices.push(device.ADDRESS);
|
|
2169
|
-
|
|
2407
|
+
const known = this.metadata.devices[iface][device.ADDRESS];
|
|
2408
|
+
if (!known) {
|
|
2170
2409
|
this.newDevice(iface, device);
|
|
2171
|
-
|
|
2410
|
+
summary.added += 1;
|
|
2411
|
+
} else if (deviceChanged(known, device)) {
|
|
2412
|
+
// task 9: a TYPE/VERSION change since the cache was written
|
|
2413
|
+
this.newDevice(iface, device);
|
|
2414
|
+
summary.changed += 1;
|
|
2172
2415
|
}
|
|
2173
2416
|
});
|
|
2174
2417
|
|
|
2175
2418
|
Object.keys(this.metadata.devices[iface]).forEach((addr) => {
|
|
2176
2419
|
if (!knownDevices.includes(addr)) {
|
|
2177
2420
|
this.deleteDevice(iface, addr);
|
|
2178
|
-
|
|
2421
|
+
summary.removed += 1;
|
|
2179
2422
|
}
|
|
2180
2423
|
});
|
|
2181
2424
|
|
|
2182
|
-
|
|
2425
|
+
summary.total = Object.keys(this.metadata.devices[iface]).length;
|
|
2426
|
+
if (summary.added || summary.changed || summary.removed) {
|
|
2183
2427
|
this.saveMetadata();
|
|
2184
2428
|
}
|
|
2185
2429
|
|
|
2186
|
-
resolve();
|
|
2430
|
+
resolve(summary);
|
|
2187
2431
|
})
|
|
2188
2432
|
.catch(reject);
|
|
2189
2433
|
});
|
|
2190
2434
|
}
|
|
2191
2435
|
|
|
2436
|
+
/**
|
|
2437
|
+
* task 9: re-read the device table of every connected interface from its
|
|
2438
|
+
* process (the button in the connection node's dialog). New and changed
|
|
2439
|
+
* devices get their paramset descriptions fetched, vanished ones go.
|
|
2440
|
+
* @returns {Promise<object>} per interface: total, added, changed, removed - or an error text
|
|
2441
|
+
*/
|
|
2442
|
+
rereadDevices() {
|
|
2443
|
+
const ifaces = Object.keys(this.ifaceTypes).filter(
|
|
2444
|
+
(iface) => this.ifaceTypes[iface].enabled && this.ifaceTypes[iface].init && iface !== 'CUxD',
|
|
2445
|
+
);
|
|
2446
|
+
this.logger.info('re-reading device data:', ifaces.join(', '));
|
|
2447
|
+
return Promise.all(
|
|
2448
|
+
ifaces.map((iface) =>
|
|
2449
|
+
this.getDevices(iface)
|
|
2450
|
+
.then((summary) => [iface, summary])
|
|
2451
|
+
.catch((error) => [iface, {error: error.message || String(error)}]),
|
|
2452
|
+
),
|
|
2453
|
+
).then((entries) => {
|
|
2454
|
+
const summary = {};
|
|
2455
|
+
entries.forEach(([iface, result]) => {
|
|
2456
|
+
summary[iface] = result;
|
|
2457
|
+
});
|
|
2458
|
+
this.logger.info('device data re-read', JSON.stringify(summary));
|
|
2459
|
+
return summary;
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
/**
|
|
2464
|
+
* B-29: the liveness ping of an interface with a `pingInterval` (HmIP-RF).
|
|
2465
|
+
* hmipserver forgets its clients when it restarts and does not tell them:
|
|
2466
|
+
* `init` had succeeded, so nothing failed, and the events simply stopped
|
|
2467
|
+
* until the 600 s silence timeout of rpcCheckInit(). After `pingInterval`
|
|
2468
|
+
* seconds without an event a `ping` goes out; when no event (the PONG)
|
|
2469
|
+
* arrives within `pongTimeout` seconds the subscription is lost and `init`
|
|
2470
|
+
* is called again at once - task 11's retry then covers the time the
|
|
2471
|
+
* process is down. Only an event counts as proof; a device callback
|
|
2472
|
+
* (newDevices after a restart) triggers the ping instead. An interface
|
|
2473
|
+
* that never delivered an event since its init is not re-subscribed by
|
|
2474
|
+
* this (a callback address the CCU cannot reach would cost an init, and
|
|
2475
|
+
* hmipserver's full newDevices re-send, every 40 s).
|
|
2476
|
+
* @param iface
|
|
2477
|
+
*/
|
|
2478
|
+
startLiveness(iface) {
|
|
2479
|
+
const {pingInterval} = this.ifaceTypes[iface] || {};
|
|
2480
|
+
if (!pingInterval || !this.rpcPingEnabled || this.closing) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
this.stopLiveness(iface);
|
|
2485
|
+
this.liveness[iface] = {initAt: now(), pingAt: 0, timer: null, pongTimer: null, quietLogged: false};
|
|
2486
|
+
this.scheduleLiveness(iface, pingInterval * 1000);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
/** @param iface */
|
|
2490
|
+
stopLiveness(iface) {
|
|
2491
|
+
const state = this.liveness[iface];
|
|
2492
|
+
if (state) {
|
|
2493
|
+
clearTimeout(state.timer);
|
|
2494
|
+
clearTimeout(state.pongTimer);
|
|
2495
|
+
delete this.liveness[iface];
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/**
|
|
2500
|
+
* @param iface
|
|
2501
|
+
* @param {number} delay ms until the next check
|
|
2502
|
+
*/
|
|
2503
|
+
scheduleLiveness(iface, delay) {
|
|
2504
|
+
const state = this.liveness[iface];
|
|
2505
|
+
if (!state) {
|
|
2506
|
+
return;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
clearTimeout(state.timer);
|
|
2510
|
+
state.timer = setTimeout(() => this.livenessCheck(iface), Math.max(delay, 100));
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/** @param iface */
|
|
2514
|
+
livenessCheck(iface) {
|
|
2515
|
+
const state = this.liveness[iface];
|
|
2516
|
+
if (!state || this.closing) {
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
const {pingInterval} = this.ifaceTypes[iface];
|
|
2521
|
+
const sinceEvent = now() - (this.lastRealEvent[iface] || 0);
|
|
2522
|
+
if (sinceEvent < pingInterval * 1000) {
|
|
2523
|
+
// events arrive: the subscription is alive, no ping needed
|
|
2524
|
+
this.scheduleLiveness(iface, pingInterval * 1000 - sinceEvent);
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
this.livenessPing(iface);
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
/** @param iface */
|
|
2532
|
+
livenessPing(iface) {
|
|
2533
|
+
const state = this.liveness[iface];
|
|
2534
|
+
if (!state || state.pongTimer || this.closing) {
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
const {pongTimeout} = this.ifaceTypes[iface];
|
|
2539
|
+
clearTimeout(state.timer);
|
|
2540
|
+
state.pingAt = now();
|
|
2541
|
+
this.logger.debug('liveness ping', iface);
|
|
2542
|
+
state.pongTimer = setTimeout(() => this.livenessPongMissing(iface), pongTimeout * 1000);
|
|
2543
|
+
this.methodCall(iface, 'ping', ['nr'], {quiet: true}).catch((error) => {
|
|
2544
|
+
// the process itself does not answer: no need to wait for the PONG
|
|
2545
|
+
const current = this.liveness[iface];
|
|
2546
|
+
if (current && current.pongTimer) {
|
|
2547
|
+
clearTimeout(current.pongTimer);
|
|
2548
|
+
current.pongTimer = null;
|
|
2549
|
+
this.livenessLost(iface, 'ping failed: ' + (error && error.message ? error.message : error));
|
|
2550
|
+
}
|
|
2551
|
+
});
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/** the PONG did not arrive in time @param iface */
|
|
2555
|
+
livenessPongMissing(iface) {
|
|
2556
|
+
const state = this.liveness[iface];
|
|
2557
|
+
if (!state) {
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
state.pongTimer = null;
|
|
2562
|
+
const {pingInterval, pongTimeout} = this.ifaceTypes[iface];
|
|
2563
|
+
if (!this.lastRealEvent[iface] || this.lastRealEvent[iface] < state.initAt) {
|
|
2564
|
+
// never an event since the init: do not re-subscribe on that alone
|
|
2565
|
+
if (!state.quietLogged) {
|
|
2566
|
+
state.quietLogged = true;
|
|
2567
|
+
this.logger.debug(
|
|
2568
|
+
'liveness',
|
|
2569
|
+
iface,
|
|
2570
|
+
'no PONG within ' + pongTimeout + ' s and no event since init - not subscribing again',
|
|
2571
|
+
);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
this.scheduleLiveness(iface, pingInterval * 1000);
|
|
2575
|
+
return;
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
this.livenessLost(iface, 'no answer to a ping within ' + pongTimeout + ' s');
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
/**
|
|
2582
|
+
* The subscription is lost: init again at once.
|
|
2583
|
+
* @param iface
|
|
2584
|
+
* @param {string} reason
|
|
2585
|
+
*/
|
|
2586
|
+
livenessLost(iface, reason) {
|
|
2587
|
+
if (!this.liveness[iface] || this.closing) {
|
|
2588
|
+
return;
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
this.stopLiveness(iface);
|
|
2592
|
+
this.logger.warn(iface + ': ' + reason + ' - subscribing again');
|
|
2593
|
+
this.hadTimeout.add(iface);
|
|
2594
|
+
this.setIfaceStatus(iface, false, true);
|
|
2595
|
+
this.rpcInit(iface)
|
|
2596
|
+
.then(() => {
|
|
2597
|
+
this.logger.info(iface + ' subscribed again');
|
|
2598
|
+
this.rpcInitSucceeded(iface);
|
|
2599
|
+
})
|
|
2600
|
+
.catch((error) => this.rpcInitFailed(iface, error));
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/**
|
|
2604
|
+
* An event arrived (PONG included): the subscription delivers.
|
|
2605
|
+
* @param iface
|
|
2606
|
+
*/
|
|
2607
|
+
livenessEvent(iface) {
|
|
2608
|
+
this.lastRealEvent[iface] = now();
|
|
2609
|
+
const state = this.liveness[iface];
|
|
2610
|
+
if (state && state.pongTimer) {
|
|
2611
|
+
clearTimeout(state.pongTimer);
|
|
2612
|
+
state.pongTimer = null;
|
|
2613
|
+
this.scheduleLiveness(iface, this.ifaceTypes[iface].pingInterval * 1000);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
/**
|
|
2618
|
+
* A device callback (newDevices, deleteDevices, ...) arrived. After a
|
|
2619
|
+
* restart hmipserver sends newDevices to the read-back handler about
|
|
2620
|
+
* 16 s in, before any event: ask for the PONG at once instead of
|
|
2621
|
+
* waiting for the interval.
|
|
2622
|
+
* @param iface
|
|
2623
|
+
*/
|
|
2624
|
+
deviceCallback(iface) {
|
|
2625
|
+
const state = this.liveness[iface];
|
|
2626
|
+
if (state && !state.pongTimer && !this.closing) {
|
|
2627
|
+
this.livenessPing(iface);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2192
2631
|
/**
|
|
2193
2632
|
*
|
|
2194
2633
|
* @param iface
|
|
@@ -2198,15 +2637,21 @@ module.exports = function (RED) {
|
|
|
2198
2637
|
return;
|
|
2199
2638
|
}
|
|
2200
2639
|
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2640
|
+
// task 9 (old B-11): the watchdog used to return here for an interface without
|
|
2641
|
+
// devices in the cache, so such an interface never pinged and never re-initialised
|
|
2642
|
+
// after a CCU reboot. It pings now; it re-initialises only when the interface has
|
|
2643
|
+
// devices or has ever delivered an event (a PONG counts) - a process that answers
|
|
2644
|
+
// no ping at all is not asked to init every timeout.
|
|
2645
|
+
const hasDevices = Boolean(
|
|
2646
|
+
this.metadata.devices[iface] && Object.keys(this.metadata.devices[iface]).length > 0,
|
|
2647
|
+
);
|
|
2648
|
+
const mayReinit = hasDevices || Boolean(this.lastRealEvent[iface]);
|
|
2204
2649
|
|
|
2205
2650
|
clearTimeout(this.rpcPingTimer[iface]);
|
|
2206
2651
|
const pingTimeout = this.ifaceTypes[iface].pingTimeout || this.rpcPingTimeout;
|
|
2207
2652
|
const elapsed = Math.round((now() - (this.lastEvent[iface] || 0)) / 1000);
|
|
2208
2653
|
this.logger.debug('rpcCheckInit', iface, elapsed, pingTimeout);
|
|
2209
|
-
if (elapsed > pingTimeout) {
|
|
2654
|
+
if (elapsed > pingTimeout && mayReinit) {
|
|
2210
2655
|
this.hadTimeout.add(iface);
|
|
2211
2656
|
this.setIfaceStatus(iface, false);
|
|
2212
2657
|
this.logger.warn('ping timeout', iface, elapsed);
|
|
@@ -2358,12 +2803,10 @@ module.exports = function (RED) {
|
|
|
2358
2803
|
}
|
|
2359
2804
|
|
|
2360
2805
|
this.logger.debug('rpc <', protocol, method, JSON.stringify(parameters));
|
|
2361
|
-
|
|
2362
|
-
method = 'eventSingle';
|
|
2363
|
-
}
|
|
2806
|
+
const handler = method === 'event' ? 'eventSingle' : method;
|
|
2364
2807
|
|
|
2365
2808
|
if (isIterable(parameters)) {
|
|
2366
|
-
this.
|
|
2809
|
+
this.callRpcMethod(handler, err, parameters, callback);
|
|
2367
2810
|
} else {
|
|
2368
2811
|
this.logger.error(
|
|
2369
2812
|
'rpc <',
|
|
@@ -2373,7 +2816,7 @@ module.exports = function (RED) {
|
|
|
2373
2816
|
'params not iterable',
|
|
2374
2817
|
JSON.stringify(parameters),
|
|
2375
2818
|
);
|
|
2376
|
-
callback(null,
|
|
2819
|
+
callback(null, this.rpcDefaultAnswer(method));
|
|
2377
2820
|
}
|
|
2378
2821
|
});
|
|
2379
2822
|
});
|
|
@@ -2558,7 +3001,29 @@ module.exports = function (RED) {
|
|
|
2558
3001
|
this.metadata.types[iface] = {};
|
|
2559
3002
|
}
|
|
2560
3003
|
|
|
2561
|
-
|
|
3004
|
+
const known = this.metadata.devices[iface][device.ADDRESS];
|
|
3005
|
+
if (known && deviceChanged(known, device)) {
|
|
3006
|
+
// task 9 (#146, #181): a channel whose TYPE (or the device's VERSION/FIRMWARE)
|
|
3007
|
+
// changed in place kept its old datapoints; the paramset key changes with
|
|
3008
|
+
// it, so the description is fetched anew below, and the old type list
|
|
3009
|
+
// forgets the address
|
|
3010
|
+
this.logger.info(
|
|
3011
|
+
'device data changed',
|
|
3012
|
+
iface,
|
|
3013
|
+
device.ADDRESS,
|
|
3014
|
+
known.TYPE + '/' + (known.FIRMWARE || '-') + '/' + (known.VERSION || '-'),
|
|
3015
|
+
'->',
|
|
3016
|
+
device.TYPE + '/' + (device.FIRMWARE || '-') + '/' + (device.VERSION || '-'),
|
|
3017
|
+
);
|
|
3018
|
+
if (this.metadata.types[iface][known.TYPE]) {
|
|
3019
|
+
this.metadata.types[iface][known.TYPE] = this.metadata.types[iface][known.TYPE].filter(
|
|
3020
|
+
(addr) => addr !== device.ADDRESS,
|
|
3021
|
+
);
|
|
3022
|
+
if (this.metadata.types[iface][known.TYPE].length === 0) {
|
|
3023
|
+
delete this.metadata.types[iface][known.TYPE];
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
} else if (known) {
|
|
2562
3027
|
this.logger.trace('newDevice (already known)', iface, device.ADDRESS);
|
|
2563
3028
|
} else {
|
|
2564
3029
|
this.logger.debug('newDevice', iface, device.ADDRESS);
|
|
@@ -2607,10 +3072,11 @@ module.exports = function (RED) {
|
|
|
2607
3072
|
if (this.metadata.devices[iface]) {
|
|
2608
3073
|
Object.keys(this.metadata.devices[iface]).forEach((addr) => {
|
|
2609
3074
|
const dev = this.metadata.devices[iface][addr];
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
3075
|
+
// The CCU's own virtual remote control (HmIP-RCV-1, type HmIP-RCV-50) is listed
|
|
3076
|
+
// like any other device. It was left out since CCU3 firmware 3.43.15 (2019),
|
|
3077
|
+
// whose hmipserver failed on an answer that held it; current hmipservers do
|
|
3078
|
+
// not, and without it every init made hmipserver log a handleIDMigration
|
|
3079
|
+
// warning and send the whole virtual remote again with newDevices (task 13).
|
|
2614
3080
|
|
|
2615
3081
|
if (dev.TYPE === 'MULTI_MODE_INPUT_TRANSMITTER') {
|
|
2616
3082
|
//this.paramsQueuePush(iface, dev);
|
|
@@ -2684,98 +3150,208 @@ module.exports = function (RED) {
|
|
|
2684
3150
|
* @returns {*}
|
|
2685
3151
|
*/
|
|
2686
3152
|
getIfaceFromIdInit(idInit) {
|
|
3153
|
+
if (typeof idInit !== 'string') {
|
|
3154
|
+
return null;
|
|
3155
|
+
}
|
|
3156
|
+
|
|
2687
3157
|
if (idInit === 'CUxD') {
|
|
2688
3158
|
return idInit;
|
|
2689
3159
|
}
|
|
2690
3160
|
|
|
2691
3161
|
const match = idInit.match(/^nr_[\da-zA-Z]{6}_([a-zA-Z-]+)$/);
|
|
2692
|
-
return match && match[1];
|
|
3162
|
+
return match && this.ifaceTypes[match[1]] ? match[1] : null;
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
/**
|
|
3166
|
+
* B-32: the interface a callback call belongs to, or null with a debug line when the
|
|
3167
|
+
* first parameter is not one of our own init ids. Anything on the system can connect
|
|
3168
|
+
* to the callback ports on the loopback; such a call is answered, never thrown on.
|
|
3169
|
+
* @param {string} method
|
|
3170
|
+
* @param {Array} parameters
|
|
3171
|
+
* @returns {string|null}
|
|
3172
|
+
*/
|
|
3173
|
+
rpcIface(method, parameters) {
|
|
3174
|
+
const [idInit] = parameters;
|
|
3175
|
+
const iface = this.getIfaceFromIdInit(idInit);
|
|
3176
|
+
if (!iface) {
|
|
3177
|
+
this.logger.debug('rpc <', method, 'unknown interface id', JSON.stringify(idInit));
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
return iface;
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
/**
|
|
3184
|
+
* B-32: what a callback call is answered with when it cannot be handled.
|
|
3185
|
+
* @param {string} method
|
|
3186
|
+
* @returns {Array|string}
|
|
3187
|
+
*/
|
|
3188
|
+
rpcDefaultAnswer(method) {
|
|
3189
|
+
if (method === 'system.listMethods') {
|
|
3190
|
+
return Object.keys(this.rpcMethods);
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3193
|
+
if (method === 'listDevices' || method === 'system.multicall') {
|
|
3194
|
+
return [];
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
return '';
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
/**
|
|
3201
|
+
* B-32: run a callback handler so that a throw inside it is logged and answered
|
|
3202
|
+
* instead of ending the process (the RPC servers call us from an event emitter,
|
|
3203
|
+
* so an exception there is uncaught). The callback is answered exactly once.
|
|
3204
|
+
* @param {string} method the key in rpcMethods
|
|
3205
|
+
* @param {*} err
|
|
3206
|
+
* @param {Array} parameters
|
|
3207
|
+
* @param {function} callback
|
|
3208
|
+
*/
|
|
3209
|
+
callRpcMethod(method, err, parameters, callback) {
|
|
3210
|
+
let answered = false;
|
|
3211
|
+
const answer = (error, result) => {
|
|
3212
|
+
if (!answered) {
|
|
3213
|
+
answered = true;
|
|
3214
|
+
callback(error, result);
|
|
3215
|
+
}
|
|
3216
|
+
};
|
|
3217
|
+
|
|
3218
|
+
try {
|
|
3219
|
+
this.rpcMethods[method](err, parameters, answer);
|
|
3220
|
+
} catch (error) {
|
|
3221
|
+
this.logger.error(
|
|
3222
|
+
'rpc <',
|
|
3223
|
+
method,
|
|
3224
|
+
'failed:',
|
|
3225
|
+
error && error.message ? error.message : String(error),
|
|
3226
|
+
JSON.stringify(parameters),
|
|
3227
|
+
);
|
|
3228
|
+
answer(null, this.rpcDefaultAnswer(method === 'eventSingle' ? 'event' : method));
|
|
3229
|
+
}
|
|
2693
3230
|
}
|
|
2694
3231
|
|
|
2695
3232
|
get rpcMethods() {
|
|
2696
3233
|
return {
|
|
2697
3234
|
'system.listMethods': (_, parameters, callback) => {
|
|
2698
|
-
const
|
|
2699
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
2700
|
-
this.lastEvent[iface] = now();
|
|
2701
|
-
this.setIfaceStatus(iface, true);
|
|
3235
|
+
const iface = this.rpcIface('system.listMethods', parameters);
|
|
2702
3236
|
const res = Object.keys(this.rpcMethods);
|
|
3237
|
+
if (iface) {
|
|
3238
|
+
this.lastEvent[iface] = now();
|
|
3239
|
+
this.setIfaceStatus(iface, true);
|
|
3240
|
+
}
|
|
3241
|
+
|
|
2703
3242
|
this.logger.debug(' >', iface, 'system.listMethods', JSON.stringify(res));
|
|
2704
3243
|
callback(null, res);
|
|
2705
3244
|
},
|
|
2706
3245
|
setReadyConfig: (_, parameters, callback) => {
|
|
2707
|
-
const
|
|
2708
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
3246
|
+
const iface = this.rpcIface('setReadyConfig', parameters);
|
|
2709
3247
|
this.logger.debug(' >', iface, 'setReadyConfig ""');
|
|
2710
3248
|
callback(null, '');
|
|
2711
3249
|
},
|
|
2712
3250
|
updateDevice: (_, parameters, callback) => {
|
|
2713
|
-
const
|
|
2714
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
3251
|
+
const iface = this.rpcIface('updateDevice', parameters);
|
|
2715
3252
|
this.logger.debug(' >', iface, 'updateDevice ""');
|
|
2716
3253
|
callback(null, '');
|
|
3254
|
+
if (iface) {
|
|
3255
|
+
this.deviceCallback(iface);
|
|
3256
|
+
}
|
|
2717
3257
|
},
|
|
2718
3258
|
replaceDevice: (_, parameters, callback) => {
|
|
2719
|
-
const
|
|
2720
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
3259
|
+
const iface = this.rpcIface('replaceDevice', parameters);
|
|
2721
3260
|
this.logger.debug(' >', iface, 'replaceDevice ""');
|
|
2722
3261
|
callback(null, '');
|
|
3262
|
+
if (iface) {
|
|
3263
|
+
this.deviceCallback(iface);
|
|
3264
|
+
}
|
|
2723
3265
|
},
|
|
2724
3266
|
readdedDevice: (_, parameters, callback) => {
|
|
2725
|
-
const
|
|
2726
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
3267
|
+
const iface = this.rpcIface('readdedDevice', parameters);
|
|
2727
3268
|
this.logger.debug(' >', iface, 'readdedDevice ""');
|
|
2728
3269
|
callback(null, '');
|
|
3270
|
+
if (iface) {
|
|
3271
|
+
this.deviceCallback(iface);
|
|
3272
|
+
}
|
|
2729
3273
|
},
|
|
2730
3274
|
newDevices: (_, parameters, callback) => {
|
|
2731
|
-
const [
|
|
2732
|
-
const iface = this.
|
|
3275
|
+
const [, devices] = parameters;
|
|
3276
|
+
const iface = this.rpcIface('newDevices', parameters);
|
|
2733
3277
|
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
3278
|
+
let changed = false;
|
|
3279
|
+
if (iface && Array.isArray(devices)) {
|
|
3280
|
+
devices.forEach((device) => {
|
|
3281
|
+
if (device && typeof device.ADDRESS === 'string' && device.TYPE) {
|
|
3282
|
+
this.newDevice(iface, device);
|
|
3283
|
+
changed = true;
|
|
3284
|
+
} else {
|
|
3285
|
+
this.logger.warn(
|
|
3286
|
+
'newDevices',
|
|
3287
|
+
iface,
|
|
3288
|
+
'skipping malformed entry',
|
|
3289
|
+
JSON.stringify(device),
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
});
|
|
3293
|
+
}
|
|
2737
3294
|
|
|
2738
3295
|
this.logger.debug(' >', iface, 'newDevices ""');
|
|
2739
3296
|
callback(null, '');
|
|
2740
3297
|
|
|
2741
|
-
|
|
3298
|
+
if (changed) {
|
|
3299
|
+
this.saveMetadata();
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
if (iface) {
|
|
3303
|
+
this.deviceCallback(iface);
|
|
3304
|
+
}
|
|
2742
3305
|
},
|
|
2743
3306
|
deleteDevices: (_, parameters, callback) => {
|
|
2744
|
-
const [
|
|
2745
|
-
const iface = this.
|
|
3307
|
+
const [, devices] = parameters;
|
|
3308
|
+
const iface = this.rpcIface('deleteDevices', parameters);
|
|
2746
3309
|
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
3310
|
+
let changed = false;
|
|
3311
|
+
if (iface && Array.isArray(devices) && this.metadata.devices[iface]) {
|
|
3312
|
+
devices.forEach((device) => {
|
|
3313
|
+
if (typeof device === 'string' && this.metadata.devices[iface][device]) {
|
|
3314
|
+
this.deleteDevice(iface, device);
|
|
3315
|
+
changed = true;
|
|
3316
|
+
}
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
2750
3319
|
|
|
2751
3320
|
this.logger.debug(' >', iface, 'deleteDevices ""');
|
|
2752
3321
|
callback(null, '');
|
|
2753
3322
|
|
|
2754
|
-
|
|
3323
|
+
if (changed) {
|
|
3324
|
+
this.saveMetadata();
|
|
3325
|
+
}
|
|
3326
|
+
|
|
3327
|
+
if (iface) {
|
|
3328
|
+
this.deviceCallback(iface);
|
|
3329
|
+
}
|
|
2755
3330
|
},
|
|
2756
3331
|
listDevices: (_, parameters, callback) => {
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
3332
|
+
const iface = this.rpcIface('listDevices', parameters);
|
|
3333
|
+
let res = [];
|
|
3334
|
+
if (iface) {
|
|
3335
|
+
this.lastEvent[iface] = now();
|
|
3336
|
+
this.setIfaceStatus(iface, true);
|
|
3337
|
+
res = this.listDevices(iface) || [];
|
|
3338
|
+
}
|
|
3339
|
+
|
|
2762
3340
|
this.logger.debug(' >', iface, 'listDevices', JSON.stringify(res));
|
|
2763
3341
|
callback(null, res);
|
|
2764
3342
|
},
|
|
2765
3343
|
event: (_, parameters, callback) => {
|
|
2766
|
-
const
|
|
2767
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
3344
|
+
const iface = this.rpcIface('event', parameters);
|
|
2768
3345
|
this.logger.debug(' >', iface, 'event ""');
|
|
2769
3346
|
this.publishEvent(parameters);
|
|
2770
3347
|
callback(null, '');
|
|
2771
3348
|
},
|
|
2772
3349
|
eventSingle: (_, parameters, callback) => {
|
|
2773
|
-
const
|
|
2774
|
-
const iface = this.getIfaceFromIdInit(idInit);
|
|
3350
|
+
const iface = this.rpcIface('event', parameters);
|
|
2775
3351
|
this.logger.debug(' >', iface, 'event ""');
|
|
2776
3352
|
this.publishEvent(parameters);
|
|
2777
3353
|
|
|
2778
|
-
if (parameters[2] !== 'PONG') {
|
|
3354
|
+
if (iface && parameters[2] !== 'PONG') {
|
|
2779
3355
|
if (this.rxCounters[iface]) {
|
|
2780
3356
|
this.rxCounters[iface] += 1;
|
|
2781
3357
|
} else {
|
|
@@ -2796,8 +3372,8 @@ module.exports = function (RED) {
|
|
|
2796
3372
|
if (isIterable(parameters[0])) {
|
|
2797
3373
|
parameters[0].forEach((call) => {
|
|
2798
3374
|
if (call && call.methodName === 'event') {
|
|
2799
|
-
queue.push(call);
|
|
2800
3375
|
if (isIterable(call.params)) {
|
|
3376
|
+
queue.push(call);
|
|
2801
3377
|
const [idInit, , datapoint, value] = call.params;
|
|
2802
3378
|
if (datapoint !== 'PONG') {
|
|
2803
3379
|
pong = false;
|
|
@@ -2819,16 +3395,32 @@ module.exports = function (RED) {
|
|
|
2819
3395
|
}
|
|
2820
3396
|
}
|
|
2821
3397
|
|
|
2822
|
-
iface = this.getIfaceFromIdInit(idInit);
|
|
3398
|
+
iface = this.getIfaceFromIdInit(idInit) || iface;
|
|
3399
|
+
} else {
|
|
3400
|
+
this.logger.debug(
|
|
3401
|
+
'rpc <',
|
|
3402
|
+
'event',
|
|
3403
|
+
'params not iterable',
|
|
3404
|
+
JSON.stringify(call.params),
|
|
3405
|
+
);
|
|
2823
3406
|
}
|
|
2824
3407
|
|
|
2825
3408
|
result.push('');
|
|
2826
|
-
} else if (
|
|
3409
|
+
} else if (
|
|
3410
|
+
call &&
|
|
3411
|
+
call.methodName !== 'system.multicall' &&
|
|
3412
|
+
this.rpcMethods[call.methodName]
|
|
3413
|
+
) {
|
|
2827
3414
|
pong = false;
|
|
2828
3415
|
if (isIterable(call.params)) {
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
3416
|
+
let answered = false;
|
|
3417
|
+
this.callRpcMethod(call.methodName, null, call.params, (_, res) => {
|
|
3418
|
+
answered = true;
|
|
3419
|
+
result.push(res);
|
|
3420
|
+
});
|
|
3421
|
+
if (!answered) {
|
|
3422
|
+
result.push(this.rpcDefaultAnswer(call.methodName));
|
|
3423
|
+
}
|
|
2832
3424
|
} else {
|
|
2833
3425
|
this.logger.error(
|
|
2834
3426
|
'rpc <',
|
|
@@ -2836,7 +3428,11 @@ module.exports = function (RED) {
|
|
|
2836
3428
|
'params not iterable',
|
|
2837
3429
|
JSON.stringify(call.params),
|
|
2838
3430
|
);
|
|
3431
|
+
result.push(this.rpcDefaultAnswer(call.methodName));
|
|
2839
3432
|
}
|
|
3433
|
+
} else {
|
|
3434
|
+
// B-32: an unknown or nested method keeps the answer's shape
|
|
3435
|
+
result.push('');
|
|
2840
3436
|
}
|
|
2841
3437
|
});
|
|
2842
3438
|
queue.forEach((call) => {
|
|
@@ -3063,7 +3659,14 @@ module.exports = function (RED) {
|
|
|
3063
3659
|
const [idInit, channel, datapoint, payload] = parameters;
|
|
3064
3660
|
const iface = this.getIfaceFromIdInit(idInit);
|
|
3065
3661
|
|
|
3662
|
+
if (!iface || typeof channel !== 'string' || typeof datapoint !== 'string') {
|
|
3663
|
+
// B-32: not one of our interfaces, or not an event's shape - nothing to publish
|
|
3664
|
+
this.logger.debug('rpc <', 'event', 'ignored', JSON.stringify(parameters));
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3066
3668
|
this.lastEvent[iface] = now();
|
|
3669
|
+
this.livenessEvent(iface);
|
|
3067
3670
|
if (this.hadTimeout.has(iface)) {
|
|
3068
3671
|
this.setIfaceStatus(iface, true);
|
|
3069
3672
|
}
|
|
@@ -3371,7 +3974,14 @@ module.exports = function (RED) {
|
|
|
3371
3974
|
const datapointName = iface + '.' + address + '.' + datapoint;
|
|
3372
3975
|
const currentValue = this.values[datapointName] && this.values[datapointName].value;
|
|
3373
3976
|
const cache = this.values[datapointName] && this.values[datapointName].cache;
|
|
3374
|
-
|
|
3977
|
+
// B-19: a cached value that is the echo of our own last write does not
|
|
3978
|
+
// prove the actuator is there - a blind moved by hand keeps its control
|
|
3979
|
+
// channel's set point (#151). Only a value from another source dedupes:
|
|
3980
|
+
// one that differs from what we wrote last, or one reported before it.
|
|
3981
|
+
const lastWrite = this.lastWrite[datapointName];
|
|
3982
|
+
const cachedTs = this.values[datapointName] && this.values[datapointName].ts;
|
|
3983
|
+
const ownEcho = Boolean(lastWrite) && lastWrite.value === currentValue && cachedTs > lastWrite.ts;
|
|
3984
|
+
if (force || value !== currentValue || cache || ownEcho || datapoint.startsWith('PRESS_')) {
|
|
3375
3985
|
this.setValueQueue.push({iface, address, datapoint, value, burst, resolve, reject});
|
|
3376
3986
|
this.setValueShiftQueue();
|
|
3377
3987
|
} else {
|
|
@@ -3433,8 +4043,16 @@ module.exports = function (RED) {
|
|
|
3433
4043
|
* @returns {Promise<any>}
|
|
3434
4044
|
*/
|
|
3435
4045
|
setValue(iface, address, datapoint, value, burst) {
|
|
4046
|
+
this.lastWrite[iface + '.' + address + '.' + datapoint] = {value, ts: now()};
|
|
3436
4047
|
const device = this.metadata.devices[iface] && this.metadata.devices[iface][address];
|
|
3437
4048
|
const description = this.paramsetDescriptions[this.paramsetName(iface, device, 'VALUES')];
|
|
4049
|
+
if (isPartyWrite(datapoint, description)) {
|
|
4050
|
+
// an HmIP thermostat takes PARTY_TIME_START, PARTY_TIME_END and
|
|
4051
|
+
// PARTY_SET_POINT_TEMPERATURE only together - one alone resets the
|
|
4052
|
+
// other two (#156, #161): write all three in one putParamset
|
|
4053
|
+
return this.setPartyValues(iface, address, datapoint, value);
|
|
4054
|
+
}
|
|
4055
|
+
|
|
3438
4056
|
const combined = combinedParameterValue(datapoint, value, description);
|
|
3439
4057
|
if (combined !== null) {
|
|
3440
4058
|
// HmIP actuators accept but ignore a lone LEVEL_2 write; the CCU
|
|
@@ -3477,6 +4095,80 @@ module.exports = function (RED) {
|
|
|
3477
4095
|
});
|
|
3478
4096
|
}
|
|
3479
4097
|
|
|
4098
|
+
/**
|
|
4099
|
+
* Party mode on an HmIP thermostat (task 2, #156, #161): the written
|
|
4100
|
+
* datapoint merged with the current values of the other two - our own
|
|
4101
|
+
* last write when it is newer than the cache, else the cached value,
|
|
4102
|
+
* else the values read from the interface process (hmipserver answers
|
|
4103
|
+
* getParamset from its own state) - into one putParamset on VALUES.
|
|
4104
|
+
* @param {string} iface
|
|
4105
|
+
* @param {string} address the channel
|
|
4106
|
+
* @param {string} datapoint the one being written
|
|
4107
|
+
* @param {*} value
|
|
4108
|
+
* @returns {Promise<any>}
|
|
4109
|
+
*/
|
|
4110
|
+
setPartyValues(iface, address, datapoint, value) {
|
|
4111
|
+
const current = {};
|
|
4112
|
+
for (const name of PARTY_DATAPOINTS) {
|
|
4113
|
+
if (name === datapoint) {
|
|
4114
|
+
continue;
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
const key = iface + '.' + address + '.' + name;
|
|
4118
|
+
const cached = this.values[key];
|
|
4119
|
+
const written = this.lastWrite[key];
|
|
4120
|
+
if (written && (!cached || !(cached.ts > written.ts))) {
|
|
4121
|
+
current[name] = written.value;
|
|
4122
|
+
} else if (cached && cached.value !== undefined) {
|
|
4123
|
+
current[name] = cached.value;
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
let merged = mergePartyValues(datapoint, value, current);
|
|
4128
|
+
const read =
|
|
4129
|
+
merged.missing.length > 0
|
|
4130
|
+
? this.methodCall(iface, 'getParamset', [address, 'VALUES']).then((values) => {
|
|
4131
|
+
merged.missing.forEach((name) => {
|
|
4132
|
+
if (values && values[name] !== undefined) {
|
|
4133
|
+
current[name] = values[name];
|
|
4134
|
+
}
|
|
4135
|
+
});
|
|
4136
|
+
merged = mergePartyValues(datapoint, value, current);
|
|
4137
|
+
})
|
|
4138
|
+
: Promise.resolve();
|
|
4139
|
+
|
|
4140
|
+
return read.then(() => {
|
|
4141
|
+
if (merged.missing.length > 0) {
|
|
4142
|
+
throw new Error(
|
|
4143
|
+
'party mode: no current value for ' +
|
|
4144
|
+
merged.missing.join(', ') +
|
|
4145
|
+
' on ' +
|
|
4146
|
+
iface +
|
|
4147
|
+
' ' +
|
|
4148
|
+
address,
|
|
4149
|
+
);
|
|
4150
|
+
}
|
|
4151
|
+
|
|
4152
|
+
const parameters = {};
|
|
4153
|
+
Object.keys(merged.values).forEach((name) => {
|
|
4154
|
+
parameters[name] = this.paramCast(iface, address, 'VALUES', name, merged.values[name]);
|
|
4155
|
+
this.lastWrite[iface + '.' + address + '.' + name] = {value: merged.values[name], ts: now()};
|
|
4156
|
+
});
|
|
4157
|
+
this.logger.debug('setValue', datapoint, value, '-> putParamset VALUES', JSON.stringify(parameters));
|
|
4158
|
+
return this.methodCall(iface, 'putParamset', [address, 'VALUES', parameters]).catch((error) => {
|
|
4159
|
+
this.logger.error(
|
|
4160
|
+
'rpc >',
|
|
4161
|
+
iface,
|
|
4162
|
+
'putParamset',
|
|
4163
|
+
JSON.stringify([address, 'VALUES', parameters]),
|
|
4164
|
+
'<',
|
|
4165
|
+
error,
|
|
4166
|
+
);
|
|
4167
|
+
throw error;
|
|
4168
|
+
});
|
|
4169
|
+
});
|
|
4170
|
+
}
|
|
4171
|
+
|
|
3480
4172
|
/**
|
|
3481
4173
|
*
|
|
3482
4174
|
* @param id
|
|
@@ -3540,5 +4232,10 @@ module.exports = function (RED) {
|
|
|
3540
4232
|
}
|
|
3541
4233
|
}
|
|
3542
4234
|
|
|
3543
|
-
RED.nodes.registerType('ccu-connection', CcuConnectionNode, {
|
|
4235
|
+
RED.nodes.registerType('ccu-connection', CcuConnectionNode, {
|
|
4236
|
+
credentials: {
|
|
4237
|
+
password: {type: 'password'},
|
|
4238
|
+
metaToken: {type: 'password'},
|
|
4239
|
+
},
|
|
4240
|
+
});
|
|
3544
4241
|
};
|