node-red-contrib-ccu 4.2.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 +96 -0
- package/HANDOFF.md +11 -3
- package/README.md +29 -0
- package/ROADMAP.md +112 -33
- package/nodes/ccu-alexa.html +3 -1
- package/nodes/ccu-connection.html +26 -0
- package/nodes/ccu-connection.js +87 -11
- package/nodes/ccu-display.js +29 -13
- package/nodes/ccu-get-value.html +3 -1
- package/nodes/ccu-mqtt.html +21 -4
- package/nodes/ccu-mqtt.js +126 -102
- 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/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 +1 -1
- package/patch-certainonly.py +51 -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-switch.html
CHANGED
|
@@ -239,7 +239,9 @@
|
|
|
239
239
|
arr.sort((a, b) => a.localeCompare(b));
|
|
240
240
|
$nodeInputChannel.autocomplete('option', 'source', arr);
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
// ${ENV} placeholders are substituted by Node-RED before the node is
|
|
243
|
+
// constructed (subflow parameters, #158) - do not clear them
|
|
244
|
+
if (!/^\$\{[^}]+\}$/.test($nodeInputChannel.val()) && !data[$nodeInputChannel.val().split(' ')[0]]) {
|
|
243
245
|
$nodeInputChannel.val('');
|
|
244
246
|
$nodeInputDatapoint.val('');
|
|
245
247
|
}
|
|
@@ -345,12 +347,12 @@
|
|
|
345
347
|
selectField.append($('<option></option>').val(operators[d].v).text(node._(operators[d].t)));
|
|
346
348
|
}
|
|
347
349
|
}
|
|
348
|
-
const valueField = $('<input/>', {class: 'node-input-rule-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row).typedInput({default: 'str', types: ['msg', 'flow', 'global', 'str', 'num', 'jsonata', 'env', previousValueType]});
|
|
350
|
+
const valueField = $('<input/>', {class: 'node-input-rule-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row).typedInput({default: 'str', types: ['msg', 'flow', 'global', 'str', 'num', 'bool', 'jsonata', 'env', previousValueType]});
|
|
349
351
|
const numValueField = $('<input/>', {class: 'node-input-rule-num-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row).typedInput({default: 'num', types: ['flow', 'global', 'num', 'jsonata', 'env']});
|
|
350
352
|
const expValueField = $('<input/>', {class: 'node-input-rule-exp-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row).typedInput({default: 'jsonata', types: ['jsonata']});
|
|
351
|
-
const btwnValueField = $('<input/>', {class: 'node-input-rule-btwn-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row).typedInput({default: 'num', types: ['msg', 'flow', 'global', 'str', 'num', 'jsonata', 'env', previousValueType]});
|
|
353
|
+
const btwnValueField = $('<input/>', {class: 'node-input-rule-btwn-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row).typedInput({default: 'num', types: ['msg', 'flow', 'global', 'str', 'num', 'bool', 'jsonata', 'env', previousValueType]});
|
|
352
354
|
const btwnAndLabel = $('<span/>', {class: 'node-input-rule-btwn-label'}).text(' ' + andLabel + ' ').appendTo(row3);
|
|
353
|
-
const btwnValue2Field = $('<input/>', {class: 'node-input-rule-btwn-value2', type: 'text', style: 'margin-left:2px;'}).appendTo(row3).typedInput({default: 'num', types: ['msg', 'flow', 'global', 'str', 'num', 'jsonata', 'env', previousValueType]});
|
|
355
|
+
const btwnValue2Field = $('<input/>', {class: 'node-input-rule-btwn-value2', type: 'text', style: 'margin-left:2px;'}).appendTo(row3).typedInput({default: 'num', types: ['msg', 'flow', 'global', 'str', 'num', 'bool', 'jsonata', 'env', previousValueType]});
|
|
354
356
|
const typeValueField = $('<input/>', {class: 'node-input-rule-type-value', type: 'text', style: 'margin-left: 5px;'}).appendTo(row)
|
|
355
357
|
.typedInput({default: 'string', types: [
|
|
356
358
|
{value: 'string', label: 'string', hasValue: false},
|
|
@@ -801,6 +803,17 @@
|
|
|
801
803
|
<p>Der ccu switch Node verhält sich ähnlich wie der switch Node von Node-RED. Er leitet eine Nachricht abhängig von einem Zustand eines Aktors oder einer Systemvariablen weiter, wahlweise auch je nach Zustand an unterschiedliche Ausgänge.</p>
|
|
802
804
|
<h4>Dokumentation</h4>
|
|
803
805
|
<a target="_blank" href="https://github.com/rdmtc/RedMatic/wiki/CCU-Nodes#ccu-switch-node">ccu switch node im RedMatic Wiki</a>
|
|
806
|
+
|
|
807
|
+
<h3>Dynamische Konfiguration</h3>
|
|
808
|
+
<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>
|
|
809
|
+
<ul>
|
|
810
|
+
<li><code>iface</code></li>
|
|
811
|
+
<li><code>channel</code></li>
|
|
812
|
+
<li><code>datapoint</code></li>
|
|
813
|
+
<li><code>datapointProperty</code></li>
|
|
814
|
+
<li><code>sysvar</code></li>
|
|
815
|
+
<li><code>sysvarProperty</code></li>
|
|
816
|
+
</ul>
|
|
804
817
|
</script>
|
|
805
818
|
|
|
806
819
|
|
package/nodes/ccu-switch.js
CHANGED
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
**/
|
|
16
16
|
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
const {effectiveConfig} = require(path.join(__dirname, '/lib/dynconfig.js'));
|
|
20
|
+
|
|
21
|
+
/** Source selection a message may override through msg.config (B-2, #103). */
|
|
22
|
+
const DYNAMIC_KEYS = ['iface', 'channel', 'datapoint', 'datapointProperty', 'sysvar', 'sysvarProperty'];
|
|
23
|
+
|
|
17
24
|
module.exports = function (RED) {
|
|
18
25
|
'use strict';
|
|
19
26
|
|
|
@@ -132,11 +139,28 @@ module.exports = function (RED) {
|
|
|
132
139
|
|
|
133
140
|
function getProperty(node, msg) {
|
|
134
141
|
return new Promise((resolve, reject) => {
|
|
135
|
-
|
|
136
|
-
|
|
142
|
+
// which datapoint or variable to look at can come from the
|
|
143
|
+
// message, so one switch node can serve a whole subflow (#103)
|
|
144
|
+
const {config} = effectiveConfig(
|
|
145
|
+
{
|
|
146
|
+
iface: node.iface,
|
|
147
|
+
channel: node.channel,
|
|
148
|
+
datapoint: node.datapoint,
|
|
149
|
+
datapointProperty: node.datapointProperty,
|
|
150
|
+
sysvar: node.sysvar,
|
|
151
|
+
sysvarProperty: node.sysvarProperty
|
|
152
|
+
},
|
|
153
|
+
msg,
|
|
154
|
+
DYNAMIC_KEYS
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (config.iface === 'ReGaHSS') {
|
|
158
|
+
const sysvar = node.ccu.sysvar[config.sysvar];
|
|
159
|
+
resolve(sysvar && sysvar[config.sysvarProperty || 'value']);
|
|
137
160
|
} else {
|
|
138
|
-
const address =
|
|
139
|
-
|
|
161
|
+
const address = config.iface + '.' + String(config.channel).split(' ')[0] + '.' + config.datapoint;
|
|
162
|
+
const value = node.ccu.values[address];
|
|
163
|
+
resolve(value && value[config.datapointProperty || 'value']);
|
|
140
164
|
}
|
|
141
165
|
});
|
|
142
166
|
}
|
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};
|