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.
Files changed (45) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/HANDOFF.md +11 -3
  3. package/README.md +29 -0
  4. package/ROADMAP.md +112 -33
  5. package/nodes/ccu-alexa.html +3 -1
  6. package/nodes/ccu-connection.html +26 -0
  7. package/nodes/ccu-connection.js +87 -11
  8. package/nodes/ccu-display.js +29 -13
  9. package/nodes/ccu-get-value.html +3 -1
  10. package/nodes/ccu-mqtt.html +21 -4
  11. package/nodes/ccu-mqtt.js +126 -102
  12. package/nodes/ccu-rpc-event.html +6 -0
  13. package/nodes/ccu-rpc-event.js +31 -12
  14. package/nodes/ccu-set-value.html +38 -0
  15. package/nodes/ccu-set-value.js +59 -235
  16. package/nodes/ccu-signal.html +55 -1
  17. package/nodes/ccu-signal.js +52 -9
  18. package/nodes/ccu-switch.html +17 -4
  19. package/nodes/ccu-switch.js +28 -4
  20. package/nodes/ccu-sysvar.html +40 -1
  21. package/nodes/ccu-sysvar.js +78 -8
  22. package/nodes/ccu-value.html +27 -1
  23. package/nodes/ccu-value.js +69 -19
  24. package/nodes/lib/cast.js +59 -14
  25. package/nodes/lib/channelfilter.js +136 -0
  26. package/nodes/lib/dynconfig.js +81 -0
  27. package/nodes/lib/localccu.js +81 -0
  28. package/nodes/lib/message.js +6 -2
  29. package/nodes/lib/valuestatus.js +145 -0
  30. package/nodes/locales/de/ccu-display.html +17 -0
  31. package/nodes/locales/de/ccu-rpc-event.json +1 -0
  32. package/nodes/locales/de/ccu-sysvar.json +3 -2
  33. package/nodes/locales/de/ccu-value.json +2 -1
  34. package/nodes/locales/en-US/ccu-display.html +17 -0
  35. package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
  36. package/nodes/locales/en-US/ccu-switch.html +12 -0
  37. package/nodes/locales/en-US/ccu-sysvar.json +3 -2
  38. package/nodes/locales/en-US/ccu-value.json +2 -1
  39. package/package.json +1 -1
  40. package/patch-certainonly.py +51 -0
  41. package/roadmap-archive/B-2.md +69 -0
  42. package/roadmap-archive/B-3.md +57 -0
  43. package/roadmap-archive/B-4.md +44 -0
  44. package/roadmap-archive/B-5.md +57 -0
  45. package/roadmap-archive/README.md +11 -0
@@ -239,7 +239,9 @@
239
239
  arr.sort((a, b) => a.localeCompare(b));
240
240
  $nodeInputChannel.autocomplete('option', 'source', arr);
241
241
 
242
- if (!data[$nodeInputChannel.val().split(' ')[0]]) {
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
 
@@ -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
- if (node.iface === 'ReGaHSS') {
136
- resolve(node.ccu.sysvar[node.sysvar] && node.ccu.sysvar[node.sysvar][node.sysvarProperty]);
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 = node.iface + '.' + String(node.channel).split(' ')[0] + '.' + node.datapoint;
139
- resolve(node.ccu.values[address] && node.ccu.values[address][node.datapointProperty]);
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
  }
@@ -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>
@@ -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.status({fill: 'green', shape: 'ring', text: String(message.payload)});
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
- const name = this.name || message.topic;
46
- const value = message.payload;
47
- this.ccu
48
- .setVariable(name, value)
49
- .then(() => {
50
- this.status({fill: 'green', shape: 'ring', text: String(value)});
51
- done();
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);
@@ -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
- if (!data[$nodeInputChannel.val().split(' ')[0]]) {
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>
@@ -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
- if (config.iface && config.channel && config.datapoint) {
22
- const filter = {
23
- cache: config.cache,
24
- change: config.change,
25
- stable: config.working,
26
- iface: config.iface,
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
- const iface = config.iface || message.interface || message.iface || tIface;
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 payload (PR #173)
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/ENUM/MIN/MAX),
2
- extracted unchanged from ccu-connection.js paramCast (Phase 3).
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: nodes/ccu-mqtt.js still carries its own divergent variant (with
5
- MIN/MAX clamping and different INTEGER handling) — unifying that one is
6
- roadmap item B-3, deliberately not done in this behavior-preserving
7
- extraction. */
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, ENUM, ...);
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
- if (typeof value === 'string') {
47
- if (description.ENUM && description.ENUM.includes(value)) {
48
- value = description.ENUM.indexOf(value);
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};