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.
- package/.prettierignore +10 -0
- package/.prettierrc +14 -0
- package/AGENTS.md +96 -0
- package/CHANGELOG.md +66 -0
- package/README.md +17 -21
- package/ROADMAP.md +402 -0
- package/eslint.config.js +45 -0
- package/examples/rpc-events.json +65 -0
- package/examples/switch-datapoint.json +86 -0
- package/examples/system-variable.json +60 -0
- package/nodes/ccu-alexa.html +12 -7
- package/nodes/ccu-alexa.js +43 -15
- package/nodes/ccu-connection.html +9 -1
- package/nodes/ccu-connection.js +602 -337
- package/nodes/ccu-display.html +11 -4
- package/nodes/ccu-display.js +26 -9
- package/nodes/ccu-get-value.html +23 -14
- package/nodes/ccu-get-value.js +10 -5
- package/nodes/ccu-mqtt.html +9 -10
- package/nodes/ccu-mqtt.js +55 -29
- package/nodes/ccu-program.html +1 -1
- package/nodes/ccu-program.js +9 -7
- package/nodes/ccu-rpc-event.html +12 -7
- package/nodes/ccu-rpc-event.js +4 -5
- package/nodes/ccu-rpc.html +12 -5
- package/nodes/ccu-rpc.js +8 -6
- package/nodes/ccu-script.html +1 -1
- package/nodes/ccu-script.js +4 -3
- package/nodes/ccu-set-value.html +11 -7
- package/nodes/ccu-set-value.js +47 -16
- package/nodes/ccu-signal.html +11 -5
- package/nodes/ccu-signal.js +34 -23
- package/nodes/ccu-switch.html +23 -13
- package/nodes/ccu-sysvar.html +1 -1
- package/nodes/ccu-sysvar.js +11 -7
- package/nodes/ccu-value.html +12 -7
- package/nodes/ccu-value.js +36 -13
- package/nodes/lib/base62.js +22 -0
- package/nodes/lib/discover.js +127 -0
- package/nodes/lib/nextport.js +30 -0
- package/nodes/lib/similarity.js +51 -0
- package/nodes/lib/status.js +1 -1
- package/package.json +105 -144
- package/roadmap-archive/README.md +20 -0
- package/roadmap-archive/phase-1.md +48 -0
- package/roadmap-archive/todo-history.md +39 -0
- package/TODO.md +0 -41
- package/ccu_rega_localhost.json +0 -1
- package/ccu_values_localhost.json +0 -1
package/nodes/ccu-switch.html
CHANGED
|
@@ -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
|
-
|
|
713
|
-
|
|
714
|
-
for (
|
|
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
|
-
|
|
718
|
-
height -= (parseInt(editorRow.css(
|
|
719
|
-
|
|
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">
|
package/nodes/ccu-sysvar.html
CHANGED
|
@@ -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}'},
|
|
8
|
+
topic: {value: 'ReGaHSS/${Name}'},
|
|
9
9
|
change: {value: true},
|
|
10
10
|
cache: {value: true}
|
|
11
11
|
},
|
package/nodes/ccu-sysvar.js
CHANGED
|
@@ -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(
|
|
32
|
-
|
|
33
|
-
message
|
|
34
|
-
|
|
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
|
|
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);
|
package/nodes/ccu-value.html
CHANGED
|
@@ -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}'}
|
|
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
|
});
|
package/nodes/ccu-value.js
CHANGED
|
@@ -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 = (
|
|
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'](
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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};
|
package/nodes/lib/status.js
CHANGED