node-red-contrib-ccu 4.5.0 → 4.5.2
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 +40 -0
- package/nodes/ccu-connection.js +262 -14
- package/package.json +2 -8
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,46 @@ Notable changes to node-red-contrib-ccu. Format follows
|
|
|
5
5
|
user-visible symptom and the cause, not the commit list (the release notes
|
|
6
6
|
append commits automatically).
|
|
7
7
|
|
|
8
|
+
## 4.5.2 (2026-10-02)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **No HmIP-RF events for up to 10 minutes after hmipserver restarted**
|
|
13
|
+
(B-37). hmipserver keeps its subscribers over a restart: it calls
|
|
14
|
+
`listDevices` and `newDevices` on them and answers their pings, but
|
|
15
|
+
delivers no event to such a kept subscription until it is subscribed
|
|
16
|
+
afresh. The liveness ping saw its PONG and was content, so the
|
|
17
|
+
subscription came back only with the 600 s silence timeout. A
|
|
18
|
+
`listDevices` or `newDevices` call that does not follow the connection's
|
|
19
|
+
own `init` now makes it subscribe again at once, and events are back
|
|
20
|
+
within seconds. A device paired while Node-RED runs costs one such `init`
|
|
21
|
+
as well.
|
|
22
|
+
|
|
23
|
+
## 4.5.1 (2026-10-02)
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **A burst of `getParamsetDescription Error: XML-RPC fault: Invalid device`
|
|
28
|
+
at the first start after an update** (B-36). A device deleted from the CCU
|
|
29
|
+
while Node-RED was not running stayed in the cached device table
|
|
30
|
+
(`ccu_<host>.json`) and in the values cache (`ccu_values_<host>.json`).
|
|
31
|
+
At the start its paramset descriptions were queued from the cached table;
|
|
32
|
+
hmipserver answered `-2 Invalid device`, a failed description was never
|
|
33
|
+
remembered, so every lookup queued it again (one item per channel and
|
|
34
|
+
paramset, duplicates included), each fault was logged twice without the
|
|
35
|
+
address, and each one threw away and rebuilt the interface's RPC client.
|
|
36
|
+
Seen on a CCU3 after a remote had been replaced: 150 failed calls in 40 s.
|
|
37
|
+
Now a description the interface refuses with a fault is asked for once
|
|
38
|
+
per address and paramset in a run, with one warning that names the
|
|
39
|
+
address, the paramset and the fault (a device paired anew is asked
|
|
40
|
+
again); the queue holds each description key once; `deleteDevices` drops
|
|
41
|
+
the device's queued fetches and its address from the type lists, and the
|
|
42
|
+
values cache is pruned against the device table at the start and a few
|
|
43
|
+
seconds after a `deleteDevices` or the ReGa's value read (the ReGa keeps
|
|
44
|
+
reporting the datapoints of a deleted device). An XML-RPC fault no longer
|
|
45
|
+
closes and recreates the RPC client: only a transport error does; the
|
|
46
|
+
fault is logged once, with the arguments of the call.
|
|
47
|
+
|
|
8
48
|
## 4.5.0 (2026-09-29)
|
|
9
49
|
|
|
10
50
|
### Added
|
package/nodes/ccu-connection.js
CHANGED
|
@@ -23,6 +23,11 @@ const binrpc = require('binrpc');
|
|
|
23
23
|
|
|
24
24
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
25
25
|
|
|
26
|
+
// B-37: how long after our init the process's listDevices/newDevices still belong to it -
|
|
27
|
+
// hmipserver answers the init at once and calls listDevices and newDevices afterwards. Such a
|
|
28
|
+
// call later than this is the process calling on its own: it restarted and kept the subscription.
|
|
29
|
+
const INIT_GRACE_MS = 30000;
|
|
30
|
+
|
|
26
31
|
/**
|
|
27
32
|
* check if an object is iterable
|
|
28
33
|
* @link https://stackoverflow.com/a/37837872
|
|
@@ -630,6 +635,10 @@ module.exports = function (RED) {
|
|
|
630
635
|
// and which of them are waiting for an unreachable process
|
|
631
636
|
this.initRetry = {};
|
|
632
637
|
this.ifaceWaiting = {};
|
|
638
|
+
// B-37: per interface {inFlight, at, reinit} of our own init, and how long after it the
|
|
639
|
+
// process's listDevices/newDevices still answer it (see keptSubscription())
|
|
640
|
+
this.initState = {};
|
|
641
|
+
this.initGrace = INIT_GRACE_MS;
|
|
633
642
|
this.closing = false;
|
|
634
643
|
this.serverError = {};
|
|
635
644
|
this.queueTimeout = Number.parseInt(config.queueTimeout, 10) || 5000;
|
|
@@ -662,6 +671,13 @@ module.exports = function (RED) {
|
|
|
662
671
|
|
|
663
672
|
this.newParamsetDescriptionCount = 0;
|
|
664
673
|
this.paramsetQueue = [];
|
|
674
|
+
// B-36: iface/address/paramset whose description the interface refused
|
|
675
|
+
// with a fault (-2 Invalid device for an address it does not know) - not
|
|
676
|
+
// asked again in this run, so a stale cache entry costs one call, not one
|
|
677
|
+
// per lookup
|
|
678
|
+
this.paramsetFailed = new Set();
|
|
679
|
+
this.valuesPruneTimer = null;
|
|
680
|
+
this.valuesPruneDelay = 5000;
|
|
665
681
|
this.paramsQueue = [];
|
|
666
682
|
|
|
667
683
|
this.paramsetFile = path.join(RED.settings.userDir || path.join(__dirname, '..'), 'paramsets.json');
|
|
@@ -726,6 +742,7 @@ module.exports = function (RED) {
|
|
|
726
742
|
this.loadMetadata();
|
|
727
743
|
this.loadRegadata();
|
|
728
744
|
this.loadValues();
|
|
745
|
+
this.pruneCaches();
|
|
729
746
|
|
|
730
747
|
this.setContext();
|
|
731
748
|
|
|
@@ -1264,6 +1281,7 @@ module.exports = function (RED) {
|
|
|
1264
1281
|
clearTimeout(this.rpcPingTimer[iface]);
|
|
1265
1282
|
});
|
|
1266
1283
|
Object.keys(this.liveness).forEach((iface) => this.stopLiveness(iface));
|
|
1284
|
+
clearTimeout(this.valuesPruneTimer);
|
|
1267
1285
|
|
|
1268
1286
|
this.saveRegadata();
|
|
1269
1287
|
this.saveValues();
|
|
@@ -1642,6 +1660,8 @@ module.exports = function (RED) {
|
|
|
1642
1660
|
});
|
|
1643
1661
|
this.cachedValuesReceived = true;
|
|
1644
1662
|
this.saveValues();
|
|
1663
|
+
// B-36: ReGa keeps the datapoints of a deleted device
|
|
1664
|
+
this.scheduleValuesPrune();
|
|
1645
1665
|
resolve();
|
|
1646
1666
|
}
|
|
1647
1667
|
});
|
|
@@ -2334,9 +2354,12 @@ module.exports = function (RED) {
|
|
|
2334
2354
|
this.createClient(iface, {quiet: true});
|
|
2335
2355
|
}
|
|
2336
2356
|
|
|
2357
|
+
// B-37: listDevices/newDevices while this init runs or shortly after belong to it
|
|
2358
|
+
this.initState[iface] = {inFlight: true, at: 0, reinit: false};
|
|
2337
2359
|
// a failed init is logged by the retry (InitRetry), not here
|
|
2338
2360
|
this.methodCall(iface, 'init', [initUrl, initId], {quiet: true})
|
|
2339
2361
|
.then(() => {
|
|
2362
|
+
this.initState[iface] = {inFlight: false, at: now(), reinit: false};
|
|
2340
2363
|
// the ping/re-init liveness starts after a successful init only;
|
|
2341
2364
|
// a failed one is retried by rpcInitFailed (task 11)
|
|
2342
2365
|
if (this.ifaceTypes[iface].ping && !this.closing) {
|
|
@@ -2359,10 +2382,57 @@ module.exports = function (RED) {
|
|
|
2359
2382
|
resolve(iface);
|
|
2360
2383
|
}
|
|
2361
2384
|
})
|
|
2362
|
-
.catch((error) =>
|
|
2385
|
+
.catch((error) => {
|
|
2386
|
+
this.initState[iface] = {inFlight: false, at: 0, reinit: false};
|
|
2387
|
+
reject(error);
|
|
2388
|
+
});
|
|
2363
2389
|
});
|
|
2364
2390
|
}
|
|
2365
2391
|
|
|
2392
|
+
/**
|
|
2393
|
+
* B-37: the interface process called listDevices or newDevices. Outside our own init it
|
|
2394
|
+
* restarted and kept the subscription: hmipserver restores its subscribers from its
|
|
2395
|
+
* handlers file, calls listDevices and newDevices on them and answers their pings (the
|
|
2396
|
+
* PONG arrives, so neither the liveness ping nor the 600 s watchdog fires), but delivers
|
|
2397
|
+
* no event to such a kept subscription until a fresh init (openccu-lite B-286, measured).
|
|
2398
|
+
* So init again at once. A device paired while Node-RED runs costs one init the same way.
|
|
2399
|
+
* @param iface
|
|
2400
|
+
* @param {string} method
|
|
2401
|
+
* @returns {boolean} whether a fresh init was started
|
|
2402
|
+
*/
|
|
2403
|
+
keptSubscription(iface, method) {
|
|
2404
|
+
const type = this.ifaceTypes[iface];
|
|
2405
|
+
const state = this.initState[iface];
|
|
2406
|
+
if (this.closing || !type || !type.enabled || !type.init) {
|
|
2407
|
+
return false;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
// never subscribed, an init running or already started for this restart
|
|
2411
|
+
if (!state || state.inFlight || state.reinit || !state.at) {
|
|
2412
|
+
return false;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
if (this.initRetry[iface] && this.initRetry[iface].pending) {
|
|
2416
|
+
return false;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
if (now() - state.at < this.initGrace) {
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
state.reinit = true;
|
|
2424
|
+
this.logger.info(
|
|
2425
|
+
iface +
|
|
2426
|
+
': ' +
|
|
2427
|
+
method +
|
|
2428
|
+
' outside our init - the interface process restarted and kept the subscription, subscribing afresh',
|
|
2429
|
+
);
|
|
2430
|
+
this.rpcInit(iface)
|
|
2431
|
+
.then(() => this.rpcInitSucceeded(iface))
|
|
2432
|
+
.catch((error) => this.rpcInitFailed(iface, error));
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2366
2436
|
/**
|
|
2367
2437
|
* Returns the links of a specific channel
|
|
2368
2438
|
* @param {String} iface
|
|
@@ -2427,6 +2497,10 @@ module.exports = function (RED) {
|
|
|
2427
2497
|
this.saveMetadata();
|
|
2428
2498
|
}
|
|
2429
2499
|
|
|
2500
|
+
if (summary.removed) {
|
|
2501
|
+
this.scheduleValuesPrune();
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2430
2504
|
resolve(summary);
|
|
2431
2505
|
})
|
|
2432
2506
|
.catch(reject);
|
|
@@ -2896,7 +2970,13 @@ module.exports = function (RED) {
|
|
|
2896
2970
|
if (device && device.PARAMSETS) {
|
|
2897
2971
|
device.PARAMSETS.forEach((paramset) => {
|
|
2898
2972
|
const name = this.paramsetName(iface, device, paramset);
|
|
2899
|
-
if (
|
|
2973
|
+
if (
|
|
2974
|
+
!this.paramsetDescriptions[name] &&
|
|
2975
|
+
// B-36: once per description key in the queue, and never again
|
|
2976
|
+
// for an address/paramset the interface refused in this run
|
|
2977
|
+
!this.paramsetFailed.has(iface + '/' + device.ADDRESS + '/' + paramset) &&
|
|
2978
|
+
!(name && this.paramsetQueue.some((item) => item.name === name))
|
|
2979
|
+
) {
|
|
2900
2980
|
this.paramsetQueue.push({
|
|
2901
2981
|
iface,
|
|
2902
2982
|
name,
|
|
@@ -2932,7 +3012,7 @@ module.exports = function (RED) {
|
|
|
2932
3012
|
clearTimeout(this.getParamsetTimeout);
|
|
2933
3013
|
setImmediate(() => this.paramsetQueueShift());
|
|
2934
3014
|
} else {
|
|
2935
|
-
this.methodCall(iface, 'getParamsetDescription', [address, paramset])
|
|
3015
|
+
this.methodCall(iface, 'getParamsetDescription', [address, paramset], {quiet: true})
|
|
2936
3016
|
.then((res) => {
|
|
2937
3017
|
//this.logger.trace('paramsetDescription', name);
|
|
2938
3018
|
this.newParamsetDescriptionCount += 1;
|
|
@@ -2944,7 +3024,31 @@ module.exports = function (RED) {
|
|
|
2944
3024
|
this.saveParamsets();
|
|
2945
3025
|
}
|
|
2946
3026
|
})
|
|
2947
|
-
.catch((error) =>
|
|
3027
|
+
.catch((error) => {
|
|
3028
|
+
// B-36: one line naming the address and the paramset; a
|
|
3029
|
+
// fault is remembered, a transport error may be retried
|
|
3030
|
+
if (error && error.faultCode !== undefined) {
|
|
3031
|
+
this.paramsetFailed.add(iface + '/' + address + '/' + paramset);
|
|
3032
|
+
this.logger.warn(
|
|
3033
|
+
'getParamsetDescription',
|
|
3034
|
+
iface,
|
|
3035
|
+
address,
|
|
3036
|
+
paramset,
|
|
3037
|
+
'fault',
|
|
3038
|
+
error.faultCode,
|
|
3039
|
+
error.faultString || error.message,
|
|
3040
|
+
'- not asked again until the device is added anew',
|
|
3041
|
+
);
|
|
3042
|
+
} else {
|
|
3043
|
+
this.logger.error(
|
|
3044
|
+
'getParamsetDescription',
|
|
3045
|
+
iface,
|
|
3046
|
+
address,
|
|
3047
|
+
paramset,
|
|
3048
|
+
error && error.message ? error.message : error,
|
|
3049
|
+
);
|
|
3050
|
+
}
|
|
3051
|
+
})
|
|
2948
3052
|
.then(() => {
|
|
2949
3053
|
this.paramsetPending = false;
|
|
2950
3054
|
clearTimeout(this.getParamsetTimeout);
|
|
@@ -3029,6 +3133,11 @@ module.exports = function (RED) {
|
|
|
3029
3133
|
this.logger.debug('newDevice', iface, device.ADDRESS);
|
|
3030
3134
|
}
|
|
3031
3135
|
|
|
3136
|
+
if (!known || deviceChanged(known, device)) {
|
|
3137
|
+
// B-36: a device added anew (re-paired, or its data changed) is asked again
|
|
3138
|
+
this.forgetParamsetFailures(iface, device.ADDRESS);
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3032
3141
|
this.metadata.devices[iface][device.ADDRESS] = device;
|
|
3033
3142
|
|
|
3034
3143
|
if (!device.TYPE) {
|
|
@@ -3036,13 +3145,12 @@ module.exports = function (RED) {
|
|
|
3036
3145
|
throw new Error('device type undefined: ' + JSON.stringify(device));
|
|
3037
3146
|
}
|
|
3038
3147
|
|
|
3039
|
-
if (
|
|
3040
|
-
this.metadata.types[iface][device.TYPE] &&
|
|
3041
|
-
!this.metadata.types[iface][device.TYPE].includes(device.ADDRESS)
|
|
3042
|
-
) {
|
|
3043
|
-
this.metadata.types[iface][device.TYPE].push(device.ADDRESS);
|
|
3044
|
-
} else {
|
|
3148
|
+
if (!this.metadata.types[iface][device.TYPE]) {
|
|
3045
3149
|
this.metadata.types[iface][device.TYPE] = [device.ADDRESS];
|
|
3150
|
+
} else if (!this.metadata.types[iface][device.TYPE].includes(device.ADDRESS)) {
|
|
3151
|
+
// an address already listed stays listed with the others (the old
|
|
3152
|
+
// else branch replaced the whole list with it)
|
|
3153
|
+
this.metadata.types[iface][device.TYPE].push(device.ADDRESS);
|
|
3046
3154
|
}
|
|
3047
3155
|
|
|
3048
3156
|
if (device.TYPE === 'MULTI_MODE_INPUT_TRANSMITTER') {
|
|
@@ -3059,7 +3167,116 @@ module.exports = function (RED) {
|
|
|
3059
3167
|
*/
|
|
3060
3168
|
deleteDevice(iface, device) {
|
|
3061
3169
|
this.logger.debug('deleteDevice', iface, device);
|
|
3062
|
-
|
|
3170
|
+
const known = this.metadata.devices[iface] && this.metadata.devices[iface][device];
|
|
3171
|
+
if (this.metadata.devices[iface]) {
|
|
3172
|
+
delete this.metadata.devices[iface][device];
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// B-36: nothing of a deleted device is fetched or listed by type any more -
|
|
3176
|
+
// its queued descriptions (with those of its channels) and its address in
|
|
3177
|
+
// metadata.types go with it. Its cached values go with the next
|
|
3178
|
+
// scheduleValuesPrune(): hmipserver has been seen to answer an init with
|
|
3179
|
+
// deleteDevices and newDevices for every device it has (eq-3/occu#45), so
|
|
3180
|
+
// a value dropped here could belong to a device that is back a moment later.
|
|
3181
|
+
const ofDevice = (address) => address === device || address.startsWith(device + ':');
|
|
3182
|
+
this.paramsetQueue = this.paramsetQueue.filter((item) => item.iface !== iface || !ofDevice(item.address));
|
|
3183
|
+
this.paramsQueue = this.paramsQueue.filter((item) => item.iface !== iface || !ofDevice(item.address));
|
|
3184
|
+
|
|
3185
|
+
const types = this.metadata.types && this.metadata.types[iface];
|
|
3186
|
+
if (types) {
|
|
3187
|
+
const typeNames = known && known.TYPE ? [known.TYPE] : Object.keys(types);
|
|
3188
|
+
typeNames.forEach((type) => {
|
|
3189
|
+
if (Array.isArray(types[type])) {
|
|
3190
|
+
types[type] = types[type].filter((addr) => addr !== device);
|
|
3191
|
+
if (types[type].length === 0) {
|
|
3192
|
+
delete types[type];
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/**
|
|
3200
|
+
* B-36: the device table decides which cached values and type lists are
|
|
3201
|
+
* kept - at the start the cache files may still hold a device that was
|
|
3202
|
+
* deleted while Node-RED was not running.
|
|
3203
|
+
*/
|
|
3204
|
+
pruneCaches() {
|
|
3205
|
+
const types = (this.metadata && this.metadata.types) || {};
|
|
3206
|
+
Object.keys(types).forEach((iface) => {
|
|
3207
|
+
const devices = this.metadata.devices && this.metadata.devices[iface];
|
|
3208
|
+
if (!devices) {
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
Object.keys(types[iface]).forEach((type) => {
|
|
3213
|
+
if (!Array.isArray(types[iface][type])) {
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
types[iface][type] = types[iface][type].filter((addr) => devices[addr]);
|
|
3218
|
+
if (types[iface][type].length === 0) {
|
|
3219
|
+
delete types[iface][type];
|
|
3220
|
+
}
|
|
3221
|
+
});
|
|
3222
|
+
});
|
|
3223
|
+
|
|
3224
|
+
const pruned = this.pruneValues();
|
|
3225
|
+
if (pruned) {
|
|
3226
|
+
this.logger.info('values cache: dropped', pruned, 'values of devices not in the device table');
|
|
3227
|
+
this.saveValues();
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
/**
|
|
3232
|
+
* B-36: prune the values cache against the device table a few seconds after
|
|
3233
|
+
* the last deleteDevices or ReGa getValues - once hmipserver's
|
|
3234
|
+
* deleteDevices/newDevices pair of an init has settled, and after ReGa
|
|
3235
|
+
* reported the datapoints it keeps of a deleted device again.
|
|
3236
|
+
*/
|
|
3237
|
+
scheduleValuesPrune() {
|
|
3238
|
+
clearTimeout(this.valuesPruneTimer);
|
|
3239
|
+
this.valuesPruneTimer = setTimeout(() => {
|
|
3240
|
+
this.valuesPruneTimer = null;
|
|
3241
|
+
const pruned = this.pruneValues();
|
|
3242
|
+
if (pruned) {
|
|
3243
|
+
this.logger.info('values cache: dropped', pruned, 'values of devices not in the device table');
|
|
3244
|
+
this.saveValues();
|
|
3245
|
+
}
|
|
3246
|
+
}, this.valuesPruneDelay);
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* B-36: drop cached values whose channel is not in its interface's device
|
|
3251
|
+
* table. An interface without a table (not read yet) keeps its values.
|
|
3252
|
+
* @returns {number} how many were dropped
|
|
3253
|
+
*/
|
|
3254
|
+
pruneValues() {
|
|
3255
|
+
let pruned = 0;
|
|
3256
|
+
const tables = (this.metadata && this.metadata.devices) || {};
|
|
3257
|
+
Object.keys(this.values).forEach((datapointName) => {
|
|
3258
|
+
const [iface, channel] = datapointName.split('.');
|
|
3259
|
+
const devices = tables[iface];
|
|
3260
|
+
if (channel && devices && Object.keys(devices).length > 0 && !devices[channel]) {
|
|
3261
|
+
delete this.values[datapointName];
|
|
3262
|
+
pruned += 1;
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
return pruned;
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
/**
|
|
3269
|
+
* B-36: a device added anew is asked for its descriptions again.
|
|
3270
|
+
* @param iface
|
|
3271
|
+
* @param address device or channel address
|
|
3272
|
+
*/
|
|
3273
|
+
forgetParamsetFailures(iface, address) {
|
|
3274
|
+
const prefix = iface + '/' + address;
|
|
3275
|
+
for (const key of this.paramsetFailed) {
|
|
3276
|
+
if (key.startsWith(prefix + '/') || key.startsWith(prefix + ':')) {
|
|
3277
|
+
this.paramsetFailed.delete(key);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3063
3280
|
}
|
|
3064
3281
|
|
|
3065
3282
|
/**
|
|
@@ -3299,7 +3516,8 @@ module.exports = function (RED) {
|
|
|
3299
3516
|
this.saveMetadata();
|
|
3300
3517
|
}
|
|
3301
3518
|
|
|
3302
|
-
|
|
3519
|
+
// B-37: a restart that kept the subscription gets a fresh init, no ping
|
|
3520
|
+
if (iface && !this.keptSubscription(iface, 'newDevices')) {
|
|
3303
3521
|
this.deviceCallback(iface);
|
|
3304
3522
|
}
|
|
3305
3523
|
},
|
|
@@ -3322,6 +3540,7 @@ module.exports = function (RED) {
|
|
|
3322
3540
|
|
|
3323
3541
|
if (changed) {
|
|
3324
3542
|
this.saveMetadata();
|
|
3543
|
+
this.scheduleValuesPrune();
|
|
3325
3544
|
}
|
|
3326
3545
|
|
|
3327
3546
|
if (iface) {
|
|
@@ -3339,6 +3558,9 @@ module.exports = function (RED) {
|
|
|
3339
3558
|
|
|
3340
3559
|
this.logger.debug(' >', iface, 'listDevices', JSON.stringify(res));
|
|
3341
3560
|
callback(null, res);
|
|
3561
|
+
if (iface) {
|
|
3562
|
+
this.keptSubscription(iface, 'listDevices');
|
|
3563
|
+
}
|
|
3342
3564
|
},
|
|
3343
3565
|
event: (_, parameters, callback) => {
|
|
3344
3566
|
const iface = this.rpcIface('event', parameters);
|
|
@@ -3907,7 +4129,25 @@ module.exports = function (RED) {
|
|
|
3907
4129
|
if (this.clients[iface]) {
|
|
3908
4130
|
this.logger.debug('rpc >', iface, method, JSON.stringify(parameters));
|
|
3909
4131
|
this.clients[iface].methodCall(method, parameters, (err, res) => {
|
|
3910
|
-
if (err) {
|
|
4132
|
+
if (err && err.faultCode !== undefined) {
|
|
4133
|
+
// B-36: homematic-xmlrpc delivers a fault as an Error with
|
|
4134
|
+
// faultCode/faultString. The process answered - the client is
|
|
4135
|
+
// fine and stays; one line, with the arguments that were refused.
|
|
4136
|
+
this.logger[quiet ? 'debug' : 'error'](
|
|
4137
|
+
' <',
|
|
4138
|
+
iface,
|
|
4139
|
+
method,
|
|
4140
|
+
JSON.stringify(parameters),
|
|
4141
|
+
'fault',
|
|
4142
|
+
err.faultCode,
|
|
4143
|
+
err.faultString,
|
|
4144
|
+
);
|
|
4145
|
+
if (err.faultString === undefined) {
|
|
4146
|
+
err.faultString = err.message;
|
|
4147
|
+
}
|
|
4148
|
+
|
|
4149
|
+
reject(err);
|
|
4150
|
+
} else if (err) {
|
|
3911
4151
|
this.logger[quiet ? 'debug' : 'error'](' <', iface, method, err);
|
|
3912
4152
|
this.closeClient(iface);
|
|
3913
4153
|
// a closed node creates no new client (it would reconnect forever)
|
|
@@ -3917,7 +4157,15 @@ module.exports = function (RED) {
|
|
|
3917
4157
|
|
|
3918
4158
|
reject(err);
|
|
3919
4159
|
} else if (res && res.faultCode) {
|
|
3920
|
-
this.logger[quiet ? 'debug' : 'error'](
|
|
4160
|
+
this.logger[quiet ? 'debug' : 'error'](
|
|
4161
|
+
' <',
|
|
4162
|
+
iface,
|
|
4163
|
+
method,
|
|
4164
|
+
JSON.stringify(parameters),
|
|
4165
|
+
'fault',
|
|
4166
|
+
res.faultCode,
|
|
4167
|
+
res.faultString,
|
|
4168
|
+
);
|
|
3921
4169
|
const fault = new Error(res.faultString);
|
|
3922
4170
|
fault.faultCode = res.faultCode;
|
|
3923
4171
|
fault.faultString = res.faultString;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-ccu",
|
|
3
|
-
"version": "4.5.
|
|
3
|
+
"version": "4.5.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "none",
|
|
6
6
|
"keywords": [
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"eslint-config-prettier": "^10.1.8",
|
|
65
65
|
"eslint-plugin-html": "^8.2.0",
|
|
66
66
|
"globals": "^16.5.0",
|
|
67
|
-
"hm-simulator": "^
|
|
67
|
+
"hm-simulator": "^1.3.1",
|
|
68
68
|
"mocha": "^11.7.1",
|
|
69
69
|
"node-red": "^4.1.0",
|
|
70
70
|
"node-red-node-test-helper": "^0.3.6",
|
|
@@ -72,12 +72,6 @@
|
|
|
72
72
|
"prettier": "^3.9.6",
|
|
73
73
|
"should": "^13.2.3"
|
|
74
74
|
},
|
|
75
|
-
"overrides": {
|
|
76
|
-
"hm-simulator": {
|
|
77
|
-
"homematic-xmlrpc": "^2.0.0",
|
|
78
|
-
"binrpc": "^4.3.0"
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
75
|
"engines": {
|
|
82
76
|
"node": "^20.19 || ^22.12 || >=24"
|
|
83
77
|
},
|