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
@@ -29,6 +29,7 @@
29
29
  change: {value: false},
30
30
  working: {value: false},
31
31
  cache: {value: false},
32
+ certainOnly: {value: false},
32
33
  topic: {value: '${CCU}/${Interface}/${channelName}/${datapoint}'}
33
34
  },
34
35
  inputs: 0,
@@ -596,6 +597,11 @@
596
597
  <input type="checkbox" id="node-input-cache">
597
598
  <span data-i18n="ccu-rpc-event.cache"></span>
598
599
  </label>
600
+
601
+ <label class="ccu-checkbox">
602
+ <input type="checkbox" id="node-input-certainOnly">
603
+ <span data-i18n="ccu-rpc-event.certainOnly"></span>
604
+ </label>
599
605
  </div>
600
606
  </div>
601
607
  <div class="form-row">
@@ -1,6 +1,20 @@
1
1
  const path = require('path');
2
2
 
3
3
  const statusHelper = require(path.join(__dirname, '/lib/status.js'));
4
+ const {valueStatus} = require(path.join(__dirname, '/lib/valuestatus.js'));
5
+
6
+ const FILTER_ATTRIBUTES = [
7
+ 'rooms',
8
+ 'functions',
9
+ 'device',
10
+ 'deviceName',
11
+ 'deviceType',
12
+ 'channel',
13
+ 'channelName',
14
+ 'channelType',
15
+ 'channelIndex',
16
+ 'datapoint',
17
+ ];
4
18
 
5
19
  module.exports = function (RED) {
6
20
  class CcuRpcEventNode {
@@ -21,20 +35,15 @@ module.exports = function (RED) {
21
35
  iface: config.iface,
22
36
  };
23
37
 
38
+ // opt-in: only present when the option is enabled, so existing
39
+ // subscriptions keep receiving uncertain values (#96)
40
+ if (config.certainOnly) {
41
+ filter.uncertain = false;
42
+ }
43
+
24
44
  this.ccu.register(this);
25
45
 
26
- [
27
- 'rooms',
28
- 'functions',
29
- 'device',
30
- 'deviceName',
31
- 'deviceType',
32
- 'channel',
33
- 'channelName',
34
- 'channelType',
35
- 'channelIndex',
36
- 'datapoint',
37
- ].forEach((attr) => {
46
+ FILTER_ATTRIBUTES.forEach((attr) => {
38
47
  if (!config[attr]) {
39
48
  return;
40
49
  }
@@ -45,8 +54,14 @@ module.exports = function (RED) {
45
54
  filter[attr] = config[attr];
46
55
  }
47
56
  });
57
+
58
+ // The node used to show nothing at all; show what came through
59
+ // last and how long ago (#52)
60
+ this.valueStatus = valueStatus(this);
61
+
48
62
  this.idSubscription = this.ccu.subscribe(filter, (message) => {
49
63
  message.topic = this.ccu.topicReplace(config.topic, message);
64
+ this.valueStatus.set(message.topic + ' = ' + message.payload, message.ts);
50
65
  this.send(message);
51
66
  });
52
67
  this.on('close', this._destructor);
@@ -54,6 +69,10 @@ module.exports = function (RED) {
54
69
 
55
70
  _destructor(done) {
56
71
  this.debug('ccu-rpc-event close');
72
+ if (this.valueStatus) {
73
+ this.valueStatus.stop();
74
+ }
75
+
57
76
  this.ccu.unsubscribe(this.idSubscription);
58
77
  this.ccu.deregister(this);
59
78
  done();
@@ -624,8 +624,46 @@
624
624
 
625
625
  <h3>Input</h3>
626
626
 
627
+
628
+ <h3>Dynamische Konfiguration</h3>
629
+ <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>
630
+ <ul>
631
+ <li><code>iface</code></li>
632
+ <li><code>rooms</code></li>
633
+ <li><code>functions</code></li>
634
+ <li><code>device</code></li>
635
+ <li><code>deviceType</code></li>
636
+ <li><code>deviceName</code></li>
637
+ <li><code>channel</code></li>
638
+ <li><code>channelType</code></li>
639
+ <li><code>channelIndex</code></li>
640
+ <li><code>channelName</code></li>
641
+ <li><code>datapoint</code></li>
642
+ <li><code>force</code></li>
643
+ <li><code>sowie die zugehörigen <code>...Rx</code>-Selektoren</code></li>
644
+ </ul>
645
+ <p>Zusätzlich füllen gleichnamige Eigenschaften der Nachricht selbst (<code>msg.rooms</code> usw.) leer gelassene Felder — so war es schon immer.</p>
627
646
  </script>
628
647
 
629
648
  <script type="text/x-red" data-help-name="ccu-set-value" lang="en-US">
630
649
  <p>Control multiple devices at once.</p>
650
+
651
+ <h3>Dynamic configuration</h3>
652
+ <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>
653
+ <ul>
654
+ <li><code>iface</code></li>
655
+ <li><code>rooms</code></li>
656
+ <li><code>functions</code></li>
657
+ <li><code>device</code></li>
658
+ <li><code>deviceType</code></li>
659
+ <li><code>deviceName</code></li>
660
+ <li><code>channel</code></li>
661
+ <li><code>channelType</code></li>
662
+ <li><code>channelIndex</code></li>
663
+ <li><code>channelName</code></li>
664
+ <li><code>datapoint</code></li>
665
+ <li><code>force</code></li>
666
+ <li><code>and the matching <code>...Rx</code> selectors</code></li>
667
+ </ul>
668
+ <p>Properties of the message itself (<code>msg.rooms</code> and so on) also fill in fields left empty in the dialog, as they always did.</p>
631
669
  </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, 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
  });