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
@@ -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};
@@ -0,0 +1,81 @@
1
+ /* Per-message dynamic node configuration (B-2, issues #172 #71 #103 #80
2
+ #56 #148 #133).
3
+
4
+ Two mechanisms, in increasing precedence:
5
+
6
+ 1. Flat properties (`msg.channel`, `msg.rooms`, ...). Long-standing
7
+ behaviour of ccu-set-value/ccu-value/ccu-get-value: they fill in
8
+ configuration the node itself leaves empty. Kept as-is because
9
+ messages coming straight from an event node carry exactly these
10
+ names, which is what makes it convenient - and, for the same
11
+ reason, only ever used for the nodes that already did it.
12
+ 2. `msg.config`, an object of overrides. Always wins, works for every
13
+ configurable field including lists, and cannot collide with the
14
+ properties an event message carries (`msg.ccu` is the CCU host, so
15
+ it is not available as a namespace). This is what makes the
16
+ settings of the signal/display/switch/sysvar nodes reachable from
17
+ a flow.
18
+
19
+ The old inline implementation in ccu-set-value wrote message
20
+ properties into the node's own `config` object, so only the *first*
21
+ message ever configured the node: every later message hit the
22
+ `if (!config[key])` guard and was ignored (#133). Nothing here mutates
23
+ the stored configuration. */
24
+
25
+ /**
26
+ * Build the effective configuration for one message.
27
+ *
28
+ * @param {object} config the node's stored configuration (never mutated)
29
+ * @param {object} message the incoming message
30
+ * @param {string[]} keys the keys that may be overridden; anything else
31
+ * in `msg.config` is ignored, so a stray property cannot reach
32
+ * node internals
33
+ * @param {object} [options]
34
+ * @param {boolean} [options.flat=false] also fill empty keys from
35
+ * top-level message properties (mechanism 1)
36
+ * @returns {{config: object, dynamic: boolean}} effective config plus
37
+ * whether anything actually came from the message
38
+ */
39
+ function effectiveConfig(config, message, keys, options) {
40
+ const effective = {...config};
41
+ let dynamic = false;
42
+
43
+ if (!message || typeof message !== 'object') {
44
+ return {config: effective, dynamic};
45
+ }
46
+
47
+ if (options && options.flat) {
48
+ keys.forEach((key) => {
49
+ if (!effective[key] && key in message) {
50
+ effective[key] = message[key];
51
+ dynamic = true;
52
+ }
53
+ });
54
+ }
55
+
56
+ const overrides = message.config;
57
+ if (overrides && typeof overrides === 'object' && !Array.isArray(overrides)) {
58
+ keys.forEach((key) => {
59
+ if (key in overrides) {
60
+ effective[key] = overrides[key];
61
+ dynamic = true;
62
+ }
63
+ });
64
+ }
65
+
66
+ return {config: effective, dynamic};
67
+ }
68
+
69
+ /**
70
+ * A stable identity for a set of configuration keys, used to decide
71
+ * whether a cache derived from them is still valid. Two effective configs
72
+ * that produce the same key select the same devices/datapoints.
73
+ * @param {object} config
74
+ * @param {string[]} keys
75
+ * @returns {string}
76
+ */
77
+ function configCacheKey(config, keys) {
78
+ return keys.map((key) => JSON.stringify(config[key] === undefined ? null : config[key])).join(' ');
79
+ }
80
+
81
+ module.exports = {effectiveConfig, configCacheKey};
@@ -0,0 +1,81 @@
1
+ /* Detecting "Node-RED runs on the CCU itself" (B-4).
2
+
3
+ When it does, the interface processes can be reached directly on their
4
+ 3xxxx ports (BINRPC where available) instead of going through the CCU's
5
+ lighttpd proxy on 1999/2000/2001/2010/9292 - fewer hops, and BINRPC
6
+ instead of XML-RPC for BidCos.
7
+
8
+ The old test grepped /etc/lighttpd/conf.d/proxy.conf for
9
+ `"port" => 32001`. On current firmware that file is a one-line
10
+ `include "/var/etc/lighttpd_proxy.conf"`, which itself only includes
11
+ proxy_normal.conf - the port markers are two levels down, so the regex
12
+ has not matched since (verified on CCU3 firmware 3.89.8, where 31999,
13
+ 32001, 32010 and 39292 are all listening while proxy.conf says nothing
14
+ about them). Every local install has silently been running through the
15
+ proxy.
16
+
17
+ Rather than chasing lighttpd's include chain, ask the kernel: a
18
+ listener on the direct port is exactly the condition that makes using
19
+ it correct. /proc/net/tcp is a plain file, so this stays synchronous -
20
+ the interface table is built in the connection node's constructor. */
21
+
22
+ const fs = require('fs');
23
+
24
+ /** rfd's direct BINRPC port on firmware >= 3.41; the marker for "on the CCU". */
25
+ const LOCAL_PROBE_PORTS = [32001, 31999];
26
+
27
+ const PROC_NET_TCP = ['/proc/net/tcp', '/proc/net/tcp6'];
28
+
29
+ /** /proc/net/tcp connection state 0A = TCP_LISTEN. */
30
+ const TCP_LISTEN = '0A';
31
+
32
+ /**
33
+ * The TCP ports something is listening on, from /proc/net/tcp{,6}.
34
+ * @param {string[]} [files] override, for tests
35
+ * @returns {Set<number>} empty when /proc is unavailable (non-Linux)
36
+ */
37
+ function listeningPorts(files) {
38
+ const ports = new Set();
39
+
40
+ (files || PROC_NET_TCP).forEach((file) => {
41
+ let content;
42
+ try {
43
+ content = fs.readFileSync(file, 'utf8');
44
+ } catch {
45
+ return;
46
+ }
47
+
48
+ content.split('\n').forEach((line) => {
49
+ // sl local_address rem_address st ...
50
+ const columns = line.trim().split(/\s+/);
51
+ if (columns.length < 4 || columns[3] !== TCP_LISTEN) {
52
+ return;
53
+ }
54
+
55
+ const port = Number.parseInt(String(columns[1]).split(':')[1], 16);
56
+ if (Number.isInteger(port)) {
57
+ ports.add(port);
58
+ }
59
+ });
60
+ });
61
+
62
+ return ports;
63
+ }
64
+
65
+ /**
66
+ * Are we talking to a CCU we are running on, with the direct ports open?
67
+ * @param {string} host the configured CCU host
68
+ * @param {object} [options]
69
+ * @param {string[]} [options.files] /proc files to read, for tests
70
+ * @returns {boolean}
71
+ */
72
+ function isLocalCcu(host, options) {
73
+ if (!host || !(String(host).startsWith('127.') || host === 'localhost' || host === '::1')) {
74
+ return false;
75
+ }
76
+
77
+ const ports = listeningPorts(options && options.files);
78
+ return LOCAL_PROBE_PORTS.some((port) => ports.has(port));
79
+ }
80
+
81
+ module.exports = {isLocalCcu, listeningPorts, LOCAL_PROBE_PORTS};
@@ -18,6 +18,8 @@
18
18
  * @param {object} [additions] merged into the message (cache, working, uncertain, ts, lc, ...)
19
19
  * @returns {object} the message
20
20
  */
21
+ const {enumList} = require('./cast.js');
22
+
21
23
  function createMessage(ccu, iface, channel, datapoint, payload, additions) {
22
24
  const datapointName = iface + '.' + channel + '.' + datapoint;
23
25
  if (!ccu.values[datapointName]) {
@@ -73,12 +75,14 @@ function createMessage(ccu, iface, channel, datapoint, payload, additions) {
73
75
  datapointType: description.TYPE,
74
76
  datapointMin: description.MIN,
75
77
  datapointMax: description.MAX,
76
- datapointEnum: description.ENUM,
78
+ // the CCU sends the names in VALUE_LIST; reading description.ENUM
79
+ // meant both of these were always undefined (B-3, ROADMAP 8.2)
80
+ datapointEnum: enumList(description),
77
81
  datapointDefault: description.DEFAULT,
78
82
  datapointControl: description.CONTROL,
79
83
  value: payload,
80
84
  valuePrevious: ccu.values[datapointName].value,
81
- valueEnum: description.ENUM ? description.ENUM[Number(payload)] : undefined,
85
+ valueEnum: enumList(description) ? enumList(description)[Number(payload)] : undefined,
82
86
  valueStable,
83
87
  rooms: ccu.channelRooms[channel] || [],
84
88
  room:
@@ -0,0 +1,145 @@
1
+ /* Node status line showing the last known value and how old it is
2
+ (B-5, issues #54 and #52).
3
+
4
+ The data was always there - the connection keeps value, ts and lc for
5
+ every datapoint and system variable - the nodes just never showed it,
6
+ and the sysvar node only showed anything at all when "emit value on
7
+ start" was enabled. The age is re-rendered on a timer so the line does
8
+ not silently become stale between events. */
9
+
10
+ /** Node-RED status lines are narrow - keep them readable. */
11
+ const MAX_TEXT = 45;
12
+
13
+ const MINUTE = 60;
14
+ const HOUR = 60 * MINUTE;
15
+ const DAY = 24 * HOUR;
16
+
17
+ /** How often the age is re-rendered, matched to the resolution shown. */
18
+ function refreshInterval(seconds) {
19
+ if (seconds < MINUTE) {
20
+ return 5000;
21
+ }
22
+
23
+ if (seconds < HOUR) {
24
+ return 30000;
25
+ }
26
+
27
+ return 300000;
28
+ }
29
+
30
+ /**
31
+ * Compact age of a timestamp, in the style Node-RED status lines use.
32
+ * @param {number} ts epoch milliseconds
33
+ * @param {number} now epoch milliseconds
34
+ * @returns {string} e.g. '5s', '12m', '3h', '2d'
35
+ */
36
+ function relativeTime(ts, now) {
37
+ const seconds = Math.max(0, Math.round((now - ts) / 1000));
38
+ if (seconds < MINUTE) {
39
+ return seconds + 's';
40
+ }
41
+
42
+ if (seconds < HOUR) {
43
+ return Math.floor(seconds / MINUTE) + 'm';
44
+ }
45
+
46
+ if (seconds < DAY) {
47
+ return Math.floor(seconds / HOUR) + 'h';
48
+ }
49
+
50
+ return Math.floor(seconds / DAY) + 'd';
51
+ }
52
+
53
+ /**
54
+ * The status text for a value with a timestamp.
55
+ * @param {*} value
56
+ * @param {number} [ts] epoch milliseconds; omitted renders the value alone
57
+ * @param {number} now epoch milliseconds
58
+ * @returns {string}
59
+ */
60
+ function formatValueStatus(value, ts, now) {
61
+ let text = String(value);
62
+ if (text.length > MAX_TEXT) {
63
+ text = text.slice(0, MAX_TEXT - 1) + '…';
64
+ }
65
+
66
+ if (!ts) {
67
+ return text;
68
+ }
69
+
70
+ return text + ' (' + relativeTime(ts, now) + ')';
71
+ }
72
+
73
+ /**
74
+ * Keeps a node's status line showing the last value and its age.
75
+ *
76
+ * @param {object} node the Node-RED node (needs .status())
77
+ * @param {object} [options]
78
+ * @param {Function} [options.now] clock, for tests
79
+ * @returns {{set: Function, clear: Function, stop: Function}}
80
+ */
81
+ function valueStatus(node, options) {
82
+ const now = (options && options.now) || Date.now;
83
+ let timer = null;
84
+ let current = null;
85
+
86
+ function render() {
87
+ if (!current) {
88
+ return;
89
+ }
90
+
91
+ node.status({
92
+ fill: 'green',
93
+ shape: 'ring',
94
+ text: formatValueStatus(current.value, current.ts, now()),
95
+ });
96
+ }
97
+
98
+ function schedule() {
99
+ if (timer) {
100
+ clearInterval(timer);
101
+ timer = null;
102
+ }
103
+
104
+ if (!current || !current.ts) {
105
+ return;
106
+ }
107
+
108
+ const interval = refreshInterval(Math.round((now() - current.ts) / 1000));
109
+ timer = setInterval(() => {
110
+ render();
111
+ // the resolution coarsens as the value ages - re-arm accordingly
112
+ schedule();
113
+ }, interval);
114
+ if (typeof timer.unref === 'function') {
115
+ timer.unref();
116
+ }
117
+ }
118
+
119
+ return {
120
+ /**
121
+ * @param {*} value
122
+ * @param {number} [ts] epoch milliseconds
123
+ */
124
+ set(value, ts) {
125
+ current = {value, ts};
126
+ render();
127
+ schedule();
128
+ },
129
+ clear() {
130
+ current = null;
131
+ if (timer) {
132
+ clearInterval(timer);
133
+ timer = null;
134
+ }
135
+ },
136
+ stop() {
137
+ if (timer) {
138
+ clearInterval(timer);
139
+ timer = null;
140
+ }
141
+ },
142
+ };
143
+ }
144
+
145
+ module.exports = {valueStatus, formatValueStatus, relativeTime};
@@ -4,4 +4,21 @@
4
4
  als Nachricht übergeben werden diese
5
5
  anstelle der Konfiguration genutzt.</p>
6
6
  <p>Mit <code>msg.disbleX</code> kann explizit gesetzt werden, dass keine Daten für die Zeile übertragen werden.</p>
7
+
8
+ <h3>Dynamische Konfiguration</h3>
9
+ <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>
10
+ <ul>
11
+ <li><code>iface</code></li>
12
+ <li><code>channel</code></li>
13
+ <li><code>channelType</code></li>
14
+ <li><code>sound</code></li>
15
+ <li><code>repeat</code></li>
16
+ <li><code>pause</code></li>
17
+ <li><code>signal</code></li>
18
+ <li><code>line1 ... line6</code></li>
19
+ <li><code>color1 ... color6</code></li>
20
+ <li><code>icon1 ... icon6</code></li>
21
+ <li><code>disable1 ... disable6</code></li>
22
+ </ul>
23
+ <p><code>msg.line1</code> usw. funktionieren unverändert weiter.</p>
7
24
  </script>
@@ -4,5 +4,6 @@
4
4
  "change": "Nur geänderte Werte ausgeben",
5
5
  "cache": "Beim Start letzten bekannten Wert ausgeben",
6
6
  "working": "Während WORKING keine Werte ausgeben",
7
+ "certainOnly": "Unsichere Werte verwerfen"
7
8
  }
8
9
  }
@@ -2,6 +2,7 @@
2
2
  "ccu-sysvar": {
3
3
  "tip": "Wird keine Variable ausgewählt kann der Variablen-Name über msg.topic übergeben werden.",
4
4
  "change": "Nur geänderte Werte ausgeben",
5
- "cache": "Beim Start aktuellen Wert ausgeben"
5
+ "cache": "Beim Start aktuellen Wert ausgeben",
6
+ "property": "Wert aus"
6
7
  }
7
- }
8
+ }
@@ -8,6 +8,7 @@
8
8
  "change": "Nur geänderte Werte ausgeben",
9
9
  "cache": "Beim Start letzten bekannten Wert ausgeben",
10
10
  "working": "Während WORKING keine Werte ausgeben",
11
- "queue": "Queue"
11
+ "queue": "Queue",
12
+ "certainOnly": "Unsichere Werte verwerfen"
12
13
  }
13
14
  }
@@ -3,4 +3,21 @@
3
3
  <p>If <code>msg.lineX</code>, <code>msg.iconX</code> or <code>msg.colorX</code>
4
4
  are set in the message these are used instead of the configuration.</p>
5
5
  <p> With <code>msg.disbleX</code> you can explicitly set that no data is transferred for the line.</p>
6
+
7
+ <h3>Dynamic configuration</h3>
8
+ <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>
9
+ <ul>
10
+ <li><code>iface</code></li>
11
+ <li><code>channel</code></li>
12
+ <li><code>channelType</code></li>
13
+ <li><code>sound</code></li>
14
+ <li><code>repeat</code></li>
15
+ <li><code>pause</code></li>
16
+ <li><code>signal</code></li>
17
+ <li><code>line1 ... line6</code></li>
18
+ <li><code>color1 ... color6</code></li>
19
+ <li><code>icon1 ... icon6</code></li>
20
+ <li><code>disable1 ... disable6</code></li>
21
+ </ul>
22
+ <p><code>msg.line1</code> and friends keep working unchanged.</p>
6
23
  </script>
@@ -3,6 +3,7 @@
3
3
  "tip": "Tip: ",
4
4
  "change": "Emit only changed values",
5
5
  "cache": "Emit cached value on start",
6
- "working": "Don't emit values while actuator is in WORKING state"
6
+ "working": "Don't emit values while actuator is in WORKING state",
7
+ "certainOnly": "Discard uncertain values"
7
8
  }
8
- }
9
+ }
@@ -0,0 +1,12 @@
1
+ <script type="text/html" data-help-name="ccu-switch">
2
+ <h3>Dynamic configuration</h3>
3
+ <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>
4
+ <ul>
5
+ <li><code>iface</code></li>
6
+ <li><code>channel</code></li>
7
+ <li><code>datapoint</code></li>
8
+ <li><code>datapointProperty</code></li>
9
+ <li><code>sysvar</code></li>
10
+ <li><code>sysvarProperty</code></li>
11
+ </ul>
12
+ </script>
@@ -2,6 +2,7 @@
2
2
  "ccu-sysvar": {
3
3
  "tip": "Omit the variable name to supply it via msg.payload",
4
4
  "change": "Emit changed values only",
5
- "cache": "Emit value on start"
5
+ "cache": "Emit value on start",
6
+ "property": "Value from"
6
7
  }
7
- }
8
+ }
@@ -8,6 +8,7 @@
8
8
  "change": "Emit changed values only",
9
9
  "cache": "Emit cached value on start",
10
10
  "working": "Don't emit values while actuator is in WORKING state",
11
- "queue": "Queue Commands"
11
+ "queue": "Queue Commands",
12
+ "certainOnly": "Discard uncertain values"
12
13
  }
13
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.2.0",
3
+ "version": "4.3.0",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [
@@ -0,0 +1,51 @@
1
+ import io
2
+ import json
3
+
4
+ # --- editors: certainOnly checkbox (#96) -----------------------------------
5
+ for name, default_anchor in (
6
+ ("ccu-value", " cache: {value: false},\n"),
7
+ ("ccu-rpc-event", " cache: {value: false},\n"),
8
+ ):
9
+ p = "nodes/%s.html" % name
10
+ s = io.open(p, encoding="utf-8").read()
11
+
12
+ assert default_anchor in s, "defaults anchor in " + p
13
+ s = s.replace(default_anchor, default_anchor + " certainOnly: {value: false},\n", 1)
14
+
15
+ old = """ <label class="ccu-checkbox">
16
+ <input type="checkbox" id="node-input-cache">
17
+ <span data-i18n="%s.cache"></span>
18
+ </label>
19
+ </div>
20
+ </div>""" % name
21
+ new = """ <label class="ccu-checkbox">
22
+ <input type="checkbox" id="node-input-cache">
23
+ <span data-i18n="%s.cache"></span>
24
+ </label>
25
+
26
+ <label class="ccu-checkbox">
27
+ <input type="checkbox" id="node-input-certainOnly">
28
+ <span data-i18n="%s.certainOnly"></span>
29
+ </label>
30
+ </div>
31
+ </div>""" % (name, name)
32
+ assert old in s, "checkbox block in " + p
33
+ s = s.replace(old, new, 1)
34
+
35
+ io.open(p, "w", encoding="utf-8", newline="\n").write(s)
36
+ print("patched", p)
37
+
38
+ # --- locales ---------------------------------------------------------------
39
+ LABELS = {
40
+ "de": "Unsichere Werte verwerfen",
41
+ "en-US": "Discard uncertain values",
42
+ }
43
+ for loc, label in LABELS.items():
44
+ for name in ("ccu-value", "ccu-rpc-event"):
45
+ p = "nodes/locales/%s/%s.json" % (loc, name)
46
+ d = json.load(io.open(p, encoding="utf-8"))
47
+ d[name]["certainOnly"] = label
48
+ io.open(p, "w", encoding="utf-8", newline="\n").write(
49
+ json.dumps(d, indent=2, ensure_ascii=False) + "\n"
50
+ )
51
+ print("locale", p)