node-red-contrib-ccu 4.1.0 → 4.3.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/CHANGELOG.md +125 -0
- package/HANDOFF.md +67 -16
- package/README.md +42 -0
- package/ROADMAP.md +129 -47
- package/examples/home-assistant.json +127 -0
- package/nodes/ccu-alexa.html +3 -1
- package/nodes/ccu-connection.html +26 -0
- package/nodes/ccu-connection.js +124 -13
- package/nodes/ccu-display.js +29 -13
- package/nodes/ccu-get-value.html +3 -1
- package/nodes/ccu-homeassistant.html +334 -0
- package/nodes/ccu-homeassistant.js +272 -0
- package/nodes/ccu-mqtt.html +21 -4
- package/nodes/ccu-mqtt.js +188 -105
- package/nodes/ccu-rpc-event.html +6 -0
- package/nodes/ccu-rpc-event.js +31 -12
- package/nodes/ccu-set-value.html +38 -0
- package/nodes/ccu-set-value.js +59 -235
- package/nodes/ccu-signal.html +55 -1
- package/nodes/ccu-signal.js +52 -9
- package/nodes/ccu-switch.html +17 -4
- package/nodes/ccu-switch.js +28 -4
- package/nodes/ccu-sysvar.html +40 -1
- package/nodes/ccu-sysvar.js +78 -8
- package/nodes/ccu-value.html +27 -1
- package/nodes/ccu-value.js +69 -19
- package/nodes/lib/cast.js +59 -14
- package/nodes/lib/channelfilter.js +136 -0
- package/nodes/lib/dynconfig.js +81 -0
- package/nodes/lib/hadiscovery.js +846 -0
- package/nodes/lib/haroles.js +193 -0
- package/nodes/lib/localccu.js +81 -0
- package/nodes/lib/message.js +6 -2
- package/nodes/lib/valuestatus.js +145 -0
- package/nodes/locales/de/ccu-display.html +17 -0
- package/nodes/locales/de/ccu-rpc-event.json +1 -0
- package/nodes/locales/de/ccu-sysvar.json +3 -2
- package/nodes/locales/de/ccu-value.json +2 -1
- package/nodes/locales/en-US/ccu-display.html +17 -0
- package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
- package/nodes/locales/en-US/ccu-switch.html +12 -0
- package/nodes/locales/en-US/ccu-sysvar.json +3 -2
- package/nodes/locales/en-US/ccu-value.json +2 -1
- package/package.json +2 -1
- package/patch-certainonly.py +51 -0
- package/roadmap-archive/B-16.md +31 -0
- package/roadmap-archive/B-2.md +69 -0
- package/roadmap-archive/B-3.md +57 -0
- package/roadmap-archive/B-4.md +44 -0
- package/roadmap-archive/B-5.md +57 -0
- package/roadmap-archive/README.md +11 -0
package/nodes/ccu-sysvar.html
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
ccuConfig: {value: 'localhost', type: 'ccu-connection', required: true},
|
|
8
8
|
topic: {value: 'ReGaHSS/${Name}'},
|
|
9
9
|
change: {value: true},
|
|
10
|
-
cache: {value: true}
|
|
10
|
+
cache: {value: true},
|
|
11
|
+
property: {value: 'payload'},
|
|
12
|
+
propertyType: {value: 'msg'}
|
|
11
13
|
},
|
|
12
14
|
inputs: 1,
|
|
13
15
|
outputs: 1,
|
|
@@ -24,6 +26,24 @@
|
|
|
24
26
|
oneditprepare() {
|
|
25
27
|
const cname = this.name;
|
|
26
28
|
|
|
29
|
+
// Where the value to write comes from (#56). Older nodes have no
|
|
30
|
+
// property/propertyType - default them to msg.payload, which is
|
|
31
|
+
// what the node always used.
|
|
32
|
+
if (!this.property) {
|
|
33
|
+
this.property = 'payload';
|
|
34
|
+
$('#node-input-property').val('payload');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!this.propertyType) {
|
|
38
|
+
this.propertyType = 'msg';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
$('#node-input-property').typedInput({
|
|
42
|
+
default: this.propertyType,
|
|
43
|
+
types: ['msg', 'flow', 'global', 'str', 'num', 'bool', 'json', 'env'],
|
|
44
|
+
typeField: $('#node-input-propertyType')
|
|
45
|
+
});
|
|
46
|
+
|
|
27
47
|
// Migration
|
|
28
48
|
if (typeof this.change === 'undefined') {
|
|
29
49
|
$('#node-input-change').attr('checked', true);
|
|
@@ -97,6 +117,11 @@
|
|
|
97
117
|
<select id="select-input-name" style="display:none" class="ccu-dropdown">
|
|
98
118
|
</select>
|
|
99
119
|
</div>
|
|
120
|
+
<div class="form-row">
|
|
121
|
+
<label for="node-input-property"><i class="fa fa-sign-in"></i> <span data-i18n="ccu-sysvar.property"></span></label>
|
|
122
|
+
<input type="text" id="node-input-property" placeholder="payload" style="width:70%;">
|
|
123
|
+
<input type="hidden" id="node-input-propertyType">
|
|
124
|
+
</div>
|
|
100
125
|
<div class="form-row">
|
|
101
126
|
<label for="node-input-change"><i class="icon-bookmark"></i> </label>
|
|
102
127
|
<div style="width: 70%; display: inline-block; vertical-align: text-top;">
|
|
@@ -148,6 +173,13 @@
|
|
|
148
173
|
</code></pre>
|
|
149
174
|
</li>
|
|
150
175
|
</ol>
|
|
176
|
+
|
|
177
|
+
<h3>Dynamische Konfiguration</h3>
|
|
178
|
+
<p><code>msg.config</code> ist ein Objekt, dessen Eigenschaften die Konfiguration dieses Nodes für genau diese Nachricht überschreiben. Die gespeicherte Konfiguration bleibt unverändert.</p>
|
|
179
|
+
<ul>
|
|
180
|
+
<li><code>name</code></li>
|
|
181
|
+
</ul>
|
|
182
|
+
<p>Woher der zu schreibende Wert kommt, stellt das Feld <em>Wert aus</em> ein (msg/flow/global/env) — er muss nicht mehr in <code>msg.payload</code> stehen.</p>
|
|
151
183
|
</script>
|
|
152
184
|
|
|
153
185
|
<script type="text/x-red" data-help-name="ccu-sysvar" lang="en-US">
|
|
@@ -161,4 +193,11 @@
|
|
|
161
193
|
The value that should be assigned to the variable.
|
|
162
194
|
</dd>
|
|
163
195
|
</dl>
|
|
196
|
+
|
|
197
|
+
<h3>Dynamic configuration</h3>
|
|
198
|
+
<p><code>msg.config</code> is an object whose properties override this node's configuration for that one message. The stored configuration is never modified.</p>
|
|
199
|
+
<ul>
|
|
200
|
+
<li><code>name</code></li>
|
|
201
|
+
</ul>
|
|
202
|
+
<p>The <em>Value from</em> field selects where the value to write comes from (msg/flow/global/env); it no longer has to be <code>msg.payload</code>.</p>
|
|
164
203
|
</script>
|
package/nodes/ccu-sysvar.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
3
|
const statusHelper = require(path.join(__dirname, '/lib/status.js'));
|
|
4
|
+
const {effectiveConfig} = require(path.join(__dirname, '/lib/dynconfig.js'));
|
|
5
|
+
const {valueStatus} = require(path.join(__dirname, '/lib/valuestatus.js'));
|
|
6
|
+
|
|
7
|
+
/** What a message may override through msg.config (B-2). */
|
|
8
|
+
const DYNAMIC_KEYS = ['name'];
|
|
4
9
|
|
|
5
10
|
module.exports = function (RED) {
|
|
6
11
|
class CcuSysvarNode {
|
|
@@ -28,27 +33,53 @@ module.exports = function (RED) {
|
|
|
28
33
|
this.name = config.name;
|
|
29
34
|
this.topic = config.topic;
|
|
30
35
|
|
|
36
|
+
// Where the value to write comes from. Defaults to msg.payload,
|
|
37
|
+
// which is what the node has always used (#56).
|
|
38
|
+
this.property = config.property || 'payload';
|
|
39
|
+
this.propertyType = config.propertyType || 'msg';
|
|
40
|
+
|
|
41
|
+
this.valueStatus = valueStatus(this);
|
|
42
|
+
|
|
31
43
|
this.idSubscription = this.ccu.subscribeSysvar(
|
|
32
44
|
{name: this.name, cache: config.cache, change: config.change},
|
|
33
45
|
(message) => {
|
|
34
|
-
this.
|
|
46
|
+
this.valueStatus.set(message.payload, message.ts);
|
|
35
47
|
message.topic = this.ccu.topicReplace(config.topic, message);
|
|
36
48
|
this.send(message);
|
|
37
49
|
},
|
|
38
50
|
);
|
|
39
51
|
|
|
52
|
+
// Show the current value even when "emit value on start" is off:
|
|
53
|
+
// the connection knows it either way (#54)
|
|
54
|
+
if (!config.cache && this.name) {
|
|
55
|
+
const known = this.ccu.sysvar[this.name];
|
|
56
|
+
if (known) {
|
|
57
|
+
this.valueStatus.set(known.value, known.ts);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
this.on('input', this._input);
|
|
41
62
|
this.on('close', this._destructor);
|
|
42
63
|
}
|
|
43
64
|
|
|
44
65
|
_input(message, send, done) {
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
done = done || ((error) => error && this.error(error, message));
|
|
67
|
+
const {config} = effectiveConfig({name: this.name}, message, DYNAMIC_KEYS);
|
|
68
|
+
const name = config.name || message.topic;
|
|
69
|
+
|
|
70
|
+
this.getValue(message)
|
|
71
|
+
.then((value) => {
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
// nothing to write - do not turn an empty message into
|
|
74
|
+
// a variable write (same guard as the value node)
|
|
75
|
+
done();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return this.ccu.setVariable(name, value).then(() => {
|
|
80
|
+
this.valueStatus.set(value, Date.now());
|
|
81
|
+
done();
|
|
82
|
+
});
|
|
52
83
|
})
|
|
53
84
|
.catch((error) => {
|
|
54
85
|
this.currentStatus = 'red';
|
|
@@ -57,7 +88,46 @@ module.exports = function (RED) {
|
|
|
57
88
|
});
|
|
58
89
|
}
|
|
59
90
|
|
|
91
|
+
/**
|
|
92
|
+
* The value to write, from the configured source (#56).
|
|
93
|
+
* @param {object} message
|
|
94
|
+
* @returns {Promise<*>}
|
|
95
|
+
*/
|
|
96
|
+
getValue(message) {
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
switch (this.propertyType) {
|
|
99
|
+
case 'flow':
|
|
100
|
+
case 'global': {
|
|
101
|
+
const contextKey = RED.util.parseContextStore(this.property);
|
|
102
|
+
this.context()[this.propertyType].get(contextKey.key, contextKey.store, (err, res) => {
|
|
103
|
+
if (err) {
|
|
104
|
+
reject(err);
|
|
105
|
+
} else {
|
|
106
|
+
resolve(res);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case 'str':
|
|
113
|
+
case 'num':
|
|
114
|
+
case 'bool':
|
|
115
|
+
case 'json':
|
|
116
|
+
case 'env':
|
|
117
|
+
resolve(RED.util.evaluateNodeProperty(this.property, this.propertyType, this, message));
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
default:
|
|
121
|
+
resolve(RED.util.getMessageProperty(message, this.property));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
60
126
|
_destructor(done) {
|
|
127
|
+
if (this.valueStatus) {
|
|
128
|
+
this.valueStatus.stop();
|
|
129
|
+
}
|
|
130
|
+
|
|
61
131
|
if (this.idSubscription) {
|
|
62
132
|
this.debug('unsubscribe');
|
|
63
133
|
this.ccu.unsubscribeSysvar(this.idSubscription);
|
package/nodes/ccu-value.html
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
start: {value: true},
|
|
16
16
|
change: {value: true},
|
|
17
17
|
cache: {value: false},
|
|
18
|
+
certainOnly: {value: false},
|
|
18
19
|
queue: {value: false},
|
|
19
20
|
on: {value: ''},
|
|
20
21
|
onType: {value: 'undefined'},
|
|
@@ -176,7 +177,9 @@
|
|
|
176
177
|
arr.sort((a, b) => a.localeCompare(b));
|
|
177
178
|
$nodeInputChannel.autocomplete('option', 'source', arr);
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
// ${ENV} placeholders are substituted by Node-RED before the node is
|
|
181
|
+
// constructed (subflow parameters, #158) - do not clear them
|
|
182
|
+
if (!/^\$\{[^}]+\}$/.test($nodeInputChannel.val()) && !data[$nodeInputChannel.val().split(' ')[0]]) {
|
|
180
183
|
$nodeInputChannel.val('');
|
|
181
184
|
$nodeInputDatapoint.val('');
|
|
182
185
|
}
|
|
@@ -480,6 +483,11 @@
|
|
|
480
483
|
<input type="checkbox" id="node-input-cache">
|
|
481
484
|
<span data-i18n="ccu-value.cache"></span>
|
|
482
485
|
</label>
|
|
486
|
+
|
|
487
|
+
<label class="ccu-checkbox">
|
|
488
|
+
<input type="checkbox" id="node-input-certainOnly">
|
|
489
|
+
<span data-i18n="ccu-value.certainOnly"></span>
|
|
490
|
+
</label>
|
|
483
491
|
</div>
|
|
484
492
|
</div>
|
|
485
493
|
|
|
@@ -580,8 +588,26 @@
|
|
|
580
588
|
<h3>Output</h3>
|
|
581
589
|
<p>Der Value Node gibt nur Events aus, wenn Interface, Channel und Datapoint gesetzt sind.</p>
|
|
582
590
|
|
|
591
|
+
|
|
592
|
+
<h3>Dynamische Konfiguration</h3>
|
|
593
|
+
<p><code>msg.config</code> ist ein Objekt, dessen Eigenschaften die Konfiguration dieses Nodes für genau diese Nachricht überschreiben. Die gespeicherte Konfiguration bleibt unverändert.</p>
|
|
594
|
+
<ul>
|
|
595
|
+
<li><code>iface</code></li>
|
|
596
|
+
<li><code>channel</code></li>
|
|
597
|
+
<li><code>datapoint</code></li>
|
|
598
|
+
</ul>
|
|
599
|
+
<p>Eine Nachricht ohne <code>payload</code> schreibt nichts, richtet aber die Ausgabe-Seite des Nodes neu aus.</p>
|
|
583
600
|
</script>
|
|
584
601
|
|
|
585
602
|
<script type="text/x-red" data-help-name="ccu-value" lang="en-US">
|
|
586
603
|
<p>Set and read channel datapoints.</p>
|
|
604
|
+
|
|
605
|
+
<h3>Dynamic configuration</h3>
|
|
606
|
+
<p><code>msg.config</code> is an object whose properties override this node's configuration for that one message. The stored configuration is never modified.</p>
|
|
607
|
+
<ul>
|
|
608
|
+
<li><code>iface</code></li>
|
|
609
|
+
<li><code>channel</code></li>
|
|
610
|
+
<li><code>datapoint</code></li>
|
|
611
|
+
</ul>
|
|
612
|
+
<p>A message without a <code>payload</code> writes nothing but retargets the node's output side.</p>
|
|
587
613
|
</script>
|
package/nodes/ccu-value.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
|
|
3
3
|
const statusHelper = require(path.join(__dirname, '/lib/status.js'));
|
|
4
|
+
const {effectiveConfig, configCacheKey} = require(path.join(__dirname, '/lib/dynconfig.js'));
|
|
5
|
+
|
|
6
|
+
/** What a message may supply or override (B-2, #172). */
|
|
7
|
+
const DYNAMIC_KEYS = ['iface', 'channel', 'datapoint'];
|
|
4
8
|
|
|
5
9
|
module.exports = function (RED) {
|
|
6
10
|
class CcuValue {
|
|
@@ -15,37 +19,33 @@ module.exports = function (RED) {
|
|
|
15
19
|
|
|
16
20
|
this.iface = config.iface;
|
|
17
21
|
this.queue = config.queue;
|
|
22
|
+
this.config = config;
|
|
18
23
|
|
|
19
24
|
this.ccu.register(this);
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
channel: String(config.channel).split(' ')[0],
|
|
28
|
-
datapoint: config.datapoint,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
this.idSubscription = this.ccu.subscribe(filter, (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
|
-
});
|
|
36
|
-
}
|
|
26
|
+
this.subscriptionKey = null;
|
|
27
|
+
this.subscribeTo({
|
|
28
|
+
iface: config.iface,
|
|
29
|
+
channel: config.channel,
|
|
30
|
+
datapoint: config.datapoint,
|
|
31
|
+
});
|
|
37
32
|
|
|
38
33
|
this.on('input', (message, send, done) => {
|
|
39
34
|
const [tIface, tChannel, tDatapoint] = (message.topic || '').split('.');
|
|
40
|
-
|
|
35
|
+
// msg.config wins over the node's own configuration; the flat
|
|
36
|
+
// msg.iface/channel/datapoint chain below is unchanged
|
|
37
|
+
const {config: over} = effectiveConfig({}, message, DYNAMIC_KEYS);
|
|
38
|
+
|
|
39
|
+
const iface = over.iface || config.iface || message.interface || message.iface || tIface;
|
|
41
40
|
const channel = (
|
|
41
|
+
over.channel ||
|
|
42
42
|
config.channel ||
|
|
43
43
|
this.ccu.findChannel(message.channelName, true) ||
|
|
44
44
|
message.channel ||
|
|
45
45
|
tChannel ||
|
|
46
46
|
''
|
|
47
47
|
).split(' ')[0];
|
|
48
|
-
const datapoint = config.datapoint || message.datapoint || tDatapoint;
|
|
48
|
+
const datapoint = over.datapoint || config.datapoint || message.datapoint || tDatapoint;
|
|
49
49
|
|
|
50
50
|
if (!iface) {
|
|
51
51
|
this.error('interface undefined');
|
|
@@ -63,7 +63,13 @@ module.exports = function (RED) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (message.payload === undefined) {
|
|
66
|
-
// don't write to the CCU when the incoming msg carries no
|
|
66
|
+
// don't write to the CCU when the incoming msg carries no
|
|
67
|
+
// payload (PR #173). Such a message may still retarget the
|
|
68
|
+
// output side, which is what "configure through message"
|
|
69
|
+
// asked for (#172) - for a node with its own channel and
|
|
70
|
+
// datapoint configured this is a no-op unless msg.config
|
|
71
|
+
// explicitly overrides them.
|
|
72
|
+
this.subscribeTo({iface, channel, datapoint});
|
|
67
73
|
done();
|
|
68
74
|
return;
|
|
69
75
|
}
|
|
@@ -146,6 +152,50 @@ module.exports = function (RED) {
|
|
|
146
152
|
this.on('close', this._destructor);
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
/**
|
|
156
|
+
* (Re-)subscribe the output side. A no-op when the target is
|
|
157
|
+
* unchanged, so a node configured in the editor keeps exactly one
|
|
158
|
+
* subscription for its lifetime as before (#172).
|
|
159
|
+
* @param {{iface: string, channel: string, datapoint: string}} target
|
|
160
|
+
*/
|
|
161
|
+
subscribeTo(target) {
|
|
162
|
+
const {config} = this;
|
|
163
|
+
if (!target.iface || !target.channel || !target.datapoint) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const filter = {
|
|
168
|
+
cache: config.cache,
|
|
169
|
+
change: config.change,
|
|
170
|
+
stable: config.working,
|
|
171
|
+
iface: target.iface,
|
|
172
|
+
channel: String(target.channel).split(' ')[0],
|
|
173
|
+
datapoint: target.datapoint,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// opt-in: only present when the option is enabled, so existing
|
|
177
|
+
// subscriptions keep receiving uncertain values (#96)
|
|
178
|
+
if (config.certainOnly) {
|
|
179
|
+
filter.uncertain = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const key = configCacheKey(filter, ['iface', 'channel', 'datapoint']);
|
|
183
|
+
if (key === this.subscriptionKey) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (this.idSubscription) {
|
|
188
|
+
this.ccu.unsubscribe(this.idSubscription);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.subscriptionKey = key;
|
|
192
|
+
this.idSubscription = this.ccu.subscribe(filter, (message) => {
|
|
193
|
+
this.status({fill: 'green', shape: 'ring', text: String(message.payload)});
|
|
194
|
+
message.topic = this.ccu.topicReplace(config.topic, message);
|
|
195
|
+
this.send(message);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
149
199
|
_destructor(done) {
|
|
150
200
|
if (this.idSubscription) {
|
|
151
201
|
this.debug('ccu-value close');
|
package/nodes/lib/cast.js
CHANGED
|
@@ -1,20 +1,57 @@
|
|
|
1
|
-
/* Pure value casting against a paramset description (TYPE/
|
|
2
|
-
extracted
|
|
1
|
+
/* Pure value casting against a paramset description (TYPE/VALUE_LIST/MIN/MAX),
|
|
2
|
+
extracted from ccu-connection.js paramCast (Phase 3) and unified with the
|
|
3
|
+
ccu-mqtt node's divergent copy (B-3): that one clamped to MIN/MAX and cast
|
|
4
|
+
INTEGER slightly differently, which is now the `clamp` option rather than a
|
|
5
|
+
second implementation.
|
|
3
6
|
|
|
4
|
-
Note:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
Note on enum names: the CCU returns them in `VALUE_LIST`, not `ENUM`. The
|
|
8
|
+
old code read `description.ENUM`, which no interface process ever sends -
|
|
9
|
+
all 9494 ENUM-typed datapoints in the shipped paramsets.json carry
|
|
10
|
+
VALUE_LIST and none carries ENUM - so casting an enum by name never worked
|
|
11
|
+
and message.datapointEnum/valueEnum were always undefined (B-3, ROADMAP
|
|
12
|
+
8.2). `ENUM` is still accepted as a fallback for hand-written data. */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The list of enum names for a description, or undefined.
|
|
16
|
+
* @param {object} [description]
|
|
17
|
+
* @returns {string[]|undefined}
|
|
18
|
+
*/
|
|
19
|
+
function enumList(description) {
|
|
20
|
+
if (!description) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const list = description.VALUE_LIST || description.ENUM;
|
|
25
|
+
return Array.isArray(list) ? list : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clampNumber(value, description) {
|
|
29
|
+
if (typeof description.MIN === 'number' && value < description.MIN) {
|
|
30
|
+
return description.MIN;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (typeof description.MAX === 'number' && value > description.MAX) {
|
|
34
|
+
return description.MAX;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
8
39
|
|
|
9
40
|
/**
|
|
10
41
|
* Cast a value according to a paramset description.
|
|
11
42
|
* @param {*} value
|
|
12
|
-
* @param {object} [description] paramset description (TYPE,
|
|
43
|
+
* @param {object} [description] paramset description (TYPE, VALUE_LIST, ...);
|
|
13
44
|
* when missing, numbers are stringified (works for FLOAT/INTEGER
|
|
14
45
|
* datapoints) and everything else passes through unchanged.
|
|
46
|
+
* @param {object} [options]
|
|
47
|
+
* @param {boolean} [options.clamp=false] limit numbers to MIN/MAX. Off for
|
|
48
|
+
* the value nodes (#74), on for the mqtt node, which has clamped
|
|
49
|
+
* since it was written.
|
|
15
50
|
* @returns {*} the casted value; FLOAT becomes {explicitDouble: number}
|
|
16
51
|
*/
|
|
17
|
-
function castValue(value, description) {
|
|
52
|
+
function castValue(value, description, options) {
|
|
53
|
+
const clamp = Boolean(options && options.clamp);
|
|
54
|
+
|
|
18
55
|
if (!description) {
|
|
19
56
|
// Fallback: use string for numbers, this should work for double and integer datapoints
|
|
20
57
|
if (typeof value === 'number') {
|
|
@@ -40,14 +77,18 @@ function castValue(value, description) {
|
|
|
40
77
|
case 'FLOAT':
|
|
41
78
|
value = Number.parseFloat(value) || 0;
|
|
42
79
|
/* Todo: rethink, deactivate boundary check for now (https://github.com/rdmtc/node-red-contrib-ccu/issues/74) */
|
|
80
|
+
if (clamp) {
|
|
81
|
+
value = clampNumber(value, description);
|
|
82
|
+
}
|
|
83
|
+
|
|
43
84
|
value = {explicitDouble: value};
|
|
44
85
|
break;
|
|
45
|
-
case 'ENUM':
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
86
|
+
case 'ENUM': {
|
|
87
|
+
const list = enumList(description);
|
|
88
|
+
if (typeof value === 'string' && list && list.includes(value)) {
|
|
89
|
+
value = list.indexOf(value);
|
|
50
90
|
}
|
|
91
|
+
}
|
|
51
92
|
|
|
52
93
|
// Fallthrough by intention
|
|
53
94
|
case 'INTEGER':
|
|
@@ -57,6 +98,10 @@ function castValue(value, description) {
|
|
|
57
98
|
value = Number.parseInt(value, 10) || 0;
|
|
58
99
|
}
|
|
59
100
|
|
|
101
|
+
if (clamp) {
|
|
102
|
+
value = clampNumber(value, description);
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
break;
|
|
61
106
|
case 'STRING':
|
|
62
107
|
value = String(value);
|
|
@@ -103,4 +148,4 @@ function castSysvar(value, sysvar) {
|
|
|
103
148
|
return value;
|
|
104
149
|
}
|
|
105
150
|
|
|
106
|
-
module.exports = {castValue, castSysvar};
|
|
151
|
+
module.exports = {castValue, castSysvar, enumList};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/* Channel matching for the filter form shared by ccu-set-value (and, from
|
|
2
|
+
4.3.0, reusable by anything else that resolves a filter to channels).
|
|
3
|
+
|
|
4
|
+
Extracted from ccu-set-value.js as part of B-2 so the matching is pure
|
|
5
|
+
and unit-testable, and so the per-message effective config (see
|
|
6
|
+
dynconfig.js) can be applied without the node caching the wrong thing.
|
|
7
|
+
|
|
8
|
+
Each criterion has a companion `<name>Rx` selector: 'str' compares
|
|
9
|
+
literally, 're' treats the configured value as a regular expression. */
|
|
10
|
+
|
|
11
|
+
/** The filter criteria, in the order they are evaluated. */
|
|
12
|
+
const FILTER_KEYS = [
|
|
13
|
+
'device',
|
|
14
|
+
'deviceType',
|
|
15
|
+
'deviceName',
|
|
16
|
+
'channel',
|
|
17
|
+
'channelType',
|
|
18
|
+
'channelIndex',
|
|
19
|
+
'channelName',
|
|
20
|
+
'rooms',
|
|
21
|
+
'functions',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** All configuration keys that make up a filter, including the Rx selectors. */
|
|
25
|
+
const CONFIG_KEYS = ['iface', ...FILTER_KEYS, 'datapoint', ...FILTER_KEYS.map((key) => key + 'Rx'), 'datapointRx'];
|
|
26
|
+
|
|
27
|
+
function matches(value, pattern, rx) {
|
|
28
|
+
if (typeof value !== 'string') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return rx === 're' ? Boolean(value.match(new RegExp(pattern))) : value === pattern;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function matchesList(list, pattern, rx) {
|
|
36
|
+
if (!Array.isArray(list)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return rx === 're' ? list.some((item) => matches(item, pattern, 're')) : list.includes(pattern);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Does one channel address pass the configured filter?
|
|
45
|
+
*
|
|
46
|
+
* @param {object} ccu connection-shaped dependency: metadata.devices,
|
|
47
|
+
* channelNames, channelRooms, channelFunctions
|
|
48
|
+
* @param {string} iface
|
|
49
|
+
* @param {string} address channel address, e.g. `ABC1234567:1`
|
|
50
|
+
* @param {object} config effective configuration (see dynconfig.js)
|
|
51
|
+
* @returns {boolean} true when the channel is selected by the filter
|
|
52
|
+
*/
|
|
53
|
+
function channelMatches(ccu, iface, address, config) {
|
|
54
|
+
const channel = ccu.metadata.devices[iface] && ccu.metadata.devices[iface][address];
|
|
55
|
+
|
|
56
|
+
// Device-level entries (no PARENT) are never targets, only their channels
|
|
57
|
+
if (!channel || !channel.PARENT) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const device = ccu.metadata.devices[iface][channel.PARENT] || {};
|
|
62
|
+
|
|
63
|
+
if (config.device && !matches(channel.PARENT, config.device, config.deviceRx)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (config.deviceType && !matches(device.TYPE, config.deviceType, config.deviceTypeRx)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Note: this used to test whether the *channel* had a ReGa name while
|
|
72
|
+
// comparing the *device* name, so channels without a name of their own
|
|
73
|
+
// were dropped even when their device matched (fixed in 4.3.0).
|
|
74
|
+
if (config.deviceName && !matches(ccu.channelNames[channel.PARENT], config.deviceName, config.deviceNameRx)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (config.channel && !matches(address, config.channel, config.channelRx)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (config.channelType && !matches(channel.TYPE, config.channelType, config.channelTypeRx)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (config.channelIndex) {
|
|
87
|
+
const index = address.split(':')[1];
|
|
88
|
+
if (config.channelIndexRx === 're') {
|
|
89
|
+
if (!matches(index, String(config.channelIndex), 're')) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
} else if (!address.endsWith(':' + config.channelIndex)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (config.channelName && !matches(ccu.channelNames[address], config.channelName, config.channelNameRx)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (config.rooms && !matchesList(ccu.channelRooms[address], config.rooms, config.roomsRx)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (config.functions && !matchesList(ccu.channelFunctions[address], config.functions, config.functionsRx)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The datapoints of one channel that pass the datapoint filter.
|
|
114
|
+
* @param {object} ccu connection-shaped dependency: paramsetName, paramsetDescriptions
|
|
115
|
+
* @param {string} iface
|
|
116
|
+
* @param {string} address
|
|
117
|
+
* @param {object} config effective configuration
|
|
118
|
+
* @returns {string[]} datapoint names (empty when the channel has no VALUES description)
|
|
119
|
+
*/
|
|
120
|
+
function datapointsOf(ccu, iface, address, config) {
|
|
121
|
+
const channel = ccu.metadata.devices[iface] && ccu.metadata.devices[iface][address];
|
|
122
|
+
const psKey = channel && ccu.paramsetName(iface, channel, 'VALUES');
|
|
123
|
+
const description = psKey && ccu.paramsetDescriptions[psKey];
|
|
124
|
+
if (!description) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// No special case for an empty datapoint filter: with str it selects
|
|
129
|
+
// nothing (a literal comparison against undefined) and with re it
|
|
130
|
+
// selects everything, exactly as before the extraction. Silently
|
|
131
|
+
// widening str to "all datapoints" would write the payload to every
|
|
132
|
+
// datapoint of every matched channel.
|
|
133
|
+
return Object.keys(description).filter((dp) => matches(dp, config.datapoint, config.datapointRx));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {channelMatches, datapointsOf, FILTER_KEYS, CONFIG_KEYS};
|