node-red-contrib-ccu 3.4.2 → 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 +33 -14
  13. package/nodes/ccu-connection.html +9 -1
  14. package/nodes/ccu-connection.js +588 -335
  15. package/nodes/ccu-display.html +11 -4
  16. package/nodes/ccu-display.js +26 -9
  17. package/nodes/ccu-get-value.html +13 -4
  18. package/nodes/ccu-get-value.js +2 -2
  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 +40 -16
  31. package/nodes/ccu-signal.html +11 -5
  32. package/nodes/ccu-signal.js +34 -23
  33. package/nodes/ccu-switch.html +13 -5
  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 -155
  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
@@ -22,7 +22,7 @@
22
22
  rampType: {value: 'undefined'},
23
23
  working: {value: false},
24
24
  ccuConfig: {value: 'localhost', type: 'ccu-connection', required: true},
25
- topic: {value: '${CCU}/${Interface}/${channel}/${datapoint}'} // eslint-disable-line no-template-curly-in-string
25
+ topic: {value: '${CCU}/${Interface}/${channel}/${datapoint}'}
26
26
  },
27
27
  inputs: 1,
28
28
  outputs: 1,
@@ -43,7 +43,6 @@
43
43
  return this.name ? 'node_label_italic' : '';
44
44
  },
45
45
  oneditprepare() {
46
- console.log(this);
47
46
 
48
47
  const $nodeInputIface = $('#node-input-iface');
49
48
  const $nodeInputCcuConfig = $('#node-input-ccuConfig');
@@ -56,6 +55,7 @@
56
55
  let channelList;
57
56
  let ifacesLoaded = false;
58
57
  let ifacesPending = false;
58
+ let ifaceLoadRetries = 5;
59
59
 
60
60
  function loadIfaces(iface, cb) {
61
61
  if (ifacesPending) {
@@ -63,7 +63,6 @@
63
63
  }
64
64
 
65
65
  ifacesPending = true;
66
- console.log('loadIfaces()');
67
66
  $nodeInputIface.html('<option value=""></option>');
68
67
  const nodeId = $nodeInputCcuConfig.val();
69
68
  if (nodeId === '_ADD_') {
@@ -83,6 +82,12 @@
83
82
  cb();
84
83
  ifacesPending = false;
85
84
  }
85
+ }).fail(() => {
86
+ // config node not (yet) available in the runtime - allow retrying
87
+ ifacesPending = false;
88
+ if (ifaceLoadRetries-- > 0) {
89
+ setTimeout(() => loadIfaces(iface, cb), 1000);
90
+ }
86
91
  });
87
92
  }
88
93
  }
@@ -92,19 +97,16 @@
92
97
  return;
93
98
  }
94
99
 
95
- console.log('loadConfig()');
96
100
  const nodeId = $nodeInputCcuConfig.val();
97
101
  const url = 'ccu?config=' + nodeId + '&type=channels&iface=' + $nodeInputIface.val();
98
102
  $.getJSON(url, d => {
99
103
  data = d;
100
- console.log(d);
101
104
  autocompleteChannel();
102
105
  autocompleteDatapoint();
103
106
  });
104
107
  }
105
108
 
106
109
  $nodeInputCcuConfig.change(() => {
107
- console.log('$nodeInputCcuConfig change');
108
110
  loadIfaces(this.iface, () => {
109
111
  ifacesLoaded = true;
110
112
  $nodeInputIface.removeAttr('disabled');
@@ -113,6 +115,10 @@
113
115
  });
114
116
  });
115
117
 
118
+ // Node-RED 4.x does not reliably fire the change event on dialog
119
+ // open - trigger the initial load explicitly (see ROADMAP.md 8.1)
120
+ $nodeInputCcuConfig.trigger('change');
121
+
116
122
  $('#node-input-onEnabled').change(() => {
117
123
  if ($('#node-input-onEnabled').is(':checked')) {
118
124
  $('#node-input-on').removeAttr('disabled');
@@ -211,7 +217,6 @@
211
217
  }
212
218
 
213
219
  $nodeInputIface.change(() => {
214
- console.log('$nodeInputIface change');
215
220
  $('.BURST').hide();
216
221
  loadConfig();
217
222
  });
@@ -25,10 +25,10 @@ module.exports = function (RED) {
25
25
  stable: config.working,
26
26
  iface: config.iface,
27
27
  channel: String(config.channel).split(' ')[0],
28
- datapoint: config.datapoint
28
+ datapoint: config.datapoint,
29
29
  };
30
30
 
31
- this.idSubscription = this.ccu.subscribe(filter, message => {
31
+ this.idSubscription = this.ccu.subscribe(filter, (message) => {
32
32
  this.status({fill: 'green', shape: 'ring', text: String(message.payload)});
33
33
  message.topic = this.ccu.topicReplace(config.topic, message);
34
34
  this.send(message);
@@ -38,7 +38,13 @@ module.exports = function (RED) {
38
38
  this.on('input', (message, send, done) => {
39
39
  const [tIface, tChannel, tDatapoint] = (message.topic || '').split('.');
40
40
  const iface = config.iface || message.interface || message.iface || tIface;
41
- const channel = (config.channel || this.ccu.findChannel(message.channelName, true) || message.channel || tChannel || '').split(' ')[0];
41
+ const channel = (
42
+ config.channel ||
43
+ this.ccu.findChannel(message.channelName, true) ||
44
+ message.channel ||
45
+ tChannel ||
46
+ ''
47
+ ).split(' ')[0];
42
48
  const datapoint = config.datapoint || message.datapoint || tDatapoint;
43
49
 
44
50
  if (!iface) {
@@ -56,6 +62,12 @@ module.exports = function (RED) {
56
62
  return;
57
63
  }
58
64
 
65
+ if (message.payload === undefined) {
66
+ // don't write to the CCU when the incoming msg carries no payload (PR #173)
67
+ done();
68
+ return;
69
+ }
70
+
59
71
  let ramp;
60
72
  switch (config.rampType) {
61
73
  case 'msg':
@@ -95,11 +107,19 @@ module.exports = function (RED) {
95
107
  on = Number.parseFloat(on);
96
108
 
97
109
  if (!ramp && !on) {
98
- this.ccu[this.queue ? 'setValueQueued' : 'setValue'](iface, channel, datapoint, message.payload, config.burst).then(() => {
99
- done();
100
- }).catch(error => {
101
- done(error);
102
- });
110
+ this.ccu[this.queue ? 'setValueQueued' : 'setValue'](
111
+ iface,
112
+ channel,
113
+ datapoint,
114
+ message.payload,
115
+ config.burst,
116
+ )
117
+ .then(() => {
118
+ done();
119
+ })
120
+ .catch((error) => {
121
+ done(error);
122
+ });
103
123
  } else {
104
124
  const parameters = {};
105
125
  if (on) {
@@ -112,11 +132,14 @@ module.exports = function (RED) {
112
132
 
113
133
  parameters[datapoint] = this.ccu.paramCast(iface, channel, 'VALUES', datapoint, message.payload);
114
134
  // Todo queue
115
- this.ccu.methodCall(iface, 'putParamset', [channel, 'VALUES', parameters]).then(() => {
116
- done();
117
- }).catch(error => {
118
- done(error);
119
- });
135
+ this.ccu
136
+ .methodCall(iface, 'putParamset', [channel, 'VALUES', parameters])
137
+ .then(() => {
138
+ done();
139
+ })
140
+ .catch((error) => {
141
+ done(error);
142
+ });
120
143
  }
121
144
  });
122
145
 
@@ -0,0 +1,22 @@
1
+ /* Replaces the buffer-base62 dependency (which pulled in big-number,
2
+ buffer-reverse and underscore). BigInt implementation, byte-for-byte
3
+ identical output: big-endian buffer value, alphabet 0-9A-Za-z. */
4
+
5
+ const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
6
+
7
+ function toBase62(buffer) {
8
+ let n = 0n;
9
+ for (const byte of buffer) {
10
+ n = n * 256n + BigInt(byte);
11
+ }
12
+
13
+ let out = '';
14
+ do {
15
+ out = alphabet[Number(n % 62n)] + out;
16
+ n /= 62n;
17
+ } while (n > 0n);
18
+
19
+ return out;
20
+ }
21
+
22
+ module.exports = {toBase62};
@@ -0,0 +1,127 @@
1
+ /* Vendored from https://github.com/hobbyquaker/hm-discover (MIT, same
2
+ author), rewritten without the abandoned `binary` and `async`
3
+ dependencies. Sends the eQ-3 UDP broadcast discovery datagram and
4
+ probes the known interface ports on every responding CCU. */
5
+
6
+ const net = require('net');
7
+ const dgram = require('dgram');
8
+
9
+ const SERVICE_PORTS = {
10
+ ReGaHSS: 1999,
11
+ 'BidCos-Wired': 2000,
12
+ 'BidCos-RF': 2001,
13
+ 'HmIP-RF': 2010,
14
+ VirtualDevices: 9292,
15
+ CUxD: 8701,
16
+ 'CCU-Jack': 2121,
17
+ };
18
+
19
+ function checkService(host, port) {
20
+ return new Promise((resolve) => {
21
+ const c = net.connect({port, host, timeout: 1200}, () => {
22
+ resolve(true);
23
+ c.end();
24
+ });
25
+ c.on('timeout', () => {
26
+ resolve(false);
27
+ c.destroy();
28
+ });
29
+ c.on('error', () => {
30
+ resolve(false);
31
+ });
32
+ });
33
+ }
34
+
35
+ /* Response layout: 5-byte header 02 8f 91 c0 01, then NUL-terminated
36
+ type, NUL-terminated serial, 3 bytes, NUL-terminated version. */
37
+ function parseResponse(message) {
38
+ if (message.length < 5 || message.slice(0, 5).toString('hex') !== '028f91c001') {
39
+ return null;
40
+ }
41
+
42
+ let offset = 5;
43
+ const readString = () => {
44
+ const end = message.indexOf(0, offset);
45
+ if (end === -1) {
46
+ return null;
47
+ }
48
+
49
+ const string = message.slice(offset, end).toString();
50
+ offset = end + 1;
51
+ return string;
52
+ };
53
+
54
+ const type = readString();
55
+ const serial = readString();
56
+ offset += 3;
57
+ const version = readString();
58
+
59
+ if (type === null || serial === null || version === null) {
60
+ return null;
61
+ }
62
+
63
+ return {type, serial, version};
64
+ }
65
+
66
+ function hmDiscover(options, callback) {
67
+ if (typeof options === 'function') {
68
+ callback = options;
69
+ options = {};
70
+ } else if (typeof options !== 'object' || options === null) {
71
+ options = {};
72
+ }
73
+
74
+ const timeout = options.timeout || 1200;
75
+ const remoteport = 43439;
76
+ // Kept verbatim from hm-discover — Buffer.from() coerces the string
77
+ // elements to 0x00, and that is the datagram CCUs answer to.
78
+ const message = Buffer.from([0x02, 0x8f, 0x91, 0xc0, 0x01, 'e', 'Q', '3', 0x2d, 0x2a, 0x00, 0x2a, 0x00, 0x49]);
79
+ const found = [];
80
+ const foundAddresses = [];
81
+ const client = dgram.createSocket('udp4');
82
+
83
+ client.on('error', () => {
84
+ /* ignore — discovery is best-effort; an unhandled 'error' event
85
+ would crash the whole process */
86
+ });
87
+
88
+ client.on('message', (msg, remote) => {
89
+ const parsed = parseResponse(msg);
90
+ if (!parsed || foundAddresses.includes(remote.address)) {
91
+ return;
92
+ }
93
+
94
+ foundAddresses.push(remote.address);
95
+ const device = {
96
+ type: parsed.type,
97
+ serial: parsed.serial,
98
+ version: parsed.version,
99
+ address: remote.address,
100
+ };
101
+
102
+ Promise.all(
103
+ Object.keys(SERVICE_PORTS).map((name) =>
104
+ checkService(remote.address, SERVICE_PORTS[name]).then((ok) => [name, ok]),
105
+ ),
106
+ ).then((entries) => {
107
+ const interfaces = {};
108
+ entries.forEach(([name, ok]) => {
109
+ interfaces[name] = ok;
110
+ });
111
+ device.interfaces = interfaces;
112
+ found.push(device);
113
+ });
114
+ });
115
+
116
+ client.bind(() => {
117
+ client.setBroadcast(true);
118
+ client.send(message, 0, message.length, remoteport, '255.255.255.255');
119
+ });
120
+
121
+ setTimeout(() => {
122
+ client.close();
123
+ callback(found);
124
+ }, timeout);
125
+ }
126
+
127
+ module.exports = hmDiscover;
@@ -0,0 +1,30 @@
1
+ /* Vendored from https://github.com/hobbyquaker/nextport (MIT, same author).
2
+ Finds the next free TCP port at or above the given port. */
3
+
4
+ const net = require('net');
5
+
6
+ function nextport(port, address, cb) {
7
+ if (typeof address === 'function') {
8
+ cb = address;
9
+ address = '0.0.0.0';
10
+ }
11
+
12
+ const server = net.createServer();
13
+ server.on('listening', () => {
14
+ server.close();
15
+ });
16
+ server.on('close', () => {
17
+ cb(port);
18
+ });
19
+ server.on('error', () => {
20
+ port += 1;
21
+ if (port <= 65535) {
22
+ nextport(port, address, cb);
23
+ } else {
24
+ cb(null);
25
+ }
26
+ });
27
+ server.listen(port, address);
28
+ }
29
+
30
+ module.exports = nextport;
@@ -0,0 +1,51 @@
1
+ /* Replaces the deprecated string-similarity package. Same algorithm as its
2
+ compareTwoStrings: Sørensen–Dice coefficient on bigram multisets, with
3
+ whitespace stripped first. */
4
+
5
+ function compareTwoStrings(first, second) {
6
+ first = first.replace(/\s+/g, '');
7
+ second = second.replace(/\s+/g, '');
8
+
9
+ if (first === second) {
10
+ return 1;
11
+ }
12
+
13
+ if (first.length < 2 || second.length < 2) {
14
+ return 0;
15
+ }
16
+
17
+ const firstBigrams = new Map();
18
+ for (let i = 0; i < first.length - 1; i++) {
19
+ const bigram = first.substring(i, i + 2);
20
+ firstBigrams.set(bigram, (firstBigrams.get(bigram) || 0) + 1);
21
+ }
22
+
23
+ let intersectionSize = 0;
24
+ for (let i = 0; i < second.length - 1; i++) {
25
+ const bigram = second.substring(i, i + 2);
26
+ const count = firstBigrams.get(bigram) || 0;
27
+ if (count > 0) {
28
+ firstBigrams.set(bigram, count - 1);
29
+ intersectionSize++;
30
+ }
31
+ }
32
+
33
+ return (2 * intersectionSize) / (first.length + second.length - 2);
34
+ }
35
+
36
+ function bestMatch(mainString, targetStrings) {
37
+ let best = targetStrings[0];
38
+ let bestRating = -1;
39
+
40
+ for (const target of targetStrings) {
41
+ const rating = compareTwoStrings(mainString, target);
42
+ if (rating > bestRating) {
43
+ bestRating = rating;
44
+ best = target;
45
+ }
46
+ }
47
+
48
+ return best;
49
+ }
50
+
51
+ module.exports = {compareTwoStrings, bestMatch};
@@ -3,7 +3,7 @@ module.exports = (that, data) => {
3
3
  if (!that.iface) {
4
4
  if (data.ifaceStatus) {
5
5
  let status = 0;
6
- Object.keys(data.ifaceStatus).forEach(s => {
6
+ Object.keys(data.ifaceStatus).forEach((s) => {
7
7
  if (data.ifaceStatus[s]) {
8
8
  status += 1;
9
9
  }