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.
@@ -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
- this.getRegaData().then(() => {
644
- this.regaPoll();
645
- this.initIfaces(config);
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(this.logger.error);
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) => this.logger.error('refreshRegaData', 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
  });