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.
- package/CHANGELOG.md +125 -0
- package/HANDOFF.md +67 -16
- package/README.md +42 -0
- package/ROADMAP.md +129 -47
- package/examples/home-assistant.json +127 -0
- package/nodes/ccu-alexa.html +3 -1
- package/nodes/ccu-connection.html +26 -0
- package/nodes/ccu-connection.js +124 -13
- package/nodes/ccu-display.js +29 -13
- package/nodes/ccu-get-value.html +3 -1
- package/nodes/ccu-homeassistant.html +334 -0
- package/nodes/ccu-homeassistant.js +272 -0
- package/nodes/ccu-mqtt.html +21 -4
- package/nodes/ccu-mqtt.js +188 -105
- 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/hadiscovery.js +846 -0
- package/nodes/lib/haroles.js +193 -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 +2 -1
- package/patch-certainonly.py +51 -0
- package/roadmap-archive/B-16.md +31 -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
package/nodes/ccu-mqtt.js
CHANGED
|
@@ -2,6 +2,19 @@ const path = require('path');
|
|
|
2
2
|
const mw = require('mqtt-wildcard');
|
|
3
3
|
|
|
4
4
|
const statusHelper = require(path.join(__dirname, '/lib/status.js'));
|
|
5
|
+
const {castValue} = require(path.join(__dirname, '/lib/cast.js'));
|
|
6
|
+
|
|
7
|
+
/** HM thermostats: CONTROL_MODE is read-only, the modes are set through actions */
|
|
8
|
+
const HM_MODES = {
|
|
9
|
+
'AUTO-MODE': () => ['AUTO_MODE', true],
|
|
10
|
+
'MANU-MODE': (setpoint) => ['MANU_MODE', setpoint],
|
|
11
|
+
'BOOST-MODE': () => ['BOOST_MODE', true],
|
|
12
|
+
'COMFORT-MODE': () => ['COMFORT_MODE', true],
|
|
13
|
+
'LOWERING-MODE': () => ['LOWERING_MODE', true],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Words Home Assistant sends to LEVEL; 1.005 is the actuators' "old level" value */
|
|
17
|
+
const LEVEL_WORDS = {OPEN: 1, CLOSE: 0, ON: 1.005, OFF: 0};
|
|
5
18
|
|
|
6
19
|
module.exports = function (RED) {
|
|
7
20
|
class CcuMqttNode {
|
|
@@ -23,7 +36,10 @@ module.exports = function (RED) {
|
|
|
23
36
|
this.topicInputPutParam = config.topicInputPutParam;
|
|
24
37
|
this.topicInputPutParamset = config.topicInputPutParamset;
|
|
25
38
|
|
|
39
|
+
this.topicInputGet = config.topicInputGet;
|
|
40
|
+
|
|
26
41
|
this.topicInputRpc = config.topicInputRpc;
|
|
42
|
+
this.topicOutputRpc = config.topicOutputRpc;
|
|
27
43
|
|
|
28
44
|
this.topicCounters = config.topicCounters;
|
|
29
45
|
this.rxCounters = {};
|
|
@@ -98,12 +114,15 @@ module.exports = function (RED) {
|
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
event(message) {
|
|
101
|
-
|
|
117
|
+
// channels without a ReGa name used to produce hm/status//STATE-style
|
|
118
|
+
// topics — fall back to the address (B-3)
|
|
119
|
+
const topicMessage = message.channelName ? message : {...message, channelName: message.channel};
|
|
120
|
+
const topic = this.ccu.topicReplace(this.topicOutputEvent, topicMessage);
|
|
102
121
|
const retain = !(message.datapoint && message.datapoint.startsWith('PRESS_'));
|
|
103
122
|
this.send({topic, payload: this.output(message), retain});
|
|
104
123
|
|
|
105
124
|
if (['LEVEL', 'STATE'].includes(message.datapoint) && message.working === false) {
|
|
106
|
-
const messageNotWorking = RED.util.cloneMessage(
|
|
125
|
+
const messageNotWorking = RED.util.cloneMessage(topicMessage);
|
|
107
126
|
messageNotWorking.datapoint += '_NOTWORKING';
|
|
108
127
|
messageNotWorking.datapointName += '_NOTWORKING';
|
|
109
128
|
this.send({
|
|
@@ -164,6 +183,7 @@ module.exports = function (RED) {
|
|
|
164
183
|
this.debug('input ' + topic + ' ' + JSON.stringify(payload).slice(0, 40));
|
|
165
184
|
|
|
166
185
|
const topicList = {
|
|
186
|
+
get: this.topicInputGet,
|
|
167
187
|
setValue: this.topicInputSetValue,
|
|
168
188
|
sysvar: this.topicInputSysvar,
|
|
169
189
|
putParam: this.topicInputPutParam,
|
|
@@ -174,7 +194,8 @@ module.exports = function (RED) {
|
|
|
174
194
|
let command;
|
|
175
195
|
let filter;
|
|
176
196
|
Object.keys(topicList).forEach((key) => {
|
|
177
|
-
|
|
197
|
+
// a topic left empty in the editor disables that command
|
|
198
|
+
if (!command && topicList[key]) {
|
|
178
199
|
const parts = topicList[key].split('/');
|
|
179
200
|
const patternArray = [];
|
|
180
201
|
const placeholders = [];
|
|
@@ -205,33 +226,177 @@ module.exports = function (RED) {
|
|
|
205
226
|
}
|
|
206
227
|
}
|
|
207
228
|
|
|
208
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Resolve `channelNameOrAddress` to a channel address and its
|
|
231
|
+
* interface, reporting the same errors the commands used to report
|
|
232
|
+
* inline.
|
|
233
|
+
* @param {object} filter parsed from the topic; `channel` is filled in
|
|
234
|
+
* @param {boolean} [exact] pass through to findChannel
|
|
235
|
+
* @returns {string|null} the interface, or null when unresolvable
|
|
236
|
+
*/
|
|
237
|
+
resolveChannel(filter, exact) {
|
|
209
238
|
if (filter.channelNameOrAddress) {
|
|
210
239
|
if (this.ccu.channelNames[filter.channelNameOrAddress]) {
|
|
211
240
|
filter.channel = filter.channelNameOrAddress;
|
|
212
241
|
} else {
|
|
213
|
-
filter.channel = this.ccu.findChannel(filter.channelNameOrAddress,
|
|
242
|
+
filter.channel = this.ccu.findChannel(filter.channelNameOrAddress, exact);
|
|
214
243
|
}
|
|
215
244
|
|
|
216
245
|
if (!filter.channel) {
|
|
217
246
|
this.error('channel ' + filter.channelNameOrAddress + ' not found');
|
|
218
|
-
return;
|
|
247
|
+
return null;
|
|
219
248
|
}
|
|
220
249
|
}
|
|
221
250
|
|
|
222
251
|
if (!filter.channel) {
|
|
223
252
|
this.error('channel undefined');
|
|
224
|
-
return;
|
|
253
|
+
return null;
|
|
225
254
|
}
|
|
226
255
|
|
|
227
256
|
const iface = this.ccu.findIface(filter.channel);
|
|
228
257
|
|
|
229
258
|
if (!iface) {
|
|
230
259
|
this.error('no interface found for channel ' + filter.channel);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return iface;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Republish the last known value of a datapoint on its status topic,
|
|
268
|
+
* so a flow can ask for a refresh over MQTT instead of waiting for the
|
|
269
|
+
* next event (#115). An empty datapoint republishes the whole channel.
|
|
270
|
+
* @param {object} filter
|
|
271
|
+
*/
|
|
272
|
+
get(filter) {
|
|
273
|
+
const iface = this.resolveChannel(filter, true);
|
|
274
|
+
if (!iface) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const prefix = iface + '.' + filter.channel + '.';
|
|
279
|
+
const names = filter.datapoint
|
|
280
|
+
? [prefix + filter.datapoint]
|
|
281
|
+
: Object.keys(this.ccu.values).filter((name) => name.startsWith(prefix));
|
|
282
|
+
|
|
283
|
+
if (names.length === 0) {
|
|
284
|
+
this.error('unknown datapoint ' + prefix + (filter.datapoint || '#'));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
names.forEach((name) => {
|
|
289
|
+
const value = this.ccu.values[name];
|
|
290
|
+
if (value) {
|
|
291
|
+
this.event({...value});
|
|
292
|
+
} else {
|
|
293
|
+
this.error('unknown datapoint ' + name);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Call an arbitrary RPC method on an interface and publish the result
|
|
300
|
+
* (#22). The topic supplies interface, method and a caller-chosen id
|
|
301
|
+
* that the response topic echoes; the payload is the parameter array
|
|
302
|
+
* (a single value is accepted and wrapped).
|
|
303
|
+
* @param {object} filter
|
|
304
|
+
* @param {*} payload
|
|
305
|
+
*/
|
|
306
|
+
rpc(filter, payload) {
|
|
307
|
+
if (!filter.iface) {
|
|
308
|
+
this.error('interface undefined');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!filter.method) {
|
|
313
|
+
this.error('method undefined');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let parameters = payload;
|
|
318
|
+
if (parameters === undefined || parameters === null || parameters === '') {
|
|
319
|
+
parameters = [];
|
|
320
|
+
} else if (!Array.isArray(parameters)) {
|
|
321
|
+
parameters = [parameters];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const respond = (result, error) => {
|
|
325
|
+
if (!this.topicOutputRpc) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.send({
|
|
330
|
+
topic: this.ccu.topicReplace(this.topicOutputRpc, {
|
|
331
|
+
iface: filter.iface,
|
|
332
|
+
method: filter.method,
|
|
333
|
+
callid: filter.callid,
|
|
334
|
+
}),
|
|
335
|
+
payload: error ? {error: error.message} : {result},
|
|
336
|
+
retain: false,
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
this.ccu
|
|
341
|
+
.methodCall(filter.iface, filter.method, parameters)
|
|
342
|
+
.then((result) => respond(result))
|
|
343
|
+
.catch((error) => {
|
|
344
|
+
this.error(error.message);
|
|
345
|
+
respond(undefined, error);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
setValue(filter, payload) {
|
|
350
|
+
const iface = this.resolveChannel(filter, true);
|
|
351
|
+
if (!iface) {
|
|
231
352
|
return;
|
|
232
353
|
}
|
|
233
354
|
|
|
234
|
-
this.
|
|
355
|
+
const command = this.translateCommand(iface, filter.channel, filter.datapoint, payload);
|
|
356
|
+
this.ccu.setValue(iface, filter.channel, command.datapoint, command.payload).catch(() => {});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Words Home Assistant's single-topic conventions send (see the
|
|
361
|
+
* ccu-homeassistant node): LEVEL accepts OPEN, CLOSE, STOP, ON (restore the
|
|
362
|
+
* last level) and OFF; HM thermostats accept the CONTROL_MODE names
|
|
363
|
+
* (AUTO-MODE, MANU-MODE, BOOST-MODE, COMFORT-MODE, LOWERING-MODE), which
|
|
364
|
+
* are translated to the *_MODE actions because CONTROL_MODE itself is
|
|
365
|
+
* read-only there. Everything else passes through unchanged.
|
|
366
|
+
* @returns {{datapoint: string, payload: *}}
|
|
367
|
+
*/
|
|
368
|
+
translateCommand(iface, channel, datapoint, payload) {
|
|
369
|
+
if (typeof payload !== 'string') {
|
|
370
|
+
return {datapoint, payload};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const word = payload.trim().toUpperCase();
|
|
374
|
+
const device = this.ccu.metadata.devices[iface] && this.ccu.metadata.devices[iface][channel];
|
|
375
|
+
const description = (device && this.ccu.getParamsetDescription(iface, device, 'VALUES')) || {};
|
|
376
|
+
|
|
377
|
+
if (datapoint === 'LEVEL') {
|
|
378
|
+
if (word === 'STOP' && description.STOP) {
|
|
379
|
+
return {datapoint: 'STOP', payload: true};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (word in LEVEL_WORDS) {
|
|
383
|
+
return {datapoint, payload: LEVEL_WORDS[word]};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (
|
|
388
|
+
datapoint === 'CONTROL_MODE' &&
|
|
389
|
+
HM_MODES[word] &&
|
|
390
|
+
description.CONTROL_MODE &&
|
|
391
|
+
!(description.CONTROL_MODE.OPERATIONS & 2)
|
|
392
|
+
) {
|
|
393
|
+
const current = this.ccu.values[iface + '.' + channel + '.SET_TEMPERATURE'];
|
|
394
|
+
const setpoint = current && typeof current.value === 'number' ? current.value : 20;
|
|
395
|
+
const [dp, value] = HM_MODES[word](setpoint);
|
|
396
|
+
return {datapoint: dp, payload: value};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {datapoint, payload};
|
|
235
400
|
}
|
|
236
401
|
|
|
237
402
|
sysvar(filter, payload) {
|
|
@@ -254,28 +419,8 @@ module.exports = function (RED) {
|
|
|
254
419
|
}
|
|
255
420
|
|
|
256
421
|
putParam(filter, payload) {
|
|
257
|
-
|
|
258
|
-
if (this.ccu.channelNames[filter.channelNameOrAddress]) {
|
|
259
|
-
filter.channel = filter.channelNameOrAddress;
|
|
260
|
-
} else {
|
|
261
|
-
filter.channel = this.ccu.findChannel(filter.channelNameOrAddress);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
if (!filter.channel) {
|
|
265
|
-
this.error('channel ' + filter.channelNameOrAddress + ' not found');
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (!filter.channel) {
|
|
271
|
-
this.error('channel undefined');
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const iface = this.ccu.findIface(filter.channel);
|
|
276
|
-
|
|
422
|
+
const iface = this.resolveChannel(filter);
|
|
277
423
|
if (!iface) {
|
|
278
|
-
this.error('no interface found for channel ' + filter.channel);
|
|
279
424
|
return;
|
|
280
425
|
}
|
|
281
426
|
|
|
@@ -286,11 +431,14 @@ module.exports = function (RED) {
|
|
|
286
431
|
);
|
|
287
432
|
const paramsetDescription = this.ccu.paramsetDescriptions[psName];
|
|
288
433
|
if (paramsetDescription && paramsetDescription[filter.param]) {
|
|
289
|
-
|
|
434
|
+
// was `!(OPERATIONS) && 2`, which is always false - the check
|
|
435
|
+
// never fired and the write went out regardless (B-3)
|
|
436
|
+
if (!(paramsetDescription[filter.param].OPERATIONS & 2)) {
|
|
290
437
|
this.error('param ' + filter.param + ' not writeable');
|
|
438
|
+
return;
|
|
291
439
|
}
|
|
292
440
|
|
|
293
|
-
payload =
|
|
441
|
+
payload = castValue(payload, paramsetDescription[filter.param], {clamp: true});
|
|
294
442
|
} else {
|
|
295
443
|
this.warn('unknown paramset/param ' + filter.paramset + ' ' + filter.param);
|
|
296
444
|
}
|
|
@@ -309,28 +457,8 @@ module.exports = function (RED) {
|
|
|
309
457
|
return;
|
|
310
458
|
}
|
|
311
459
|
|
|
312
|
-
|
|
313
|
-
if (this.ccu.channelNames[filter.channelNameOrAddress]) {
|
|
314
|
-
filter.channel = filter.channelNameOrAddress;
|
|
315
|
-
} else {
|
|
316
|
-
filter.channel = this.ccu.findChannel(filter.channelNameOrAddress);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (!filter.channel) {
|
|
320
|
-
this.error('channel ' + filter.channelNameOrAddress + ' not found');
|
|
321
|
-
return;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (!filter.channel) {
|
|
326
|
-
this.error('channel undefined');
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const iface = this.ccu.findIface(filter.channel);
|
|
331
|
-
|
|
460
|
+
const iface = this.resolveChannel(filter);
|
|
332
461
|
if (!iface) {
|
|
333
|
-
this.error('no interface found for channel ' + filter.channel);
|
|
334
462
|
return;
|
|
335
463
|
}
|
|
336
464
|
|
|
@@ -345,11 +473,17 @@ module.exports = function (RED) {
|
|
|
345
473
|
|
|
346
474
|
Object.keys(payload).forEach((parameter) => {
|
|
347
475
|
if (paramsetDescription && paramsetDescription[parameter]) {
|
|
348
|
-
|
|
476
|
+
// was `!(OPERATIONS) && 2` (always false), and the cast
|
|
477
|
+
// used paramsetDescription[filter.param], which is
|
|
478
|
+
// undefined here - so every value went out uncast (B-3)
|
|
479
|
+
if (!(paramsetDescription[parameter].OPERATIONS & 2)) {
|
|
349
480
|
this.error('param ' + parameter + ' not writeable');
|
|
481
|
+
return;
|
|
350
482
|
}
|
|
351
483
|
|
|
352
|
-
paramset[parameter] =
|
|
484
|
+
paramset[parameter] = castValue(payload[parameter], paramsetDescription[parameter], {
|
|
485
|
+
clamp: true,
|
|
486
|
+
});
|
|
353
487
|
} else {
|
|
354
488
|
this.warn('unknown paramset/param ' + filter.paramset + ' ' + parameter);
|
|
355
489
|
paramset[parameter] = payload[parameter];
|
|
@@ -360,57 +494,6 @@ module.exports = function (RED) {
|
|
|
360
494
|
.methodCall(iface, 'putParamset', [filter.channel, filter.paramset, paramset])
|
|
361
495
|
.catch((error) => this.error(error.message));
|
|
362
496
|
}
|
|
363
|
-
|
|
364
|
-
paramCast(value, paramset) {
|
|
365
|
-
switch (paramset && paramset.TYPE) {
|
|
366
|
-
case 'BOOL':
|
|
367
|
-
// Fallthrough by intention
|
|
368
|
-
case 'ACTION':
|
|
369
|
-
// OMG this is so ugly...
|
|
370
|
-
if (value === 'false') {
|
|
371
|
-
value = false;
|
|
372
|
-
} else if (!isNaN(value)) {
|
|
373
|
-
// Make sure that the string "0" gets casted to boolean false
|
|
374
|
-
value = Number(value);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
value = Boolean(value);
|
|
378
|
-
break;
|
|
379
|
-
case 'FLOAT':
|
|
380
|
-
value = Number.parseFloat(value);
|
|
381
|
-
if (value < paramset.MIN) {
|
|
382
|
-
value = paramset.MIN;
|
|
383
|
-
} else if (value > paramset.MAX) {
|
|
384
|
-
value = paramset.MAX;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
value = {explicitDouble: value};
|
|
388
|
-
break;
|
|
389
|
-
case 'ENUM':
|
|
390
|
-
if (typeof value === 'string') {
|
|
391
|
-
if (paramset.ENUM && paramset.ENUM.includes(value)) {
|
|
392
|
-
value = paramset.ENUM.indexOf(value);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Fallthrough by intention
|
|
397
|
-
case 'INTEGER':
|
|
398
|
-
value = Number.parseInt(value, 10);
|
|
399
|
-
if (value < paramset.MIN) {
|
|
400
|
-
value = paramset.MIN;
|
|
401
|
-
} else if (value > paramset.MAX) {
|
|
402
|
-
value = paramset.MAX;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
break;
|
|
406
|
-
case 'STRING':
|
|
407
|
-
value = String(value);
|
|
408
|
-
break;
|
|
409
|
-
default:
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return value;
|
|
413
|
-
}
|
|
414
497
|
}
|
|
415
498
|
|
|
416
499
|
RED.nodes.registerType('ccu-mqtt', CcuMqttNode);
|
package/nodes/ccu-rpc-event.html
CHANGED
|
@@ -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">
|
package/nodes/ccu-rpc-event.js
CHANGED
|
@@ -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();
|
package/nodes/ccu-set-value.html
CHANGED
|
@@ -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>
|