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.
- 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 +33 -14
- package/nodes/ccu-connection.html +9 -1
- package/nodes/ccu-connection.js +588 -335
- package/nodes/ccu-display.html +11 -4
- package/nodes/ccu-display.js +26 -9
- package/nodes/ccu-get-value.html +13 -4
- package/nodes/ccu-get-value.js +2 -2
- 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 +40 -16
- package/nodes/ccu-signal.html +11 -5
- package/nodes/ccu-signal.js +34 -23
- package/nodes/ccu-switch.html +13 -5
- 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 -155
- 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-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