node-red-contrib-ccu 4.2.0 → 4.4.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/.prettierignore +3 -0
- package/CHANGELOG.md +140 -0
- package/HANDOFF.md +46 -10
- package/README.md +71 -0
- package/ROADMAP.md +130 -47
- package/nodes/ccu-alexa.html +3 -1
- package/nodes/ccu-connection.html +51 -0
- package/nodes/ccu-connection.js +323 -16
- package/nodes/ccu-display.js +29 -13
- package/nodes/ccu-get-value.html +3 -1
- package/nodes/ccu-mqtt.html +21 -4
- package/nodes/ccu-mqtt.js +126 -102
- package/nodes/ccu-poll.js +6 -0
- package/nodes/ccu-rpc-event.html +6 -0
- package/nodes/ccu-rpc-event.js +31 -12
- package/nodes/ccu-set-value.html +38 -0
- package/nodes/ccu-set-value.js +59 -235
- package/nodes/ccu-signal.html +55 -1
- package/nodes/ccu-signal.js +52 -9
- package/nodes/ccu-switch.html +17 -4
- package/nodes/ccu-switch.js +28 -4
- package/nodes/ccu-sysvar.html +40 -1
- package/nodes/ccu-sysvar.js +78 -8
- package/nodes/ccu-value.html +27 -1
- package/nodes/ccu-value.js +69 -19
- package/nodes/lib/cast.js +59 -14
- package/nodes/lib/channelfilter.js +136 -0
- package/nodes/lib/dynconfig.js +81 -0
- package/nodes/lib/localccu.js +81 -0
- package/nodes/lib/message.js +6 -2
- package/nodes/lib/metaprovider.js +716 -0
- package/nodes/lib/valuestatus.js +145 -0
- package/nodes/locales/de/ccu-display.html +17 -0
- package/nodes/locales/de/ccu-rpc-event.json +1 -0
- package/nodes/locales/de/ccu-sysvar.json +3 -2
- package/nodes/locales/de/ccu-value.json +2 -1
- package/nodes/locales/en-US/ccu-display.html +17 -0
- package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
- package/nodes/locales/en-US/ccu-switch.html +12 -0
- package/nodes/locales/en-US/ccu-sysvar.json +3 -2
- package/nodes/locales/en-US/ccu-value.json +2 -1
- package/package.json +1 -1
- package/patch-certainonly.py +51 -0
- package/roadmap-archive/B-17.md +106 -0
- package/roadmap-archive/B-2.md +69 -0
- package/roadmap-archive/B-3.md +57 -0
- package/roadmap-archive/B-4.md +44 -0
- package/roadmap-archive/B-5.md +57 -0
- package/roadmap-archive/README.md +14 -0
package/nodes/ccu-connection.js
CHANGED
|
@@ -9,10 +9,12 @@ const {castValue, castSysvar} = require('./lib/cast.js');
|
|
|
9
9
|
const {combinedParameterValue} = require('./lib/combined.js');
|
|
10
10
|
const {createMessage} = require('./lib/message.js');
|
|
11
11
|
const {topicReplace} = require('./lib/topic.js');
|
|
12
|
+
const {isLocalCcu} = require('./lib/localccu.js');
|
|
12
13
|
const {bestMatch} = require('./lib/similarity.js');
|
|
13
14
|
const nextport = require('./lib/nextport.js');
|
|
14
15
|
const hmDiscover = require('./lib/discover.js');
|
|
15
16
|
const {Rega} = require('homematic-rega'); // ES module - require(esm) needs Node >= 20.19 / >= 22.12
|
|
17
|
+
const metaProvider = require('./lib/metaprovider.js');
|
|
16
18
|
const xmlrpc = require('homematic-xmlrpc');
|
|
17
19
|
const binrpc = require('binrpc');
|
|
18
20
|
|
|
@@ -407,15 +409,13 @@ module.exports = function (RED) {
|
|
|
407
409
|
|
|
408
410
|
this.checkDuplicateConfig(config);
|
|
409
411
|
|
|
410
|
-
this.
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
} catch {}
|
|
412
|
+
// B-4: this used to grep /etc/lighttpd/conf.d/proxy.conf for the
|
|
413
|
+
// direct port. That file is a bare include on current firmware, so
|
|
414
|
+
// the check never matched and every local install went through the
|
|
415
|
+
// proxy. Ask the kernel for a listener instead.
|
|
416
|
+
this.isLocal = isLocalCcu(config.host);
|
|
417
|
+
if (this.isLocal) {
|
|
418
|
+
this.logger.info('local connection on ccu >= v3.41 detected');
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
this.ifaceTypes = {
|
|
@@ -529,6 +529,10 @@ module.exports = function (RED) {
|
|
|
529
529
|
this.rpcBinPort = Number.parseInt(config.rpcBinPort, 10);
|
|
530
530
|
this.rpcXmlPort = Number.parseInt(config.rpcXmlPort, 10);
|
|
531
531
|
this.rpcPingTimeout = Number.parseInt(config.rpcPingTimeout, 10) || 60;
|
|
532
|
+
// #44: opt out of the periodic ping/timeout supervision. Off means
|
|
533
|
+
// no pings and no timeout-triggered re-init for this connection -
|
|
534
|
+
// reconnecting after a CCU restart is then up to the CCU.
|
|
535
|
+
this.rpcPingEnabled = config.rpcPing === undefined ? true : Boolean(config.rpcPing);
|
|
532
536
|
this.rpcPingTimer = {};
|
|
533
537
|
this.ifaceStatus = {};
|
|
534
538
|
this.serverError = {};
|
|
@@ -539,6 +543,19 @@ module.exports = function (RED) {
|
|
|
539
543
|
|
|
540
544
|
this.regaEnabled = config.regaEnabled;
|
|
541
545
|
this.regaPollEnabled = config.regaPoll;
|
|
546
|
+
// B-17 openccu-lite: the metadata api sits behind the box's
|
|
547
|
+
// lighttpd, i.e. on the plain http(s) port. The field exists for
|
|
548
|
+
// reverse proxies and tests, it is empty in every normal install.
|
|
549
|
+
this.tlsEnabled = Boolean(config.tls);
|
|
550
|
+
this.inSecure = Boolean(config.inSecure);
|
|
551
|
+
this.metaPort = Number.parseInt(config.metaPort, 10) || (config.tls ? 443 : 80);
|
|
552
|
+
this.metaToken = config.metaToken || metaProvider.readLocalToken();
|
|
553
|
+
this.metaMode = false;
|
|
554
|
+
this.meta = null;
|
|
555
|
+
// #167: minutes between re-reads of channel names, rooms and
|
|
556
|
+
// functions. 0 switches it off.
|
|
557
|
+
this.regaMetaInterval =
|
|
558
|
+
config.regaMetaInterval === undefined ? 15 : Number.parseInt(config.regaMetaInterval, 10) || 0;
|
|
542
559
|
this.regaInterval = Number.parseInt(config.regaInterval, 10);
|
|
543
560
|
this.hadTimeout = new Set();
|
|
544
561
|
|
|
@@ -632,10 +649,27 @@ module.exports = function (RED) {
|
|
|
632
649
|
});
|
|
633
650
|
|
|
634
651
|
if (config.regaEnabled) {
|
|
635
|
-
this.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
652
|
+
this.lastRegaDataRefresh = now();
|
|
653
|
+
// B-17: ask the box once whether it speaks the openccu-lite
|
|
654
|
+
// metadata api. It does not on a CCU/RaspberryMatic/OpenCCU,
|
|
655
|
+
// where everything below runs exactly as before.
|
|
656
|
+
this.lastMetaProbe = now();
|
|
657
|
+
this.detectMeta()
|
|
658
|
+
.then((info) => {
|
|
659
|
+
if (info) {
|
|
660
|
+
return this.startMeta(info);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return this.getRegaData().then(() => {
|
|
664
|
+
this.regaPoll();
|
|
665
|
+
});
|
|
666
|
+
})
|
|
667
|
+
.catch((error) => {
|
|
668
|
+
this.logger.error('name sync ' + error.message);
|
|
669
|
+
})
|
|
670
|
+
.then(() => {
|
|
671
|
+
this.initIfaces(config);
|
|
672
|
+
});
|
|
639
673
|
} else {
|
|
640
674
|
this.initIfaces(config);
|
|
641
675
|
}
|
|
@@ -814,6 +848,8 @@ module.exports = function (RED) {
|
|
|
814
848
|
regaChannels: this.regaChannels,
|
|
815
849
|
channelRooms: this.channelRooms,
|
|
816
850
|
channelFunctions: this.channelFunctions,
|
|
851
|
+
rooms: this.rooms,
|
|
852
|
+
functions: this.functions,
|
|
817
853
|
groups: this.groups,
|
|
818
854
|
sysvar: this.sysvar,
|
|
819
855
|
program: this.program,
|
|
@@ -877,6 +913,16 @@ module.exports = function (RED) {
|
|
|
877
913
|
this.channelRooms = regadata.channelRooms;
|
|
878
914
|
this.channelFunctions = regadata.channelFunctions;
|
|
879
915
|
this.groups = regadata.groups;
|
|
916
|
+
// written since 4.4.0 - the editor's room/function pickers
|
|
917
|
+
// were empty until the first successful sync before that
|
|
918
|
+
if (Array.isArray(regadata.rooms)) {
|
|
919
|
+
this.rooms = regadata.rooms;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (Array.isArray(regadata.functions)) {
|
|
923
|
+
this.functions = regadata.functions;
|
|
924
|
+
}
|
|
925
|
+
|
|
880
926
|
/*
|
|
881
927
|
this.sysvar = regadata.sysvar;
|
|
882
928
|
Object.keys(this.sysvar).forEach(s => {
|
|
@@ -986,6 +1032,12 @@ module.exports = function (RED) {
|
|
|
986
1032
|
this.cancelRegaPoll = true;
|
|
987
1033
|
clearTimeout(this.regaPollTimeout);
|
|
988
1034
|
|
|
1035
|
+
if (this.meta) {
|
|
1036
|
+
this.logger.debug('stop metadata event stream');
|
|
1037
|
+
this.meta.stop();
|
|
1038
|
+
this.meta = null;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
989
1041
|
Object.keys(this.rpcPingTimer).forEach((iface) => {
|
|
990
1042
|
this.logger.debug('clear rpcPingTimer', iface);
|
|
991
1043
|
clearTimeout(this.rpcPingTimer[iface]);
|
|
@@ -1065,7 +1117,165 @@ module.exports = function (RED) {
|
|
|
1065
1117
|
.then(() => this.getRegaFunctions())
|
|
1066
1118
|
.then(() => this.getRegaValues())
|
|
1067
1119
|
.then(() => this.getGroupsData())
|
|
1068
|
-
.catch(
|
|
1120
|
+
.catch((error) => {
|
|
1121
|
+
this.logger.error(error);
|
|
1122
|
+
this.recheckMeta();
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* B-17: is this box an openccu-lite? `GET /api/meta/v1/version` needs
|
|
1128
|
+
* no credential and answers only there - a CCU replies 404 or HTML.
|
|
1129
|
+
* Never rejects; an unreachable box is simply "no".
|
|
1130
|
+
* @returns {Promise<object|null>}
|
|
1131
|
+
*/
|
|
1132
|
+
detectMeta() {
|
|
1133
|
+
return metaProvider.detect({
|
|
1134
|
+
host: this.host,
|
|
1135
|
+
port: this.metaPort,
|
|
1136
|
+
tls: Boolean(this.tlsEnabled),
|
|
1137
|
+
insecure: Boolean(this.inSecure),
|
|
1138
|
+
logger: this.logger,
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* Take names, rooms and functions from the openccu-lite metadata api
|
|
1144
|
+
* instead of from the ReGaHSS. Resolves as soon as the first snapshot
|
|
1145
|
+
* has been applied (or after 5 s, so a missing credential does not hold
|
|
1146
|
+
* up the interfaces).
|
|
1147
|
+
* @param {object} info the /version document
|
|
1148
|
+
* @returns {Promise<any>}
|
|
1149
|
+
*/
|
|
1150
|
+
startMeta(info) {
|
|
1151
|
+
this.metaMode = true;
|
|
1152
|
+
this.lastMetaProbe = now();
|
|
1153
|
+
clearTimeout(this.regaPollTimeout);
|
|
1154
|
+
this.logger.info(
|
|
1155
|
+
'openccu-lite detected (' +
|
|
1156
|
+
(info.implementation || 'meta api') +
|
|
1157
|
+
', api version ' +
|
|
1158
|
+
info.version +
|
|
1159
|
+
') - names, rooms and functions come from its metadata api',
|
|
1160
|
+
);
|
|
1161
|
+
this.logger.info(
|
|
1162
|
+
'this box has no ReGaHSS: system variables, programs and HM-Script are not available (sysvar, program, poll and script nodes stay idle)',
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
if (info.version > 1) {
|
|
1166
|
+
this.logger.warn(
|
|
1167
|
+
'the box speaks metadata api version ' +
|
|
1168
|
+
info.version +
|
|
1169
|
+
', this version of node-red-contrib-ccu implements version 1',
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
if (!this.metaToken) {
|
|
1174
|
+
this.logger.warn(
|
|
1175
|
+
'no metadata api token: create one on the box (Users page) and enter it in the connection node, or run on the box where ' +
|
|
1176
|
+
metaProvider.LOCAL_TOKEN_FILE +
|
|
1177
|
+
' is readable. Without it the nodes work with addresses only.',
|
|
1178
|
+
);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return new Promise((resolve) => {
|
|
1182
|
+
const done = setTimeout(resolve, 5000);
|
|
1183
|
+
this.meta = new metaProvider.MetaProvider({
|
|
1184
|
+
host: this.host,
|
|
1185
|
+
port: this.metaPort,
|
|
1186
|
+
tls: Boolean(this.tlsEnabled),
|
|
1187
|
+
insecure: Boolean(this.inSecure),
|
|
1188
|
+
token: this.metaToken,
|
|
1189
|
+
logger: this.logger,
|
|
1190
|
+
onNames: (names) => {
|
|
1191
|
+
this.applyMetaNames(names);
|
|
1192
|
+
clearTimeout(done);
|
|
1193
|
+
resolve();
|
|
1194
|
+
},
|
|
1195
|
+
onStatus: (connected) => {
|
|
1196
|
+
this.setIfaceStatus('ReGaHSS', connected);
|
|
1197
|
+
},
|
|
1198
|
+
onGone: () => {
|
|
1199
|
+
this.metaMode = false;
|
|
1200
|
+
this.meta = null;
|
|
1201
|
+
this.lastMetaProbe = 0;
|
|
1202
|
+
this.logger.warn('this box no longer answers as openccu-lite - detecting again');
|
|
1203
|
+
this.detectMeta().then((again) => {
|
|
1204
|
+
if (again) {
|
|
1205
|
+
return this.startMeta(again);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
return this.getRegaData().then(() => this.regaPoll());
|
|
1209
|
+
});
|
|
1210
|
+
},
|
|
1211
|
+
});
|
|
1212
|
+
this.meta.start();
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
/**
|
|
1217
|
+
* The metadata api's answer, in the shape the rest of the code expects
|
|
1218
|
+
* from the ReGa: names by address, rooms and functions as arrays of
|
|
1219
|
+
* names. Called for the snapshot and again for every change event.
|
|
1220
|
+
* @param {object} names
|
|
1221
|
+
*/
|
|
1222
|
+
applyMetaNames(names) {
|
|
1223
|
+
this.channelNames = names.channelNames;
|
|
1224
|
+
this.channelRooms = names.channelRooms;
|
|
1225
|
+
this.channelFunctions = names.channelFunctions;
|
|
1226
|
+
this.rooms = names.rooms;
|
|
1227
|
+
this.functions = names.functions;
|
|
1228
|
+
this.logger.debug(
|
|
1229
|
+
'meta revision ' +
|
|
1230
|
+
names.revision +
|
|
1231
|
+
': ' +
|
|
1232
|
+
Object.keys(names.channelNames).length +
|
|
1233
|
+
' names, ' +
|
|
1234
|
+
names.rooms.length +
|
|
1235
|
+
' rooms, ' +
|
|
1236
|
+
names.functions.length +
|
|
1237
|
+
' functions',
|
|
1238
|
+
);
|
|
1239
|
+
this.saveRegadata();
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* The ReGa did not answer. Maybe this box became an openccu-lite (a
|
|
1244
|
+
* restored backup, a firmware swap) - probe again, at most every five
|
|
1245
|
+
* minutes, so a box that changes underneath a running Node-RED is
|
|
1246
|
+
* picked up without a redeploy.
|
|
1247
|
+
*/
|
|
1248
|
+
recheckMeta() {
|
|
1249
|
+
if (this.metaMode || this.cancelRegaPoll || this.metaRecheckPending) {
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (now() - (this.lastMetaProbe || 0) < 300000) {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
this.lastMetaProbe = now();
|
|
1258
|
+
this.metaRecheckPending = true;
|
|
1259
|
+
this.detectMeta()
|
|
1260
|
+
.then((info) => {
|
|
1261
|
+
this.metaRecheckPending = false;
|
|
1262
|
+
if (info && !this.metaMode) {
|
|
1263
|
+
return this.startMeta(info);
|
|
1264
|
+
}
|
|
1265
|
+
})
|
|
1266
|
+
.catch(() => {
|
|
1267
|
+
this.metaRecheckPending = false;
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* The error a ReGa-only feature answers with on openccu-lite. The
|
|
1273
|
+
* nodes stay in the palette and in the flow, every message gets this.
|
|
1274
|
+
* @param {string} feature
|
|
1275
|
+
* @returns {Error}
|
|
1276
|
+
*/
|
|
1277
|
+
regaMissingError(feature) {
|
|
1278
|
+
return new Error(feature + ' are not available on this box (openccu-lite has no ReGaHSS)');
|
|
1069
1279
|
}
|
|
1070
1280
|
|
|
1071
1281
|
/**
|
|
@@ -1084,7 +1294,14 @@ module.exports = function (RED) {
|
|
|
1084
1294
|
const ts = dp.ts;
|
|
1085
1295
|
const [iface, channel, datapoint] = dp.name.split('.');
|
|
1086
1296
|
if (this.enabledIfaces.includes(iface) && datapoint) {
|
|
1087
|
-
if (
|
|
1297
|
+
if (
|
|
1298
|
+
['RSSI_DEVICE', 'RSSI_PEER'].includes(datapoint) &&
|
|
1299
|
+
typeof dp.value === 'number' &&
|
|
1300
|
+
dp.value > 127
|
|
1301
|
+
) {
|
|
1302
|
+
// the ReGa reports the unsigned byte; without
|
|
1303
|
+
// the guard an already-signed value or the 0
|
|
1304
|
+
// for "never received" became -318 / -256 (#183)
|
|
1088
1305
|
dp.value -= 256;
|
|
1089
1306
|
}
|
|
1090
1307
|
|
|
@@ -1218,6 +1435,11 @@ module.exports = function (RED) {
|
|
|
1218
1435
|
*/
|
|
1219
1436
|
programActive(name, active) {
|
|
1220
1437
|
return new Promise((resolve, reject) => {
|
|
1438
|
+
if (this.metaMode) {
|
|
1439
|
+
reject(this.regaMissingError('programs'));
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1221
1443
|
const program = this.program[name];
|
|
1222
1444
|
if (program) {
|
|
1223
1445
|
const script = `dom.GetObject(${program.id}).Active(${active});`;
|
|
@@ -1245,6 +1467,11 @@ module.exports = function (RED) {
|
|
|
1245
1467
|
*/
|
|
1246
1468
|
programExecute(name) {
|
|
1247
1469
|
return new Promise((resolve, reject) => {
|
|
1470
|
+
if (this.metaMode) {
|
|
1471
|
+
reject(this.regaMissingError('programs'));
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1248
1475
|
const program = this.program[name];
|
|
1249
1476
|
if (program) {
|
|
1250
1477
|
const d = new Date();
|
|
@@ -1279,6 +1506,11 @@ module.exports = function (RED) {
|
|
|
1279
1506
|
*/
|
|
1280
1507
|
setVariable(name, value) {
|
|
1281
1508
|
return new Promise((resolve, reject) => {
|
|
1509
|
+
if (this.metaMode) {
|
|
1510
|
+
reject(this.regaMissingError('system variables'));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1282
1514
|
if (!this.hasRegaVariables) {
|
|
1283
1515
|
this.logger.debug('variables not yet known. defer setVariable ' + name);
|
|
1284
1516
|
clearTimeout(this.setVariableQueueTimeout[name]);
|
|
@@ -1319,8 +1551,19 @@ module.exports = function (RED) {
|
|
|
1319
1551
|
*/
|
|
1320
1552
|
regaPoll() {
|
|
1321
1553
|
//this.logger.trace('regaPoll');
|
|
1554
|
+
if (this.metaMode) {
|
|
1555
|
+
// nothing to poll: no variables, no programs, and names arrive
|
|
1556
|
+
// over the metadata api's event stream
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1322
1560
|
if (this.regaPollPending) {
|
|
1323
|
-
|
|
1561
|
+
// #166: writing several variables at once used to lose all but
|
|
1562
|
+
// the first immediate re-poll, so their new values only showed
|
|
1563
|
+
// up at the next scheduled one (30 s by default). Remember the
|
|
1564
|
+
// request and run exactly one more poll afterwards.
|
|
1565
|
+
this.logger.debug('rega poll already pending, will repeat');
|
|
1566
|
+
this.regaPollAgain = true;
|
|
1324
1567
|
} else {
|
|
1325
1568
|
this.regaPollPending = true;
|
|
1326
1569
|
clearTimeout(this.regaPollTimeout);
|
|
@@ -1342,6 +1585,14 @@ module.exports = function (RED) {
|
|
|
1342
1585
|
|
|
1343
1586
|
this.firstRegaPollDone = true;
|
|
1344
1587
|
this.regaPollPending = false;
|
|
1588
|
+
|
|
1589
|
+
if (this.regaPollAgain && !this.cancelRegaPoll) {
|
|
1590
|
+
this.regaPollAgain = false;
|
|
1591
|
+
this.regaPoll();
|
|
1592
|
+
} else {
|
|
1593
|
+
this.regaPollAgain = false;
|
|
1594
|
+
this.refreshRegaDataIfDue();
|
|
1595
|
+
}
|
|
1345
1596
|
});
|
|
1346
1597
|
}
|
|
1347
1598
|
}
|
|
@@ -1481,6 +1732,38 @@ module.exports = function (RED) {
|
|
|
1481
1732
|
* Poll ReGaHSS variables and call subscription callbacks
|
|
1482
1733
|
* @returns {Promise}
|
|
1483
1734
|
*/
|
|
1735
|
+
/**
|
|
1736
|
+
* Re-read the ReGa metadata that only ever got fetched once, in the
|
|
1737
|
+
* constructor: channel names, rooms and functions. A room or function
|
|
1738
|
+
* added in the CCU WebUI was invisible until the flow was redeployed
|
|
1739
|
+
* (#167). Cheap enough for a slow schedule - three small scripts.
|
|
1740
|
+
* @returns {Promise<any>}
|
|
1741
|
+
*/
|
|
1742
|
+
refreshRegaData() {
|
|
1743
|
+
this.lastRegaDataRefresh = now();
|
|
1744
|
+
this.logger.debug('refreshRegaData');
|
|
1745
|
+
return this.getRegaChannels()
|
|
1746
|
+
.then(() => this.getRegaRooms())
|
|
1747
|
+
.then(() => this.getRegaFunctions())
|
|
1748
|
+
.then(() => this.saveRegadata())
|
|
1749
|
+
.catch((error) => {
|
|
1750
|
+
this.logger.error('refreshRegaData', error);
|
|
1751
|
+
this.recheckMeta();
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** Runs refreshRegaData() when the configured interval has elapsed. */
|
|
1756
|
+
refreshRegaDataIfDue() {
|
|
1757
|
+
if (!this.regaMetaInterval || this.cancelRegaPoll) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
const elapsed = now() - (this.lastRegaDataRefresh || 0);
|
|
1762
|
+
if (elapsed >= this.regaMetaInterval * 60000) {
|
|
1763
|
+
this.refreshRegaData();
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1484
1767
|
getRegaVariables() {
|
|
1485
1768
|
return new Promise((resolve, reject) => {
|
|
1486
1769
|
this.logger.debug('getRegaVariables');
|
|
@@ -1766,6 +2049,10 @@ module.exports = function (RED) {
|
|
|
1766
2049
|
* @param iface
|
|
1767
2050
|
*/
|
|
1768
2051
|
rpcCheckInit(iface) {
|
|
2052
|
+
if (!this.rpcPingEnabled) {
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1769
2056
|
if (!this.metadata.devices[iface] || !Object.keys(this.metadata.devices[iface]).length > 0) {
|
|
1770
2057
|
return;
|
|
1771
2058
|
}
|
|
@@ -2509,6 +2796,7 @@ module.exports = function (RED) {
|
|
|
2509
2796
|
'change',
|
|
2510
2797
|
'cache',
|
|
2511
2798
|
'stable',
|
|
2799
|
+
'uncertain',
|
|
2512
2800
|
'iface',
|
|
2513
2801
|
'device',
|
|
2514
2802
|
'deviceType',
|
|
@@ -2714,6 +3002,15 @@ module.exports = function (RED) {
|
|
|
2714
3002
|
let matchChange;
|
|
2715
3003
|
let matchStable;
|
|
2716
3004
|
|
|
3005
|
+
// Checked before the attribute loop below: that loop may break
|
|
3006
|
+
// early for a whitelisted datapoint, and this decision has to be
|
|
3007
|
+
// made for every message regardless of key order. Only present in
|
|
3008
|
+
// the filter when a node opted in, so existing subscriptions keep
|
|
3009
|
+
// receiving values the ReGa could not timestamp (#96).
|
|
3010
|
+
if (filter && filter.uncertain === false && message.uncertain) {
|
|
3011
|
+
return false;
|
|
3012
|
+
}
|
|
3013
|
+
|
|
2717
3014
|
if (filter) {
|
|
2718
3015
|
const arrayAttr = Object.keys(filter);
|
|
2719
3016
|
|
|
@@ -2753,6 +3050,11 @@ module.exports = function (RED) {
|
|
|
2753
3050
|
continue;
|
|
2754
3051
|
}
|
|
2755
3052
|
|
|
3053
|
+
if (attr === 'uncertain') {
|
|
3054
|
+
// already decided above, and not a value to compare
|
|
3055
|
+
continue;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
2756
3058
|
if (this.callbackWhitelists[message.datapointName].has(id)) {
|
|
2757
3059
|
if (matchCache && matchChange && matchStable) {
|
|
2758
3060
|
break;
|
|
@@ -3058,6 +3360,11 @@ module.exports = function (RED) {
|
|
|
3058
3360
|
*/
|
|
3059
3361
|
script(script) {
|
|
3060
3362
|
return new Promise((resolve, reject) => {
|
|
3363
|
+
if (this.metaMode) {
|
|
3364
|
+
reject(this.regaMissingError('HM-Script and ReGaHSS scripts'));
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3061
3368
|
execToCallback(this.rega.exec(script), (err, payload, objects) => {
|
|
3062
3369
|
if (err) {
|
|
3063
3370
|
reject(err);
|
package/nodes/ccu-display.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
3
|
const statusHelper = require(path.join(__dirname, '/lib/status.js'));
|
|
4
|
+
const {effectiveConfig} = require(path.join(__dirname, '/lib/dynconfig.js'));
|
|
5
|
+
|
|
6
|
+
/** Everything a message may override through msg.config (B-2, #80 #185). */
|
|
7
|
+
const DYNAMIC_KEYS = [
|
|
8
|
+
'iface',
|
|
9
|
+
'channel',
|
|
10
|
+
'channelType',
|
|
11
|
+
'sound',
|
|
12
|
+
'repeat',
|
|
13
|
+
'pause',
|
|
14
|
+
'signal',
|
|
15
|
+
...[1, 2, 3, 4, 5, 6].flatMap((i) => ['line' + i, 'color' + i, 'icon' + i, 'disable' + i]),
|
|
16
|
+
];
|
|
4
17
|
|
|
5
18
|
module.exports = function (RED) {
|
|
6
19
|
class CcuDisplay {
|
|
@@ -143,43 +156,46 @@ module.exports = function (RED) {
|
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
this.on('input', (message, send, done) => {
|
|
159
|
+
// msg.line1 & co. keep working as before; msg.config
|
|
160
|
+
// additionally reaches the channel and the EP-WM55 settings
|
|
161
|
+
const {config: effective} = effectiveConfig(config, message, DYNAMIC_KEYS);
|
|
146
162
|
let payload = '0x02';
|
|
147
163
|
|
|
148
164
|
let countLines = 6; // HM-Dis-WM55
|
|
149
|
-
if (
|
|
165
|
+
if (effective.channelType === 'HM-Dis-EP-WM55') {
|
|
150
166
|
payload += ',0x0A';
|
|
151
167
|
countLines = 3;
|
|
152
168
|
}
|
|
153
169
|
|
|
154
170
|
for (let i = 1; i <= countLines; i++) {
|
|
155
|
-
if (!message['disable' + i] && !
|
|
156
|
-
payload += convertString(message['line' + i] ||
|
|
157
|
-
if (
|
|
158
|
-
payload += convertColor(message['color' + i] ||
|
|
171
|
+
if (!message['disable' + i] && !effective['disable' + i]) {
|
|
172
|
+
payload += convertString(message['line' + i] || effective['line' + i]);
|
|
173
|
+
if (effective.channelType === 'HM-Dis-WM55') {
|
|
174
|
+
payload += convertColor(message['color' + i] || effective['color' + i]);
|
|
159
175
|
}
|
|
160
176
|
|
|
161
|
-
payload += convertIcon(message['icon' + i] ||
|
|
177
|
+
payload += convertIcon(message['icon' + i] || effective['icon' + i]);
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
payload += ',0x0A';
|
|
165
181
|
}
|
|
166
182
|
|
|
167
|
-
if (
|
|
168
|
-
if (
|
|
169
|
-
payload += ',0x14,' +
|
|
183
|
+
if (effective.channelType === 'HM-Dis-EP-WM55') {
|
|
184
|
+
if (effective.sound) {
|
|
185
|
+
payload += ',0x14,' + effective.sound + ',0x1C';
|
|
170
186
|
}
|
|
171
187
|
|
|
172
|
-
payload += ',' +
|
|
188
|
+
payload += ',' + effective.repeat + ',0x1D,' + effective.pause + ',0x16';
|
|
173
189
|
|
|
174
|
-
if (
|
|
175
|
-
payload += ',' +
|
|
190
|
+
if (effective.signal) {
|
|
191
|
+
payload += ',' + effective.signal;
|
|
176
192
|
}
|
|
177
193
|
}
|
|
178
194
|
|
|
179
195
|
payload += ',0x03';
|
|
180
196
|
|
|
181
197
|
this.ccu
|
|
182
|
-
.setValue(
|
|
198
|
+
.setValue(effective.iface, effective.channel, 'SUBMIT', payload)
|
|
183
199
|
.then(() => {
|
|
184
200
|
done();
|
|
185
201
|
})
|
package/nodes/ccu-get-value.html
CHANGED
|
@@ -195,7 +195,9 @@
|
|
|
195
195
|
arr.sort((a, b) => a.localeCompare(b));
|
|
196
196
|
$nodeInputChannel.autocomplete('option', 'source', arr);
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
// ${ENV} placeholders are substituted by Node-RED before the node is
|
|
199
|
+
// constructed (subflow parameters, #158) - do not clear them
|
|
200
|
+
if (!/^\$\{[^}]+\}$/.test($nodeInputChannel.val()) && !data[$nodeInputChannel.val().split(' ')[0]]) {
|
|
199
201
|
$nodeInputChannel.val('');
|
|
200
202
|
$nodeInputDatapoint.val('');
|
|
201
203
|
}
|
package/nodes/ccu-mqtt.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
topicOutputEvent: {value: 'hm/status/${channelName}/${datapoint}'},
|
|
12
12
|
topicInputSetValue: {value: 'hm/set/${channelNameOrAddress}/${datapoint}'},
|
|
13
|
+
topicInputGet: {value: 'hm/get/${channelNameOrAddress}/${datapoint}'},
|
|
13
14
|
|
|
14
15
|
topicOutputSysvar: {value: 'hm/status/${name}'},
|
|
15
16
|
topicInputSysvar: {value: 'hm/set/${name}'},
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
topicInputPutParam: {value: 'hm/paramset/${channelNameOrAddress}/${paramset}/${param}'},
|
|
18
19
|
topicInputPutParamset: {value: 'hm/paramset/${channelNameOrAddress}/${paramset}'},
|
|
19
20
|
|
|
20
|
-
topicInputRpc: {value: 'hm/rpc/${iface}/${method}/${
|
|
21
|
+
topicInputRpc: {value: 'hm/rpc/${iface}/${method}/${callid}'},
|
|
21
22
|
topicOutputRpc: {value: 'hm/response/${callid}'},
|
|
22
23
|
|
|
23
24
|
topicCounters: {value: 'hm/status/counter/${iface}/${rxtx}'},
|
|
@@ -36,6 +37,20 @@
|
|
|
36
37
|
labelStyle() {
|
|
37
38
|
return this.name ? 'node_label_italic' : '';
|
|
38
39
|
},
|
|
40
|
+
oneditprepare() {
|
|
41
|
+
// The RPC command was never implemented, so its topic template
|
|
42
|
+
// carried a meaningless ${command} segment. Nothing can have
|
|
43
|
+
// depended on it - migrate nodes that still hold the old default.
|
|
44
|
+
if (this.topicInputRpc === 'hm/rpc/${iface}/${method}/${command}/${callid}') {
|
|
45
|
+
this.topicInputRpc = 'hm/rpc/${iface}/${method}/${callid}';
|
|
46
|
+
$('#node-input-topicInputRpc').val(this.topicInputRpc);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (typeof this.topicInputGet === 'undefined') {
|
|
50
|
+
this.topicInputGet = 'hm/get/${channelNameOrAddress}/${datapoint}';
|
|
51
|
+
$('#node-input-topicInputGet').val(this.topicInputGet);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
39
54
|
oneditsave() {
|
|
40
55
|
}
|
|
41
56
|
});
|
|
@@ -84,6 +99,10 @@
|
|
|
84
99
|
<label for="node-input-topicInputSetValue">setValue</label>
|
|
85
100
|
<input type="text" id="node-input-topicInputSetValue">
|
|
86
101
|
</div>
|
|
102
|
+
<div class="form-row">
|
|
103
|
+
<label for="node-input-topicInputGet">get</label>
|
|
104
|
+
<input type="text" id="node-input-topicInputGet">
|
|
105
|
+
</div>
|
|
87
106
|
<div class="form-row">
|
|
88
107
|
<label for="node-input-topicOutputSysvar">Sysvar/Program Change</label>
|
|
89
108
|
<input type="text" id="node-input-topicOutputSysvar">
|
|
@@ -100,16 +119,14 @@
|
|
|
100
119
|
<label for="node-input-topicInputPutParamset">putParamset</label>
|
|
101
120
|
<input type="text" id="node-input-topicInputPutParamset">
|
|
102
121
|
</div>
|
|
103
|
-
<!--
|
|
104
122
|
<div class="form-row">
|
|
105
123
|
<label for="node-input-topicInputRpc">RPC</label>
|
|
106
124
|
<input type="text" id="node-input-topicInputRpc">
|
|
107
125
|
</div>
|
|
108
126
|
<div class="form-row">
|
|
109
|
-
<label for="node-input-
|
|
127
|
+
<label for="node-input-topicOutputRpc">RPC Response</label>
|
|
110
128
|
<input type="text" id="node-input-topicOutputRpc">
|
|
111
129
|
</div>
|
|
112
|
-
-->
|
|
113
130
|
<div class="form-row">
|
|
114
131
|
<label for="node-input-topicCounters">Message Counters</label>
|
|
115
132
|
<input type="text" id="node-input-topicCounters">
|