node-red-contrib-knx-ultimate 5.0.0 → 5.0.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 CHANGED
@@ -6,6 +6,21 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 5.0.2** - July 2026<br/>
10
+
11
+ - **IoT Bridge** node, **MQTT / Home Assistant (native)** mode:<br/>
12
+ - New **Read only** flag per group address in the *Group addresses to expose* list: a read-only GA is still published to Home Assistant (its state stays visible) but never accepts commands back to the KNX bus (switches are exposed as `binary_sensor`, numbers/text as `sensor`). Added *Set read only* / *Clear read only* buttons that apply it to all currently shown (filtered) addresses.<br/>
13
+ - New **KNX bus connection** selector: *Stand-alone* (default, talks to the KNX gateway directly) or *Flow messages*, which enables the node's input/output pins — wire a **KNXUltimate** node in Universal mode to the input (KNX bus → MQTT) and another to the output (MQTT → KNX bus). Pins are shown only when needed (none in stand-alone, one input + one output in flow mode).<br/>
14
+ - Editor help and online docs updated in EN/IT/DE/FR/ES/zh-CN.<br/>
15
+
16
+ **Version 5.0.1** - June 2026<br/>
17
+
18
+ - **IoT Bridge** node (renamed **MQTT - IoT** in the UI): new **MQTT / Home Assistant (native)** mode selectable via a *Mode* dropdown. In this mode the node connects directly to an MQTT broker and bridges KNX ↔ MQTT both ways, publishing **Home Assistant MQTT Discovery** so KNX appears automatically in Home Assistant (no `mqtt in`/`mqtt out` wiring needed). The classic IoT bridge mode is unchanged.<br/>
19
+ - Every group address imported in the KNX gateway (ETS list) can be **exposed automatically** as a Home Assistant entity (switch, sensor, binary_sensor, number, text), typed from its DPT. A checkbox list with filter and *Select all / none* lets you choose exactly which GAs to publish.<br/>
20
+ - New **Covers & Thermostats** editor: composite entities that aggregate several GAs (cover with up/down, stop and position including KNX↔HA position inversion; thermostat with current temperature, setpoint and optional on/off). The cover/thermostat GA fields have **ETS group-address autocomplete**, like the KNX device node.<br/>
21
+ - Robust lifecycle: the MQTT broker is closed gracefully on deploy / Node-RED exit (retained `offline`, forced disconnect, hard-capped so a slow or unreachable broker never blocks a deploy); all MQTT event handlers are fully guarded.<br/>
22
+ - Editor help and online docs updated, plus Home Assistant / MQTT logos on the docs pages. Localized in EN/IT/DE/FR/ES/zh-CN.<br/>
23
+
9
24
  **Version 5.0.0** - June 2026<br/>
10
25
 
11
26
  - KNXUltimate engine: updated to **6.0.1** (see the engine [CHANGELOG](https://github.com/Supergiovane/knxultimate/blob/main/CHANGELOG.md)).<br/>
@@ -981,8 +981,6 @@
981
981
  $("#tabs").tabs();
982
982
  // *****************************
983
983
 
984
-
985
-
986
984
  var sRetDebugText = "";
987
985
  $("#getinfocam").click(function () {
988
986
  sRetDebugText = "";
@@ -1634,7 +1632,7 @@
1634
1632
  </div>
1635
1633
 
1636
1634
  </div>
1637
-
1635
+
1638
1636
  </script>
1639
1637
 
1640
1638
  <script type="text/html" data-help-name="knxUltimate-config">
@@ -11,11 +11,39 @@
11
11
  emitOnChangeOnly: { value: true },
12
12
  readOnDeploy: { value: true },
13
13
  acceptFlowInput: { value: true },
14
- mappings: { value: [] }
14
+ mappings: { value: [] },
15
+ nodeMode: { value: 'iot' },
16
+ haBusMode: { value: 'standalone' },
17
+ inputs: { value: 1 },
18
+ outputs: { value: 2 },
19
+ mqttUrl: { value: '' },
20
+ mqttBaseTopic: { value: 'knx-ultimate' },
21
+ mqttDiscovery: { value: true },
22
+ mqttDiscoveryPrefix: { value: 'homeassistant' },
23
+ mqttCustomEntities: { value: [] },
24
+ mqttExposedGAs: { value: [] },
25
+ mqttReadOnlyGAs: { value: [] },
26
+ mqttExposeConfigured: { value: false }
27
+ },
28
+ credentials: {
29
+ mqttUsername: { type: 'text' },
30
+ mqttPassword: { type: 'password' }
15
31
  },
16
32
  inputs: 1,
17
33
  outputs: 2,
34
+ inputLabels: function () {
35
+ // Flow-msg mode: the input pin carries KNX bus telegrams from a universal node.
36
+ if (this.nodeMode === 'homeassistant' && this.haBusMode === 'flow') {
37
+ return this._('knxUltimateIoTBridge.labels.inputKnxBus');
38
+ }
39
+ return undefined;
40
+ },
18
41
  outputLabels: function (index) {
42
+ if (this.nodeMode === 'homeassistant') {
43
+ // Stand-alone HA has no outputs; flow mode emits KNX writes on the single pin.
44
+ if (this.haBusMode === 'flow' && index === 0) return this._('knxUltimateIoTBridge.labels.outputKnxWrite');
45
+ return undefined;
46
+ }
19
47
  if (index === 0) return this._('knxUltimateIoTBridge.labels.outputKnxToIoT');
20
48
  if (index === 1) return this._('knxUltimateIoTBridge.labels.outputIoTToKnx');
21
49
  },
@@ -277,6 +305,193 @@
277
305
  (node.mappings || []).forEach((m) => {
278
306
  container.editableList('addItem', { mapping: m });
279
307
  });
308
+
309
+ // Operation mode toggle: show IoT mapping fields or Home Assistant (MQTT) fields.
310
+ try {
311
+ if (node.mqttDiscovery === undefined) $('#node-input-mqttDiscovery').prop('checked', true);
312
+ function refreshMode() {
313
+ const ha = $('#node-input-nodeMode').val() === 'homeassistant';
314
+ $('.ha-mode-row').toggle(ha);
315
+ $('.iot-mode-row').toggle(!ha);
316
+ // The flow-mode hint is only relevant while the "flow msg" bus mode is picked.
317
+ $('.ha-flow-hint').toggle(ha && $('#node-input-haBusMode').val() === 'flow');
318
+ }
319
+ $('#node-input-nodeMode').on('change', refreshMode);
320
+ $('#node-input-haBusMode').on('change', refreshMode);
321
+ refreshMode();
322
+ } catch (error) { }
323
+
324
+ // Home Assistant: selectable list of group addresses to expose as simple entities.
325
+ try {
326
+ const savedSet = new Set(Array.isArray(node.mqttExposedGAs) ? node.mqttExposedGAs : []);
327
+ const readOnlySet = new Set(Array.isArray(node.mqttReadOnlyGAs) ? node.mqttReadOnlyGAs : []);
328
+ const exposeConfigured = node.mqttExposeConfigured === true;
329
+
330
+ function updateGaCount() {
331
+ const total = $('#mqtt-ga-list input.mqtt-ga-cb').length;
332
+ const sel = $('#mqtt-ga-list input.mqtt-ga-cb:checked').length;
333
+ $('#mqtt-ga-count').text(sel + ' / ' + total + ' ' + node._('knxUltimateIoTBridge.ha.exposed_count'));
334
+ }
335
+
336
+ function buildGaList() {
337
+ const list = $('#mqtt-ga-list').empty();
338
+ const sid = oNodeServer ? oNodeServer.id : ($('#node-input-server').val() || '');
339
+ if (!sid) {
340
+ list.append($('<div/>').css('color', '#999').text(node._('knxUltimateIoTBridge.ha.no_gateway')));
341
+ updateGaCount();
342
+ return;
343
+ }
344
+ $.getJSON('knxUltimatecsv?nodeID=' + sid, function (data) {
345
+ list.empty();
346
+ if (!Array.isArray(data) || data.length === 0) {
347
+ list.append($('<div/>').css('color', '#999').text(node._('knxUltimateIoTBridge.ha.no_ga')));
348
+ updateGaCount();
349
+ return;
350
+ }
351
+ data.forEach(function (item) {
352
+ if (!item || !item.ga) return;
353
+ // Use a <div> (not <label>) so Node-RED's ".form-row label" width rule
354
+ // doesn't squash the columns; keep everything on a single line.
355
+ const row = $('<div/>').addClass('mqtt-ga-row').css({
356
+ display: 'flex', gap: '8px', alignItems: 'center', padding: '3px 2px',
357
+ width: '100%', whiteSpace: 'nowrap', cursor: 'pointer', borderBottom: '1px solid #f3f3f3'
358
+ }).appendTo(list);
359
+ const cb = $('<input/>', { type: 'checkbox', class: 'mqtt-ga-cb', value: item.ga, style: 'width:auto; margin:0; flex:0 0 auto;' }).appendTo(row);
360
+ cb.prop('checked', exposeConfigured ? savedSet.has(item.ga) : true);
361
+ $('<span/>').css({ flex: '0 0 auto', fontFamily: 'monospace', minWidth: '64px' }).text(item.ga).appendTo(row);
362
+ $('<span/>').css({ flex: '1 1 auto', minWidth: '0', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', color: '#444' }).attr('title', item.devicename || '').text(item.devicename || '').appendTo(row);
363
+ $('<span/>').css({ flex: '0 0 auto', color: '#888', fontSize: '11px' }).text('DPT' + (item.dpt || '')).appendTo(row);
364
+ // Read-only toggle: expose the GA to HA but never accept commands back to KNX.
365
+ const roLabel = $('<label/>').css({ flex: '0 0 auto', display: 'flex', alignItems: 'center', gap: '3px', margin: '0', fontSize: '11px', color: '#888', cursor: 'pointer' }).appendTo(row);
366
+ const ro = $('<input/>', { type: 'checkbox', class: 'mqtt-ga-ro', value: item.ga, style: 'width:auto; margin:0;' }).appendTo(roLabel);
367
+ ro.prop('checked', readOnlySet.has(item.ga));
368
+ $('<span/>').text(node._('knxUltimateIoTBridge.ha.read_only')).appendTo(roLabel);
369
+ roLabel.on('click', function (ev) { ev.stopPropagation(); });
370
+ row.on('click', function (ev) {
371
+ if (ev.target === cb[0] || ev.target === ro[0]) return;
372
+ cb.prop('checked', !cb.prop('checked'));
373
+ updateGaCount();
374
+ });
375
+ });
376
+ $('#mqtt-ga-list input.mqtt-ga-cb').on('change', updateGaCount);
377
+ updateGaCount();
378
+ }).fail(function () {
379
+ list.empty().append($('<div/>').css('color', '#c0392b').text(node._('knxUltimateIoTBridge.ha.csv_error')));
380
+ updateGaCount();
381
+ });
382
+ }
383
+
384
+ $('#mqtt-ga-all').on('click', function (e) { e.preventDefault(); $('#mqtt-ga-list .mqtt-ga-row:visible').find('input.mqtt-ga-cb').prop('checked', true); updateGaCount(); });
385
+ $('#mqtt-ga-none').on('click', function (e) { e.preventDefault(); $('#mqtt-ga-list .mqtt-ga-row:visible').find('input.mqtt-ga-cb').prop('checked', false); updateGaCount(); });
386
+ // Flag / unflag read-only for the currently visible GAs (respects the filter).
387
+ $('#mqtt-ga-ro-all').on('click', function (e) { e.preventDefault(); $('#mqtt-ga-list .mqtt-ga-row:visible').find('input.mqtt-ga-ro').prop('checked', true); });
388
+ $('#mqtt-ga-ro-none').on('click', function (e) { e.preventDefault(); $('#mqtt-ga-list .mqtt-ga-row:visible').find('input.mqtt-ga-ro').prop('checked', false); });
389
+ $('#mqtt-ga-filter').on('input', function () {
390
+ const term = ($(this).val() || '').toLowerCase();
391
+ $('#mqtt-ga-list .mqtt-ga-row').each(function () {
392
+ $(this).toggle($(this).text().toLowerCase().indexOf(term) !== -1);
393
+ });
394
+ });
395
+
396
+ buildGaList();
397
+ $('#node-input-server').on('change', buildGaList);
398
+ } catch (error) { }
399
+
400
+ // Home Assistant composite entities (cover / climate): editable list.
401
+ try {
402
+ const T = (k) => node._('knxUltimateIoTBridge.ha.' + k);
403
+ const coverFields = ['gaUpDown', 'gaStop', 'gaPosSet', 'gaPosState'];
404
+ const climateFields = ['gaCurrentTemp', 'gaSetpointSet', 'gaSetpointState', 'gaOnOff'];
405
+
406
+ // Group-address autocomplete, identical to the mapping GA field (and the
407
+ // knxUltimate node): suggests GAs from the gateway's ETS CSV.
408
+ function attachGaAutocomplete(input) {
409
+ try {
410
+ if (oNodeServer && oNodeServer.id && typeof KNX_enableSecureFormatting === 'function') {
411
+ KNX_enableSecureFormatting(input, oNodeServer.id);
412
+ }
413
+ } catch (error) { }
414
+ input.autocomplete({
415
+ minLength: 0,
416
+ source: function (request, response) {
417
+ const sid = oNodeServer ? oNodeServer.id : ($('#node-input-server').val() || '');
418
+ if (!sid) { response([]); return; }
419
+ $.getJSON('knxUltimatecsv?nodeID=' + sid, (data) => {
420
+ response($.map(data || [], (value) => {
421
+ const search = (value.ga + ' (' + value.devicename + ') DPT' + value.dpt);
422
+ if (htmlUtilsfullCSVSearch(search, request.term + ' 1.')) {
423
+ return {
424
+ label: value.ga + ' # ' + value.devicename + ' # ' + value.dpt,
425
+ value: value.ga
426
+ };
427
+ }
428
+ return null;
429
+ }));
430
+ });
431
+ }
432
+ });
433
+ input.on('focus.knxUltimateIoTBridge click.knxUltimateIoTBridge', function () {
434
+ try { $(this).autocomplete('search', ''); } catch (error) { }
435
+ });
436
+ }
437
+
438
+ function gaRow(parent, key, value) {
439
+ const row = $('<div/>').addClass('form-row').css({ margin: '2px 0' }).appendTo(parent);
440
+ $('<label/>').css({ width: '160px' }).text(T('ce_' + key)).appendTo(row);
441
+ const input = $('<input/>', { type: 'text', class: 'ce-' + key })
442
+ .attr('placeholder', '1/2/3').css({ width: '130px' }).val(value || '').appendTo(row);
443
+ attachGaAutocomplete(input);
444
+ return input;
445
+ }
446
+ function numRow(parent, key, value) {
447
+ const row = $('<div/>').addClass('form-row').css({ margin: '2px 0' }).appendTo(parent);
448
+ $('<label/>').css({ width: '160px' }).text(T('ce_' + key)).appendTo(row);
449
+ return $('<input/>', { type: 'number', class: 'ce-' + key, step: 'any' })
450
+ .css({ width: '90px' }).val(value).appendTo(row);
451
+ }
452
+
453
+ $('#node-input-mqttentities-container').css('min-width', '560px').editableList({
454
+ sortable: true,
455
+ removable: true,
456
+ addItem: function (rowEl, index, data) {
457
+ const e = data.entity || {};
458
+ const block = $('<div/>').css({ padding: '4px 2px' }).appendTo(rowEl);
459
+ const head = $('<div/>').addClass('form-row').css({ display: 'flex', gap: '10px', alignItems: 'center', margin: '2px 0' }).appendTo(block);
460
+ const typeSel = $('<select/>', { class: 'ce-type' }).css({ width: '120px' }).appendTo(head);
461
+ typeSel.append($('<option/>', { value: 'cover', text: T('ce_type_cover') }));
462
+ typeSel.append($('<option/>', { value: 'climate', text: T('ce_type_climate') }));
463
+ typeSel.val(e.type === 'climate' ? 'climate' : 'cover');
464
+ $('<input/>', { type: 'text', class: 'ce-name' })
465
+ .attr('placeholder', T('ce_name')).css({ flex: '1 1 180px' }).val(e.name || '').appendTo(head);
466
+
467
+ const coverBox = $('<div/>', { class: 'ce-cover-box' }).appendTo(block);
468
+ coverFields.forEach((f) => gaRow(coverBox, f, e[f]));
469
+ const invRow = $('<div/>').addClass('form-row').css({ margin: '2px 0' }).appendTo(coverBox);
470
+ const invLabel = $('<label/>', { style: 'width:auto; display:flex; align-items:center; gap:6px; cursor:pointer;' }).appendTo(invRow);
471
+ const inv = $('<input/>', { type: 'checkbox', class: 'ce-invertPosition', style: 'margin:0; width:auto;' }).appendTo(invLabel);
472
+ $('<span/>').text(T('ce_invertPosition')).appendTo(invLabel);
473
+ inv.prop('checked', e.invertPosition !== false);
474
+
475
+ const climateBox = $('<div/>', { class: 'ce-climate-box' }).appendTo(block);
476
+ climateFields.forEach((f) => gaRow(climateBox, f, e[f]));
477
+ numRow(climateBox, 'minTemp', e.minTemp === undefined ? 5 : e.minTemp);
478
+ numRow(climateBox, 'maxTemp', e.maxTemp === undefined ? 35 : e.maxTemp);
479
+ numRow(climateBox, 'tempStep', e.tempStep === undefined ? 0.5 : e.tempStep);
480
+
481
+ function refreshType() {
482
+ const t = typeSel.val();
483
+ coverBox.toggle(t === 'cover');
484
+ climateBox.toggle(t === 'climate');
485
+ }
486
+ typeSel.on('change', refreshType);
487
+ refreshType();
488
+ }
489
+ });
490
+
491
+ (node.mqttCustomEntities || []).forEach((en) => {
492
+ $('#node-input-mqttentities-container').editableList('addItem', { entity: en });
493
+ });
494
+ } catch (error) { }
280
495
  },
281
496
  oneditsave: function () {
282
497
  const node = this;
@@ -303,6 +518,60 @@
303
518
  property: row.find('.bridge-property').val()
304
519
  });
305
520
  });
521
+ // Serialize Home Assistant composite entities (cover / climate).
522
+ try {
523
+ const entities = [];
524
+ $('#node-input-mqttentities-container').editableList('items').each(function () {
525
+ const row = $(this);
526
+ const type = row.find('.ce-type').val();
527
+ const entity = { type: type, name: (row.find('.ce-name').val() || '').trim() };
528
+ if (type === 'cover') {
529
+ ['gaUpDown', 'gaStop', 'gaPosSet', 'gaPosState'].forEach((f) => {
530
+ entity[f] = (row.find('.ce-' + f).val() || '').trim();
531
+ });
532
+ entity.invertPosition = row.find('.ce-invertPosition').is(':checked');
533
+ } else {
534
+ ['gaCurrentTemp', 'gaSetpointSet', 'gaSetpointState', 'gaOnOff'].forEach((f) => {
535
+ entity[f] = (row.find('.ce-' + f).val() || '').trim();
536
+ });
537
+ entity.minTemp = parseFloat(row.find('.ce-minTemp').val());
538
+ entity.maxTemp = parseFloat(row.find('.ce-maxTemp').val());
539
+ entity.tempStep = parseFloat(row.find('.ce-tempStep').val());
540
+ }
541
+ entities.push(entity);
542
+ });
543
+ node.mqttCustomEntities = entities;
544
+ } catch (error) { }
545
+ // Serialize the selected group addresses to expose (only when the list is populated,
546
+ // so a failed/unopened list never wipes a previous selection).
547
+ try {
548
+ if ($('#node-input-nodeMode').val() === 'homeassistant') {
549
+ const cbs = $('#mqtt-ga-list input.mqtt-ga-cb');
550
+ if (cbs.length > 0) {
551
+ const exposed = [];
552
+ const readOnly = [];
553
+ cbs.each(function () { if ($(this).is(':checked')) exposed.push($(this).val()); });
554
+ $('#mqtt-ga-list input.mqtt-ga-ro').each(function () { if ($(this).is(':checked')) readOnly.push($(this).val()); });
555
+ node.mqttExposedGAs = exposed;
556
+ node.mqttReadOnlyGAs = readOnly;
557
+ node.mqttExposeConfigured = true;
558
+ }
559
+ }
560
+ } catch (error) { }
561
+ // Adjust the pins to the selected mode:
562
+ // - IoT: 1 input, 2 outputs (stream + ack)
563
+ // - HA flow-msg: 1 input, 1 output (KNX bus in / KNX write out)
564
+ // - HA stand-alone: 0 inputs, 0 outputs (talks to the gateway only)
565
+ try {
566
+ if ($('#node-input-nodeMode').val() === 'homeassistant') {
567
+ const flow = $('#node-input-haBusMode').val() === 'flow';
568
+ node.inputs = flow ? 1 : 0;
569
+ node.outputs = flow ? 1 : 0;
570
+ } else {
571
+ node.inputs = 1;
572
+ node.outputs = 2;
573
+ }
574
+ } catch (error) { }
306
575
  try {
307
576
  RED.sidebar.show('info');
308
577
  } catch (error) { }
@@ -331,36 +600,93 @@
331
600
  <input type="text" id="node-input-name" />
332
601
  </div>
333
602
  <div class="form-row">
603
+ <label for="node-input-nodeMode" data-i18n="knxUltimateIoTBridge.node-input-nodeMode"></label>
604
+ <select id="node-input-nodeMode" style="width:auto;">
605
+ <option value="iot" data-i18n="knxUltimateIoTBridge.mode.iot"></option>
606
+ <option value="homeassistant" data-i18n="knxUltimateIoTBridge.mode.homeassistant"></option>
607
+ </select>
608
+ </div>
609
+
610
+ <!-- IoT (classic) mode fields -->
611
+ <div class="form-row iot-mode-row">
334
612
  <label for="node-input-outputtopic" data-i18n="knxUltimateIoTBridge.node-input-outputtopic"></label>
335
613
  <input type="text" id="node-input-outputtopic" />
336
614
  </div>
337
- <div class="form-row" style="display:flex; align-items:flex-start; gap:8px;">
615
+ <div class="form-row iot-mode-row" style="display:flex; align-items:flex-start; gap:8px;">
338
616
  <input type="checkbox" id="node-input-emitOnChangeOnly" style="width:auto; margin-top:4px;" />
339
- <label for="node-input-emitOnChangeOnly" style="flex:1; margin:0;">
340
- <span data-i18n="knxUltimateIoTBridge.node-input-emitOnChangeOnly"></span>
341
- </label>
617
+ <label for="node-input-emitOnChangeOnly" style="flex:1; margin:0;" data-i18n="knxUltimateIoTBridge.node-input-emitOnChangeOnly"></label>
342
618
  </div>
343
- <div class="form-row" style="display:flex; align-items:flex-start; gap:8px;">
619
+ <div class="form-row iot-mode-row" style="display:flex; align-items:flex-start; gap:8px;">
344
620
  <input type="checkbox" id="node-input-readOnDeploy" style="width:auto; margin-top:4px;" />
345
- <label for="node-input-readOnDeploy" style="flex:1; margin:0;">
346
- <span data-i18n="knxUltimateIoTBridge.node-input-readOnDeploy"></span>
347
- </label>
621
+ <label for="node-input-readOnDeploy" style="flex:1; margin:0;" data-i18n="knxUltimateIoTBridge.node-input-readOnDeploy"></label>
348
622
  </div>
349
- <div class="form-row" style="display:flex; align-items:flex-start; gap:8px;">
623
+ <div class="form-row iot-mode-row" style="display:flex; align-items:flex-start; gap:8px;">
350
624
  <input type="checkbox" id="node-input-acceptFlowInput" style="width:auto; margin-top:4px;" />
351
- <label for="node-input-acceptFlowInput" style="flex:1; margin:0;">
352
- <span data-i18n="knxUltimateIoTBridge.node-input-acceptFlowInput"></span>
353
- </label>
625
+ <label for="node-input-acceptFlowInput" style="flex:1; margin:0;" data-i18n="knxUltimateIoTBridge.node-input-acceptFlowInput"></label>
354
626
  </div>
355
- <div class="form-row node-input-mapping-container-row">
627
+ <div class="form-row node-input-mapping-container-row iot-mode-row">
356
628
  <label style="width:auto;" data-i18n="knxUltimateIoTBridge.section_mappings"></label>
357
629
  <ol id="node-input-mapping-container"></ol>
358
630
  </div>
631
+
632
+ <!-- Home Assistant (MQTT) mode fields -->
633
+ <div class="form-row ha-mode-row">
634
+ <label for="node-input-haBusMode" data-i18n="knxUltimateIoTBridge.ha.bus_mode"></label>
635
+ <select id="node-input-haBusMode" style="width:auto;">
636
+ <option value="standalone" data-i18n="knxUltimateIoTBridge.ha.bus_standalone"></option>
637
+ <option value="flow" data-i18n="knxUltimateIoTBridge.ha.bus_flow"></option>
638
+ </select>
639
+ </div>
640
+ <div class="form-row ha-mode-row ha-flow-hint" style="margin-top:-4px;">
641
+ <label style="width:auto;">&nbsp;</label>
642
+ <span style="font-size:11px; color:#888;" data-i18n="knxUltimateIoTBridge.ha.bus_flow_hint"></span>
643
+ </div>
644
+ <div class="form-row ha-mode-row">
645
+ <label for="node-input-mqttUrl" data-i18n="knxUltimateIoTBridge.ha.broker_url"></label>
646
+ <input type="text" id="node-input-mqttUrl" placeholder="mqtt://localhost:1883" />
647
+ </div>
648
+ <div class="form-row ha-mode-row">
649
+ <label for="node-input-mqttUsername" data-i18n="knxUltimateIoTBridge.ha.username"></label>
650
+ <input type="text" id="node-input-mqttUsername" data-i18n="[placeholder]knxUltimateIoTBridge.ha.optional" />
651
+ </div>
652
+ <div class="form-row ha-mode-row">
653
+ <label for="node-input-mqttPassword" data-i18n="knxUltimateIoTBridge.ha.password"></label>
654
+ <input type="password" id="node-input-mqttPassword" data-i18n="[placeholder]knxUltimateIoTBridge.ha.optional" />
655
+ </div>
656
+ <div class="form-row ha-mode-row">
657
+ <label for="node-input-mqttBaseTopic" data-i18n="knxUltimateIoTBridge.ha.base_topic"></label>
658
+ <input type="text" id="node-input-mqttBaseTopic" placeholder="knx-ultimate" />
659
+ </div>
660
+ <div class="form-row ha-mode-row" style="display:flex; align-items:flex-start; gap:8px;">
661
+ <input type="checkbox" id="node-input-mqttDiscovery" style="width:auto; margin-top:4px;" />
662
+ <label for="node-input-mqttDiscovery" style="flex:1; margin:0;" data-i18n="knxUltimateIoTBridge.ha.discovery"></label>
663
+ </div>
664
+ <div class="form-row ha-mode-row">
665
+ <label for="node-input-mqttDiscoveryPrefix" data-i18n="knxUltimateIoTBridge.ha.discovery_prefix"></label>
666
+ <input type="text" id="node-input-mqttDiscoveryPrefix" placeholder="homeassistant" />
667
+ </div>
668
+ <div class="form-row ha-mode-row" id="rowMqttGaSelect">
669
+ <label style="width:auto; font-weight:600;" data-i18n="knxUltimateIoTBridge.ha.exposed_gas"></label>
670
+ <div style="margin:4px 0; display:flex; gap:6px; align-items:center;">
671
+ <input type="text" id="mqtt-ga-filter" data-i18n="[placeholder]knxUltimateIoTBridge.ha.filter_placeholder" style="flex:1 1 auto;">
672
+ <button type="button" id="mqtt-ga-all" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="knxUltimateIoTBridge.ha.select_all"></button>
673
+ <button type="button" id="mqtt-ga-none" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="knxUltimateIoTBridge.ha.select_none"></button>
674
+ <span style="border-left:1px solid #ccc; align-self:stretch;"></span>
675
+ <button type="button" id="mqtt-ga-ro-all" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="[title]knxUltimateIoTBridge.ha.read_only_bulk;knxUltimateIoTBridge.ha.read_only_all"></button>
676
+ <button type="button" id="mqtt-ga-ro-none" class="ui-button ui-corner-all ui-widget" style="width:auto;" data-i18n="[title]knxUltimateIoTBridge.ha.read_only_bulk;knxUltimateIoTBridge.ha.read_only_none"></button>
677
+ </div>
678
+ <div id="mqtt-ga-count" style="font-size:11px; color:#666; margin:2px 0;"></div>
679
+ <div id="mqtt-ga-list" style="max-height:220px; overflow:auto; border:1px solid #ccc; padding:6px; border-radius:4px; background:#fff;"></div>
680
+ </div>
681
+ <div class="form-row ha-mode-row node-input-mqttentities-container-row">
682
+ <label style="width:auto; font-weight:600;" data-i18n="knxUltimateIoTBridge.ha.custom_entities"></label>
683
+ <ol id="node-input-mqttentities-container"></ol>
684
+ </div>
359
685
  <br/><br/><br/><br/>
360
686
  </script>
361
687
 
362
688
  <script type="text/markdown" data-help-name="knxUltimateIoTBridge">
363
- ## KNX IoT Bridge
689
+ # MQTT Home Assistant - IoT
364
690
 
365
691
  Configure bidirectional maps between KNX group addresses and IoT backends such as MQTT, REST APIs or Modbus registers. Each mapping can scale values, format payloads and define single direction behaviour.
366
692
 
@@ -384,4 +710,36 @@ Configure bidirectional maps between KNX group addresses and IoT backends such a
384
710
  - Use `msg.bridge.id` to route acknowledgements or correlate responses.
385
711
  - Enable "Read KNX values on deploy" to bootstrap dashboards after deploys.
386
712
 
713
+ ## Mode
714
+
715
+ The **Mode** selector switches the node between two behaviours:
716
+
717
+ - **IoT bridge** (default): the classic behaviour described above (mapping list, MQTT/REST/Modbus output messages).
718
+ - **MQTT / Home Assistant (native)**: the node connects directly to an MQTT broker and bridges KNX ↔ MQTT both ways, publishing Home Assistant MQTT Discovery so entities appear automatically.
719
+
720
+ ## MQTT / Home Assistant mode
721
+
722
+ Native MQTT bridge with Home Assistant discovery. Every group address imported in the KNX gateway (ETS list) can be exposed automatically as a Home Assistant entity (switch, sensor, binary_sensor, number, text), chosen from its datapoint type (DPT). KNX bus values are published to MQTT and writable datapoints accept commands from Home Assistant.
723
+
724
+ Requires an MQTT broker reachable by both Node-RED and Home Assistant, with the MQTT integration enabled in HA. Entities appear under a device named after this node.
725
+
726
+ ### KNX bus connection
727
+ Choose how the node exchanges telegrams with KNX:
728
+
729
+ - **Stand-alone** (default): the node talks to the KNX gateway directly and shows no input/output pins.
730
+ - **Flow messages**: the node exposes an input pin and an output pin. Wire the output of a KNXUltimate node in **Universal** mode to the input (KNX bus → MQTT) and the output pin to the input of another KNXUltimate node in **Universal** mode (MQTT → KNX bus).
731
+
732
+ ### Group addresses to expose
733
+ Tick the group addresses you want to publish to Home Assistant. By default every imported address is selected. Addresses used by a cover/thermostat (below) are handled there and don't need to be ticked here. Use the filter box and the Select all / Select none buttons to curate large lists.
734
+
735
+ Each row also has a **Read only** toggle: a read-only address is still published to Home Assistant (its state stays visible) but never accepts commands back to the KNX bus (switches become binary_sensors, numbers become sensors). The Set read only / Clear read only buttons apply it to all currently shown addresses.
736
+
737
+ ### Covers & Thermostats
738
+ Covers and thermostats group several group addresses into one Home Assistant entity, so they cannot be created automatically from the DPT - add them in the list:
739
+
740
+ - **Cover**: Up/Down GA (DPT 1.008), optional Stop GA (1.007), optional Set/Status position GA (5.001). *Invert position* maps the KNX convention (0% = open) to Home Assistant (100% = open).
741
+ - **Thermostat**: Current temperature GA (9.001), Setpoint set/status GA (9.001), optional On/Off GA (1.001 → off/heat), plus min/max temperature and step.
742
+
743
+ Datapoint types come from the imported ETS list when available, otherwise from the KNX defaults shown above. For reliable status feedback, those group addresses should be present in the ETS import.
744
+
387
745
  </script>