node-red-contrib-ccu 3.4.1 → 4.0.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.
Files changed (49) hide show
  1. package/.prettierignore +10 -0
  2. package/.prettierrc +14 -0
  3. package/AGENTS.md +96 -0
  4. package/CHANGELOG.md +66 -0
  5. package/README.md +17 -21
  6. package/ROADMAP.md +402 -0
  7. package/eslint.config.js +45 -0
  8. package/examples/rpc-events.json +65 -0
  9. package/examples/switch-datapoint.json +86 -0
  10. package/examples/system-variable.json +60 -0
  11. package/nodes/ccu-alexa.html +12 -7
  12. package/nodes/ccu-alexa.js +43 -15
  13. package/nodes/ccu-connection.html +9 -1
  14. package/nodes/ccu-connection.js +602 -337
  15. package/nodes/ccu-display.html +11 -4
  16. package/nodes/ccu-display.js +26 -9
  17. package/nodes/ccu-get-value.html +23 -14
  18. package/nodes/ccu-get-value.js +10 -5
  19. package/nodes/ccu-mqtt.html +9 -10
  20. package/nodes/ccu-mqtt.js +55 -29
  21. package/nodes/ccu-program.html +1 -1
  22. package/nodes/ccu-program.js +9 -7
  23. package/nodes/ccu-rpc-event.html +12 -7
  24. package/nodes/ccu-rpc-event.js +4 -5
  25. package/nodes/ccu-rpc.html +12 -5
  26. package/nodes/ccu-rpc.js +8 -6
  27. package/nodes/ccu-script.html +1 -1
  28. package/nodes/ccu-script.js +4 -3
  29. package/nodes/ccu-set-value.html +11 -7
  30. package/nodes/ccu-set-value.js +47 -16
  31. package/nodes/ccu-signal.html +11 -5
  32. package/nodes/ccu-signal.js +34 -23
  33. package/nodes/ccu-switch.html +23 -13
  34. package/nodes/ccu-sysvar.html +1 -1
  35. package/nodes/ccu-sysvar.js +11 -7
  36. package/nodes/ccu-value.html +12 -7
  37. package/nodes/ccu-value.js +36 -13
  38. package/nodes/lib/base62.js +22 -0
  39. package/nodes/lib/discover.js +127 -0
  40. package/nodes/lib/nextport.js +30 -0
  41. package/nodes/lib/similarity.js +51 -0
  42. package/nodes/lib/status.js +1 -1
  43. package/package.json +105 -144
  44. package/roadmap-archive/README.md +20 -0
  45. package/roadmap-archive/phase-1.md +48 -0
  46. package/roadmap-archive/todo-history.md +39 -0
  47. package/TODO.md +0 -41
  48. package/ccu_rega_localhost.json +0 -1
  49. package/ccu_values_localhost.json +0 -1
@@ -4,15 +4,11 @@ const path = require('path');
4
4
  const fs = require('fs');
5
5
  const crypto = require('crypto');
6
6
 
7
- const promiseFinally = require('promise.prototype.finally');
8
-
9
- promiseFinally.shim();
10
-
11
- const base62 = require('buffer-base62').toBase62;
12
- const stringSimilarity = require('string-similarity');
13
- const nextport = require('nextport');
14
- const hmDiscover = require('hm-discover');
15
- const Rega = require('homematic-rega');
7
+ const base62 = require('./lib/base62.js').toBase62;
8
+ const {bestMatch} = require('./lib/similarity.js');
9
+ const nextport = require('./lib/nextport.js');
10
+ const hmDiscover = require('./lib/discover.js');
11
+ const {Rega} = require('homematic-rega'); // ES module - require(esm) needs Node >= 20.19 / >= 22.12
16
12
  const xmlrpc = require('homematic-xmlrpc');
17
13
  const binrpc = require('binrpc');
18
14
 
@@ -24,8 +20,24 @@ const pkg = require(path.join(__dirname, '..', 'package.json'));
24
20
  * @param obj
25
21
  * @returns {boolean}
26
22
  */
23
+ /* homematic-rega 2.x has a promise API; these adapters feed the existing
24
+ callback-style call sites. execToCallback maps exec()'s {output, objects}
25
+ to the old (err, output, objects) signature. */
26
+ function toCallback(promise, callback) {
27
+ promise.then(
28
+ (res) => callback(null, res),
29
+ (err) => callback(err),
30
+ );
31
+ }
32
+
33
+ function execToCallback(promise, callback) {
34
+ promise.then(
35
+ ({output, objects}) => callback(null, output, objects),
36
+ (err) => callback(err),
37
+ );
38
+ }
39
+
27
40
  function isIterable(object) {
28
- // eslint-disable-next-line eqeqeq, no-eq-null
29
41
  return object != null && typeof object[Symbol.iterator] === 'function' && typeof object.forEach === 'function';
30
42
  }
31
43
 
@@ -41,7 +53,7 @@ module.exports = function (RED) {
41
53
  */
42
54
  function findport(start) {
43
55
  return new Promise((resolve, reject) => {
44
- nextport(start, port => {
56
+ nextport(start, (port) => {
45
57
  if (port) {
46
58
  ccu.network.ports.push(port);
47
59
  resolve();
@@ -52,13 +64,13 @@ module.exports = function (RED) {
52
64
  });
53
65
  }
54
66
 
55
- hmDiscover(res => {
67
+ hmDiscover((res) => {
56
68
  ccu.network.discover = res;
57
69
  });
58
70
 
59
71
  const networkInterfaces = os.networkInterfaces();
60
- Object.keys(networkInterfaces).forEach(name => {
61
- networkInterfaces[name].forEach(addr => {
72
+ Object.keys(networkInterfaces).forEach((name) => {
73
+ networkInterfaces[name].forEach((addr) => {
62
74
  if (addr.family === 'IPv4') {
63
75
  ccu.network.listen.push(addr.address);
64
76
  }
@@ -78,10 +90,10 @@ module.exports = function (RED) {
78
90
 
79
91
  switch (request.query.type) {
80
92
  case 'ifaces': {
81
- Object.keys(config.ifaceTypes).forEach(iface => {
93
+ Object.keys(config.ifaceTypes).forEach((iface) => {
82
94
  object[iface] = {
83
95
  enabled: Boolean(config.ifaceTypes[iface].enabled),
84
- connected: Boolean(config.ifaceStatus[iface])
96
+ connected: Boolean(config.ifaceStatus[iface]),
85
97
  };
86
98
  });
87
99
  res.status(200).send(JSON.stringify(object));
@@ -91,14 +103,14 @@ module.exports = function (RED) {
91
103
  case 'channels': {
92
104
  const devices = config.metadata.devices[request.query.iface];
93
105
  if (devices) {
94
- Object.keys(devices).forEach(addr => {
106
+ Object.keys(devices).forEach((addr) => {
95
107
  if (addr.match(/:\d+$/)) {
96
108
  const psKey = config.paramsetName(request.query.iface, devices[addr], 'VALUES');
97
109
  if (config.paramsetDescriptions[psKey]) {
98
110
  object[addr] = {
99
111
  name: config.channelNames[addr],
100
112
  datapoints: Object.keys(config.paramsetDescriptions[psKey]),
101
- rxMode: devices[devices[addr].PARENT] && devices[devices[addr].PARENT].RX_MODE
113
+ rxMode: devices[devices[addr].PARENT] && devices[devices[addr].PARENT].RX_MODE,
102
114
  };
103
115
  }
104
116
  }
@@ -115,7 +127,7 @@ module.exports = function (RED) {
115
127
  return;
116
128
  }
117
129
 
118
- Object.keys(devices).forEach(addr => {
130
+ Object.keys(devices).forEach((addr) => {
119
131
  if (addr.match(/:\d+$/)) {
120
132
  const psKey = config.paramsetName(iface, devices[addr], 'VALUES');
121
133
  if (config.paramsetDescriptions[psKey]) {
@@ -123,26 +135,26 @@ module.exports = function (RED) {
123
135
  const dps = [];
124
136
  const chName = config.channelNames[addr];
125
137
 
126
- Object.keys(config.paramsetDescriptions[psKey]).forEach(dp => {
138
+ Object.keys(config.paramsetDescriptions[psKey]).forEach((dp) => {
127
139
  dps.push({
128
140
  id: iface + '.' + addr + '.' + dp,
129
141
  iface,
130
- channel: (chName) ? addr + ' ' + chName : addr,
142
+ channel: chName ? addr + ' ' + chName : addr,
131
143
  label: dp,
132
144
  icon: 'fa fa-tag fa-fw',
133
- class: request.query.classDp
145
+ class: request.query.classDp,
134
146
  });
135
147
  });
136
148
  dps.sort((a, b) => a.label.localeCompare(b.label));
137
149
  const channel = {
138
150
  id: iface + '.' + addr,
139
151
  iface,
140
- label: (chName) ? chName + ' (' + addr + ')' : addr,
152
+ label: chName ? chName + ' (' + addr + ')' : addr,
141
153
  children: dps,
142
154
  rooms: config.channelRooms[addr],
143
155
  functions: config.channelFunctions[addr],
144
156
  icon: 'fa fa-tags fa-fw',
145
- class: request.query.classCh
157
+ class: request.query.classCh,
146
158
  };
147
159
  callback(iface, devID, channel, addr);
148
160
  }
@@ -161,12 +173,14 @@ module.exports = function (RED) {
161
173
  object[devID] = {
162
174
  id: iface + '.' + devID,
163
175
  name: config.channelNames[devID],
164
- label: (config.channelNames[devID]) ? config.channelNames[devID] + ' (' + devID + ')' : devID,
176
+ label: config.channelNames[devID]
177
+ ? config.channelNames[devID] + ' (' + devID + ')'
178
+ : devID,
165
179
  type: devices[devID].TYPE,
166
180
  iface,
167
181
  icon: 'fa fa-archive fa-fw',
168
182
  channels: {},
169
- children: []
183
+ children: [],
170
184
  };
171
185
  }
172
186
 
@@ -174,7 +188,7 @@ module.exports = function (RED) {
174
188
  object[devID].children.push(channel);
175
189
  });
176
190
  } else {
177
- Object.keys(config.metadata.devices).forEach(iface => {
191
+ Object.keys(config.metadata.devices).forEach((iface) => {
178
192
  const devices = config.metadata.devices[iface];
179
193
  processChannels(iface, devices, (iface, devID, channel, chID) => {
180
194
  if (!channel.children || channel.children.length === 0) {
@@ -191,15 +205,19 @@ module.exports = function (RED) {
191
205
  }
192
206
 
193
207
  case 'rooms':
194
- res.status(200).send(JSON.stringify({
195
- rooms: config.rooms
196
- }));
208
+ res.status(200).send(
209
+ JSON.stringify({
210
+ rooms: config.rooms,
211
+ }),
212
+ );
197
213
  break;
198
214
 
199
215
  case 'functions':
200
- res.status(200).send(JSON.stringify({
201
- functions: config.functions
202
- }));
216
+ res.status(200).send(
217
+ JSON.stringify({
218
+ functions: config.functions,
219
+ }),
220
+ );
203
221
  break;
204
222
 
205
223
  case 'sysvar':
@@ -213,30 +231,29 @@ module.exports = function (RED) {
213
231
  case 'signal': {
214
232
  const devices = config.metadata.devices[request.query.iface];
215
233
  if (devices) {
216
- Object.keys(devices).forEach(addr => {
217
- if ([
218
- 'SIGNAL_CHIME',
219
- 'SIGNAL_LED',
220
- 'ALARM_SWITCH_VIRTUAL_RECEIVER'
221
- ].includes(devices[addr].TYPE)) {
234
+ Object.keys(devices).forEach((addr) => {
235
+ if (
236
+ ['SIGNAL_CHIME', 'SIGNAL_LED', 'ALARM_SWITCH_VIRTUAL_RECEIVER'].includes(
237
+ devices[addr].TYPE,
238
+ )
239
+ ) {
222
240
  object[addr] = {
223
241
  name: config.channelNames[addr],
224
242
  type: devices[addr].TYPE,
225
- deviceType: devices[addr].PARENT_TYPE
243
+ deviceType: devices[addr].PARENT_TYPE,
226
244
  };
227
245
  }
228
246
 
229
- if ([
230
- 'HmIP-MP3P',
231
- 'HmIP-BSL'
232
- ].includes(devices[addr].PARENT_TYPE) && [
233
- 'ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER',
234
- 'DIMMER_VIRTUAL_RECEIVER'
235
- ].includes(devices[addr].TYPE)) {
247
+ if (
248
+ ['HmIP-MP3P', 'HmIP-BSL'].includes(devices[addr].PARENT_TYPE) &&
249
+ ['ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER', 'DIMMER_VIRTUAL_RECEIVER'].includes(
250
+ devices[addr].TYPE,
251
+ )
252
+ ) {
236
253
  object[addr] = {
237
254
  name: config.channelNames[addr],
238
255
  type: devices[addr].TYPE,
239
- deviceType: devices[addr].PARENT_TYPE
256
+ deviceType: devices[addr].PARENT_TYPE,
240
257
  };
241
258
  }
242
259
  });
@@ -249,14 +266,15 @@ module.exports = function (RED) {
249
266
  case 'display': {
250
267
  const devices = config.metadata.devices[request.query.iface];
251
268
  if (devices) {
252
- Object.keys(devices).forEach(addr => {
269
+ Object.keys(devices).forEach((addr) => {
253
270
  if (
254
271
  (addr.endsWith(':3') && devices[addr].PARENT_TYPE.match(/HM-Dis-EP-WM55/)) ||
255
- ((addr.endsWith(':1') || addr.endsWith(':2')) && devices[addr].PARENT_TYPE.match(/HM-Dis-WM55/))
272
+ ((addr.endsWith(':1') || addr.endsWith(':2')) &&
273
+ devices[addr].PARENT_TYPE.match(/HM-Dis-WM55/))
256
274
  ) {
257
275
  object[addr] = {
258
276
  name: config.channelNames[addr],
259
- type: devices[addr].PARENT_TYPE
277
+ type: devices[addr].PARENT_TYPE,
260
278
  };
261
279
  }
262
280
  });
@@ -267,25 +285,29 @@ module.exports = function (RED) {
267
285
  }
268
286
 
269
287
  default:
270
- res.status(200).send(JSON.stringify({
271
- channelNames: config.channelNames,
272
- metadata: config.metadata,
273
- paramsetDescriptions: config.paramsetDescriptions,
274
- rooms: config.rooms,
275
- functions: config.functions,
276
- sysvar: config.sysvar,
277
- program: config.program,
278
- channelRooms: config.channelRooms,
279
- channelFunctions: config.channelFunctions,
280
- enabledIfaces: config.enabledIfaces
281
- }));
288
+ res.status(200).send(
289
+ JSON.stringify({
290
+ channelNames: config.channelNames,
291
+ metadata: config.metadata,
292
+ paramsetDescriptions: config.paramsetDescriptions,
293
+ rooms: config.rooms,
294
+ functions: config.functions,
295
+ sysvar: config.sysvar,
296
+ program: config.program,
297
+ channelRooms: config.channelRooms,
298
+ channelFunctions: config.channelFunctions,
299
+ enabledIfaces: config.enabledIfaces,
300
+ }),
301
+ );
282
302
  }
283
303
  } else {
284
304
  ccu.network.ports = [];
285
305
  const start = 2040 + Math.floor(Math.random() * 50);
286
- findport(start).then(() => findport(ccu.network.ports[0] + 1)).then(() => {
287
- res.status(200).send(JSON.stringify(ccu.network));
288
- });
306
+ findport(start)
307
+ .then(() => findport(ccu.network.ports[0] + 1))
308
+ .then(() => {
309
+ res.status(200).send(JSON.stringify(ccu.network));
310
+ });
289
311
  }
290
312
  });
291
313
 
@@ -294,7 +316,7 @@ module.exports = function (RED) {
294
316
  * @returns {number}
295
317
  */
296
318
  function now() {
297
- return (new Date()).getTime();
319
+ return new Date().getTime();
298
320
  }
299
321
 
300
322
  /**
@@ -311,10 +333,18 @@ module.exports = function (RED) {
311
333
  return addr;
312
334
  }
313
335
 
314
- return new Promise(resolve => {
315
- if (/^([01]?\d?\d|2[0-4]\d|25[0-5])\\.([01]?\d?\d|2[0-4]\d|25[0-5])\\.([01]?\d?\d|2[0-4]\d|25[0-5])\\.([01]?\d?\d|2[0-4]\d|25[0-5])$/g.test(host)) {
336
+ return new Promise((resolve) => {
337
+ if (
338
+ /^([01]?\d?\d|2[0-4]\d|25[0-5])\\.([01]?\d?\d|2[0-4]\d|25[0-5])\\.([01]?\d?\d|2[0-4]\d|25[0-5])\\.([01]?\d?\d|2[0-4]\d|25[0-5])$/g.test(
339
+ host,
340
+ )
341
+ ) {
316
342
  resolve(unifyLoopback(host));
317
- } else if (/^([\dA-Fa-f]{1,4})((?::[\dA-Fa-f]{1,4}))*::([\dA-Fa-f]{1,4})((?::[\dA-Fa-f]{1,4}))*|([\dA-Fa-f]{1,4})((?::[\dA-Fa-f]{1,4})){7}$/g.test(host)) {
343
+ } else if (
344
+ /^([\dA-Fa-f]{1,4})((?::[\dA-Fa-f]{1,4}))*::([\dA-Fa-f]{1,4})((?::[\dA-Fa-f]{1,4}))*|([\dA-Fa-f]{1,4})((?::[\dA-Fa-f]{1,4})){7}$/g.test(
345
+ host,
346
+ )
347
+ ) {
318
348
  resolve(host);
319
349
  } else {
320
350
  dns.lookup(host, (err, addr) => {
@@ -358,25 +388,25 @@ module.exports = function (RED) {
358
388
  port: this.isLocal ? 31999 : 1999,
359
389
  protocol: this.isLocal ? 'binrpc' : 'http',
360
390
  init: false,
361
- ping: false
391
+ ping: false,
362
392
  },
363
393
  'BidCos-RF': {
364
394
  conf: 'bcrf',
365
- rpc: (this.isLocal || config.bcrfBinRpc) ? binrpc : xmlrpc,
366
- port: this.isLocal ? 32001 : (config.tls ? 42001 : 2001),
367
- protocol: (this.isLocal || config.bcrfBinRpc) ? 'binrpc' : 'http',
395
+ rpc: this.isLocal || config.bcrfBinRpc ? binrpc : xmlrpc,
396
+ port: this.isLocal ? 32001 : config.tls ? 42001 : 2001,
397
+ protocol: this.isLocal || config.bcrfBinRpc ? 'binrpc' : 'http',
368
398
  auth: config.authentication,
369
399
  user: config.username,
370
400
  pass: config.password,
371
401
  tls: config.tls,
372
402
  inSecure: config.inSecure,
373
403
  init: true,
374
- ping: true
404
+ ping: true,
375
405
  },
376
406
  'BidCos-Wired': {
377
407
  conf: 'bcwi',
378
408
  rpc: this.isLocal ? binrpc : xmlrpc,
379
- port: this.isLocal ? 32000 : (config.tls ? 42000 : 2000),
409
+ port: this.isLocal ? 32000 : config.tls ? 42000 : 2000,
380
410
  protocol: this.isLocal ? 'binrpc' : 'http',
381
411
  auth: config.authentication,
382
412
  user: config.username,
@@ -384,12 +414,12 @@ module.exports = function (RED) {
384
414
  tls: config.tls,
385
415
  inSecure: config.inSecure,
386
416
  init: true,
387
- ping: true
417
+ ping: true,
388
418
  },
389
419
  'HmIP-RF': {
390
420
  conf: 'iprf',
391
421
  rpc: xmlrpc,
392
- port: this.isLocal ? 32010 : (config.tls ? 42010 : 2010),
422
+ port: this.isLocal ? 32010 : config.tls ? 42010 : 2010,
393
423
  protocol: 'http',
394
424
  auth: config.authentication,
395
425
  user: config.username,
@@ -398,12 +428,12 @@ module.exports = function (RED) {
398
428
  inSecure: config.inSecure,
399
429
  init: true,
400
430
  ping: true, // Todo https://github.com/eq-3/occu/issues/42 - should be fixed, but isn't
401
- pingTimeout: 600 // Overwrites ccu-connection config
431
+ pingTimeout: 600, // Overwrites ccu-connection config
402
432
  },
403
433
  VirtualDevices: {
404
434
  conf: 'virt',
405
435
  rpc: xmlrpc,
406
- port: this.isLocal ? 39292 : (config.tls ? 49292 : 9292),
436
+ port: this.isLocal ? 39292 : config.tls ? 49292 : 9292,
407
437
  path: 'groups',
408
438
  protocol: 'http',
409
439
  auth: config.authentication,
@@ -412,7 +442,7 @@ module.exports = function (RED) {
412
442
  tls: config.tls,
413
443
  inSecure: config.inSecure,
414
444
  init: true,
415
- ping: false // Todo ?
445
+ ping: false, // Todo ?
416
446
  },
417
447
  CUxD: {
418
448
  conf: 'cuxd',
@@ -420,8 +450,22 @@ module.exports = function (RED) {
420
450
  port: 8701,
421
451
  protocol: 'binrpc',
422
452
  init: true,
423
- ping: true
424
- }
453
+ ping: true,
454
+ },
455
+ 'CCU-Jack': {
456
+ conf: 'jack',
457
+ rpc: xmlrpc,
458
+ port: Number(config.jackPort) || (config.tls ? 2122 : 2121),
459
+ path: 'RPC3',
460
+ protocol: 'http',
461
+ auth: config.authentication,
462
+ user: config.username,
463
+ pass: config.password,
464
+ tls: config.tls,
465
+ inSecure: config.inSecure,
466
+ init: true,
467
+ ping: false,
468
+ },
425
469
  };
426
470
 
427
471
  this.name = config.name;
@@ -434,8 +478,14 @@ module.exports = function (RED) {
434
478
  if (ccu.network.listen.includes(config.rpcServerHost)) {
435
479
  this.rpcServerHost = config.rpcServerHost;
436
480
  } else {
437
- this.rpcServerHost = stringSimilarity.findBestMatch(config.rpcServerHost, ccu.network.listen).bestMatch.target;
438
- this.logger.error('Local address ' + config.rpcServerHost + ' not available. Using ' + this.rpcServerHost + ' instead.');
481
+ this.rpcServerHost = bestMatch(config.rpcServerHost, ccu.network.listen);
482
+ this.logger.error(
483
+ 'Local address ' +
484
+ config.rpcServerHost +
485
+ ' not available. Using ' +
486
+ this.rpcServerHost +
487
+ ' instead.',
488
+ );
439
489
  }
440
490
 
441
491
  this.rpcInitAddress = config.rpcInitAddress || this.rpcServerHost;
@@ -507,27 +557,36 @@ module.exports = function (RED) {
507
557
  this.rxCounters = {};
508
558
  this.txCounters = {};
509
559
 
510
- this.metadataFile = path.join(RED.settings.userDir || path.join(__dirname, '..'), 'ccu_' + this.host + '.json');
511
- this.regadataFile = path.join(RED.settings.userDir || path.join(__dirname, '..'), 'ccu_rega_' + this.host + '.json');
512
- this.valuesFile = path.join(RED.settings.userDir || path.join(__dirname, '..'), 'ccu_values_' + this.host + '.json');
560
+ this.metadataFile = path.join(
561
+ RED.settings.userDir || path.join(__dirname, '..'),
562
+ 'ccu_' + this.host + '.json',
563
+ );
564
+ this.regadataFile = path.join(
565
+ RED.settings.userDir || path.join(__dirname, '..'),
566
+ 'ccu_rega_' + this.host + '.json',
567
+ );
568
+ this.valuesFile = path.join(
569
+ RED.settings.userDir || path.join(__dirname, '..'),
570
+ 'ccu_values_' + this.host + '.json',
571
+ );
572
+
513
573
  this.loadMetadata();
514
574
  this.loadRegadata();
515
- //this.loadValues();
575
+ this.loadValues();
516
576
 
517
577
  this.setContext();
518
578
 
519
579
  this.rega = new Rega({
520
580
  host: this.host,
521
- port: this.isLocal ? 8183 : (config.tls ? 48181 : 8181),
581
+ port: this.isLocal ? 8183 : config.tls ? 48181 : 8181,
522
582
  tls: config.tls,
523
- inSecure: config.inSecure,
524
- auth: config.authentication,
525
- user: config.username,
526
- pass: config.password
583
+ insecure: config.inSecure,
584
+ username: config.authentication ? config.username : undefined,
585
+ password: config.authentication ? config.password : undefined,
527
586
  });
528
587
 
529
588
  this.enabledIfaces = [];
530
- Object.keys(this.ifaceTypes).forEach(iface => {
589
+ Object.keys(this.ifaceTypes).forEach((iface) => {
531
590
  const enabled = config[this.ifaceTypes[iface].conf + 'Enabled'];
532
591
  if (enabled) {
533
592
  this.enabledIfaces.push(iface);
@@ -536,11 +595,10 @@ module.exports = function (RED) {
536
595
  });
537
596
 
538
597
  if (config.regaEnabled) {
539
- this.getRegaData()
540
- .then(() => {
541
- this.regaPoll();
542
- this.initIfaces(config);
543
- });
598
+ this.getRegaData().then(() => {
599
+ this.regaPoll();
600
+ this.initIfaces(config);
601
+ });
544
602
  } else {
545
603
  this.initIfaces(config);
546
604
  }
@@ -566,7 +624,7 @@ module.exports = function (RED) {
566
624
  },
567
625
  error: (...args) => {
568
626
  this.error(args.join(' ').slice(0, 300));
569
- }
627
+ },
570
628
  };
571
629
  }
572
630
 
@@ -575,12 +633,18 @@ module.exports = function (RED) {
575
633
  * @param config
576
634
  */
577
635
  checkDuplicateConfig(config) {
578
- resolveHost(config.host).then(myAddr => {
579
- RED.nodes.eachNode(n => {
636
+ resolveHost(config.host).then((myAddr) => {
637
+ RED.nodes.eachNode((n) => {
580
638
  if (n.type === this.type && n.id !== this.id) {
581
- resolveHost(n.host).then(addr => {
639
+ resolveHost(n.host).then((addr) => {
582
640
  if (myAddr === addr) {
583
- this.logger.error('ccu-connection node ' + n.name + ' (' + n.id + ') is configured to connect to the same ccu. this leads to problems - only one ccu-connection node per ccu should exist!');
641
+ this.logger.error(
642
+ 'ccu-connection node ' +
643
+ n.name +
644
+ ' (' +
645
+ n.id +
646
+ ') is configured to connect to the same ccu. this leads to problems - only one ccu-connection node per ccu should exist!',
647
+ );
584
648
  }
585
649
  });
586
650
  }
@@ -593,11 +657,15 @@ module.exports = function (RED) {
593
657
  */
594
658
  setContext() {
595
659
  if (this.contextStore) {
596
- this.globalContext.set('ccu-' + this.host.replace(/\./g, '_'), {
597
- values: this.values,
598
- sysvar: this.sysvar,
599
- program: this.program
600
- }, this.contextStore);
660
+ this.globalContext.set(
661
+ 'ccu-' + this.host.replace(/\./g, '_'),
662
+ {
663
+ values: this.values,
664
+ sysvar: this.sysvar,
665
+ program: this.program,
666
+ },
667
+ this.contextStore,
668
+ );
601
669
  }
602
670
  }
603
671
 
@@ -632,15 +700,24 @@ module.exports = function (RED) {
632
700
 
633
701
  if (RED.settings.logging) {
634
702
  const [firstLogger] = Object.keys(RED.settings.logging);
635
- if (RED.settings.logging[firstLogger] && RED.settings.logging[firstLogger].level !== 'debug' && RED.settings.logging[firstLogger].level !== 'trace') {
703
+ if (
704
+ RED.settings.logging[firstLogger] &&
705
+ RED.settings.logging[firstLogger].level !== 'debug' &&
706
+ RED.settings.logging[firstLogger].level !== 'trace'
707
+ ) {
636
708
  return;
637
709
  }
638
710
  }
639
711
 
640
712
  this.statsInterval = setInterval(() => {
641
- this.logger.debug('stats rpc rx: ' + JSON.stringify(this.rxCounters) + ' tx: ' + JSON.stringify(this.txCounters));
713
+ this.logger.debug(
714
+ 'stats rpc rx: ' + JSON.stringify(this.rxCounters) + ' tx: ' + JSON.stringify(this.txCounters),
715
+ );
642
716
  this.logger.debug('stats rpc subscribers ' + Object.keys(this.callbacks).length);
643
- this.logger.debug('stats rega subscribers ' + (Object.keys(this.programCallbacks).length + Object.keys(this.sysvarCallbacks).length));
717
+ this.logger.debug(
718
+ 'stats rega subscribers ' +
719
+ (Object.keys(this.programCallbacks).length + Object.keys(this.sysvarCallbacks).length),
720
+ );
644
721
  }, 60000);
645
722
  }
646
723
 
@@ -654,11 +731,17 @@ module.exports = function (RED) {
654
731
  if (iface === 'ReGaHSS') {
655
732
  this.logger.info('Interface', iface, connected ? 'connected' : 'disconnected');
656
733
  } else {
657
- this.logger.info('Interface', iface, connected ? (this.ifaceTypes[iface].protocol + ' port ' + this.ifaceTypes[iface].port + ' connected') : 'disconnected');
734
+ this.logger.info(
735
+ 'Interface',
736
+ iface,
737
+ connected
738
+ ? this.ifaceTypes[iface].protocol + ' port ' + this.ifaceTypes[iface].port + ' connected'
739
+ : 'disconnected',
740
+ );
658
741
  }
659
742
 
660
743
  this.ifaceStatus[iface] = !this.serverError[iface] && connected;
661
- Object.keys(this.users).forEach(id => {
744
+ Object.keys(this.users).forEach((id) => {
662
745
  if (typeof this.users[id].setStatus === 'function') {
663
746
  this.users[id].setStatus({ifaceStatus: this.ifaceStatus});
664
747
  }
@@ -671,7 +754,7 @@ module.exports = function (RED) {
671
754
  * @returns {Promise<any>}
672
755
  */
673
756
  saveMetadata() {
674
- return new Promise(resolve => {
757
+ return new Promise((resolve) => {
675
758
  fs.writeFileSync(this.metadataFile, JSON.stringify(this.metadata));
676
759
  this.logger.info('metadata saved to', this.metadataFile);
677
760
  resolve();
@@ -683,17 +766,20 @@ module.exports = function (RED) {
683
766
  * @returns {Promise<any>}
684
767
  */
685
768
  saveRegadata() {
686
- return new Promise(resolve => {
687
- fs.writeFileSync(this.regadataFile, JSON.stringify({
688
- channelNames: this.channelNames,
689
- regaIdChannel: this.regaIdChannel,
690
- regaChannels: this.regaChannels,
691
- channelRooms: this.channelRooms,
692
- channelFunctions: this.channelFunctions,
693
- groups: this.groups,
694
- sysvar: this.sysvar,
695
- program: this.program
696
- }));
769
+ return new Promise((resolve) => {
770
+ fs.writeFileSync(
771
+ this.regadataFile,
772
+ JSON.stringify({
773
+ channelNames: this.channelNames,
774
+ regaIdChannel: this.regaIdChannel,
775
+ regaChannels: this.regaChannels,
776
+ channelRooms: this.channelRooms,
777
+ channelFunctions: this.channelFunctions,
778
+ groups: this.groups,
779
+ sysvar: this.sysvar,
780
+ program: this.program,
781
+ }),
782
+ );
697
783
  this.logger.info('regadata saved to', this.regadataFile);
698
784
  resolve();
699
785
  });
@@ -704,10 +790,13 @@ module.exports = function (RED) {
704
790
  * @returns {Promise<any>}
705
791
  */
706
792
  saveValues() {
707
- return new Promise(resolve => {
708
- fs.writeFileSync(this.valuesFile, JSON.stringify({
709
- values: this.values
710
- }));
793
+ return new Promise((resolve) => {
794
+ fs.writeFileSync(
795
+ this.valuesFile,
796
+ JSON.stringify({
797
+ values: this.values,
798
+ }),
799
+ );
711
800
  this.logger.info('values saved to', this.valuesFile);
712
801
  resolve();
713
802
  });
@@ -718,16 +807,16 @@ module.exports = function (RED) {
718
807
  * @returns {Promise<any>}
719
808
  */
720
809
  loadMetadata() {
721
- return new Promise(resolve => {
810
+ return new Promise((resolve) => {
722
811
  try {
723
812
  this.metadata = JSON.parse(fs.readFileSync(this.metadataFile));
724
813
  this.logger.info('metadata loaded from', this.metadataFile);
725
814
  resolve();
726
815
  } catch {
727
- this.logger.warn('metadata new empty');
816
+ this.logger.info('no cached metadata yet, starting empty');
728
817
  this.metadata = {
729
818
  devices: {},
730
- types: {}
819
+ types: {},
731
820
  };
732
821
  resolve();
733
822
  }
@@ -739,7 +828,7 @@ module.exports = function (RED) {
739
828
  * @returns {Promise<any>}
740
829
  */
741
830
  loadRegadata() {
742
- return new Promise(resolve => {
831
+ return new Promise((resolve) => {
743
832
  try {
744
833
  const regadata = JSON.parse(fs.readFileSync(this.regadataFile));
745
834
  this.logger.info('regadata loaded from', this.regadataFile);
@@ -761,7 +850,12 @@ module.exports = function (RED) {
761
850
  */
762
851
  resolve();
763
852
  } catch (error) {
764
- this.logger.error('error loading regadata ' + error.message);
853
+ if (error.code === 'ENOENT') {
854
+ this.logger.info('no cached regadata yet (first start)');
855
+ } else {
856
+ this.logger.error('error loading regadata ' + error.message);
857
+ }
858
+
765
859
  resolve();
766
860
  }
767
861
  });
@@ -772,14 +866,28 @@ module.exports = function (RED) {
772
866
  * @returns {Promise<any>}
773
867
  */
774
868
  loadValues() {
775
- return new Promise(resolve => {
869
+ return new Promise((resolve) => {
776
870
  try {
777
- const valuesdata = JSON.parse(fs.readFileSync(this.valuesFile));
871
+ const {values} = JSON.parse(fs.readFileSync(this.valuesFile));
778
872
  this.logger.info('values loaded from', this.valuesFile);
779
- this.values = valuesdata.values;
873
+
874
+ for (const datapointName in values) {
875
+ this.values[datapointName] = {
876
+ ...values[datapointName],
877
+ cache: true,
878
+ change: false,
879
+ uncertain: true,
880
+ };
881
+ }
882
+
780
883
  resolve();
781
884
  } catch (error) {
782
- this.logger.error('error loading values ' + error.message);
885
+ if (error.code === 'ENOENT') {
886
+ this.logger.info('no cached values yet (first start)');
887
+ } else {
888
+ this.logger.error('error loading values ' + error.message);
889
+ }
890
+
783
891
  resolve();
784
892
  }
785
893
  });
@@ -790,9 +898,13 @@ module.exports = function (RED) {
790
898
  * @returns {Promise<any>}
791
899
  */
792
900
  saveParamsets() {
793
- return new Promise(resolve => {
901
+ return new Promise((resolve) => {
794
902
  fs.writeFileSync(this.paramsetFile, JSON.stringify(this.paramsetDescriptions, null, ' '));
795
- this.logger.info('paramsets saved to', this.paramsetFile, (this.paramsetDescriptions ? Object.keys(this.paramsetDescriptions).length : 0));
903
+ this.logger.info(
904
+ 'paramsets saved to',
905
+ this.paramsetFile,
906
+ this.paramsetDescriptions ? Object.keys(this.paramsetDescriptions).length : 0,
907
+ );
796
908
  resolve();
797
909
  });
798
910
  }
@@ -802,8 +914,8 @@ module.exports = function (RED) {
802
914
  * @returns {Promise<any>}
803
915
  */
804
916
  loadParamsets() {
805
- return new Promise(resolve => {
806
- const load = file => {
917
+ return new Promise((resolve) => {
918
+ const load = (file) => {
807
919
  try {
808
920
  this.paramsetDescriptions = JSON.parse(fs.readFileSync(file));
809
921
  this.logger.info('paramsets loaded from', file);
@@ -835,7 +947,7 @@ module.exports = function (RED) {
835
947
  this.cancelRegaPoll = true;
836
948
  clearTimeout(this.regaPollTimeout);
837
949
 
838
- Object.keys(this.rpcPingTimer).forEach(iface => {
950
+ Object.keys(this.rpcPingTimer).forEach((iface) => {
839
951
  this.logger.debug('clear rpcPingTimer', iface);
840
952
  clearTimeout(this.rpcPingTimer[iface]);
841
953
  });
@@ -847,7 +959,8 @@ module.exports = function (RED) {
847
959
  .then(() => {
848
960
  this.logger.info('rpc close done');
849
961
  done();
850
- }).catch(error => {
962
+ })
963
+ .catch((error) => {
851
964
  this.logger.warn(error);
852
965
  done();
853
966
  });
@@ -879,24 +992,27 @@ module.exports = function (RED) {
879
992
  * @returns {Promise<any>}
880
993
  */
881
994
  getGroupsData() {
882
- return new Promise(resolve => {
995
+ return new Promise((resolve) => {
883
996
  this.logger.debug('virtualdevices get groups');
884
- this.rega.exec(`
997
+ execToCallback(
998
+ this.rega.exec(`
885
999
  var stdoutGroups;
886
1000
  var stderrGroups;
887
1001
  system.Exec("cat /etc/config/groups.gson", &stdoutGroups, &stderrGroups);
888
- `, (err, stdout, objects) => {
889
- if (!err && objects && objects.stderrGroups === 'null') {
890
- try {
891
- const {groups} = JSON.parse(objects.stdoutGroups);
892
- groups.forEach(group => {
893
- this.groups[group.id] = group;
894
- });
895
- } catch {}
896
- }
1002
+ `),
1003
+ (err, stdout, objects) => {
1004
+ if (!err && objects && objects.stderrGroups === 'null') {
1005
+ try {
1006
+ const {groups} = JSON.parse(objects.stdoutGroups);
1007
+ groups.forEach((group) => {
1008
+ this.groups[group.id] = group;
1009
+ });
1010
+ } catch {}
1011
+ }
897
1012
 
898
- resolve();
899
- });
1013
+ resolve();
1014
+ },
1015
+ );
900
1016
  });
901
1017
  }
902
1018
 
@@ -919,17 +1035,28 @@ module.exports = function (RED) {
919
1035
  */
920
1036
  getRegaValues() {
921
1037
  return new Promise((resolve, reject) => {
922
- this.logger.debug('rega getValues');
923
- this.rega.getValues((err, res) => {
1038
+ this.logger.info('rega getValues');
1039
+ toCallback(this.rega.getValues(), (err, res) => {
924
1040
  if (err) {
925
1041
  reject(new Error('rega getValues ' + err.message));
926
1042
  } else {
927
- const d = new Date();
928
- res.forEach(dp => {
929
- const ts = (new Date(dp.ts + ' UTC+' + (d.getTimezoneOffset() / -60))).getTime();
1043
+ res.forEach((dp) => {
1044
+ // dp.ts is epoch ms since homematic-rega 2.x (0 = never)
1045
+ const ts = dp.ts;
930
1046
  const [iface, channel, datapoint] = dp.name.split('.');
931
1047
  if (this.enabledIfaces.includes(iface) && datapoint) {
932
- const message = this.createMessage(iface, channel, datapoint, dp.value, {cache: true, change: false, working: false, uncertain: dp.ts === '1970-01-01 01:00:00', ts, lc: ts});
1048
+ if (['RSSI_DEVICE', 'RSSI_PEER'].includes(datapoint)) {
1049
+ dp.value -= 256;
1050
+ }
1051
+
1052
+ const message = this.createMessage(iface, channel, datapoint, dp.value, {
1053
+ cache: true,
1054
+ change: false,
1055
+ working: false,
1056
+ uncertain: dp.ts === 0,
1057
+ ts,
1058
+ lc: ts,
1059
+ });
933
1060
  this.values[message.datapointName] = message;
934
1061
  if (!datapoint.startsWith('PRESS_')) {
935
1062
  this.callCallbacks(message);
@@ -951,7 +1078,7 @@ module.exports = function (RED) {
951
1078
  getRegaChannels() {
952
1079
  return new Promise((resolve, reject) => {
953
1080
  this.logger.debug('rega getChannels');
954
- this.rega.getChannels((err, res) => {
1081
+ toCallback(this.rega.getChannels(), (err, res) => {
955
1082
  if (err) {
956
1083
  reject(new Error('rega getChannels ' + err.message));
957
1084
  } else {
@@ -961,7 +1088,7 @@ module.exports = function (RED) {
961
1088
  this.channelNames = {};
962
1089
  }
963
1090
 
964
- res.forEach(ch => {
1091
+ res.forEach((ch) => {
965
1092
  this.regaChannels.push(ch);
966
1093
  this.regaIdChannel[ch.id] = ch.address;
967
1094
  this.channelNames[ch.address] = ch.name;
@@ -979,7 +1106,7 @@ module.exports = function (RED) {
979
1106
  getRegaRooms() {
980
1107
  return new Promise((resolve, reject) => {
981
1108
  this.logger.debug('rega getRooms');
982
- this.rega.getRooms((err, res) => {
1109
+ toCallback(this.rega.getRooms(), (err, res) => {
983
1110
  if (err) {
984
1111
  reject(new Error('rega getRooms ' + err.message));
985
1112
  } else {
@@ -988,9 +1115,9 @@ module.exports = function (RED) {
988
1115
  this.channelRooms = {};
989
1116
  }
990
1117
 
991
- res.forEach(room => {
1118
+ res.forEach((room) => {
992
1119
  this.rooms.push(room.name);
993
- room.channels.forEach(chId => {
1120
+ room.channels.forEach((chId) => {
994
1121
  const regaChannel = this.getEntry(this.regaChannels, 'id', chId);
995
1122
  const address = regaChannel && regaChannel.address;
996
1123
  if (address) {
@@ -1015,7 +1142,7 @@ module.exports = function (RED) {
1015
1142
  getRegaFunctions() {
1016
1143
  return new Promise((resolve, reject) => {
1017
1144
  this.logger.debug('rega getFunctions');
1018
- this.rega.getFunctions((err, res) => {
1145
+ toCallback(this.rega.getFunctions(), (err, res) => {
1019
1146
  if (err) {
1020
1147
  reject(new Error('rega getFunctions ' + err.message));
1021
1148
  } else {
@@ -1024,9 +1151,9 @@ module.exports = function (RED) {
1024
1151
  this.channelFunctions = {};
1025
1152
  }
1026
1153
 
1027
- res.forEach(func => {
1154
+ res.forEach((func) => {
1028
1155
  this.functions.push(func.name);
1029
- func.channels.forEach(chId => {
1156
+ func.channels.forEach((chId) => {
1030
1157
  const regaChannel = this.getEntry(this.regaChannels, 'id', chId);
1031
1158
  const address = regaChannel && regaChannel.address;
1032
1159
  if (address) {
@@ -1056,12 +1183,12 @@ module.exports = function (RED) {
1056
1183
  if (program) {
1057
1184
  const script = `dom.GetObject(${program.id}).Active(${active});`;
1058
1185
  this.logger.debug('rega programActive', name, script);
1059
- this.rega.exec(script + '\n', err => {
1186
+ execToCallback(this.rega.exec(script + '\n'), (err) => {
1060
1187
  if (err) {
1061
1188
  reject(err);
1062
1189
  } else {
1063
1190
  Object.assign(program, {
1064
- active
1191
+ active,
1065
1192
  });
1066
1193
  resolve(program);
1067
1194
  }
@@ -1084,14 +1211,21 @@ module.exports = function (RED) {
1084
1211
  const d = new Date();
1085
1212
  const script = `dom.GetObject(${program.id}).ProgramExecute();`;
1086
1213
  this.logger.debug('rega programExecute', name, script);
1087
- this.rega.exec(script + `\nvar lastExecTime = dom.GetObject(${program.id}).ProgramLastExecuteTime();\n`, (err, res, objects) => {
1088
- if (err) {
1089
- reject(err);
1090
- } else {
1091
- program.ts = (new Date(objects.lastExecTime + ' UTC+' + (d.getTimezoneOffset() / -60))).getTime();
1092
- resolve(program);
1093
- }
1094
- });
1214
+ execToCallback(
1215
+ this.rega.exec(
1216
+ script + `\nvar lastExecTime = dom.GetObject(${program.id}).ProgramLastExecuteTime();\n`,
1217
+ ),
1218
+ (err, res, objects) => {
1219
+ if (err) {
1220
+ reject(err);
1221
+ } else {
1222
+ program.ts = new Date(
1223
+ objects.lastExecTime + ' UTC+' + d.getTimezoneOffset() / -60,
1224
+ ).getTime();
1225
+ resolve(program);
1226
+ }
1227
+ },
1228
+ );
1095
1229
  } else {
1096
1230
  reject(new Error('programExecute ' + name + ' not found'));
1097
1231
  }
@@ -1146,7 +1280,7 @@ module.exports = function (RED) {
1146
1280
 
1147
1281
  const script = `dom.GetObject(${sysvar.id}).State(${value});`;
1148
1282
  this.logger.debug('setVariable', name, script);
1149
- this.rega.exec(script + '\n', err => {
1283
+ execToCallback(this.rega.exec(script + '\n'), (err) => {
1150
1284
  if (err) {
1151
1285
  reject(err);
1152
1286
  } else {
@@ -1174,9 +1308,9 @@ module.exports = function (RED) {
1174
1308
  this.regaPollPending = true;
1175
1309
  clearTimeout(this.regaPollTimeout);
1176
1310
  this.getRegaVariables()
1177
- .catch(error => this.logger.error('getRegaVariables', error))
1311
+ .catch((error) => this.logger.error('getRegaVariables', error))
1178
1312
  .then(() => this.getRegaPrograms())
1179
- .catch(error => this.logger.error('getRegaPrograms', error))
1313
+ .catch((error) => this.logger.error('getRegaPrograms', error))
1180
1314
  .finally(() => {
1181
1315
  if (this.regaInterval && this.regaPollEnabled && !this.cancelRegaPoll) {
1182
1316
  //this.logger.trace('rega next poll in', this.regaInterval, 'seconds');
@@ -1248,24 +1382,39 @@ module.exports = function (RED) {
1248
1382
  unit: sysvar.unit,
1249
1383
  enum: sysvar.enum,
1250
1384
  id: sysvar.id,
1251
- cache: isNew
1385
+ cache: isNew,
1252
1386
  };
1253
1387
  if (sysvar.channel) {
1254
1388
  const channel = this.regaIdChannel[sysvar.channel];
1255
1389
  const iface = this.findIface(channel);
1256
- const device = this.metadata.devices[iface] && this.metadata.devices[iface][channel] && this.metadata.devices[iface][channel].PARENT;
1390
+ const device =
1391
+ this.metadata.devices[iface] &&
1392
+ this.metadata.devices[iface][channel] &&
1393
+ this.metadata.devices[iface][channel].PARENT;
1257
1394
  Object.assign(this.sysvar[sysvar.name], {
1258
1395
  device,
1259
1396
  deviceName: this.channelNames[device],
1260
- deviceType: this.metadata.devices[iface] && this.metadata.devices[iface][device] && this.metadata.devices[iface][device].TYPE,
1397
+ deviceType:
1398
+ this.metadata.devices[iface] &&
1399
+ this.metadata.devices[iface][device] &&
1400
+ this.metadata.devices[iface][device].TYPE,
1261
1401
  channel,
1262
1402
  channelName: this.channelNames[channel],
1263
- channelType: this.metadata.devices[iface] && this.metadata.devices[iface][channel] && this.metadata.devices[iface][channel].TYPE,
1403
+ channelType:
1404
+ this.metadata.devices[iface] &&
1405
+ this.metadata.devices[iface][channel] &&
1406
+ this.metadata.devices[iface][channel].TYPE,
1264
1407
  channelIndex: channel && Number.parseInt(channel.split(':')[1], 10),
1265
1408
  rooms: this.channelRooms[channel],
1266
- room: this.channelRooms[channel] && this.channelRooms[channel].length === 1 ? this.channelRooms[channel][0] : undefined,
1409
+ room:
1410
+ this.channelRooms[channel] && this.channelRooms[channel].length === 1
1411
+ ? this.channelRooms[channel][0]
1412
+ : undefined,
1267
1413
  functions: this.channelFunctions[channel],
1268
- function: this.channelFunctions[channel] && this.channelFunctions[channel].length === 1 ? this.channelFunctions[channel][0] : undefined
1414
+ function:
1415
+ this.channelFunctions[channel] && this.channelFunctions[channel].length === 1
1416
+ ? this.channelFunctions[channel][0]
1417
+ : undefined,
1269
1418
  });
1270
1419
  }
1271
1420
  } else if (this.sysvar[sysvar.name].fromFile) {
@@ -1283,13 +1432,16 @@ module.exports = function (RED) {
1283
1432
  valueEnumPrevious: this.sysvar[sysvar.name].valueEnum,
1284
1433
  ts: sysvar.ts,
1285
1434
  tsPrevious: this.sysvar[sysvar.name].ts,
1286
- lc: this.sysvar[sysvar.name].value !== sysvar.val && !isNew ? sysvar.ts : this.sysvar[sysvar.name].lc,
1435
+ lc:
1436
+ this.sysvar[sysvar.name].value !== sysvar.val && !isNew
1437
+ ? sysvar.ts
1438
+ : this.sysvar[sysvar.name].lc,
1287
1439
  lcPrevious: this.sysvar[sysvar.name].lc,
1288
1440
  change: isNew ? false : this.sysvar[sysvar.name].value !== sysvar.val,
1289
- cache: isNew
1441
+ cache: isNew,
1290
1442
  });
1291
1443
 
1292
- Object.keys(this.sysvarCallbacks).forEach(key => {
1444
+ Object.keys(this.sysvarCallbacks).forEach((key) => {
1293
1445
  const {filter, callback} = this.sysvarCallbacks[key];
1294
1446
  let match = !filter.name || filter.name === sysvar.name;
1295
1447
  if (this.sysvar[sysvar.name].cache && !filter.cache) {
@@ -1315,24 +1467,26 @@ module.exports = function (RED) {
1315
1467
  getRegaVariables() {
1316
1468
  return new Promise((resolve, reject) => {
1317
1469
  this.logger.debug('getRegaVariables');
1318
- this.rega.getVariables((err, res) => {
1470
+ toCallback(this.rega.getVariables(), (err, res) => {
1319
1471
  if (err) {
1320
1472
  reject(err);
1321
1473
  this.hadTimeout.add('ReGaHSS');
1322
1474
  this.setIfaceStatus('ReGaHSS', false);
1323
1475
  } else {
1324
- const d = new Date();
1325
- res.forEach(sysvar => {
1476
+ res.forEach((sysvar) => {
1326
1477
  //this.logger.trace(JSON.stringify(sysvar));
1327
- sysvar.ts = sysvar.ts ? (new Date(sysvar.ts + ' UTC+' + (d.getTimezoneOffset() / -60))).getTime() : d.getTime();
1478
+ // sysvar.ts is epoch ms since homematic-rega 2.x (0 = never)
1479
+ sysvar.ts = sysvar.ts || Date.now();
1328
1480
  this.updateRegaVariable(sysvar);
1329
1481
  });
1330
1482
  if (!this.hasRegaVariables) {
1331
1483
  this.hasRegaVariables = true;
1332
- Object.keys(this.setVariableQueueTimeout).forEach(name => clearTimeout(this.setVariableQueueTimeout[name]));
1333
- Object.keys(this.setVariableQueue).reduce((p, name) =>
1334
- p.then(_ => this.setVariable(name, this.setVariableQueue[name])),
1335
- Promise.resolve()
1484
+ Object.keys(this.setVariableQueueTimeout).forEach((name) =>
1485
+ clearTimeout(this.setVariableQueueTimeout[name]),
1486
+ );
1487
+ Object.keys(this.setVariableQueue).reduce(
1488
+ (p, name) => p.then((_) => this.setVariable(name, this.setVariableQueue[name])),
1489
+ Promise.resolve(),
1336
1490
  );
1337
1491
  }
1338
1492
 
@@ -1350,16 +1504,15 @@ module.exports = function (RED) {
1350
1504
  getRegaPrograms() {
1351
1505
  return new Promise((resolve, reject) => {
1352
1506
  this.logger.debug('getRegaPrograms');
1353
- this.rega.getPrograms((err, res) => {
1507
+ toCallback(this.rega.getPrograms(), (err, res) => {
1354
1508
  if (err) {
1355
1509
  reject(err);
1356
1510
  this.hadTimeout.add('ReGaHSS');
1357
1511
  this.setIfaceStatus('ReGaHSS', false);
1358
1512
  } else if (res && Array.isArray(res)) {
1359
- const d = new Date();
1360
- res.forEach(prg => {
1513
+ res.forEach((prg) => {
1361
1514
  prg.type = 'PROGRAM';
1362
- prg.ts = (new Date(prg.ts + ' UTC+' + (d.getTimezoneOffset() / -60))).getTime();
1515
+ // prg.ts is epoch ms since homematic-rega 2.x (0 = never)
1363
1516
  if (!this.program[prg.name]) {
1364
1517
  this.program[prg.name] = {};
1365
1518
  }
@@ -1381,9 +1534,9 @@ module.exports = function (RED) {
1381
1534
  active: prg.active,
1382
1535
  activePrevious: this.program[prg.name].active,
1383
1536
  ts: prg.ts,
1384
- tsPrevious: this.program[prg.name].ts
1537
+ tsPrevious: this.program[prg.name].ts,
1385
1538
  };
1386
- Object.keys(this.programCallbacks).forEach(key => {
1539
+ Object.keys(this.programCallbacks).forEach((key) => {
1387
1540
  const {filter, callback} = this.programCallbacks[key];
1388
1541
  if (!filter.name || filter.name === prg.name) {
1389
1542
  callback(this.program[prg.name]);
@@ -1402,20 +1555,22 @@ module.exports = function (RED) {
1402
1555
  * @param config
1403
1556
  */
1404
1557
  initIfaces(config) {
1405
- Object.keys(this.ifaceTypes).forEach(iface => {
1558
+ Object.keys(this.ifaceTypes).forEach((iface) => {
1406
1559
  const enabled = config[this.ifaceTypes[iface].conf + 'Enabled'];
1407
1560
  this.ifaceTypes[iface].enabled = enabled;
1408
1561
  if (enabled) {
1409
1562
  this.createClient(iface)
1410
1563
  .then(() => {
1411
1564
  if (this.ifaceTypes[iface].init) {
1412
- return this.rpcInit(iface).then(() => {
1413
- this.setIfaceStatus(iface, true);
1414
- }).catch(error => {
1415
- this.logger.error('init', iface, error);
1416
- this.hadTimeout.add(iface);
1417
- this.setIfaceStatus(iface, false);
1418
- });
1565
+ return this.rpcInit(iface)
1566
+ .then(() => {
1567
+ this.setIfaceStatus(iface, true);
1568
+ })
1569
+ .catch((error) => {
1570
+ this.logger.error('init', iface, error);
1571
+ this.hadTimeout.add(iface);
1572
+ this.setIfaceStatus(iface, false);
1573
+ });
1419
1574
  }
1420
1575
 
1421
1576
  this.setIfaceStatus(iface, true);
@@ -1432,7 +1587,7 @@ module.exports = function (RED) {
1432
1587
  * @returns {Promise<any>}
1433
1588
  */
1434
1589
  createClient(iface) {
1435
- return new Promise(resolve => {
1590
+ return new Promise((resolve) => {
1436
1591
  const {rpc, port, path, protocol, auth, user, pass, tls, inSecure} = this.ifaceTypes[iface];
1437
1592
  const clientOptions = {};
1438
1593
  if (path) {
@@ -1454,12 +1609,20 @@ module.exports = function (RED) {
1454
1609
  this.clients[iface] = rpc.createClient(clientOptions);
1455
1610
  }
1456
1611
 
1457
- this.logger.debug('rpc client created', iface, JSON.stringify(clientOptions));
1612
+ this.logger.info(
1613
+ 'rpc client ' +
1614
+ iface +
1615
+ ' ' +
1616
+ (protocol === 'binrpc' ? 'binrpc' : tls ? 'xmlrpc/tls' : 'xmlrpc') +
1617
+ ' ' +
1618
+ this.host +
1619
+ ':' +
1620
+ port +
1621
+ (path ? '/' + path : ''),
1622
+ );
1458
1623
  if (this.methodCallQueue[iface]) {
1459
- this.methodCallQueue[iface].forEach(c => {
1460
- this.methodCall(iface, c[0], c[1])
1461
- .then(c[2])
1462
- .catch(c[3]);
1624
+ this.methodCallQueue[iface].forEach((c) => {
1625
+ this.methodCall(iface, c[0], c[1]).then(c[2]).catch(c[3]);
1463
1626
  });
1464
1627
  delete this.methodCallQueue[iface];
1465
1628
  }
@@ -1480,20 +1643,40 @@ module.exports = function (RED) {
1480
1643
  const initId = 'nr_' + hash + '_' + iface;
1481
1644
  this.lastEvent[iface] = now();
1482
1645
 
1483
- this.logger.info('init ' + iface + ' ' + initUrl + ' ' + initId);
1646
+ const {protocol, port} = this.ifaceTypes[iface];
1647
+ this.logger.info(
1648
+ 'init ' +
1649
+ iface +
1650
+ ' (' +
1651
+ (protocol === 'binrpc' ? 'binrpc' : 'xmlrpc') +
1652
+ ' ' +
1653
+ this.host +
1654
+ ':' +
1655
+ port +
1656
+ ') callback ' +
1657
+ initUrl +
1658
+ ' ' +
1659
+ initId,
1660
+ );
1484
1661
  this.methodCall(iface, 'init', [initUrl, initId])
1485
1662
  .then(() => {
1486
1663
  if (iface === 'CUxD') {
1487
- this.getDevices(iface).then(() => resolve(iface)).catch(() => resolve(iface));
1664
+ this.getDevices(iface)
1665
+ .then(() => resolve(iface))
1666
+ .catch(() => resolve(iface));
1488
1667
  } else if (['BidCos-RF', 'BidCos-Wired', 'HmIP-RF'].includes(iface)) {
1489
- this.methodCall(iface, 'getLinks', []).then(res => {
1490
- this.links[iface] = res;
1491
- resolve(iface);
1492
- }).catch(() => resolve(iface));
1668
+ this.methodCall(iface, 'getLinks', [])
1669
+ .then((res) => {
1670
+ this.links[iface] = res;
1671
+ resolve(iface);
1672
+ })
1673
+ .catch(() => resolve(iface));
1493
1674
  } else {
1494
1675
  resolve(iface);
1495
1676
  }
1496
- }).catch(error => reject(error)).finally(() => {
1677
+ })
1678
+ .catch((error) => reject(error))
1679
+ .finally(() => {
1497
1680
  if (this.ifaceTypes[iface].ping) {
1498
1681
  this.rpcCheckInit(iface);
1499
1682
  }
@@ -1511,7 +1694,7 @@ module.exports = function (RED) {
1511
1694
  getLinks(iface, address, receiver) {
1512
1695
  const links = [];
1513
1696
  if (this.links[iface]) {
1514
- this.links[iface].forEach(link => {
1697
+ this.links[iface].forEach((link) => {
1515
1698
  if (link[receiver ? 'RECEIVER' : 'SENDER'] === address) {
1516
1699
  links.push(link[receiver ? 'SENDER' : 'RECEIVER']);
1517
1700
  }
@@ -1528,34 +1711,36 @@ module.exports = function (RED) {
1528
1711
  */
1529
1712
  getDevices(iface) {
1530
1713
  return new Promise((resolve, reject) => {
1531
- this.methodCall(iface, 'listDevices', []).then(devices => {
1532
- if (!this.metadata.devices[iface]) {
1533
- this.metadata.devices[iface] = {};
1534
- }
1535
-
1536
- const knownDevices = [];
1537
- let change = false;
1538
- devices.forEach(device => {
1539
- knownDevices.push(device.ADDRESS);
1540
- if (!this.metadata.devices[iface][device.ADDRESS]) {
1541
- this.newDevice(iface, device);
1542
- change = true;
1714
+ this.methodCall(iface, 'listDevices', [])
1715
+ .then((devices) => {
1716
+ if (!this.metadata.devices[iface]) {
1717
+ this.metadata.devices[iface] = {};
1543
1718
  }
1544
- });
1545
1719
 
1546
- Object.keys(this.metadata.devices[iface]).forEach(addr => {
1547
- if (!knownDevices.includes(addr)) {
1548
- this.deleteDevice(iface, addr);
1549
- change = true;
1550
- }
1551
- });
1720
+ const knownDevices = [];
1721
+ let change = false;
1722
+ devices.forEach((device) => {
1723
+ knownDevices.push(device.ADDRESS);
1724
+ if (!this.metadata.devices[iface][device.ADDRESS]) {
1725
+ this.newDevice(iface, device);
1726
+ change = true;
1727
+ }
1728
+ });
1552
1729
 
1553
- if (change) {
1554
- this.saveMetadata();
1555
- }
1730
+ Object.keys(this.metadata.devices[iface]).forEach((addr) => {
1731
+ if (!knownDevices.includes(addr)) {
1732
+ this.deleteDevice(iface, addr);
1733
+ change = true;
1734
+ }
1735
+ });
1556
1736
 
1557
- resolve();
1558
- }).catch(reject);
1737
+ if (change) {
1738
+ this.saveMetadata();
1739
+ }
1740
+
1741
+ resolve();
1742
+ })
1743
+ .catch(reject);
1559
1744
  });
1560
1745
  }
1561
1746
 
@@ -1576,11 +1761,11 @@ module.exports = function (RED) {
1576
1761
  this.hadTimeout.add(iface);
1577
1762
  this.setIfaceStatus(iface, false);
1578
1763
  this.logger.warn('ping timeout', iface, elapsed);
1579
- this.rpcInit(iface).catch(error => this.logger.error(error.message));
1764
+ this.rpcInit(iface).catch((error) => this.logger.error(error.message));
1580
1765
  return;
1581
1766
  }
1582
1767
 
1583
- if (elapsed >= (pingTimeout / 2)) {
1768
+ if (elapsed >= pingTimeout / 2) {
1584
1769
  //this.logger.trace('ping', iface, elapsed);
1585
1770
  this.methodCall(iface, 'ping', ['nr']).catch(() => {
1586
1771
  this.setIfaceStatus(iface, false);
@@ -1599,19 +1784,21 @@ module.exports = function (RED) {
1599
1784
  rpcClose() {
1600
1785
  this.logger.debug('rpcClose');
1601
1786
  const calls = [];
1602
- Object.keys(this.clients).forEach(iface => {
1787
+ Object.keys(this.clients).forEach((iface) => {
1603
1788
  if (this.ifaceTypes[iface].init) {
1604
1789
  this.logger.debug('queue de-init ' + iface + ' ' + this.initUrl(iface));
1605
1790
  calls.push(() => {
1606
- return new Promise(resolve => {
1791
+ return new Promise((resolve) => {
1607
1792
  this.logger.debug('de-init ' + iface + ' ' + this.initUrl(iface));
1608
1793
  this.methodCall(iface, 'init', [this.initUrl(iface), ''])
1609
1794
  .then(() => {
1610
1795
  this.logger.info('de-init ' + iface + ' ' + this.initUrl(iface) + ' done');
1611
1796
  resolve();
1612
1797
  })
1613
- .catch(error => {
1614
- this.logger.error('de-init ' + iface + ' ' + this.initUrl(iface) + ' failed ' + error);
1798
+ .catch((error) => {
1799
+ this.logger.error(
1800
+ 'de-init ' + iface + ' ' + this.initUrl(iface) + ' failed ' + error,
1801
+ );
1615
1802
  resolve();
1616
1803
  });
1617
1804
  });
@@ -1621,7 +1808,7 @@ module.exports = function (RED) {
1621
1808
 
1622
1809
  this.logger.debug('queue binrpc server closing');
1623
1810
  calls.push(() => {
1624
- return new Promise(resolve => {
1811
+ return new Promise((resolve) => {
1625
1812
  this.logger.debug('binrpc server closing');
1626
1813
  let timeout;
1627
1814
  if (this.servers.binrpc && this.servers.binrpc.server) {
@@ -1643,7 +1830,7 @@ module.exports = function (RED) {
1643
1830
 
1644
1831
  this.logger.debug('xmlrpc binrpc server closing');
1645
1832
  calls.push(() => {
1646
- return new Promise(resolve => {
1833
+ return new Promise((resolve) => {
1647
1834
  this.logger.debug('xmlrpc server closing');
1648
1835
  let timeout;
1649
1836
  if (this.servers.http && this.servers.http.close) {
@@ -1676,7 +1863,7 @@ module.exports = function (RED) {
1676
1863
  */
1677
1864
  initUrl(iface) {
1678
1865
  const {protocol} = this.ifaceTypes[iface];
1679
- const port = (protocol === 'binrpc' ? this.rpcBinPort : this.rpcXmlPort);
1866
+ const port = protocol === 'binrpc' ? this.rpcBinPort : this.rpcXmlPort;
1680
1867
  return protocol + '://' + (this.rpcInitAddress || this.rpcServerHost) + ':' + port;
1681
1868
  }
1682
1869
 
@@ -1688,7 +1875,7 @@ module.exports = function (RED) {
1688
1875
  rpcServer(iface) {
1689
1876
  const url = this.initUrl(iface);
1690
1877
  const {rpc, protocol} = this.ifaceTypes[iface];
1691
- const port = (protocol === 'binrpc' ? this.rpcBinPort : this.rpcXmlPort);
1878
+ const port = protocol === 'binrpc' ? this.rpcBinPort : this.rpcXmlPort;
1692
1879
  if (!this.servers[protocol]) {
1693
1880
  this.servers[protocol] = rpc.createServer({host: this.rpcServerHost, port}, () => {
1694
1881
  // Todo homematic-xmlrpc and binrpc module: clarify onListening callback params
@@ -1696,13 +1883,14 @@ module.exports = function (RED) {
1696
1883
  this.serverError[iface] = null;
1697
1884
  });
1698
1885
 
1699
- // Todo homematic-xmlrpc and binrpc module: emit error event on server object to eliminate access to httpServer/server
1700
- this.servers[protocol][protocol === 'binrpc' ? 'server' : 'httpServer'].on('error', err => {
1701
- this.logger.error('binrpc ' + err.message);
1886
+ // Todo binrpc module: emit error event on server object to eliminate the reach-in
1887
+ const errorEmitter = protocol === 'binrpc' ? this.servers[protocol].server : this.servers[protocol];
1888
+ errorEmitter.on('error', (err) => {
1889
+ this.logger.error(protocol + ' ' + err.message);
1702
1890
  this.serverError[iface] = err.message;
1703
1891
  });
1704
1892
 
1705
- Object.keys(this.rpcMethods).forEach(method => {
1893
+ Object.keys(this.rpcMethods).forEach((method) => {
1706
1894
  this.servers[protocol].on(method, (err, parameters, callback) => {
1707
1895
  if (err) {
1708
1896
  this.logger.error('rpc <', protocol, method, err);
@@ -1716,7 +1904,14 @@ module.exports = function (RED) {
1716
1904
  if (isIterable(parameters)) {
1717
1905
  this.rpcMethods[method](err, parameters, callback);
1718
1906
  } else {
1719
- this.logger.error('rpc <', protocol, 'method', method, 'params not iterable', JSON.stringify(parameters));
1907
+ this.logger.error(
1908
+ 'rpc <',
1909
+ protocol,
1910
+ 'method',
1911
+ method,
1912
+ 'params not iterable',
1913
+ JSON.stringify(parameters),
1914
+ );
1720
1915
  callback(null, '');
1721
1916
  }
1722
1917
  });
@@ -1763,7 +1958,7 @@ module.exports = function (RED) {
1763
1958
  this.paramsQueue.push({
1764
1959
  iface,
1765
1960
  address: device.ADDRESS,
1766
- paramset
1961
+ paramset,
1767
1962
  });
1768
1963
  }
1769
1964
 
@@ -1772,16 +1967,17 @@ module.exports = function (RED) {
1772
1967
  if (item) {
1773
1968
  const {iface, address, paramset} = item;
1774
1969
  this.methodCall(iface, 'getParamset', [address, paramset])
1775
- .then(res => {
1970
+ .then((res) => {
1776
1971
  this.params[paramset][address] = res;
1777
1972
  })
1778
- .catch(error => this.logger.error(error))
1973
+ .catch((error) => this.logger.error(error))
1779
1974
  .then(() => {
1780
1975
  clearTimeout(this.getParamsTimeout);
1781
1976
  this.getParamsTimeout = setTimeout(() => {
1782
1977
  this.paramsQueueShift();
1783
1978
  }, 200);
1784
- });
1979
+ })
1980
+ .catch(() => {});
1785
1981
  } else {
1786
1982
  this.logger.debug(JSON.stringify(this.params));
1787
1983
  }
@@ -1794,14 +1990,14 @@ module.exports = function (RED) {
1794
1990
  */
1795
1991
  paramsetQueuePush(iface, device) {
1796
1992
  if (device && device.PARAMSETS) {
1797
- device.PARAMSETS.forEach(paramset => {
1993
+ device.PARAMSETS.forEach((paramset) => {
1798
1994
  const name = this.paramsetName(iface, device, paramset);
1799
1995
  if (!this.paramsetDescriptions[name]) {
1800
1996
  this.paramsetQueue.push({
1801
1997
  iface,
1802
1998
  name,
1803
1999
  address: device.ADDRESS,
1804
- paramset
2000
+ paramset,
1805
2001
  });
1806
2002
  }
1807
2003
  });
@@ -1833,7 +2029,7 @@ module.exports = function (RED) {
1833
2029
  setImmediate(() => this.paramsetQueueShift());
1834
2030
  } else {
1835
2031
  this.methodCall(iface, 'getParamsetDescription', [address, paramset])
1836
- .then(res => {
2032
+ .then((res) => {
1837
2033
  //this.logger.trace('paramsetDescription', name);
1838
2034
  this.newParamsetDescriptionCount += 1;
1839
2035
  this.newParamsetDescription = true;
@@ -1844,14 +2040,15 @@ module.exports = function (RED) {
1844
2040
  this.saveParamsets();
1845
2041
  }
1846
2042
  })
1847
- .catch(error => this.logger.error(error))
2043
+ .catch((error) => this.logger.error(error))
1848
2044
  .then(() => {
1849
2045
  this.paramsetPending = false;
1850
2046
  clearTimeout(this.getParamsetTimeout);
1851
2047
  this.getParamsetTimeout = setTimeout(() => {
1852
2048
  this.paramsetQueueShift();
1853
2049
  }, 200);
1854
- });
2050
+ })
2051
+ .catch(() => {});
1855
2052
  }
1856
2053
  } else {
1857
2054
  this.paramsetPending = false;
@@ -1913,7 +2110,10 @@ module.exports = function (RED) {
1913
2110
  throw new Error('device type undefined: ' + JSON.stringify(device));
1914
2111
  }
1915
2112
 
1916
- if (this.metadata.types[iface][device.TYPE] && !this.metadata.types[iface][device.TYPE].includes(device.ADDRESS)) {
2113
+ if (
2114
+ this.metadata.types[iface][device.TYPE] &&
2115
+ !this.metadata.types[iface][device.TYPE].includes(device.ADDRESS)
2116
+ ) {
1917
2117
  this.metadata.types[iface][device.TYPE].push(device.ADDRESS);
1918
2118
  } else {
1919
2119
  this.metadata.types[iface][device.TYPE] = [device.ADDRESS];
@@ -1944,7 +2144,7 @@ module.exports = function (RED) {
1944
2144
  listDevices(iface) {
1945
2145
  const result = [];
1946
2146
  if (this.metadata.devices[iface]) {
1947
- Object.keys(this.metadata.devices[iface]).forEach(addr => {
2147
+ Object.keys(this.metadata.devices[iface]).forEach((addr) => {
1948
2148
  const dev = this.metadata.devices[iface][addr];
1949
2149
  if (dev.TYPE === 'HmIP-RCV-50' || dev.PARENT_TYPE === 'HmIP-RCV-50') {
1950
2150
  // Würgaround for Firmware 3.43.15
@@ -1973,8 +2173,8 @@ module.exports = function (RED) {
1973
2173
  switch (iface) {
1974
2174
  case 'HmIP-RF':
1975
2175
  // fallthrough by intention
1976
- case 'VirtualDevices':
1977
- const d = { // eslint-disable-line no-case-declarations
2176
+ case 'VirtualDevices': {
2177
+ const d = {
1978
2178
  ADDRESS: device.ADDRESS,
1979
2179
  VERSION: device.VERSION,
1980
2180
  AES_ACTIVE: device.AES_ACTIVE,
@@ -1996,9 +2196,9 @@ module.exports = function (RED) {
1996
2196
  TEAM: device.TEAM,
1997
2197
  TEAM_CHANNELS: device.TEAM_CHANNELS,
1998
2198
  TEAM_TAG: device.TEAM_TAG,
1999
- TYPE: device.TYPE
2199
+ TYPE: device.TYPE,
2000
2200
  };
2001
- Object.keys(d).forEach(k => {
2201
+ Object.keys(d).forEach((k) => {
2002
2202
  if (typeof d[k] === 'undefined') {
2003
2203
  delete d[k];
2004
2204
  }
@@ -2010,6 +2210,8 @@ module.exports = function (RED) {
2010
2210
  });
2011
2211
 
2012
2212
  return d;
2213
+ }
2214
+
2013
2215
  default:
2014
2216
  return {ADDRESS: device.ADDRESS, VERSION: device.VERSION};
2015
2217
  }
@@ -2068,7 +2270,7 @@ module.exports = function (RED) {
2068
2270
  const [idInit, devices] = parameters;
2069
2271
  const iface = this.getIfaceFromIdInit(idInit);
2070
2272
 
2071
- devices.forEach(device => {
2273
+ devices.forEach((device) => {
2072
2274
  this.newDevice(iface, device);
2073
2275
  });
2074
2276
 
@@ -2081,7 +2283,7 @@ module.exports = function (RED) {
2081
2283
  const [idInit, devices] = parameters;
2082
2284
  const iface = this.getIfaceFromIdInit(idInit);
2083
2285
 
2084
- devices.forEach(device => {
2286
+ devices.forEach((device) => {
2085
2287
  this.deleteDevice(iface, device);
2086
2288
  });
2087
2289
 
@@ -2131,7 +2333,7 @@ module.exports = function (RED) {
2131
2333
  let direction;
2132
2334
  let pong = true;
2133
2335
  if (isIterable(parameters[0])) {
2134
- parameters[0].forEach(call => {
2336
+ parameters[0].forEach((call) => {
2135
2337
  if (call && call.methodName === 'event') {
2136
2338
  queue.push(call);
2137
2339
  if (isIterable(call.params)) {
@@ -2163,13 +2365,20 @@ module.exports = function (RED) {
2163
2365
  } else if (call && this.rpcMethods[call.methodName]) {
2164
2366
  pong = false;
2165
2367
  if (isIterable(call.params)) {
2166
- this.rpcMethods[call.methodName](call.methodName, call.params, (_, res) => result.push(res));
2368
+ this.rpcMethods[call.methodName](call.methodName, call.params, (_, res) =>
2369
+ result.push(res),
2370
+ );
2167
2371
  } else {
2168
- this.logger.error('rpc <', call.methodName, 'params not iterable', JSON.stringify(call.params));
2372
+ this.logger.error(
2373
+ 'rpc <',
2374
+ call.methodName,
2375
+ 'params not iterable',
2376
+ JSON.stringify(call.params),
2377
+ );
2169
2378
  }
2170
2379
  }
2171
2380
  });
2172
- queue.forEach(call => {
2381
+ queue.forEach((call) => {
2173
2382
  this.publishEvent(call.params, working, direction);
2174
2383
  });
2175
2384
  }
@@ -2185,7 +2394,7 @@ module.exports = function (RED) {
2185
2394
  }
2186
2395
 
2187
2396
  callback(null, result);
2188
- }
2397
+ },
2189
2398
  };
2190
2399
  }
2191
2400
 
@@ -2296,7 +2505,7 @@ module.exports = function (RED) {
2296
2505
  'room',
2297
2506
  'function',
2298
2507
  'rooms',
2299
- 'functions'
2508
+ 'functions',
2300
2509
  ]);
2301
2510
 
2302
2511
  const propertiesArray = Object.keys(filter);
@@ -2315,7 +2524,7 @@ module.exports = function (RED) {
2315
2524
  this.callbacks[id] = {filter, callback};
2316
2525
 
2317
2526
  if (filter.cache && this.cachedValuesReceived) {
2318
- Object.keys(this.values).forEach(dp => {
2527
+ Object.keys(this.values).forEach((dp) => {
2319
2528
  const message = {...this.values[dp]};
2320
2529
  message.cache = true;
2321
2530
  message.change = false;
@@ -2344,11 +2553,11 @@ module.exports = function (RED) {
2344
2553
  this.logger.trace('unsubscribe', id);
2345
2554
  delete this.callbacks[id];
2346
2555
 
2347
- Object.keys(this.callbackBlacklists).forEach(dp => {
2556
+ Object.keys(this.callbackBlacklists).forEach((dp) => {
2348
2557
  this.callbackBlacklists[dp].delete(id);
2349
2558
  });
2350
2559
 
2351
- Object.keys(this.callbackWhitelists).forEach(dp => {
2560
+ Object.keys(this.callbackWhitelists).forEach((dp) => {
2352
2561
  this.callbackWhitelists[dp].delete(id);
2353
2562
  });
2354
2563
 
@@ -2371,13 +2580,13 @@ module.exports = function (RED) {
2371
2580
  }
2372
2581
 
2373
2582
  const messageLower = {};
2374
- Object.keys(message).forEach(k => {
2583
+ Object.keys(message).forEach((k) => {
2375
2584
  messageLower[k.toLowerCase()] = message[k];
2376
2585
  });
2377
2586
 
2378
2587
  const match = topic.match(/\${[^}]+}/g);
2379
2588
  if (match) {
2380
- match.forEach(v => {
2589
+ match.forEach((v) => {
2381
2590
  const key = v.substr(2, v.length - 3);
2382
2591
  const rx = new RegExp('\\${' + key + '}', 'g');
2383
2592
  let rkey = key.toLowerCase();
@@ -2407,19 +2616,25 @@ module.exports = function (RED) {
2407
2616
  this.values[datapointName] = {};
2408
2617
  }
2409
2618
 
2410
- const device = this.metadata.devices[iface] && this.metadata.devices[iface][channel] && this.metadata.devices[iface][channel].PARENT;
2619
+ const device =
2620
+ this.metadata.devices[iface] &&
2621
+ this.metadata.devices[iface][channel] &&
2622
+ this.metadata.devices[iface][channel].PARENT;
2411
2623
  const ts = now();
2412
2624
  let change = false;
2413
2625
 
2414
- const valueStable = (additions && additions.working) ? this.values[datapointName].valueStable : payload;
2626
+ const valueStable = additions && additions.working ? this.values[datapointName].valueStable : payload;
2415
2627
 
2416
2628
  let description = {};
2417
2629
  if (this.metadata.devices[iface] && this.metadata.devices[iface][channel]) {
2418
- description = this.getParamsetDescription(iface, this.metadata.devices[iface][channel], 'VALUES', datapoint) || {};
2630
+ description =
2631
+ this.getParamsetDescription(iface, this.metadata.devices[iface][channel], 'VALUES', datapoint) ||
2632
+ {};
2419
2633
  }
2420
2634
 
2421
2635
  if (
2422
2636
  description.TYPE === 'ACTION' ||
2637
+ this.values[datapointName].cache ||
2423
2638
  this.values[datapointName].payload !== payload ||
2424
2639
  this.values[datapointName].valueStable !== valueStable
2425
2640
  ) {
@@ -2428,16 +2643,23 @@ module.exports = function (RED) {
2428
2643
 
2429
2644
  this.logger.trace('createMessage', channel, datapoint, payload, 'change=' + change);
2430
2645
 
2431
- const message = {topic: '',
2646
+ const message = {
2647
+ topic: '',
2432
2648
  payload,
2433
2649
  ccu: this.host,
2434
2650
  iface,
2435
2651
  device,
2436
2652
  deviceName: this.channelNames[device],
2437
- deviceType: this.metadata.devices[iface] && this.metadata.devices[iface][device] && this.metadata.devices[iface][device].TYPE,
2653
+ deviceType:
2654
+ this.metadata.devices[iface] &&
2655
+ this.metadata.devices[iface][device] &&
2656
+ this.metadata.devices[iface][device].TYPE,
2438
2657
  channel,
2439
2658
  channelName: this.channelNames[channel],
2440
- channelType: this.metadata.devices[iface] && this.metadata.devices[iface][channel] && this.metadata.devices[iface][channel].TYPE,
2659
+ channelType:
2660
+ this.metadata.devices[iface] &&
2661
+ this.metadata.devices[iface][channel] &&
2662
+ this.metadata.devices[iface][channel].TYPE,
2441
2663
  channelIndex: channel && Number.parseInt(channel.split(':')[1], 10),
2442
2664
  datapoint,
2443
2665
  datapointName,
@@ -2452,13 +2674,21 @@ module.exports = function (RED) {
2452
2674
  valueEnum: description.ENUM ? description.ENUM[Number(payload)] : undefined,
2453
2675
  valueStable,
2454
2676
  rooms: this.channelRooms[channel] || [],
2455
- room: this.channelRooms[channel] && this.channelRooms[channel].length > 0 ? this.channelRooms[channel][0] : undefined,
2677
+ room:
2678
+ this.channelRooms[channel] && this.channelRooms[channel].length > 0
2679
+ ? this.channelRooms[channel][0]
2680
+ : undefined,
2456
2681
  functions: this.channelFunctions[channel] || [],
2457
- function: this.channelFunctions[channel] && this.channelFunctions[channel].length > 0 ? this.channelFunctions[channel][0] : undefined,
2682
+ function:
2683
+ this.channelFunctions[channel] && this.channelFunctions[channel].length > 0
2684
+ ? this.channelFunctions[channel][0]
2685
+ : undefined,
2458
2686
  ts,
2459
2687
  tsPrevious: this.values[datapointName].ts,
2460
2688
  lc: change ? ts : this.values[datapointName].lc,
2461
- change, ...additions};
2689
+ change,
2690
+ ...additions,
2691
+ };
2462
2692
 
2463
2693
  message.stable = !message.working;
2464
2694
 
@@ -2487,16 +2717,27 @@ module.exports = function (RED) {
2487
2717
 
2488
2718
  //this.logger.trace('publishEvent', JSON.stringify(params));
2489
2719
 
2490
- const message = this.createMessage(iface, channel, datapoint, payload, {cache: false, uncertain: false, working, direction});
2720
+ const message = this.createMessage(iface, channel, datapoint, payload, {
2721
+ cache: false,
2722
+ uncertain: false,
2723
+ working,
2724
+ direction,
2725
+ });
2491
2726
 
2492
2727
  let waitForWorking = false;
2493
2728
 
2494
2729
  if (message.channelType && !working) {
2495
- if (message.datapoint === 'STATE' && message.channelType.match(/SIGNAL|SWITCH|RAINDETECTOR_HEAT|ALARMACTUATOR/)) {
2730
+ if (
2731
+ message.datapoint === 'STATE' &&
2732
+ message.channelType.match(/SIGNAL|SWITCH|RAINDETECTOR_HEAT|ALARMACTUATOR/)
2733
+ ) {
2496
2734
  waitForWorking = true;
2497
2735
  } else if (message.datapoint === 'ARMSTATE' && message.channelType === 'ARMING') {
2498
2736
  waitForWorking = true;
2499
- } else if (message.datapoint.startsWith('LEVEL') && message.channelType.match(/DIMMER|DUAL_WHITE|BLIND|SHUTTER|JALOUSIE|WINMATIC|KEYMATIC/)) {
2737
+ } else if (
2738
+ message.datapoint.startsWith('LEVEL') &&
2739
+ message.channelType.match(/DIMMER|DUAL_WHITE|BLIND|SHUTTER|JALOUSIE|WINMATIC|KEYMATIC/)
2740
+ ) {
2500
2741
  waitForWorking = true;
2501
2742
  }
2502
2743
  }
@@ -2506,9 +2747,17 @@ module.exports = function (RED) {
2506
2747
  this.workingTimeout[message.datapointName] = setTimeout(() => {
2507
2748
  const datapointNamePrefix = iface + '.' + channel + '.';
2508
2749
 
2509
- if (this.values[datapointNamePrefix + 'WORKING'] || this.values[datapointNamePrefix + 'WORKING_SLATS']) {
2510
- message.working = this.values[datapointNamePrefix + 'WORKING'] && this.values[datapointNamePrefix + 'WORKING'].value;
2511
- message.working = message.working || (this.values[datapointNamePrefix + 'WORKING_SLATS'] && this.values[datapointNamePrefix + 'WORKING_SLATS'].value);
2750
+ if (
2751
+ this.values[datapointNamePrefix + 'WORKING'] ||
2752
+ this.values[datapointNamePrefix + 'WORKING_SLATS']
2753
+ ) {
2754
+ message.working =
2755
+ this.values[datapointNamePrefix + 'WORKING'] &&
2756
+ this.values[datapointNamePrefix + 'WORKING'].value;
2757
+ message.working =
2758
+ message.working ||
2759
+ (this.values[datapointNamePrefix + 'WORKING_SLATS'] &&
2760
+ this.values[datapointNamePrefix + 'WORKING_SLATS'].value);
2512
2761
  message.working = Boolean(message.working);
2513
2762
  } else if (this.values[datapointNamePrefix + 'PROCESS']) {
2514
2763
  message.working = Boolean(this.values[datapointNamePrefix + 'PROCESS'].value);
@@ -2555,7 +2804,7 @@ module.exports = function (RED) {
2555
2804
  if (filter) {
2556
2805
  const arrayAttr = Object.keys(filter);
2557
2806
 
2558
- for (let i = 0, {length} = arrayAttr; match && (i < length); i++) {
2807
+ for (let i = 0, {length} = arrayAttr; match && i < length; i++) {
2559
2808
  const attr = arrayAttr[i];
2560
2809
 
2561
2810
  if (attr === 'cache') {
@@ -2571,7 +2820,7 @@ module.exports = function (RED) {
2571
2820
 
2572
2821
  if (attr === 'change') {
2573
2822
  // if filter.change==true - Drop messages with msg.change==false - except msg.cache==true && filter.cache==true
2574
- if ((filter.change && !message.change) && !(filter.cache && message.cache)) {
2823
+ if (filter.change && !message.change && !(filter.cache && message.cache)) {
2575
2824
  //this.logger.trace('cb mismatch change ' + id + ' ' + filter.change + ' ' + msg.change + ' ' + msg.cache);
2576
2825
  return false;
2577
2826
  }
@@ -2599,7 +2848,8 @@ module.exports = function (RED) {
2599
2848
  continue;
2600
2849
  }
2601
2850
 
2602
- if (filter[attr] === '') { // TODO rethink
2851
+ if (filter[attr] === '') {
2852
+ // TODO rethink
2603
2853
  continue;
2604
2854
  }
2605
2855
 
@@ -2611,7 +2861,7 @@ module.exports = function (RED) {
2611
2861
  if (filter[attr] instanceof RegExp) {
2612
2862
  match = false;
2613
2863
  this.logger.trace('cb test regex array', id, attr, filter[attr], message[attr]);
2614
- message[attr].forEach(item => {
2864
+ message[attr].forEach((item) => {
2615
2865
  if (filter[attr].test(item)) {
2616
2866
  match = true;
2617
2867
  }
@@ -2626,7 +2876,9 @@ module.exports = function (RED) {
2626
2876
  match = false;
2627
2877
  }
2628
2878
  } else if (filter[attr] !== message[attr]) {
2629
- this.logger.trace('cb mismatch misc ' + id + ' ' + attr + ' ' + filter[attr] + ' ' + message[attr]);
2879
+ this.logger.trace(
2880
+ 'cb mismatch misc ' + id + ' ' + attr + ' ' + filter[attr] + ' ' + message[attr],
2881
+ );
2630
2882
  match = false;
2631
2883
  }
2632
2884
  }
@@ -2658,7 +2910,7 @@ module.exports = function (RED) {
2658
2910
  this.callbackWhitelists[message.datapointName] = new Set();
2659
2911
  }
2660
2912
 
2661
- Object.keys(this.callbacks).forEach(key => {
2913
+ Object.keys(this.callbacks).forEach((key) => {
2662
2914
  if (this.callbackBlacklists[message.datapointName].has(key)) {
2663
2915
  //this.logger.trace('blacklistet ' + key + ' ' + msg.datapointName);
2664
2916
  return;
@@ -2708,7 +2960,16 @@ module.exports = function (RED) {
2708
2960
  this.methodCallQueue[iface] = [[method, parameters, resolve, reject]];
2709
2961
  }
2710
2962
  } else {
2711
- reject(new Error('unknown interface ' + iface + ' ' + Object.keys(this.clients) + ' ' + Object.keys(this.ifaceTypes)));
2963
+ reject(
2964
+ new Error(
2965
+ 'unknown interface ' +
2966
+ iface +
2967
+ ' ' +
2968
+ Object.keys(this.clients) +
2969
+ ' ' +
2970
+ Object.keys(this.ifaceTypes),
2971
+ ),
2972
+ );
2712
2973
  }
2713
2974
  });
2714
2975
  }
@@ -2725,12 +2986,13 @@ module.exports = function (RED) {
2725
2986
  */
2726
2987
  setValueQueued(iface, address, datapoint, value, burst, force) {
2727
2988
  return new Promise((resolve, reject) => {
2728
- this.setValueQueue = this.setValueQueue.filter(element => {
2989
+ this.setValueQueue = this.setValueQueue.filter((element) => {
2729
2990
  return element.iface !== iface || element.address !== address || element.datapoint !== datapoint;
2730
2991
  });
2731
2992
  const datapointName = iface + '.' + address + '.' + datapoint;
2732
2993
  const currentValue = this.values[datapointName] && this.values[datapointName].value;
2733
- if (force || (value !== currentValue) || datapoint.startsWith('PRESS_')) {
2994
+ const cache = this.values[datapointName] && this.values[datapointName].cache;
2995
+ if (force || value !== currentValue || cache || datapoint.startsWith('PRESS_')) {
2734
2996
  this.setValueQueue.push({iface, address, datapoint, value, burst, resolve, reject});
2735
2997
  this.setValueShiftQueue();
2736
2998
  } else {
@@ -2766,7 +3028,7 @@ module.exports = function (RED) {
2766
3028
  resolve();
2767
3029
  }
2768
3030
  })
2769
- .catch(error => {
3031
+ .catch((error) => {
2770
3032
  if (!timeout) {
2771
3033
  reject(error);
2772
3034
  }
@@ -2815,10 +3077,12 @@ module.exports = function (RED) {
2815
3077
  }, this.setValueThrottle);
2816
3078
  }
2817
3079
 
2818
- this.methodCall(iface, 'setValue', parameters).then(resolve).catch(error => {
2819
- this.logger.error('rpc >', iface, 'setValue', JSON.stringify(parameters), '<', error);
2820
- reject(error);
2821
- });
3080
+ this.methodCall(iface, 'setValue', parameters)
3081
+ .then(resolve)
3082
+ .catch((error) => {
3083
+ this.logger.error('rpc >', iface, 'setValue', JSON.stringify(parameters), '<', error);
3084
+ reject(error);
3085
+ });
2822
3086
  }
2823
3087
  });
2824
3088
  }
@@ -2834,10 +3098,12 @@ module.exports = function (RED) {
2834
3098
  const [iface] = id.split('.');
2835
3099
  const {params, resolve, reject} = this.setValueCache[id];
2836
3100
  delete this.setValueCache[id];
2837
- return this.methodCall(iface, 'setValue', params).then(resolve).catch(error => {
2838
- this.logger.error('rpc >', iface, 'setValue', JSON.stringify(params), '<', error);
2839
- reject(error);
2840
- });
3101
+ return this.methodCall(iface, 'setValue', params)
3102
+ .then(resolve)
3103
+ .catch((error) => {
3104
+ this.logger.error('rpc >', iface, 'setValue', JSON.stringify(params), '<', error);
3105
+ reject(error);
3106
+ });
2841
3107
  }
2842
3108
  }
2843
3109
 
@@ -2861,7 +3127,8 @@ module.exports = function (RED) {
2861
3127
  case 'BOOL':
2862
3128
  if (value === 'false') {
2863
3129
  value = false;
2864
- } else if (!isNaN(value)) { // Make sure that the string "0" gets casted to boolean false
3130
+ } else if (!isNaN(value)) {
3131
+ // Make sure that the string "0" gets casted to boolean false
2865
3132
  value = Number(value);
2866
3133
  }
2867
3134
 
@@ -2880,7 +3147,7 @@ module.exports = function (RED) {
2880
3147
  break;
2881
3148
  case 'ENUM':
2882
3149
  if (typeof value === 'string') {
2883
- if (paramset.ENUM && (paramset.ENUM.includes(value))) {
3150
+ if (paramset.ENUM && paramset.ENUM.includes(value)) {
2884
3151
  value = paramset.ENUM.indexOf(value);
2885
3152
  }
2886
3153
  }
@@ -2924,7 +3191,7 @@ module.exports = function (RED) {
2924
3191
  */
2925
3192
  script(script) {
2926
3193
  return new Promise((resolve, reject) => {
2927
- this.rega.exec(script, (err, payload, objects) => {
3194
+ execToCallback(this.rega.exec(script), (err, payload, objects) => {
2928
3195
  if (err) {
2929
3196
  reject(err);
2930
3197
  } else {
@@ -2935,7 +3202,5 @@ module.exports = function (RED) {
2935
3202
  }
2936
3203
  }
2937
3204
 
2938
- RED.nodes.registerType('ccu-connection', CcuConnectionNode, {
2939
-
2940
- });
3205
+ RED.nodes.registerType('ccu-connection', CcuConnectionNode, {});
2941
3206
  };