node-red-contrib-ccu 4.5.2 → 4.6.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 +40 -0
- package/README.md +14 -0
- package/examples/home-assistant.json +16 -0
- package/nodes/ccu-homeassistant.html +10 -0
- package/nodes/ccu-homeassistant.js +112 -5
- package/nodes/lib/hadiscovery.js +59 -14
- package/nodes/locales/de/ccu-homeassistant.html +22 -3
- package/nodes/locales/en-US/ccu-homeassistant.html +22 -3
- package/package.json +1 -1
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.6.0 (2026-10-22)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **A device is created in Home Assistant in the room of its channels.** The
|
|
13
|
+
discovery config carries the room as the device's `suggested_area` (task
|
|
14
|
+
18). It did so before only when every channel of the device lay in one and
|
|
15
|
+
the same room: a device with channels in two rooms got no area, and on
|
|
16
|
+
openccu-lite, whose rooms can be nested and whose room list of a channel
|
|
17
|
+
names the parents too (`Küche`, `Erdgeschoss`), no device got one at all.
|
|
18
|
+
Now the area is the room shared by all channels of the device that lie in a
|
|
19
|
+
room, otherwise the room of the lowest-numbered channel that lies in one; a
|
|
20
|
+
channel in several rooms counts with its first (on openccu-lite the most
|
|
21
|
+
specific), and a room of the device itself comes first. The rooms are the
|
|
22
|
+
ReGaHSS's on a CCU and the system's metadata's on openccu-lite. New option
|
|
23
|
+
_Suggest the channel's room as the device's area_ on the node, on by
|
|
24
|
+
default. Home Assistant applies the suggestion only when it first creates
|
|
25
|
+
the device; a room changed later stays as assigned in Home Assistant.
|
|
26
|
+
|
|
27
|
+
## 4.5.3 (2026-10-05)
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- **Home Assistant showed every entity of a device as "unavailable" for
|
|
32
|
+
good** (B-38, reported by mail). The discovery configs name the device's
|
|
33
|
+
`…:0/UNREACH` topic as its availability, and Home Assistant treats an
|
|
34
|
+
entity as unavailable until that topic delivers a value. Only `ccu-mqtt`
|
|
35
|
+
published UNREACH: when it changed, or once in its start-up burst of
|
|
36
|
+
cached values - which is off unless the node's _Cache_ option is ticked,
|
|
37
|
+
and lost when the MQTT broker is not connected yet at that moment (Node-RED's
|
|
38
|
+
`mqtt out` drops what it gets while disconnected). A device that never went
|
|
39
|
+
unreachable therefore never got a value on that topic, while its state
|
|
40
|
+
updates arrived normally. The `ccu-homeassistant` node now publishes each
|
|
41
|
+
device's current UNREACH itself, retained, right after the discovery
|
|
42
|
+
configs (asking the CCU for it) and every change after that; _republish
|
|
43
|
+
discovery_ sends it again. The example flow also wires Home Assistant's
|
|
44
|
+
birth message (`homeassistant/status`) to the node, so a Home Assistant
|
|
45
|
+
restart republishes discovery and availability; an `offline` there is
|
|
46
|
+
ignored.
|
|
47
|
+
|
|
8
48
|
## 4.5.2 (2026-10-02)
|
|
9
49
|
|
|
10
50
|
### Fixed
|
package/README.md
CHANGED
|
@@ -76,6 +76,20 @@ 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
|
+
|
|
84
|
+
The device is created in the room of its channels: the discovery config carries it as the device's `suggested_area`
|
|
85
|
+
(Home Assistant's areas are per device, the CCU's rooms per channel). The rule: the room shared by all channels of the
|
|
86
|
+
device that lie in a room, otherwise the room of the lowest-numbered channel that lies in one; a channel in several
|
|
87
|
+
rooms counts with its first - on openccu-lite, where rooms can be nested, with the most specific one - and a room of
|
|
88
|
+
the device itself comes first. The rooms come from the ReGaHSS on a CCU and from the system's metadata on openccu-lite.
|
|
89
|
+
Home Assistant applies the suggestion only when it first creates the device; a room changed later on the CCU stays as
|
|
90
|
+
assigned in Home Assistant (change it there, or delete the device in Home Assistant and republish the discovery). The
|
|
91
|
+
node's option _Suggest the channel's room as the device's area_ (on by default) switches it off.
|
|
92
|
+
|
|
79
93
|
## Dynamic configuration
|
|
80
94
|
|
|
81
95
|
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",
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
mqttNode: {value: '', required: true},
|
|
8
8
|
prefix: {value: 'homeassistant'},
|
|
9
9
|
generic: {value: true},
|
|
10
|
+
area: {value: true},
|
|
10
11
|
devices: {value: {}},
|
|
11
12
|
},
|
|
12
13
|
inputs: 1,
|
|
@@ -267,6 +268,15 @@
|
|
|
267
268
|
</label>
|
|
268
269
|
</div>
|
|
269
270
|
</div>
|
|
271
|
+
<div class="form-row">
|
|
272
|
+
<label for="node-input-area"></label>
|
|
273
|
+
<div style="width: 70%; display: inline-block; vertical-align: text-top;">
|
|
274
|
+
<label class="ccu-checkbox">
|
|
275
|
+
<input type="checkbox" id="node-input-area">
|
|
276
|
+
<span>Suggest the channel's room as the device's area</span>
|
|
277
|
+
</label>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
270
280
|
|
|
271
281
|
<div class="form-row">
|
|
272
282
|
<b>Devices published to Home Assistant</b>
|
|
@@ -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 {
|
|
@@ -32,6 +37,7 @@ module.exports = function (RED) {
|
|
|
32
37
|
this.mqttNodeId = config.mqttNode;
|
|
33
38
|
this.prefix = String(config.prefix || 'homeassistant').replace(/\/+$/, '');
|
|
34
39
|
this.generic = config.generic !== false;
|
|
40
|
+
this.area = config.area !== false;
|
|
35
41
|
this.devices = config.devices || {};
|
|
36
42
|
this.selected = Object.keys(this.devices).filter(
|
|
37
43
|
(a) => this.devices[a] && this.devices[a].enabled === true,
|
|
@@ -43,9 +49,18 @@ module.exports = function (RED) {
|
|
|
43
49
|
|
|
44
50
|
this.ccu.register(this);
|
|
45
51
|
|
|
52
|
+
/** datapointName → {iface, channel, datapoint, topic} of the published availability topics */
|
|
53
|
+
this.availability = new Map();
|
|
54
|
+
/** datapointName → the UNREACH value last published on its topic */
|
|
55
|
+
this.availabilitySent = new Map();
|
|
56
|
+
|
|
46
57
|
this.on('input', (message, send, done) => {
|
|
47
|
-
// any message republishes the discovery configs
|
|
48
|
-
|
|
58
|
+
// any message republishes the discovery configs - except Home Assistant's
|
|
59
|
+
// own "offline" (its last will) when <prefix>/status is wired in
|
|
60
|
+
if (!this.isHomeAssistantOffline(message)) {
|
|
61
|
+
this.publish(true);
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
if (done) {
|
|
50
65
|
done();
|
|
51
66
|
}
|
|
@@ -55,6 +70,11 @@ module.exports = function (RED) {
|
|
|
55
70
|
this.event(message);
|
|
56
71
|
});
|
|
57
72
|
|
|
73
|
+
// B-38: UNREACH from the ReGa's values (cache) and every later event
|
|
74
|
+
this.idUnreachSubscription = this.ccu.subscribe({cache: true, datapoint: 'UNREACH'}, (message) => {
|
|
75
|
+
this.unreach(message);
|
|
76
|
+
});
|
|
77
|
+
|
|
58
78
|
this.on('close', (done) => {
|
|
59
79
|
this._destructor(done);
|
|
60
80
|
});
|
|
@@ -66,8 +86,10 @@ module.exports = function (RED) {
|
|
|
66
86
|
|
|
67
87
|
_destructor(done) {
|
|
68
88
|
this.trace('ccu-homeassistant close');
|
|
89
|
+
this.closed = true;
|
|
69
90
|
clearTimeout(this.readyTimer);
|
|
70
91
|
this.ccu.unsubscribe(this.idEventSubscription);
|
|
92
|
+
this.ccu.unsubscribe(this.idUnreachSubscription);
|
|
71
93
|
this.ccu.deregister(this, done);
|
|
72
94
|
}
|
|
73
95
|
|
|
@@ -193,6 +215,7 @@ module.exports = function (RED) {
|
|
|
193
215
|
origin: {name: pkg.name, sw: pkg.version, url: pkg.homepage},
|
|
194
216
|
jsonPayloads: mqtt.payloadOutput !== 'plain',
|
|
195
217
|
generic: this.generic,
|
|
218
|
+
area: this.area,
|
|
196
219
|
devices: ccu.metadata.devices,
|
|
197
220
|
selected: this.selected,
|
|
198
221
|
description: (iface, address) => {
|
|
@@ -207,7 +230,7 @@ module.exports = function (RED) {
|
|
|
207
230
|
topicReplace(mqtt.topicInputSetValue, this.topicMessage(iface, address, dp)),
|
|
208
231
|
};
|
|
209
232
|
|
|
210
|
-
const {messages, missing, empty} = discoveryMessages(ctx);
|
|
233
|
+
const {messages, availability, missing, empty} = discoveryMessages(ctx);
|
|
211
234
|
|
|
212
235
|
for (const address of this.removed) {
|
|
213
236
|
const {topic, payload} = removalMessage(this.prefix, address);
|
|
@@ -218,6 +241,8 @@ module.exports = function (RED) {
|
|
|
218
241
|
this.send({topic, payload, retain: true});
|
|
219
242
|
}
|
|
220
243
|
|
|
244
|
+
this.publishAvailability(availability);
|
|
245
|
+
|
|
221
246
|
if (missing.length > 0) {
|
|
222
247
|
this.warn('selected devices not found on the CCU: ' + missing.join(', '));
|
|
223
248
|
}
|
|
@@ -240,6 +265,88 @@ module.exports = function (RED) {
|
|
|
240
265
|
});
|
|
241
266
|
}
|
|
242
267
|
|
|
268
|
+
/** Home Assistant's last will on `<prefix>/status` - not a reason to republish */
|
|
269
|
+
isHomeAssistantOffline(message) {
|
|
270
|
+
return (
|
|
271
|
+
message &&
|
|
272
|
+
message.topic === this.prefix + '/status' &&
|
|
273
|
+
String(message.payload).trim().toLowerCase() !== 'online'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* B-38: put the current UNREACH of every published device on its availability
|
|
279
|
+
* topic, retained, right after the discovery configs - from the values the
|
|
280
|
+
* connection holds when they are certain, and from the interface process
|
|
281
|
+
* (getValue, deferred by the connection until the interface is up) in any case.
|
|
282
|
+
* @param {Array<{iface: string, channel: string, datapoint: string, topic: string}>} availability
|
|
283
|
+
*/
|
|
284
|
+
publishAvailability(availability) {
|
|
285
|
+
this.availability = new Map();
|
|
286
|
+
this.availabilitySent = new Map();
|
|
287
|
+
for (const source of availability) {
|
|
288
|
+
const datapointName = source.iface + '.' + source.channel + '.' + source.datapoint;
|
|
289
|
+
this.availability.set(datapointName, source);
|
|
290
|
+
|
|
291
|
+
const cached = this.ccu.values && this.ccu.values[datapointName];
|
|
292
|
+
if (cached && !cached.uncertain && typeof cached.value === 'boolean') {
|
|
293
|
+
this.sendAvailability(datapointName, cached.value, cached);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const known = this.availability;
|
|
297
|
+
this.ccu
|
|
298
|
+
.methodCall(source.iface, 'getValue', [source.channel, source.datapoint], {quiet: true})
|
|
299
|
+
.then((value) => {
|
|
300
|
+
// a republish in between started a new round
|
|
301
|
+
if (this.availability === known) {
|
|
302
|
+
this.sendAvailability(datapointName, Boolean(value));
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
.catch((error) => {
|
|
306
|
+
this.debug('getValue ' + datapointName + ': ' + error.message);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** an UNREACH message of the connection (cache or event) */
|
|
312
|
+
unreach(message) {
|
|
313
|
+
if (message && this.availability.has(message.datapointName) && typeof message.value === 'boolean') {
|
|
314
|
+
this.sendAvailability(message.datapointName, message.value, message);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* One retained UNREACH on an availability topic, in the ccu-mqtt node's payload
|
|
320
|
+
* format; skipped when the topic already carries that value from this node.
|
|
321
|
+
* @param {string} datapointName
|
|
322
|
+
* @param {boolean} value
|
|
323
|
+
* @param {object} [message] the connection's message for the datapoint, if there is one
|
|
324
|
+
*/
|
|
325
|
+
sendAvailability(datapointName, value, message) {
|
|
326
|
+
const source = this.availability.get(datapointName);
|
|
327
|
+
if (this.closed || !source || this.availabilitySent.get(datapointName) === value) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const mqtt = this.mqttNodeId && RED.nodes.getNode(this.mqttNodeId);
|
|
332
|
+
if (!mqtt || typeof mqtt.output !== 'function') {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
const out = {
|
|
338
|
+
ts: now,
|
|
339
|
+
lc: now,
|
|
340
|
+
...message,
|
|
341
|
+
...this.topicMessage(source.iface, source.channel, source.datapoint),
|
|
342
|
+
payload: value,
|
|
343
|
+
value,
|
|
344
|
+
};
|
|
345
|
+
delete out.topic;
|
|
346
|
+
this.availabilitySent.set(datapointName, value);
|
|
347
|
+
this.send({topic: source.topic, payload: mqtt.output(out), retain: true});
|
|
348
|
+
}
|
|
349
|
+
|
|
243
350
|
/** key presses: Home Assistant's event entity wants one topic per key → <channel>/PRESS */
|
|
244
351
|
event(message) {
|
|
245
352
|
if (!message.datapoint || !message.datapoint.startsWith('PRESS_') || !message.payload) {
|
package/nodes/lib/hadiscovery.js
CHANGED
|
@@ -51,7 +51,9 @@ const BINARY_ONLY_CLASSES = new Set([
|
|
|
51
51
|
* @property {string[]} selected device addresses to publish
|
|
52
52
|
* @property {(iface: string, address: string) => object | undefined} description VALUES description of a channel
|
|
53
53
|
* @property {(address: string) => string | undefined} channelName
|
|
54
|
-
* @property {(address: string) => string[] | undefined} [rooms]
|
|
54
|
+
* @property {(address: string) => string[] | undefined} [rooms] the rooms of a channel (or device), the
|
|
55
|
+
* most specific first where rooms are nested (openccu-lite)
|
|
56
|
+
* @property {boolean} [area] the room becomes the device's `suggested_area` (default true, task 18)
|
|
55
57
|
* @property {(iface: string, address: string, datapoint: string) => string} statusTopicFor
|
|
56
58
|
* @property {(iface: string, address: string, datapoint: string) => string} setTopicFor
|
|
57
59
|
*/
|
|
@@ -175,12 +177,47 @@ function resolveVirtualReceivers(channels) {
|
|
|
175
177
|
}
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/**
|
|
181
|
+
* The datapoint behind a device's availability topic (the maintenance channel's UNREACH).
|
|
182
|
+
* @typedef {object} Availability
|
|
183
|
+
* @property {string} iface
|
|
184
|
+
* @property {string} channel address of the maintenance channel (…:0)
|
|
185
|
+
* @property {string} datapoint always UNREACH
|
|
186
|
+
* @property {string} topic the status topic the discovery config names in `avty`
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The room Home Assistant is to suggest as the device's area (task 18). Home
|
|
191
|
+
* Assistant's area is per device, the CCU's rooms are per channel, so: a room
|
|
192
|
+
* every channel that lies in a room shares - the first one in the order of the
|
|
193
|
+
* lowest-numbered such channel - or, when they share none, the first room of
|
|
194
|
+
* the lowest-numbered channel that has one. A room of the device itself (an
|
|
195
|
+
* openccu-lite device may be in a room as a whole) comes first. Home Assistant
|
|
196
|
+
* applies `suggested_area` only when it creates the device; a room changed
|
|
197
|
+
* later stays as assigned in Home Assistant.
|
|
198
|
+
* @param {(address: string) => string[] | undefined} roomsOf
|
|
199
|
+
* @param {string} deviceAddress
|
|
200
|
+
* @param {Array<{ADDRESS: string}>} channels sorted by channel index
|
|
201
|
+
* @returns {string | undefined}
|
|
202
|
+
*/
|
|
203
|
+
function suggestedArea(roomsOf, deviceAddress, channels) {
|
|
204
|
+
const lists = [deviceAddress, ...channels.map((ch) => ch.ADDRESS)]
|
|
205
|
+
.map((address) => roomsOf(address))
|
|
206
|
+
.filter((rooms) => Array.isArray(rooms) && rooms.length > 0);
|
|
207
|
+
if (lists.length === 0) {
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const [first, ...rest] = lists;
|
|
212
|
+
return first.find((room) => rest.every((rooms) => rooms.includes(room))) || first[0];
|
|
213
|
+
}
|
|
214
|
+
|
|
178
215
|
/**
|
|
179
216
|
* Builds the discovery block of one device.
|
|
180
217
|
* @param {DiscoveryContext} ctx
|
|
181
218
|
* @param {string} iface
|
|
182
219
|
* @param {object} device device metadata (ADDRESS, TYPE, CHILDREN, FIRMWARE)
|
|
183
|
-
* @returns {{id: string, device: object, components: object, availability: object[]} | null}
|
|
220
|
+
* @returns {{id: string, device: object, components: object, availability: object[], unreach?: Availability} | null}
|
|
184
221
|
* null when the device yields no entity
|
|
185
222
|
*/
|
|
186
223
|
function deviceBlock(ctx, iface, device) {
|
|
@@ -215,12 +252,7 @@ function deviceBlock(ctx, iface, device) {
|
|
|
215
252
|
components[key] = e;
|
|
216
253
|
};
|
|
217
254
|
|
|
218
|
-
const
|
|
219
|
-
for (const ch of channels) {
|
|
220
|
-
for (const r of roomsOf(ch.ADDRESS) || []) {
|
|
221
|
-
rooms.add(r);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
255
|
+
const area = ctx.area === false ? undefined : suggestedArea(roomsOf, device.ADDRESS, channels);
|
|
224
256
|
|
|
225
257
|
const maintenance = channels.find((ch) => ch.index === 0);
|
|
226
258
|
|
|
@@ -752,13 +784,19 @@ function deviceBlock(ctx, iface, device) {
|
|
|
752
784
|
return null;
|
|
753
785
|
}
|
|
754
786
|
|
|
755
|
-
// availability: the device's UNREACH
|
|
756
|
-
//
|
|
787
|
+
// availability: the device's UNREACH — there is no bridge "connected" topic in the
|
|
788
|
+
// ccu-mqtt topic scheme. HA shows every entity "unavailable" until this topic delivers
|
|
789
|
+
// a value, and ccu-mqtt publishes UNREACH only on a change or in its start-up cache
|
|
790
|
+
// burst, so the ccu-homeassistant node publishes the current value itself (B-38):
|
|
791
|
+
// `unreach` tells it which datapoint and topic.
|
|
757
792
|
// (an entry of the availability list takes `val_tpl`; `avty_tpl` is only valid at
|
|
758
793
|
// the top level next to `avty_t` — HA rejects the whole device payload otherwise)
|
|
759
794
|
const availability = [];
|
|
795
|
+
let unreach;
|
|
760
796
|
if (maintenance && maintenance.description.UNREACH) {
|
|
761
|
-
|
|
797
|
+
const topic = st(iface, maintenance.ADDRESS, 'UNREACH');
|
|
798
|
+
availability.push({t: topic, val_tpl: t.bool('offline', 'online')});
|
|
799
|
+
unreach = {iface, channel: maintenance.ADDRESS, datapoint: 'UNREACH', topic};
|
|
762
800
|
}
|
|
763
801
|
|
|
764
802
|
return {
|
|
@@ -768,10 +806,11 @@ function deviceBlock(ctx, iface, device) {
|
|
|
768
806
|
mf: 'eQ-3',
|
|
769
807
|
mdl: device.TYPE,
|
|
770
808
|
sw: device.FIRMWARE ? String(device.FIRMWARE) : undefined,
|
|
771
|
-
sa:
|
|
809
|
+
sa: area,
|
|
772
810
|
}),
|
|
773
811
|
components,
|
|
774
812
|
availability,
|
|
813
|
+
unreach,
|
|
775
814
|
};
|
|
776
815
|
}
|
|
777
816
|
|
|
@@ -807,11 +846,13 @@ function removalMessage(prefix, address) {
|
|
|
807
846
|
/**
|
|
808
847
|
* Discovery messages for the selected devices.
|
|
809
848
|
* @param {DiscoveryContext} ctx
|
|
810
|
-
* @returns {{messages: Array<{topic: string, payload: object}>, missing: string[], empty: string[]}}
|
|
849
|
+
* @returns {{messages: Array<{topic: string, payload: object}>, availability: Availability[], missing: string[], empty: string[]}}
|
|
850
|
+
* availability: the UNREACH datapoints the configs use as availability (B-38);
|
|
811
851
|
* missing: selected addresses not found in the metadata; empty: devices without entities
|
|
812
852
|
*/
|
|
813
853
|
function discoveryMessages(ctx) {
|
|
814
854
|
const messages = [];
|
|
855
|
+
const availability = [];
|
|
815
856
|
const missing = [];
|
|
816
857
|
const empty = [];
|
|
817
858
|
for (const address of ctx.selected || []) {
|
|
@@ -829,9 +870,12 @@ function discoveryMessages(ctx) {
|
|
|
829
870
|
}
|
|
830
871
|
|
|
831
872
|
messages.push(devicePayload(ctx, block));
|
|
873
|
+
if (block.unreach) {
|
|
874
|
+
availability.push(block.unreach);
|
|
875
|
+
}
|
|
832
876
|
}
|
|
833
877
|
|
|
834
|
-
return {messages, missing, empty};
|
|
878
|
+
return {messages, availability, missing, empty};
|
|
835
879
|
}
|
|
836
880
|
|
|
837
881
|
module.exports = {
|
|
@@ -839,6 +883,7 @@ module.exports = {
|
|
|
839
883
|
discoveryTopic,
|
|
840
884
|
ifaceOf,
|
|
841
885
|
resolveVirtualReceivers,
|
|
886
|
+
suggestedArea,
|
|
842
887
|
deviceBlock,
|
|
843
888
|
devicePayload,
|
|
844
889
|
removalMessage,
|
|
@@ -16,8 +16,27 @@
|
|
|
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>
|
|
31
|
+
<h3>Raum und Bereich</h3>
|
|
32
|
+
<p>Home Assistant ordnet Geräte Bereichen zu, die CCU Kanäle Räumen. Der Node schlägt Home Assistant den
|
|
33
|
+
Raum der Kanäle als Bereich des Geräts vor (<code>suggested_area</code>): den Raum, den alle Kanäle des
|
|
34
|
+
Geräts teilen, die in einem Raum liegen, sonst den Raum des Kanals mit der niedrigsten Nummer, der in
|
|
35
|
+
einem liegt. Liegt ein Kanal in mehreren Räumen, zählt der erste; auf openccu-lite, wo Räume verschachtelt
|
|
36
|
+
sein können, der speziellste (das Zimmer vor dem Stockwerk), und ein Raum des Geräts selbst geht vor. Die
|
|
37
|
+
Räume kommen auf einer CCU aus der ReGaHSS, auf openccu-lite aus den Metadaten des Systems.</p>
|
|
38
|
+
<p>Home Assistant übernimmt den Vorschlag nur, wenn es das Gerät neu anlegt. Ein später auf der CCU
|
|
39
|
+
geänderter Raum bleibt in Home Assistant, wie er dort zugeordnet ist: dort ändern, oder das Gerät in Home
|
|
40
|
+
Assistant löschen und die Discovery erneut veröffentlichen. Die Option <i>Raum als Bereich vorschlagen</i>
|
|
41
|
+
schaltet den Vorschlag ab.</p>
|
|
23
42
|
</script>
|
|
@@ -16,8 +16,27 @@
|
|
|
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>
|
|
31
|
+
<h3>Room and area</h3>
|
|
32
|
+
<p>Home Assistant assigns devices to areas, the CCU assigns channels to rooms. The node suggests the
|
|
33
|
+
channels' room as the device's area (<code>suggested_area</code>): the room shared by all channels of
|
|
34
|
+
the device that lie in a room, otherwise the room of the lowest-numbered channel that lies in one. A
|
|
35
|
+
channel in several rooms counts with its first; on openccu-lite, where rooms can be nested, with the most
|
|
36
|
+
specific one (the room before the floor), and a room of the device itself comes first. The rooms come
|
|
37
|
+
from the ReGaHSS on a CCU and from the system's metadata on openccu-lite.</p>
|
|
38
|
+
<p>Home Assistant applies the suggestion only when it creates the device. A room changed later on the
|
|
39
|
+
CCU stays as assigned in Home Assistant: change it there, or delete the device in Home Assistant and
|
|
40
|
+
republish the discovery. The option <i>Suggest the channel's room as the device's area</i> switches the
|
|
41
|
+
suggestion off.</p>
|
|
23
42
|
</script>
|