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.
Files changed (51) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/HANDOFF.md +67 -16
  3. package/README.md +42 -0
  4. package/ROADMAP.md +129 -47
  5. package/examples/home-assistant.json +127 -0
  6. package/nodes/ccu-alexa.html +3 -1
  7. package/nodes/ccu-connection.html +26 -0
  8. package/nodes/ccu-connection.js +124 -13
  9. package/nodes/ccu-display.js +29 -13
  10. package/nodes/ccu-get-value.html +3 -1
  11. package/nodes/ccu-homeassistant.html +334 -0
  12. package/nodes/ccu-homeassistant.js +272 -0
  13. package/nodes/ccu-mqtt.html +21 -4
  14. package/nodes/ccu-mqtt.js +188 -105
  15. package/nodes/ccu-rpc-event.html +6 -0
  16. package/nodes/ccu-rpc-event.js +31 -12
  17. package/nodes/ccu-set-value.html +38 -0
  18. package/nodes/ccu-set-value.js +59 -235
  19. package/nodes/ccu-signal.html +55 -1
  20. package/nodes/ccu-signal.js +52 -9
  21. package/nodes/ccu-switch.html +17 -4
  22. package/nodes/ccu-switch.js +28 -4
  23. package/nodes/ccu-sysvar.html +40 -1
  24. package/nodes/ccu-sysvar.js +78 -8
  25. package/nodes/ccu-value.html +27 -1
  26. package/nodes/ccu-value.js +69 -19
  27. package/nodes/lib/cast.js +59 -14
  28. package/nodes/lib/channelfilter.js +136 -0
  29. package/nodes/lib/dynconfig.js +81 -0
  30. package/nodes/lib/hadiscovery.js +846 -0
  31. package/nodes/lib/haroles.js +193 -0
  32. package/nodes/lib/localccu.js +81 -0
  33. package/nodes/lib/message.js +6 -2
  34. package/nodes/lib/valuestatus.js +145 -0
  35. package/nodes/locales/de/ccu-display.html +17 -0
  36. package/nodes/locales/de/ccu-rpc-event.json +1 -0
  37. package/nodes/locales/de/ccu-sysvar.json +3 -2
  38. package/nodes/locales/de/ccu-value.json +2 -1
  39. package/nodes/locales/en-US/ccu-display.html +17 -0
  40. package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
  41. package/nodes/locales/en-US/ccu-switch.html +12 -0
  42. package/nodes/locales/en-US/ccu-sysvar.json +3 -2
  43. package/nodes/locales/en-US/ccu-value.json +2 -1
  44. package/package.json +2 -1
  45. package/patch-certainonly.py +51 -0
  46. package/roadmap-archive/B-16.md +31 -0
  47. package/roadmap-archive/B-2.md +69 -0
  48. package/roadmap-archive/B-3.md +57 -0
  49. package/roadmap-archive/B-4.md +44 -0
  50. package/roadmap-archive/B-5.md +57 -0
  51. package/roadmap-archive/README.md +11 -0
@@ -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, configCacheKey} = require(path.join(__dirname, '/lib/dynconfig.js'));
5
+ const {channelMatches, datapointsOf, CONFIG_KEYS} = require(path.join(__dirname, '/lib/channelfilter.js'));
6
+
7
+ /** Configuration keys an incoming message may supply (B-2). */
8
+ const DYNAMIC_KEYS = [...CONFIG_KEYS, 'force'];
4
9
 
5
10
  module.exports = function (RED) {
6
11
  class CcuSetValue {
@@ -15,36 +20,31 @@ module.exports = function (RED) {
15
20
 
16
21
  this.ccu.register(this);
17
22
 
18
- this.config = {
19
- iface: config.iface,
20
- rooms: config.rooms,
21
- functions: config.functions,
22
- device: config.device,
23
- deviceType: config.deviceType,
24
- deviceName: config.deviceName,
25
- channel: config.channel,
26
- channelType: config.channelType,
27
- channelIndex: config.channelIndex,
28
- channelName: config.channelName,
29
- datapoint: config.datapoint,
30
- roomsRx: config.roomsRx,
31
- functionsRx: config.functionsRx,
32
- deviceRx: config.deviceRx,
33
- deviceTypeRx: config.deviceTypeRx,
34
- deviceNameRx: config.deviceNameRx,
35
- channelRx: config.channelRx,
36
- channelTypeRx: config.channelTypeRx,
37
- channelIndexRx: config.channelIndexRx,
38
- channelNameRx: config.channelNameRx,
39
- datapointRx: config.datapointRx,
40
- force: config.force,
41
- };
23
+ this.config = {};
24
+ DYNAMIC_KEYS.forEach((key) => {
25
+ this.config[key] = config[key];
26
+ });
42
27
 
28
+ // Channels already known to (not) match the filter currently in
29
+ // effect. Keyed by that filter, so a message that changes it gets
30
+ // a fresh evaluation instead of inheriting the previous one (#133).
31
+ this.filterKey = null;
43
32
  this.blacklist = new Set();
44
33
  this.whitelist = new Set();
45
34
 
46
- this.on('input', (message) => {
47
- this.setValues(message);
35
+ this.on('input', (message, send, done) => {
36
+ try {
37
+ this.setValues(message);
38
+ if (done) {
39
+ done();
40
+ }
41
+ } catch (error) {
42
+ if (done) {
43
+ done(error);
44
+ } else {
45
+ this.error(error.message, message);
46
+ }
47
+ }
48
48
  });
49
49
 
50
50
  this.on('close', this._destructor);
@@ -63,22 +63,23 @@ module.exports = function (RED) {
63
63
  statusHelper(this, data);
64
64
  }
65
65
 
66
- setValues(message) {
67
- const {config} = this;
68
- let dynamicConfig = false;
69
- Object.keys(config).forEach((key) => {
70
- if (!config[key]) {
71
- if (key in message) {
72
- dynamicConfig = true;
73
- config[key] = message[key];
74
- }
75
- }
76
- });
77
-
78
- if (dynamicConfig) {
79
- this.whitelist.clear();
80
- this.blacklist.clear();
66
+ /**
67
+ * The match caches are only valid for the filter they were built
68
+ * from — reset them whenever the effective filter changes.
69
+ * @param {object} config effective configuration for this message
70
+ */
71
+ useFilter(config) {
72
+ const key = configCacheKey(config, CONFIG_KEYS);
73
+ if (key !== this.filterKey) {
74
+ this.filterKey = key;
75
+ this.blacklist = new Set();
76
+ this.whitelist = new Set();
81
77
  }
78
+ }
79
+
80
+ setValues(message) {
81
+ const {config} = effectiveConfig(this.config, message, DYNAMIC_KEYS, {flat: true});
82
+ this.useFilter(config);
82
83
 
83
84
  let count = 0;
84
85
  Object.keys(this.ccu.metadata.devices).forEach((iface) => {
@@ -91,206 +92,29 @@ module.exports = function (RED) {
91
92
  return;
92
93
  }
93
94
 
94
- const channel = this.ccu.metadata.devices[iface][address];
95
-
96
- if (!channel.PARENT) {
97
- this.blacklist.add(address);
98
- return;
99
- }
100
-
101
95
  if (!this.whitelist.has(address)) {
102
- const device = this.ccu.metadata.devices[iface][channel.PARENT];
103
- if (config.device) {
104
- if (config.deviceRx === 'str' && config.device !== channel.PARENT) {
105
- this.blacklist.add(address);
106
- return;
107
- }
108
-
109
- if (config.deviceRx === 're' && !channel.PARENT.match(new RegExp(config.device))) {
110
- this.blacklist.add(address);
111
- return;
112
- }
113
- }
114
-
115
- if (config.deviceType) {
116
- if (config.deviceTypeRx === 'str' && config.deviceType !== device.TYPE) {
117
- this.blacklist.add(address);
118
- return;
119
- }
120
-
121
- if (config.deviceTypeRx === 're' && !device.TYPE.match(new RegExp(config.deviceType))) {
122
- this.blacklist.add(address);
123
- return;
124
- }
125
- }
126
-
127
- if (config.deviceName) {
128
- if (!this.ccu.channelNames[address]) {
129
- this.blacklist.add(address);
130
- return;
131
- }
132
-
133
- if (
134
- config.deviceNameRx === 'str' &&
135
- this.ccu.channelNames[channel.PARENT] !== config.deviceName
136
- ) {
137
- this.blacklist.add(address);
138
- return;
139
- }
140
-
141
- if (
142
- config.deviceNameRx === 're' &&
143
- !this.ccu.channelNames[channel.PARENT].match(new RegExp(config.deviceName))
144
- ) {
145
- this.blacklist.add(address);
146
- return;
147
- }
148
- }
149
-
150
- if (config.channel) {
151
- if (config.channelRx === 'str' && config.channel !== address) {
152
- this.blacklist.add(address);
153
- return;
154
- }
155
-
156
- if (config.channelRx === 're' && !address.match(new RegExp(config.channel))) {
157
- this.blacklist.add(address);
158
- return;
159
- }
160
- }
161
-
162
- if (config.channelType) {
163
- if (config.channelTypeRx === 'str' && config.channelType !== channel.TYPE) {
164
- this.blacklist.add(address);
165
- return;
166
- }
167
-
168
- if (config.channelTypeRx === 're' && !channel.TYPE.match(new RegExp(config.channelType))) {
169
- this.blacklist.add(address);
170
- return;
171
- }
172
- }
173
-
174
- if (config.channelIndex) {
175
- if (config.channelIndexRx === 'str' && !address.endsWith(':' + config.channelIndex)) {
176
- this.blacklist.add(address);
177
- return;
178
- }
179
-
180
- if (
181
- config.channelIndexRx === 're' &&
182
- !address.split(':')[1].match(new RegExp(String(config.channelIndex)))
183
- ) {
184
- this.blacklist.add(address);
185
- return;
186
- }
187
- }
188
-
189
- if (config.channelName) {
190
- if (!this.ccu.channelNames[address]) {
191
- this.blacklist.add(address);
192
- return;
193
- }
194
-
195
- if (
196
- config.channelNameRx === 'str' &&
197
- this.ccu.channelNames[address] !== config.channelName
198
- ) {
199
- this.blacklist.add(address);
200
- return;
201
- }
202
-
203
- if (
204
- config.channelNameRx === 're' &&
205
- !this.ccu.channelNames[address].match(new RegExp(config.channelName))
206
- ) {
207
- this.blacklist.add(address);
208
- return;
209
- }
210
- }
211
-
212
- if (config.rooms) {
213
- if (!this.ccu.channelRooms[address]) {
214
- this.blacklist.add(address);
215
- return;
216
- }
217
-
218
- if (config.roomsRx === 'str' && !this.ccu.channelRooms[address].includes(config.rooms)) {
219
- this.blacklist.add(address);
220
- return;
221
- }
222
-
223
- if (config.roomsRx === 're') {
224
- let match = false;
225
- this.ccu.channelRooms[address].forEach((room) => {
226
- if (room.match(new RegExp(config.rooms))) {
227
- match = true;
228
- }
229
- });
230
- if (!match) {
231
- this.blacklist.add(address);
232
- return;
233
- }
234
- }
235
- }
236
-
237
- if (config.functions) {
238
- if (!this.ccu.channelFunctions[address]) {
239
- this.blacklist.add(address);
240
- return;
241
- }
242
-
243
- if (
244
- config.functionsRx === 'str' &&
245
- !this.ccu.channelFunctions[address].includes(config.functions)
246
- ) {
247
- this.blacklist.add(address);
248
- return;
249
- }
250
-
251
- if (config.functionsRx === 're') {
252
- let match = false;
253
- this.ccu.channelFunctions[address].forEach((func) => {
254
- if (func.match(new RegExp(config.functions))) {
255
- match = true;
256
- }
257
- });
258
- if (!match) {
259
- this.blacklist.add(address);
260
- return;
261
- }
262
- }
96
+ if (!channelMatches(this.ccu, iface, address, config)) {
97
+ this.blacklist.add(address);
98
+ return;
263
99
  }
264
100
 
265
101
  this.whitelist.add(address);
266
102
  }
267
103
 
268
- const psKey = this.ccu.paramsetName(iface, channel, 'VALUES');
269
- if (this.ccu.paramsetDescriptions[psKey]) {
270
- const rx = new RegExp(config.datapoint);
271
- Object.keys(this.ccu.paramsetDescriptions[psKey]).forEach((dp) => {
272
- if (config.datapointRx === 'str' && dp !== config.datapoint) {
273
- return;
274
- }
275
-
276
- if (config.datapointRx === 're' && !dp.match(rx)) {
277
- return;
278
- }
279
-
280
- const datapointName = iface + '.' + address + '.' + dp;
281
- const currentValue = this.ccu.values[datapointName] && this.ccu.values[datapointName].value;
282
- count += 1;
283
- if (
284
- dp.startsWith('PRESS_') ||
285
- typeof currentValue === 'undefined' ||
286
- currentValue !== message.payload
287
- ) {
288
- this.ccu
289
- .setValueQueued(iface, address, dp, message.payload, false, config.force)
290
- .catch(() => {});
291
- }
292
- });
293
- }
104
+ datapointsOf(this.ccu, iface, address, config).forEach((dp) => {
105
+ const datapointName = iface + '.' + address + '.' + dp;
106
+ const currentValue = this.ccu.values[datapointName] && this.ccu.values[datapointName].value;
107
+ count += 1;
108
+ if (
109
+ dp.startsWith('PRESS_') ||
110
+ typeof currentValue === 'undefined' ||
111
+ currentValue !== message.payload
112
+ ) {
113
+ this.ccu
114
+ .setValueQueued(iface, address, dp, message.payload, false, config.force)
115
+ .catch(() => {});
116
+ }
117
+ });
294
118
  });
295
119
  });
296
120
  this.status({fill: 'green', shape: 'ring', text: String(count) + ' datapoints'});
@@ -268,7 +268,9 @@
268
268
 
269
269
  $nodeInputChannel.autocomplete('option', 'source', arr);
270
270
 
271
- if (!data[$nodeInputChannel.val().split(' ')[0]]) {
271
+ // ${ENV} placeholders are substituted by Node-RED before the node is
272
+ // constructed (subflow parameters, #158) - do not clear them
273
+ if (!/^\$\{[^}]+\}$/.test($nodeInputChannel.val()) && !data[$nodeInputChannel.val().split(' ')[0]]) {
272
274
  $nodeInputChannel.val('');
273
275
  }
274
276
  }
@@ -645,8 +647,60 @@
645
647
 
646
648
  <script type="text/x-red" data-help-name="ccu-signal" lang="de-DE">
647
649
  <p>Senden von LED- und Tonsignalen an Displaytaster und Funk-Gongs.</p>
650
+
651
+ <h3>Dynamische Konfiguration</h3>
652
+ <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>
653
+ <ul>
654
+ <li><code>iface</code></li>
655
+ <li><code>channel</code></li>
656
+ <li><code>channelType</code></li>
657
+ <li><code>chime</code></li>
658
+ <li><code>led</code></li>
659
+ <li><code>signal</code></li>
660
+ <li><code>volume</code></li>
661
+ <li><code>repeat</code></li>
662
+ <li><code>repetitions</code></li>
663
+ <li><code>durationUnit</code></li>
664
+ <li><code>durationValue</code></li>
665
+ <li><code>rampTimeUnit</code></li>
666
+ <li><code>rampTimeValue</code></li>
667
+ <li><code>acousticAlarmSelection</code></li>
668
+ <li><code>opticalAlarmSelection</code></li>
669
+ <li><code>dimmerLevel</code></li>
670
+ <li><code>dimmerColor</code></li>
671
+ <li><code>dimmerList</code></li>
672
+ <li><code>soundLevel</code></li>
673
+ <li><code>soundList</code></li>
674
+ </ul>
675
+ <p><code>soundList</code> und <code>dimmerList</code> akzeptieren auch ein einfaches Array von Werten, z.B. <code>msg.config.soundList = [1, 2]</code>.</p>
648
676
  </script>
649
677
 
650
678
  <script type="text/x-red" data-help-name="ccu-signal" lang="en-US">
651
679
  <p>Send LED- and Sound-Signals.</p>
680
+
681
+ <h3>Dynamic configuration</h3>
682
+ <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>
683
+ <ul>
684
+ <li><code>iface</code></li>
685
+ <li><code>channel</code></li>
686
+ <li><code>channelType</code></li>
687
+ <li><code>chime</code></li>
688
+ <li><code>led</code></li>
689
+ <li><code>signal</code></li>
690
+ <li><code>volume</code></li>
691
+ <li><code>repeat</code></li>
692
+ <li><code>repetitions</code></li>
693
+ <li><code>durationUnit</code></li>
694
+ <li><code>durationValue</code></li>
695
+ <li><code>rampTimeUnit</code></li>
696
+ <li><code>rampTimeValue</code></li>
697
+ <li><code>acousticAlarmSelection</code></li>
698
+ <li><code>opticalAlarmSelection</code></li>
699
+ <li><code>dimmerLevel</code></li>
700
+ <li><code>dimmerColor</code></li>
701
+ <li><code>dimmerList</code></li>
702
+ <li><code>soundLevel</code></li>
703
+ <li><code>soundList</code></li>
704
+ </ul>
705
+ <p><code>soundList</code> and <code>dimmerList</code> also accept a plain array of values, e.g. <code>msg.config.soundList = [1, 2]</code>.</p>
652
706
  </script>
@@ -1,6 +1,46 @@
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
+
6
+ /** Fields that already carry a typed input (`<name>Type`) in the editor. */
7
+ const TYPED_VALUES = ['rampTimeValue', 'durationValue', 'repeat', 'volume', 'soundLevel'];
8
+
9
+ /** Everything a message may override through `msg.config` (B-2, #80 #148 #185). */
10
+ const DYNAMIC_KEYS = [
11
+ 'iface',
12
+ 'channel',
13
+ 'channelType',
14
+ 'chime',
15
+ 'led',
16
+ 'signal',
17
+ 'acousticAlarmSelection',
18
+ 'opticalAlarmSelection',
19
+ 'durationUnit',
20
+ 'rampTimeUnit',
21
+ 'repetitions',
22
+ 'dimmerLevel',
23
+ 'dimmerColor',
24
+ 'dimmerList',
25
+ 'soundList',
26
+ ...TYPED_VALUES,
27
+ ];
28
+
29
+ /**
30
+ * Accept both the editor's list shape and a plain array of values, so a
31
+ * flow can send `msg.config.soundList = [1, 2]` instead of building
32
+ * `[{sound: 1}, {sound: 2}]` by hand (#148).
33
+ * @param {*} list
34
+ * @param {string} key 'sound' or 'color'
35
+ * @returns {object[]}
36
+ */
37
+ function normalizeList(list, key) {
38
+ if (!Array.isArray(list)) {
39
+ return [];
40
+ }
41
+
42
+ return list.map((item) => (item !== null && typeof item === 'object' ? item : {[key]: item}));
43
+ }
4
44
 
5
45
  module.exports = function (RED) {
6
46
  class CcuSignal {
@@ -33,7 +73,10 @@ module.exports = function (RED) {
33
73
  this.error(error.message);
34
74
  })
35
75
  .then(() => {
36
- this.sendCommand({...this.config, ...values})
76
+ // msg.config wins over both the stored config and the
77
+ // typed inputs above
78
+ const {config: effective} = effectiveConfig({...this.config, ...values}, message, DYNAMIC_KEYS);
79
+ this.sendCommand(effective)
37
80
  .then(() => {
38
81
  done();
39
82
  })
@@ -51,12 +94,10 @@ module.exports = function (RED) {
51
94
 
52
95
  switch (config.channelType) {
53
96
  case 'SIGNAL_CHIME':
54
- console.log('config.chime', config.chime);
55
- payload = [config.volume / 100, config.repeat, 108000, ...config.chime.split(',')];
56
- console.log('payload', payload);
97
+ payload = [config.volume / 100, config.repeat, 108000, ...String(config.chime).split(',')];
57
98
  return this.ccu.setValue(config.iface, config.channel, 'SUBMIT', payload);
58
99
  case 'SIGNAL_LED':
59
- payload = ['1', config.repeat, 108000, ...config.led.split(',')];
100
+ payload = ['1', config.repeat, 108000, ...String(config.led).split(',')];
60
101
  return this.ccu.setValue(config.iface, config.channel, 'SUBMIT', payload);
61
102
  case 'ALARM_SWITCH_VIRTUAL_RECEIVER':
62
103
  return this.ccu.methodCall(config.iface, 'putParamset', [
@@ -70,6 +111,7 @@ module.exports = function (RED) {
70
111
  },
71
112
  ]);
72
113
  case 'DIMMER_VIRTUAL_RECEIVER': {
114
+ const dimmerList = normalizeList(config.dimmerList, 'color');
73
115
  const parameters = {
74
116
  LEVEL: config.dimmerLevel / 100,
75
117
  RAMP_TIME_UNIT: config.rampTimeUnit,
@@ -77,9 +119,9 @@ module.exports = function (RED) {
77
119
  DURATION_UNIT: config.durationUnit,
78
120
  DURATION_VALUE: Number.parseInt(config.durationValue, 10) || 0,
79
121
  REPETITIONS: Number(config.repetitions),
80
- OUTPUT_SELECT_SIZE: config.dimmerList.length,
122
+ OUTPUT_SELECT_SIZE: dimmerList.length,
81
123
  };
82
- config.dimmerList.forEach((item, i) => {
124
+ dimmerList.forEach((item, i) => {
83
125
  const index = i + 1;
84
126
  parameters['COLOR_LIST_' + index] = Number(item.color);
85
127
  parameters['ON_TIME_LIST_' + index] = Number(item.ontime);
@@ -103,6 +145,7 @@ module.exports = function (RED) {
103
145
  }
104
146
 
105
147
  case 'ACOUSTIC_SIGNAL_VIRTUAL_RECEIVER': {
148
+ const soundList = normalizeList(config.soundList, 'sound');
106
149
  const parameters = {
107
150
  LEVEL: config.soundLevel / 100,
108
151
  RAMP_TIME_UNIT: config.rampTimeUnit,
@@ -110,9 +153,9 @@ module.exports = function (RED) {
110
153
  DURATION_UNIT: config.durationUnit,
111
154
  DURATION_VALUE: Number.parseInt(config.durationValue, 10) || 0,
112
155
  REPETITIONS: Number(config.repetitions),
113
- OUTPUT_SELECT_SIZE: config.soundList.length,
156
+ OUTPUT_SELECT_SIZE: soundList.length,
114
157
  };
115
- config.soundList.forEach((item, i) => {
158
+ soundList.forEach((item, i) => {
116
159
  const index = i + 1;
117
160
  parameters['SOUNDFILE_LIST_' + index] = Number(item.sound);
118
161
  });
@@ -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
  }