node-red-contrib-ccu 4.3.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 +44 -0
- package/HANDOFF.md +40 -12
- package/README.md +42 -0
- package/ROADMAP.md +19 -15
- package/nodes/ccu-connection.html +25 -0
- package/nodes/ccu-connection.js +237 -6
- package/nodes/ccu-poll.js +6 -0
- package/nodes/lib/metaprovider.js +716 -0
- package/package.json +1 -1
- package/roadmap-archive/B-17.md +106 -0
- package/roadmap-archive/README.md +3 -0
package/nodes/ccu-connection.js
CHANGED
|
@@ -14,6 +14,7 @@ const {bestMatch} = require('./lib/similarity.js');
|
|
|
14
14
|
const nextport = require('./lib/nextport.js');
|
|
15
15
|
const hmDiscover = require('./lib/discover.js');
|
|
16
16
|
const {Rega} = require('homematic-rega'); // ES module - require(esm) needs Node >= 20.19 / >= 22.12
|
|
17
|
+
const metaProvider = require('./lib/metaprovider.js');
|
|
17
18
|
const xmlrpc = require('homematic-xmlrpc');
|
|
18
19
|
const binrpc = require('binrpc');
|
|
19
20
|
|
|
@@ -542,6 +543,15 @@ module.exports = function (RED) {
|
|
|
542
543
|
|
|
543
544
|
this.regaEnabled = config.regaEnabled;
|
|
544
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;
|
|
545
555
|
// #167: minutes between re-reads of channel names, rooms and
|
|
546
556
|
// functions. 0 switches it off.
|
|
547
557
|
this.regaMetaInterval =
|
|
@@ -640,10 +650,26 @@ module.exports = function (RED) {
|
|
|
640
650
|
|
|
641
651
|
if (config.regaEnabled) {
|
|
642
652
|
this.lastRegaDataRefresh = now();
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
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
|
+
});
|
|
647
673
|
} else {
|
|
648
674
|
this.initIfaces(config);
|
|
649
675
|
}
|
|
@@ -822,6 +848,8 @@ module.exports = function (RED) {
|
|
|
822
848
|
regaChannels: this.regaChannels,
|
|
823
849
|
channelRooms: this.channelRooms,
|
|
824
850
|
channelFunctions: this.channelFunctions,
|
|
851
|
+
rooms: this.rooms,
|
|
852
|
+
functions: this.functions,
|
|
825
853
|
groups: this.groups,
|
|
826
854
|
sysvar: this.sysvar,
|
|
827
855
|
program: this.program,
|
|
@@ -885,6 +913,16 @@ module.exports = function (RED) {
|
|
|
885
913
|
this.channelRooms = regadata.channelRooms;
|
|
886
914
|
this.channelFunctions = regadata.channelFunctions;
|
|
887
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
|
+
|
|
888
926
|
/*
|
|
889
927
|
this.sysvar = regadata.sysvar;
|
|
890
928
|
Object.keys(this.sysvar).forEach(s => {
|
|
@@ -994,6 +1032,12 @@ module.exports = function (RED) {
|
|
|
994
1032
|
this.cancelRegaPoll = true;
|
|
995
1033
|
clearTimeout(this.regaPollTimeout);
|
|
996
1034
|
|
|
1035
|
+
if (this.meta) {
|
|
1036
|
+
this.logger.debug('stop metadata event stream');
|
|
1037
|
+
this.meta.stop();
|
|
1038
|
+
this.meta = null;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
997
1041
|
Object.keys(this.rpcPingTimer).forEach((iface) => {
|
|
998
1042
|
this.logger.debug('clear rpcPingTimer', iface);
|
|
999
1043
|
clearTimeout(this.rpcPingTimer[iface]);
|
|
@@ -1073,7 +1117,165 @@ module.exports = function (RED) {
|
|
|
1073
1117
|
.then(() => this.getRegaFunctions())
|
|
1074
1118
|
.then(() => this.getRegaValues())
|
|
1075
1119
|
.then(() => this.getGroupsData())
|
|
1076
|
-
.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)');
|
|
1077
1279
|
}
|
|
1078
1280
|
|
|
1079
1281
|
/**
|
|
@@ -1233,6 +1435,11 @@ module.exports = function (RED) {
|
|
|
1233
1435
|
*/
|
|
1234
1436
|
programActive(name, active) {
|
|
1235
1437
|
return new Promise((resolve, reject) => {
|
|
1438
|
+
if (this.metaMode) {
|
|
1439
|
+
reject(this.regaMissingError('programs'));
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1236
1443
|
const program = this.program[name];
|
|
1237
1444
|
if (program) {
|
|
1238
1445
|
const script = `dom.GetObject(${program.id}).Active(${active});`;
|
|
@@ -1260,6 +1467,11 @@ module.exports = function (RED) {
|
|
|
1260
1467
|
*/
|
|
1261
1468
|
programExecute(name) {
|
|
1262
1469
|
return new Promise((resolve, reject) => {
|
|
1470
|
+
if (this.metaMode) {
|
|
1471
|
+
reject(this.regaMissingError('programs'));
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1263
1475
|
const program = this.program[name];
|
|
1264
1476
|
if (program) {
|
|
1265
1477
|
const d = new Date();
|
|
@@ -1294,6 +1506,11 @@ module.exports = function (RED) {
|
|
|
1294
1506
|
*/
|
|
1295
1507
|
setVariable(name, value) {
|
|
1296
1508
|
return new Promise((resolve, reject) => {
|
|
1509
|
+
if (this.metaMode) {
|
|
1510
|
+
reject(this.regaMissingError('system variables'));
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1297
1514
|
if (!this.hasRegaVariables) {
|
|
1298
1515
|
this.logger.debug('variables not yet known. defer setVariable ' + name);
|
|
1299
1516
|
clearTimeout(this.setVariableQueueTimeout[name]);
|
|
@@ -1334,6 +1551,12 @@ module.exports = function (RED) {
|
|
|
1334
1551
|
*/
|
|
1335
1552
|
regaPoll() {
|
|
1336
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
|
+
|
|
1337
1560
|
if (this.regaPollPending) {
|
|
1338
1561
|
// #166: writing several variables at once used to lose all but
|
|
1339
1562
|
// the first immediate re-poll, so their new values only showed
|
|
@@ -1523,7 +1746,10 @@ module.exports = function (RED) {
|
|
|
1523
1746
|
.then(() => this.getRegaRooms())
|
|
1524
1747
|
.then(() => this.getRegaFunctions())
|
|
1525
1748
|
.then(() => this.saveRegadata())
|
|
1526
|
-
.catch((error) =>
|
|
1749
|
+
.catch((error) => {
|
|
1750
|
+
this.logger.error('refreshRegaData', error);
|
|
1751
|
+
this.recheckMeta();
|
|
1752
|
+
});
|
|
1527
1753
|
}
|
|
1528
1754
|
|
|
1529
1755
|
/** Runs refreshRegaData() when the configured interval has elapsed. */
|
|
@@ -3134,6 +3360,11 @@ module.exports = function (RED) {
|
|
|
3134
3360
|
*/
|
|
3135
3361
|
script(script) {
|
|
3136
3362
|
return new Promise((resolve, reject) => {
|
|
3363
|
+
if (this.metaMode) {
|
|
3364
|
+
reject(this.regaMissingError('HM-Script and ReGaHSS scripts'));
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
|
|
3137
3368
|
execToCallback(this.rega.exec(script), (err, payload, objects) => {
|
|
3138
3369
|
if (err) {
|
|
3139
3370
|
reject(err);
|
package/nodes/ccu-poll.js
CHANGED
|
@@ -17,6 +17,12 @@ module.exports = function (RED) {
|
|
|
17
17
|
this.ccu.register(this);
|
|
18
18
|
|
|
19
19
|
this.on('input', (message, send, done) => {
|
|
20
|
+
if (this.ccu.metaMode) {
|
|
21
|
+
// openccu-lite has no ReGaHSS - nothing to poll (B-17)
|
|
22
|
+
done(this.ccu.regaMissingError('system variables and programs'));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
this.ccu.regaPoll(); // TODO catch errors
|
|
21
27
|
done();
|
|
22
28
|
});
|