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 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 — and (b) the aggregate PRESS
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
- this.publish(true);
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) {
@@ -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 rooms = new Set();
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 (retained by ccu-mqtt) — there is no
756
- // bridge "connected" topic in the ccu-mqtt topic scheme
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
- availability.push({t: st(iface, maintenance.ADDRESS, 'UNREACH'), val_tpl: t.bool('offline', 'online')});
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: rooms.size === 1 ? [...rooms][0] : undefined,
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 erneut.</p>
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>&lt;Präfix&gt;/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). Die Verfügbarkeit der Geräte wird aus dem
22
- UNREACH-Datenpunkt des Wartungskanals abgeleitet.</p>
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.</p>
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>&lt;prefix&gt;/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). Device availability is derived from the
22
- UNREACH datapoint of the maintenance channel.</p>
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-ccu",
3
- "version": "4.5.2",
3
+ "version": "4.6.0",
4
4
  "license": "MIT",
5
5
  "main": "none",
6
6
  "keywords": [