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
@@ -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};
@@ -0,0 +1,136 @@
1
+ /* Channel matching for the filter form shared by ccu-set-value (and, from
2
+ 4.3.0, reusable by anything else that resolves a filter to channels).
3
+
4
+ Extracted from ccu-set-value.js as part of B-2 so the matching is pure
5
+ and unit-testable, and so the per-message effective config (see
6
+ dynconfig.js) can be applied without the node caching the wrong thing.
7
+
8
+ Each criterion has a companion `<name>Rx` selector: 'str' compares
9
+ literally, 're' treats the configured value as a regular expression. */
10
+
11
+ /** The filter criteria, in the order they are evaluated. */
12
+ const FILTER_KEYS = [
13
+ 'device',
14
+ 'deviceType',
15
+ 'deviceName',
16
+ 'channel',
17
+ 'channelType',
18
+ 'channelIndex',
19
+ 'channelName',
20
+ 'rooms',
21
+ 'functions',
22
+ ];
23
+
24
+ /** All configuration keys that make up a filter, including the Rx selectors. */
25
+ const CONFIG_KEYS = ['iface', ...FILTER_KEYS, 'datapoint', ...FILTER_KEYS.map((key) => key + 'Rx'), 'datapointRx'];
26
+
27
+ function matches(value, pattern, rx) {
28
+ if (typeof value !== 'string') {
29
+ return false;
30
+ }
31
+
32
+ return rx === 're' ? Boolean(value.match(new RegExp(pattern))) : value === pattern;
33
+ }
34
+
35
+ function matchesList(list, pattern, rx) {
36
+ if (!Array.isArray(list)) {
37
+ return false;
38
+ }
39
+
40
+ return rx === 're' ? list.some((item) => matches(item, pattern, 're')) : list.includes(pattern);
41
+ }
42
+
43
+ /**
44
+ * Does one channel address pass the configured filter?
45
+ *
46
+ * @param {object} ccu connection-shaped dependency: metadata.devices,
47
+ * channelNames, channelRooms, channelFunctions
48
+ * @param {string} iface
49
+ * @param {string} address channel address, e.g. `ABC1234567:1`
50
+ * @param {object} config effective configuration (see dynconfig.js)
51
+ * @returns {boolean} true when the channel is selected by the filter
52
+ */
53
+ function channelMatches(ccu, iface, address, config) {
54
+ const channel = ccu.metadata.devices[iface] && ccu.metadata.devices[iface][address];
55
+
56
+ // Device-level entries (no PARENT) are never targets, only their channels
57
+ if (!channel || !channel.PARENT) {
58
+ return false;
59
+ }
60
+
61
+ const device = ccu.metadata.devices[iface][channel.PARENT] || {};
62
+
63
+ if (config.device && !matches(channel.PARENT, config.device, config.deviceRx)) {
64
+ return false;
65
+ }
66
+
67
+ if (config.deviceType && !matches(device.TYPE, config.deviceType, config.deviceTypeRx)) {
68
+ return false;
69
+ }
70
+
71
+ // Note: this used to test whether the *channel* had a ReGa name while
72
+ // comparing the *device* name, so channels without a name of their own
73
+ // were dropped even when their device matched (fixed in 4.3.0).
74
+ if (config.deviceName && !matches(ccu.channelNames[channel.PARENT], config.deviceName, config.deviceNameRx)) {
75
+ return false;
76
+ }
77
+
78
+ if (config.channel && !matches(address, config.channel, config.channelRx)) {
79
+ return false;
80
+ }
81
+
82
+ if (config.channelType && !matches(channel.TYPE, config.channelType, config.channelTypeRx)) {
83
+ return false;
84
+ }
85
+
86
+ if (config.channelIndex) {
87
+ const index = address.split(':')[1];
88
+ if (config.channelIndexRx === 're') {
89
+ if (!matches(index, String(config.channelIndex), 're')) {
90
+ return false;
91
+ }
92
+ } else if (!address.endsWith(':' + config.channelIndex)) {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ if (config.channelName && !matches(ccu.channelNames[address], config.channelName, config.channelNameRx)) {
98
+ return false;
99
+ }
100
+
101
+ if (config.rooms && !matchesList(ccu.channelRooms[address], config.rooms, config.roomsRx)) {
102
+ return false;
103
+ }
104
+
105
+ if (config.functions && !matchesList(ccu.channelFunctions[address], config.functions, config.functionsRx)) {
106
+ return false;
107
+ }
108
+
109
+ return true;
110
+ }
111
+
112
+ /**
113
+ * The datapoints of one channel that pass the datapoint filter.
114
+ * @param {object} ccu connection-shaped dependency: paramsetName, paramsetDescriptions
115
+ * @param {string} iface
116
+ * @param {string} address
117
+ * @param {object} config effective configuration
118
+ * @returns {string[]} datapoint names (empty when the channel has no VALUES description)
119
+ */
120
+ function datapointsOf(ccu, iface, address, config) {
121
+ const channel = ccu.metadata.devices[iface] && ccu.metadata.devices[iface][address];
122
+ const psKey = channel && ccu.paramsetName(iface, channel, 'VALUES');
123
+ const description = psKey && ccu.paramsetDescriptions[psKey];
124
+ if (!description) {
125
+ return [];
126
+ }
127
+
128
+ // No special case for an empty datapoint filter: with str it selects
129
+ // nothing (a literal comparison against undefined) and with re it
130
+ // selects everything, exactly as before the extraction. Silently
131
+ // widening str to "all datapoints" would write the payload to every
132
+ // datapoint of every matched channel.
133
+ return Object.keys(description).filter((dp) => matches(dp, config.datapoint, config.datapointRx));
134
+ }
135
+
136
+ module.exports = {channelMatches, datapointsOf, FILTER_KEYS, CONFIG_KEYS};