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.
- package/CHANGELOG.md +96 -0
- package/HANDOFF.md +11 -3
- package/README.md +29 -0
- package/ROADMAP.md +112 -33
- package/nodes/ccu-alexa.html +3 -1
- package/nodes/ccu-connection.html +26 -0
- package/nodes/ccu-connection.js +87 -11
- package/nodes/ccu-display.js +29 -13
- package/nodes/ccu-get-value.html +3 -1
- package/nodes/ccu-mqtt.html +21 -4
- package/nodes/ccu-mqtt.js +126 -102
- package/nodes/ccu-rpc-event.html +6 -0
- package/nodes/ccu-rpc-event.js +31 -12
- package/nodes/ccu-set-value.html +38 -0
- package/nodes/ccu-set-value.js +59 -235
- package/nodes/ccu-signal.html +55 -1
- package/nodes/ccu-signal.js +52 -9
- package/nodes/ccu-switch.html +17 -4
- package/nodes/ccu-switch.js +28 -4
- package/nodes/ccu-sysvar.html +40 -1
- package/nodes/ccu-sysvar.js +78 -8
- package/nodes/ccu-value.html +27 -1
- package/nodes/ccu-value.js +69 -19
- package/nodes/lib/cast.js +59 -14
- package/nodes/lib/channelfilter.js +136 -0
- package/nodes/lib/dynconfig.js +81 -0
- package/nodes/lib/localccu.js +81 -0
- package/nodes/lib/message.js +6 -2
- package/nodes/lib/valuestatus.js +145 -0
- package/nodes/locales/de/ccu-display.html +17 -0
- package/nodes/locales/de/ccu-rpc-event.json +1 -0
- package/nodes/locales/de/ccu-sysvar.json +3 -2
- package/nodes/locales/de/ccu-value.json +2 -1
- package/nodes/locales/en-US/ccu-display.html +17 -0
- package/nodes/locales/en-US/ccu-rpc-event.json +3 -2
- package/nodes/locales/en-US/ccu-switch.html +12 -0
- package/nodes/locales/en-US/ccu-sysvar.json +3 -2
- package/nodes/locales/en-US/ccu-value.json +2 -1
- package/package.json +1 -1
- package/patch-certainonly.py +51 -0
- package/roadmap-archive/B-2.md +69 -0
- package/roadmap-archive/B-3.md +57 -0
- package/roadmap-archive/B-4.md +44 -0
- package/roadmap-archive/B-5.md +57 -0
- 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};
|
package/nodes/lib/message.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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>
|
|
@@ -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
|
+
}
|
|
@@ -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>
|
package/package.json
CHANGED
|
@@ -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)
|