node-red-contrib-ccu 4.5.1 → 4.5.3
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 +36 -0
- package/README.md +5 -0
- package/examples/home-assistant.json +16 -0
- package/nodes/ccu-connection.js +65 -2
- package/nodes/ccu-homeassistant.js +110 -5
- package/nodes/lib/hadiscovery.js +27 -6
- package/nodes/locales/de/ccu-homeassistant.html +11 -3
- package/nodes/locales/en-US/ccu-homeassistant.html +11 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ 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.3 (2026-10-05)
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Home Assistant showed every entity of a device as "unavailable" for
|
|
13
|
+
good** (B-38, reported by mail). The discovery configs name the device's
|
|
14
|
+
`…:0/UNREACH` topic as its availability, and Home Assistant treats an
|
|
15
|
+
entity as unavailable until that topic delivers a value. Only `ccu-mqtt`
|
|
16
|
+
published UNREACH: when it changed, or once in its start-up burst of
|
|
17
|
+
cached values - which is off unless the node's _Cache_ option is ticked,
|
|
18
|
+
and lost when the MQTT broker is not connected yet at that moment (Node-RED's
|
|
19
|
+
`mqtt out` drops what it gets while disconnected). A device that never went
|
|
20
|
+
unreachable therefore never got a value on that topic, while its state
|
|
21
|
+
updates arrived normally. The `ccu-homeassistant` node now publishes each
|
|
22
|
+
device's current UNREACH itself, retained, right after the discovery
|
|
23
|
+
configs (asking the CCU for it) and every change after that; _republish
|
|
24
|
+
discovery_ sends it again. The example flow also wires Home Assistant's
|
|
25
|
+
birth message (`homeassistant/status`) to the node, so a Home Assistant
|
|
26
|
+
restart republishes discovery and availability; an `offline` there is
|
|
27
|
+
ignored.
|
|
28
|
+
|
|
29
|
+
## 4.5.2 (2026-10-02)
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **No HmIP-RF events for up to 10 minutes after hmipserver restarted**
|
|
34
|
+
(B-37). hmipserver keeps its subscribers over a restart: it calls
|
|
35
|
+
`listDevices` and `newDevices` on them and answers their pings, but
|
|
36
|
+
delivers no event to such a kept subscription until it is subscribed
|
|
37
|
+
afresh. The liveness ping saw its PONG and was content, so the
|
|
38
|
+
subscription came back only with the 600 s silence timeout. A
|
|
39
|
+
`listDevices` or `newDevices` call that does not follow the connection's
|
|
40
|
+
own `init` now makes it subscribe again at once, and events are back
|
|
41
|
+
within seconds. A device paired while Node-RED runs costs one such `init`
|
|
42
|
+
as well.
|
|
43
|
+
|
|
8
44
|
## 4.5.1 (2026-10-02)
|
|
9
45
|
|
|
10
46
|
### Fixed
|
package/README.md
CHANGED
|
@@ -76,6 +76,11 @@ device with switch, light, cover, climate, binary_sensor, event, lock and sensor
|
|
|
76
76
|
optionally be included as a disabled-by-default entity. Unticking a device removes it from Home Assistant on the
|
|
77
77
|
next deploy.
|
|
78
78
|
|
|
79
|
+
A device's availability in Home Assistant follows the `UNREACH` datapoint of its maintenance channel. The node
|
|
80
|
+
publishes its current value (retained) right after the discovery configs and every change after that, so a device
|
|
81
|
+
is available without ever having been unreachable. Any message on the node's input republishes both; the example
|
|
82
|
+
wires Home Assistant's birth message (`homeassistant/status`) to it, so a Home Assistant restart gets them again.
|
|
83
|
+
|
|
79
84
|
## Dynamic configuration
|
|
80
85
|
|
|
81
86
|
Most nodes can be reconfigured per message: put an object in `msg.config` and its
|
|
@@ -81,6 +81,22 @@
|
|
|
81
81
|
"y": 180,
|
|
82
82
|
"wires": [["ccu-ha-example"]]
|
|
83
83
|
},
|
|
84
|
+
{
|
|
85
|
+
"id": "mqtt-in-ha-status",
|
|
86
|
+
"type": "mqtt in",
|
|
87
|
+
"name": "homeassistant/status",
|
|
88
|
+
"topic": "homeassistant/status",
|
|
89
|
+
"qos": "0",
|
|
90
|
+
"datatype": "utf8",
|
|
91
|
+
"broker": "mqtt-broker-example",
|
|
92
|
+
"nl": false,
|
|
93
|
+
"rap": true,
|
|
94
|
+
"rh": 0,
|
|
95
|
+
"inputs": 0,
|
|
96
|
+
"x": 170,
|
|
97
|
+
"y": 220,
|
|
98
|
+
"wires": [["ccu-ha-example"]]
|
|
99
|
+
},
|
|
84
100
|
{
|
|
85
101
|
"id": "comment-ha",
|
|
86
102
|
"type": "comment",
|
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;
|
|
@@ -2345,9 +2354,12 @@ module.exports = function (RED) {
|
|
|
2345
2354
|
this.createClient(iface, {quiet: true});
|
|
2346
2355
|
}
|
|
2347
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};
|
|
2348
2359
|
// a failed init is logged by the retry (InitRetry), not here
|
|
2349
2360
|
this.methodCall(iface, 'init', [initUrl, initId], {quiet: true})
|
|
2350
2361
|
.then(() => {
|
|
2362
|
+
this.initState[iface] = {inFlight: false, at: now(), reinit: false};
|
|
2351
2363
|
// the ping/re-init liveness starts after a successful init only;
|
|
2352
2364
|
// a failed one is retried by rpcInitFailed (task 11)
|
|
2353
2365
|
if (this.ifaceTypes[iface].ping && !this.closing) {
|
|
@@ -2370,10 +2382,57 @@ module.exports = function (RED) {
|
|
|
2370
2382
|
resolve(iface);
|
|
2371
2383
|
}
|
|
2372
2384
|
})
|
|
2373
|
-
.catch((error) =>
|
|
2385
|
+
.catch((error) => {
|
|
2386
|
+
this.initState[iface] = {inFlight: false, at: 0, reinit: false};
|
|
2387
|
+
reject(error);
|
|
2388
|
+
});
|
|
2374
2389
|
});
|
|
2375
2390
|
}
|
|
2376
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
|
+
|
|
2377
2436
|
/**
|
|
2378
2437
|
* Returns the links of a specific channel
|
|
2379
2438
|
* @param {String} iface
|
|
@@ -3457,7 +3516,8 @@ module.exports = function (RED) {
|
|
|
3457
3516
|
this.saveMetadata();
|
|
3458
3517
|
}
|
|
3459
3518
|
|
|
3460
|
-
|
|
3519
|
+
// B-37: a restart that kept the subscription gets a fresh init, no ping
|
|
3520
|
+
if (iface && !this.keptSubscription(iface, 'newDevices')) {
|
|
3461
3521
|
this.deviceCallback(iface);
|
|
3462
3522
|
}
|
|
3463
3523
|
},
|
|
@@ -3498,6 +3558,9 @@ module.exports = function (RED) {
|
|
|
3498
3558
|
|
|
3499
3559
|
this.logger.debug(' >', iface, 'listDevices', JSON.stringify(res));
|
|
3500
3560
|
callback(null, res);
|
|
3561
|
+
if (iface) {
|
|
3562
|
+
this.keptSubscription(iface, 'listDevices');
|
|
3563
|
+
}
|
|
3501
3564
|
},
|
|
3502
3565
|
event: (_, parameters, callback) => {
|
|
3503
3566
|
const iface = this.rpcIface('event', parameters);
|
|
@@ -15,8 +15,13 @@ const READY_ATTEMPTS = 60;
|
|
|
15
15
|
* Companion to a ccu-mqtt node: reuses its topic templates and payload format, so
|
|
16
16
|
* the discovery configs point at the topics ccu-mqtt already publishes/consumes.
|
|
17
17
|
* The node itself only emits (a) the retained discovery configs — wire its output
|
|
18
|
-
* to the same mqtt-out node as the ccu-mqtt node
|
|
19
|
-
* event Home Assistant's event entities need
|
|
18
|
+
* to the same mqtt-out node as the ccu-mqtt node —, (b) the aggregate PRESS
|
|
19
|
+
* event Home Assistant's event entities need and (c) the devices' UNREACH on the
|
|
20
|
+
* availability topics the configs name (B-38): Home Assistant shows an entity
|
|
21
|
+
* "unavailable" until its availability topic delivers a value, and ccu-mqtt
|
|
22
|
+
* publishes UNREACH only when it changes or in its start-up cache burst (which
|
|
23
|
+
* is off by default and lost when the broker is not connected yet), so a device
|
|
24
|
+
* that never went unreachable would stay unavailable for good.
|
|
20
25
|
*/
|
|
21
26
|
module.exports = function (RED) {
|
|
22
27
|
class CcuHomeAssistantNode {
|
|
@@ -43,9 +48,18 @@ module.exports = function (RED) {
|
|
|
43
48
|
|
|
44
49
|
this.ccu.register(this);
|
|
45
50
|
|
|
51
|
+
/** datapointName → {iface, channel, datapoint, topic} of the published availability topics */
|
|
52
|
+
this.availability = new Map();
|
|
53
|
+
/** datapointName → the UNREACH value last published on its topic */
|
|
54
|
+
this.availabilitySent = new Map();
|
|
55
|
+
|
|
46
56
|
this.on('input', (message, send, done) => {
|
|
47
|
-
// any message republishes the discovery configs
|
|
48
|
-
|
|
57
|
+
// any message republishes the discovery configs - except Home Assistant's
|
|
58
|
+
// own "offline" (its last will) when <prefix>/status is wired in
|
|
59
|
+
if (!this.isHomeAssistantOffline(message)) {
|
|
60
|
+
this.publish(true);
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
if (done) {
|
|
50
64
|
done();
|
|
51
65
|
}
|
|
@@ -55,6 +69,11 @@ module.exports = function (RED) {
|
|
|
55
69
|
this.event(message);
|
|
56
70
|
});
|
|
57
71
|
|
|
72
|
+
// B-38: UNREACH from the ReGa's values (cache) and every later event
|
|
73
|
+
this.idUnreachSubscription = this.ccu.subscribe({cache: true, datapoint: 'UNREACH'}, (message) => {
|
|
74
|
+
this.unreach(message);
|
|
75
|
+
});
|
|
76
|
+
|
|
58
77
|
this.on('close', (done) => {
|
|
59
78
|
this._destructor(done);
|
|
60
79
|
});
|
|
@@ -66,8 +85,10 @@ module.exports = function (RED) {
|
|
|
66
85
|
|
|
67
86
|
_destructor(done) {
|
|
68
87
|
this.trace('ccu-homeassistant close');
|
|
88
|
+
this.closed = true;
|
|
69
89
|
clearTimeout(this.readyTimer);
|
|
70
90
|
this.ccu.unsubscribe(this.idEventSubscription);
|
|
91
|
+
this.ccu.unsubscribe(this.idUnreachSubscription);
|
|
71
92
|
this.ccu.deregister(this, done);
|
|
72
93
|
}
|
|
73
94
|
|
|
@@ -207,7 +228,7 @@ module.exports = function (RED) {
|
|
|
207
228
|
topicReplace(mqtt.topicInputSetValue, this.topicMessage(iface, address, dp)),
|
|
208
229
|
};
|
|
209
230
|
|
|
210
|
-
const {messages, missing, empty} = discoveryMessages(ctx);
|
|
231
|
+
const {messages, availability, missing, empty} = discoveryMessages(ctx);
|
|
211
232
|
|
|
212
233
|
for (const address of this.removed) {
|
|
213
234
|
const {topic, payload} = removalMessage(this.prefix, address);
|
|
@@ -218,6 +239,8 @@ module.exports = function (RED) {
|
|
|
218
239
|
this.send({topic, payload, retain: true});
|
|
219
240
|
}
|
|
220
241
|
|
|
242
|
+
this.publishAvailability(availability);
|
|
243
|
+
|
|
221
244
|
if (missing.length > 0) {
|
|
222
245
|
this.warn('selected devices not found on the CCU: ' + missing.join(', '));
|
|
223
246
|
}
|
|
@@ -240,6 +263,88 @@ module.exports = function (RED) {
|
|
|
240
263
|
});
|
|
241
264
|
}
|
|
242
265
|
|
|
266
|
+
/** Home Assistant's last will on `<prefix>/status` - not a reason to republish */
|
|
267
|
+
isHomeAssistantOffline(message) {
|
|
268
|
+
return (
|
|
269
|
+
message &&
|
|
270
|
+
message.topic === this.prefix + '/status' &&
|
|
271
|
+
String(message.payload).trim().toLowerCase() !== 'online'
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* B-38: put the current UNREACH of every published device on its availability
|
|
277
|
+
* topic, retained, right after the discovery configs - from the values the
|
|
278
|
+
* connection holds when they are certain, and from the interface process
|
|
279
|
+
* (getValue, deferred by the connection until the interface is up) in any case.
|
|
280
|
+
* @param {Array<{iface: string, channel: string, datapoint: string, topic: string}>} availability
|
|
281
|
+
*/
|
|
282
|
+
publishAvailability(availability) {
|
|
283
|
+
this.availability = new Map();
|
|
284
|
+
this.availabilitySent = new Map();
|
|
285
|
+
for (const source of availability) {
|
|
286
|
+
const datapointName = source.iface + '.' + source.channel + '.' + source.datapoint;
|
|
287
|
+
this.availability.set(datapointName, source);
|
|
288
|
+
|
|
289
|
+
const cached = this.ccu.values && this.ccu.values[datapointName];
|
|
290
|
+
if (cached && !cached.uncertain && typeof cached.value === 'boolean') {
|
|
291
|
+
this.sendAvailability(datapointName, cached.value, cached);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const known = this.availability;
|
|
295
|
+
this.ccu
|
|
296
|
+
.methodCall(source.iface, 'getValue', [source.channel, source.datapoint], {quiet: true})
|
|
297
|
+
.then((value) => {
|
|
298
|
+
// a republish in between started a new round
|
|
299
|
+
if (this.availability === known) {
|
|
300
|
+
this.sendAvailability(datapointName, Boolean(value));
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
.catch((error) => {
|
|
304
|
+
this.debug('getValue ' + datapointName + ': ' + error.message);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** an UNREACH message of the connection (cache or event) */
|
|
310
|
+
unreach(message) {
|
|
311
|
+
if (message && this.availability.has(message.datapointName) && typeof message.value === 'boolean') {
|
|
312
|
+
this.sendAvailability(message.datapointName, message.value, message);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* One retained UNREACH on an availability topic, in the ccu-mqtt node's payload
|
|
318
|
+
* format; skipped when the topic already carries that value from this node.
|
|
319
|
+
* @param {string} datapointName
|
|
320
|
+
* @param {boolean} value
|
|
321
|
+
* @param {object} [message] the connection's message for the datapoint, if there is one
|
|
322
|
+
*/
|
|
323
|
+
sendAvailability(datapointName, value, message) {
|
|
324
|
+
const source = this.availability.get(datapointName);
|
|
325
|
+
if (this.closed || !source || this.availabilitySent.get(datapointName) === value) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const mqtt = this.mqttNodeId && RED.nodes.getNode(this.mqttNodeId);
|
|
330
|
+
if (!mqtt || typeof mqtt.output !== 'function') {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const now = Date.now();
|
|
335
|
+
const out = {
|
|
336
|
+
ts: now,
|
|
337
|
+
lc: now,
|
|
338
|
+
...message,
|
|
339
|
+
...this.topicMessage(source.iface, source.channel, source.datapoint),
|
|
340
|
+
payload: value,
|
|
341
|
+
value,
|
|
342
|
+
};
|
|
343
|
+
delete out.topic;
|
|
344
|
+
this.availabilitySent.set(datapointName, value);
|
|
345
|
+
this.send({topic: source.topic, payload: mqtt.output(out), retain: true});
|
|
346
|
+
}
|
|
347
|
+
|
|
243
348
|
/** key presses: Home Assistant's event entity wants one topic per key → <channel>/PRESS */
|
|
244
349
|
event(message) {
|
|
245
350
|
if (!message.datapoint || !message.datapoint.startsWith('PRESS_') || !message.payload) {
|
package/nodes/lib/hadiscovery.js
CHANGED
|
@@ -175,12 +175,21 @@ function resolveVirtualReceivers(channels) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/**
|
|
179
|
+
* The datapoint behind a device's availability topic (the maintenance channel's UNREACH).
|
|
180
|
+
* @typedef {object} Availability
|
|
181
|
+
* @property {string} iface
|
|
182
|
+
* @property {string} channel address of the maintenance channel (…:0)
|
|
183
|
+
* @property {string} datapoint always UNREACH
|
|
184
|
+
* @property {string} topic the status topic the discovery config names in `avty`
|
|
185
|
+
*/
|
|
186
|
+
|
|
178
187
|
/**
|
|
179
188
|
* Builds the discovery block of one device.
|
|
180
189
|
* @param {DiscoveryContext} ctx
|
|
181
190
|
* @param {string} iface
|
|
182
191
|
* @param {object} device device metadata (ADDRESS, TYPE, CHILDREN, FIRMWARE)
|
|
183
|
-
* @returns {{id: string, device: object, components: object, availability: object[]} | null}
|
|
192
|
+
* @returns {{id: string, device: object, components: object, availability: object[], unreach?: Availability} | null}
|
|
184
193
|
* null when the device yields no entity
|
|
185
194
|
*/
|
|
186
195
|
function deviceBlock(ctx, iface, device) {
|
|
@@ -752,13 +761,19 @@ function deviceBlock(ctx, iface, device) {
|
|
|
752
761
|
return null;
|
|
753
762
|
}
|
|
754
763
|
|
|
755
|
-
// availability: the device's UNREACH
|
|
756
|
-
//
|
|
764
|
+
// availability: the device's UNREACH — there is no bridge "connected" topic in the
|
|
765
|
+
// ccu-mqtt topic scheme. HA shows every entity "unavailable" until this topic delivers
|
|
766
|
+
// a value, and ccu-mqtt publishes UNREACH only on a change or in its start-up cache
|
|
767
|
+
// burst, so the ccu-homeassistant node publishes the current value itself (B-38):
|
|
768
|
+
// `unreach` tells it which datapoint and topic.
|
|
757
769
|
// (an entry of the availability list takes `val_tpl`; `avty_tpl` is only valid at
|
|
758
770
|
// the top level next to `avty_t` — HA rejects the whole device payload otherwise)
|
|
759
771
|
const availability = [];
|
|
772
|
+
let unreach;
|
|
760
773
|
if (maintenance && maintenance.description.UNREACH) {
|
|
761
|
-
|
|
774
|
+
const topic = st(iface, maintenance.ADDRESS, 'UNREACH');
|
|
775
|
+
availability.push({t: topic, val_tpl: t.bool('offline', 'online')});
|
|
776
|
+
unreach = {iface, channel: maintenance.ADDRESS, datapoint: 'UNREACH', topic};
|
|
762
777
|
}
|
|
763
778
|
|
|
764
779
|
return {
|
|
@@ -772,6 +787,7 @@ function deviceBlock(ctx, iface, device) {
|
|
|
772
787
|
}),
|
|
773
788
|
components,
|
|
774
789
|
availability,
|
|
790
|
+
unreach,
|
|
775
791
|
};
|
|
776
792
|
}
|
|
777
793
|
|
|
@@ -807,11 +823,13 @@ function removalMessage(prefix, address) {
|
|
|
807
823
|
/**
|
|
808
824
|
* Discovery messages for the selected devices.
|
|
809
825
|
* @param {DiscoveryContext} ctx
|
|
810
|
-
* @returns {{messages: Array<{topic: string, payload: object}>, missing: string[], empty: string[]}}
|
|
826
|
+
* @returns {{messages: Array<{topic: string, payload: object}>, availability: Availability[], missing: string[], empty: string[]}}
|
|
827
|
+
* availability: the UNREACH datapoints the configs use as availability (B-38);
|
|
811
828
|
* missing: selected addresses not found in the metadata; empty: devices without entities
|
|
812
829
|
*/
|
|
813
830
|
function discoveryMessages(ctx) {
|
|
814
831
|
const messages = [];
|
|
832
|
+
const availability = [];
|
|
815
833
|
const missing = [];
|
|
816
834
|
const empty = [];
|
|
817
835
|
for (const address of ctx.selected || []) {
|
|
@@ -829,9 +847,12 @@ function discoveryMessages(ctx) {
|
|
|
829
847
|
}
|
|
830
848
|
|
|
831
849
|
messages.push(devicePayload(ctx, block));
|
|
850
|
+
if (block.unreach) {
|
|
851
|
+
availability.push(block.unreach);
|
|
852
|
+
}
|
|
832
853
|
}
|
|
833
854
|
|
|
834
|
-
return {messages, missing, empty};
|
|
855
|
+
return {messages, availability, missing, empty};
|
|
835
856
|
}
|
|
836
857
|
|
|
837
858
|
module.exports = {
|
|
@@ -16,8 +16,16 @@
|
|
|
16
16
|
Option <i>weitere Datenpunkte</i> werden alle übrigen Datenpunkte als standardmäßig deaktivierte
|
|
17
17
|
Entitäten mitgeliefert.</p>
|
|
18
18
|
<h3>Eingang</h3>
|
|
19
|
-
<p>Jede eingehende Nachricht veröffentlicht die Discovery-Konfigurationen
|
|
19
|
+
<p>Jede eingehende Nachricht veröffentlicht die Discovery-Konfigurationen und die Verfügbarkeit
|
|
20
|
+
erneut. Wer einen <b>mqtt in</b>-Node auf <code>homeassistant/status</code> (bzw.
|
|
21
|
+
<code><Präfix>/status</code>) anschließt, bekommt beides nach jedem Neustart von Home Assistant
|
|
22
|
+
neu; dessen <code>offline</code> auf diesem Topic wird ignoriert.</p>
|
|
20
23
|
<h3>Voraussetzungen</h3>
|
|
21
|
-
<p>Home Assistant ≥ 2024.11 (gerätebasierte Discovery)
|
|
22
|
-
|
|
24
|
+
<p>Home Assistant ≥ 2024.11 (gerätebasierte Discovery).</p>
|
|
25
|
+
<h3>Verfügbarkeit</h3>
|
|
26
|
+
<p>Die Verfügbarkeit der Geräte wird aus dem UNREACH-Datenpunkt des Wartungskanals abgeleitet
|
|
27
|
+
(<code>…:0/UNREACH</code>). Home Assistant zeigt alle Entitäten eines Geräts als „nicht verfügbar“,
|
|
28
|
+
bis auf diesem Topic ein Wert ankommt. Deshalb veröffentlicht der Node direkt nach den
|
|
29
|
+
Discovery-Konfigurationen den aktuellen UNREACH-Wert jedes Geräts (retained), fragt ihn dazu bei der
|
|
30
|
+
CCU ab und sendet jede Änderung. Ein Gerät, das nie unerreichbar war, ist so trotzdem verfügbar.</p>
|
|
23
31
|
</script>
|
|
@@ -16,8 +16,16 @@
|
|
|
16
16
|
<i>other datapoints</i> option every remaining datapoint is included as an entity that is
|
|
17
17
|
disabled by default.</p>
|
|
18
18
|
<h3>Input</h3>
|
|
19
|
-
<p>Any incoming message republishes the discovery configs
|
|
19
|
+
<p>Any incoming message republishes the discovery configs and the availability. Wire an
|
|
20
|
+
<b>mqtt in</b> node on <code>homeassistant/status</code> (or <code><prefix>/status</code>) to
|
|
21
|
+
the input to get both again after every Home Assistant restart; its <code>offline</code> on that
|
|
22
|
+
topic is ignored.</p>
|
|
20
23
|
<h3>Requirements</h3>
|
|
21
|
-
<p>Home Assistant ≥ 2024.11 (device-based discovery)
|
|
22
|
-
|
|
24
|
+
<p>Home Assistant ≥ 2024.11 (device-based discovery).</p>
|
|
25
|
+
<h3>Availability</h3>
|
|
26
|
+
<p>Device availability is derived from the UNREACH datapoint of the maintenance channel
|
|
27
|
+
(<code>…:0/UNREACH</code>). Home Assistant shows every entity of a device as "unavailable" until
|
|
28
|
+
a value arrives on that topic. So right after the discovery configs the node publishes each
|
|
29
|
+
device's current UNREACH (retained), asking the CCU for it, and sends every change. A device that
|
|
30
|
+
was never unreachable is available all the same.</p>
|
|
23
31
|
</script>
|