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
@@ -113,6 +113,7 @@
113
113
 
114
114
  let ifacesLoaded = false;
115
115
  let ifacesPending = false;
116
+ let ifaceLoadRetries = 5;
116
117
 
117
118
  $('.form-row.datapoint').hide();
118
119
  $('.form-row.sysvar').hide();
@@ -134,7 +135,6 @@
134
135
  return;
135
136
  }
136
137
  ifacesPending = true;
137
- console.log('loadIfaces()');
138
138
 
139
139
  $nodeInputIface.html('');
140
140
 
@@ -154,6 +154,12 @@
154
154
  cb();
155
155
  ifacesPending = false;
156
156
  }
157
+ }).fail(() => {
158
+ // config node not (yet) available in the runtime - allow retrying
159
+ ifacesPending = false;
160
+ if (ifaceLoadRetries-- > 0) {
161
+ setTimeout(() => loadIfaces(iface, cb), 1000);
162
+ }
157
163
  });
158
164
  $.getJSON('ccu?type=sysvar&config=' + nodeId, data => {
159
165
  $nodeInputSysvar.html('');
@@ -163,6 +169,8 @@
163
169
  });
164
170
  }
165
171
  });
172
+ } else {
173
+ ifacesPending = false;
166
174
  }
167
175
  }
168
176
 
@@ -170,7 +178,6 @@
170
178
  if (!ifacesLoaded) {
171
179
  return;
172
180
  }
173
- console.log('loadConfig()');
174
181
  const nodeId = $nodeInputCcuConfig.val();
175
182
  const url = 'ccu?config=' + nodeId + '&type=channels&iface=' + $nodeInputIface.val();
176
183
  $.getJSON(url, d => {
@@ -181,13 +188,16 @@
181
188
  }
182
189
 
183
190
  $nodeInputCcuConfig.change(() => {
184
- console.log('$nodeInputCcuConfig change');
185
191
  loadIfaces(this.iface, () => {
186
192
  ifacesLoaded = true;
187
193
  loadConfig();
188
194
  });
189
195
  });
190
196
 
197
+ // Node-RED 4.x does not reliably fire the change event on dialog
198
+ // open - trigger the initial load explicitly (see ROADMAP.md 8.1)
199
+ $nodeInputCcuConfig.trigger('change');
200
+
191
201
  $nodeInputChannel.autocomplete({
192
202
  source: [],
193
203
  close: () => {
@@ -247,7 +257,6 @@
247
257
  }
248
258
 
249
259
  $nodeInputIface.change(() => {
250
- console.log('$nodeInputIface change');
251
260
  if ($nodeInputIface.val() === 'ReGaHSS') {
252
261
  $('.form-row.datapoint').hide();
253
262
  $('.form-row.sysvar').show();
@@ -359,7 +368,6 @@
359
368
  const caseSensitive = $('<input/>', {id: 'node-input-rule-case-' + i, class: 'node-input-rule-case', type: 'checkbox', style: 'width:auto;vertical-align:top'}).appendTo(row2);
360
369
  $('<label/>', {for: 'node-input-rule-case-' + i, style: 'margin-left: 3px;'}).text(caseLabel).appendTo(row2);
361
370
  selectField.change(() => {
362
- resizeRule(container);
363
371
  const type = selectField.val();
364
372
  if ((type === 'btwn') || (type === 'index')) {
365
373
  valueField.typedInput('hide');
@@ -410,6 +418,7 @@
410
418
  row2.hide();
411
419
  row3.hide();
412
420
  }
421
+ resizeRule(container);
413
422
  });
414
423
  selectField.val(rule.t);
415
424
  if ((rule.t == 'btwn') || (rule.t == 'index')) {
@@ -531,7 +540,6 @@
531
540
  }
532
541
 
533
542
  $configLookupTree.on('treelistselect', (event, item) => {
534
- console.log(item);
535
543
  if (item && item.iface && item.channel && item.datapoint && item.property) {
536
544
  $('.form-row.sysvar').hide();
537
545
  $('.form-row.datapoint').show();
@@ -708,15 +716,16 @@
708
716
  });
709
717
  // this.propertyType = $("#node-input-property").typedInput('type');
710
718
  },
711
- oneditresize(size) {
712
- const rows = $('#dialog-form>div:not(.node-input-rule-container-row)');
713
- let height = size.height;
714
- for (let i = 0; i < rows.size(); i++) {
719
+ oneditresize: function(size) {
720
+ var rows = $("#dialog-form>div:not(.node-input-rule-container-row)");
721
+ var height = size.height;
722
+ for (var i=0;i<rows.length;i++) {
715
723
  height -= $(rows[i]).outerHeight(true);
716
724
  }
717
- const editorRow = $('#dialog-form>div.node-input-rule-container-row');
718
- height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom')));
719
- $('#node-input-rule-container').editableList('height', height);
725
+ var editorRow = $("#dialog-form>div.node-input-rule-container-row");
726
+ height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom")));
727
+ height += 16;
728
+ $("#node-input-rule-container").editableList('height',height);
720
729
  }
721
730
  });
722
731
  })();
@@ -783,6 +792,7 @@
783
792
  <div id="dialog-select-datapoint" title="Select Channel, Datapoint and Property">
784
793
  <div id="config-lookup-tree"></div>
785
794
  </div>
795
+
786
796
  </script>
787
797
 
788
798
  <script type="text/x-red" data-help-name="ccu-switch" lang="de-DE">
@@ -5,7 +5,7 @@
5
5
  defaults: {
6
6
  name: {value: ''},
7
7
  ccuConfig: {value: 'localhost', type: 'ccu-connection', required: true},
8
- topic: {value: 'ReGaHSS/${Name}'}, // eslint-disable-line no-template-curly-in-string
8
+ topic: {value: 'ReGaHSS/${Name}'},
9
9
  change: {value: true},
10
10
  cache: {value: true}
11
11
  },
@@ -28,11 +28,14 @@ module.exports = function (RED) {
28
28
  this.name = config.name;
29
29
  this.topic = config.topic;
30
30
 
31
- this.idSubscription = this.ccu.subscribeSysvar({name: this.name, cache: config.cache, change: config.change}, message => {
32
- this.status({fill: 'green', shape: 'ring', text: String(message.payload)});
33
- message.topic = this.ccu.topicReplace(config.topic, message);
34
- this.send(message);
35
- });
31
+ this.idSubscription = this.ccu.subscribeSysvar(
32
+ {name: this.name, cache: config.cache, change: config.change},
33
+ (message) => {
34
+ this.status({fill: 'green', shape: 'ring', text: String(message.payload)});
35
+ message.topic = this.ccu.topicReplace(config.topic, message);
36
+ this.send(message);
37
+ },
38
+ );
36
39
 
37
40
  this.on('input', this._input);
38
41
  this.on('close', this._destructor);
@@ -41,12 +44,13 @@ module.exports = function (RED) {
41
44
  _input(message, send, done) {
42
45
  const name = this.name || message.topic;
43
46
  const value = message.payload;
44
- this.ccu.setVariable(name, value)
47
+ this.ccu
48
+ .setVariable(name, value)
45
49
  .then(() => {
46
50
  this.status({fill: 'green', shape: 'ring', text: String(value)});
47
51
  done();
48
52
  })
49
- .catch(error => {
53
+ .catch((error) => {
50
54
  this.currentStatus = 'red';
51
55
  this.status({fill: 'red', shape: 'dot', text: 'error'});
52
56
  done(error);
@@ -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
  }