node-red-contrib-knx-ultimate 5.0.1 → 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,13 @@
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
+
9
16
  **Version 5.0.1** - June 2026<br/>
10
17
 
11
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/>
@@ -13,12 +13,16 @@
13
13
  acceptFlowInput: { value: true },
14
14
  mappings: { value: [] },
15
15
  nodeMode: { value: 'iot' },
16
+ haBusMode: { value: 'standalone' },
17
+ inputs: { value: 1 },
18
+ outputs: { value: 2 },
16
19
  mqttUrl: { value: '' },
17
20
  mqttBaseTopic: { value: 'knx-ultimate' },
18
21
  mqttDiscovery: { value: true },
19
22
  mqttDiscoveryPrefix: { value: 'homeassistant' },
20
23
  mqttCustomEntities: { value: [] },
21
24
  mqttExposedGAs: { value: [] },
25
+ mqttReadOnlyGAs: { value: [] },
22
26
  mqttExposeConfigured: { value: false }
23
27
  },
24
28
  credentials: {
@@ -27,7 +31,19 @@
27
31
  },
28
32
  inputs: 1,
29
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
+ },
30
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
+ }
31
47
  if (index === 0) return this._('knxUltimateIoTBridge.labels.outputKnxToIoT');
32
48
  if (index === 1) return this._('knxUltimateIoTBridge.labels.outputIoTToKnx');
33
49
  },
@@ -297,14 +313,18 @@
297
313
  const ha = $('#node-input-nodeMode').val() === 'homeassistant';
298
314
  $('.ha-mode-row').toggle(ha);
299
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');
300
318
  }
301
319
  $('#node-input-nodeMode').on('change', refreshMode);
320
+ $('#node-input-haBusMode').on('change', refreshMode);
302
321
  refreshMode();
303
322
  } catch (error) { }
304
323
 
305
324
  // Home Assistant: selectable list of group addresses to expose as simple entities.
306
325
  try {
307
326
  const savedSet = new Set(Array.isArray(node.mqttExposedGAs) ? node.mqttExposedGAs : []);
327
+ const readOnlySet = new Set(Array.isArray(node.mqttReadOnlyGAs) ? node.mqttReadOnlyGAs : []);
308
328
  const exposeConfigured = node.mqttExposeConfigured === true;
309
329
 
310
330
  function updateGaCount() {
@@ -341,8 +361,14 @@
341
361
  $('<span/>').css({ flex: '0 0 auto', fontFamily: 'monospace', minWidth: '64px' }).text(item.ga).appendTo(row);
342
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);
343
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(); });
344
370
  row.on('click', function (ev) {
345
- if (ev.target === cb[0]) return;
371
+ if (ev.target === cb[0] || ev.target === ro[0]) return;
346
372
  cb.prop('checked', !cb.prop('checked'));
347
373
  updateGaCount();
348
374
  });
@@ -357,6 +383,9 @@
357
383
 
358
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(); });
359
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); });
360
389
  $('#mqtt-ga-filter').on('input', function () {
361
390
  const term = ($(this).val() || '').toLowerCase();
362
391
  $('#mqtt-ga-list .mqtt-ga-row').each(function () {
@@ -520,12 +549,29 @@
520
549
  const cbs = $('#mqtt-ga-list input.mqtt-ga-cb');
521
550
  if (cbs.length > 0) {
522
551
  const exposed = [];
552
+ const readOnly = [];
523
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()); });
524
555
  node.mqttExposedGAs = exposed;
556
+ node.mqttReadOnlyGAs = readOnly;
525
557
  node.mqttExposeConfigured = true;
526
558
  }
527
559
  }
528
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) { }
529
575
  try {
530
576
  RED.sidebar.show('info');
531
577
  } catch (error) { }
@@ -584,6 +630,17 @@
584
630
  </div>
585
631
 
586
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>
587
644
  <div class="form-row ha-mode-row">
588
645
  <label for="node-input-mqttUrl" data-i18n="knxUltimateIoTBridge.ha.broker_url"></label>
589
646
  <input type="text" id="node-input-mqttUrl" placeholder="mqtt://localhost:1883" />
@@ -614,6 +671,9 @@
614
671
  <input type="text" id="mqtt-ga-filter" data-i18n="[placeholder]knxUltimateIoTBridge.ha.filter_placeholder" style="flex:1 1 auto;">
615
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>
616
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>
617
677
  </div>
618
678
  <div id="mqtt-ga-count" style="font-size:11px; color:#666; margin:2px 0;"></div>
619
679
  <div id="mqtt-ga-list" style="max-height:220px; overflow:auto; border:1px solid #ccc; padding:6px; border-radius:4px; background:#fff;"></div>
@@ -663,9 +723,17 @@ Native MQTT bridge with Home Assistant discovery. Every group address imported i
663
723
 
664
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.
665
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
+
666
732
  ### Group addresses to expose
667
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.
668
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
+
669
737
  ### Covers & Thermostats
670
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:
671
739
 
@@ -43,6 +43,10 @@ module.exports = function (RED) {
43
43
  // Operation mode: 'iot' (classic IoT mappings, default) or 'homeassistant' (native
44
44
  // MQTT bridge with Home Assistant discovery for every group address + cover/climate).
45
45
  node.nodeMode = config.nodeMode === 'homeassistant' ? 'homeassistant' : 'iot'
46
+ // Home Assistant bus wiring: 'standalone' (default) talks to the KNX gateway directly,
47
+ // 'flow' uses the node's input/output pins instead (wire a KNXUltimate universal node to
48
+ // both): the input pin feeds KNX bus telegrams in, the output pin emits telegrams to write.
49
+ node.haBusMode = config.haBusMode === 'flow' ? 'flow' : 'standalone'
46
50
  node.mqttUrl = typeof config.mqttUrl === 'string' ? config.mqttUrl.trim() : ''
47
51
  node.mqttBaseTopic = typeof config.mqttBaseTopic === 'string' && config.mqttBaseTopic.trim() !== '' ? config.mqttBaseTopic.trim() : 'knx-ultimate'
48
52
  node.mqttDiscovery = config.mqttDiscovery !== false && config.mqttDiscovery !== 'false'
@@ -52,6 +56,9 @@ module.exports = function (RED) {
52
56
  // (mqttExposeConfigured), only the listed GAs are exposed; otherwise all imported GAs are.
53
57
  node.mqttExposeConfigured = config.mqttExposeConfigured === true
54
58
  node.mqttExposedGAs = Array.isArray(config.mqttExposedGAs) ? config.mqttExposedGAs : []
59
+ // Group addresses the user marked as read-only: they are still exposed (state is
60
+ // published to HA) but never accept commands back to the KNX bus.
61
+ node.mqttReadOnlyGAs = Array.isArray(config.mqttReadOnlyGAs) ? config.mqttReadOnlyGAs : []
55
62
  node.mqttBridge = null
56
63
 
57
64
  const safeNumber = (value, fallback = 0) => {
@@ -179,9 +186,22 @@ module.exports = function (RED) {
179
186
  customEntities: node.mqttCustomEntities,
180
187
  // null => expose all imported GAs (until the user curates the list).
181
188
  exposedGAs: node.mqttExposeConfigured ? node.mqttExposedGAs : null,
189
+ // GAs exposed as read-only (state only, no command topic back to KNX).
190
+ readOnlyGAs: node.mqttReadOnlyGAs,
182
191
  onCommand: ({ ga, dpt, value }) => {
183
192
  // A Home Assistant command arrived: write it to the KNX bus.
184
193
  try {
194
+ if (node.haBusMode === 'flow') {
195
+ // Flow mode: emit a message on the (single) output pin for a downstream
196
+ // KNXUltimate universal node to write to the bus (destination + dpt + payload).
197
+ node.send({
198
+ topic: ga,
199
+ destination: ga,
200
+ dpt: dpt || '',
201
+ payload: value
202
+ })
203
+ return
204
+ }
185
205
  node.serverKNX.sendKNXTelegramToKNXEngine({
186
206
  grpaddr: ga,
187
207
  payload: value,
@@ -426,6 +446,18 @@ module.exports = function (RED) {
426
446
  }
427
447
  }
428
448
 
449
+ // Home Assistant mode: mirror a decoded KNX telegram to MQTT. Works with a telegram coming
450
+ // from the gateway (standalone) or from the input pin (flow mode); both share the shape
451
+ // produced by the KNXUltimate universal node ({ payload, knx: { destination, event } }).
452
+ const publishKnxToMqtt = (msg) => {
453
+ if (!msg || !node.mqttBridge) return
454
+ const destination = msg.knx && msg.knx.destination ? msg.knx.destination : sanitizeString(msg.topic)
455
+ if (!destination) return
456
+ const event = msg.knx ? msg.knx.event : undefined
457
+ if (event === 'GroupValue_Read') return // read requests carry no value
458
+ node.mqttBridge.publishState(destination, msg.payload)
459
+ }
460
+
429
461
  const handleKnxTelegram = (msg) => {
430
462
  try {
431
463
  if (!msg) return
@@ -434,10 +466,7 @@ module.exports = function (RED) {
434
466
 
435
467
  // Home Assistant mode: mirror the decoded value to MQTT and stop (no IoT mappings).
436
468
  if (node.nodeMode === 'homeassistant') {
437
- const event = msg.knx ? msg.knx.event : undefined
438
- if (node.mqttBridge && event !== 'GroupValue_Read') {
439
- node.mqttBridge.publishState(destination, msg.payload)
440
- }
469
+ publishKnxToMqtt(msg)
441
470
  return
442
471
  }
443
472
 
@@ -484,8 +513,11 @@ module.exports = function (RED) {
484
513
  node.handleSend = handleKnxTelegram
485
514
 
486
515
  node.on('input', (msg, send, done) => {
487
- // In Home Assistant mode, commands flow in over MQTT, not via the flow input.
516
+ // In Home Assistant mode, commands flow in over MQTT, not via the flow input. The one
517
+ // exception is 'flow' bus mode, where the input pin carries KNX bus telegrams (from a
518
+ // KNXUltimate universal node) that must be mirrored to MQTT.
488
519
  if (node.nodeMode === 'homeassistant') {
520
+ if (node.haBusMode === 'flow') publishKnxToMqtt(msg)
489
521
  if (done) done()
490
522
  return
491
523
  }
@@ -614,7 +646,10 @@ module.exports = function (RED) {
614
646
  }
615
647
  }
616
648
 
617
- registerClient()
649
+ // In HA 'flow' mode the KNX telegrams arrive on the input pin, so we must NOT subscribe to
650
+ // the gateway's client feed (that would double-publish and bypass the intended wiring).
651
+ const useFlowBus = node.nodeMode === 'homeassistant' && node.haBusMode === 'flow'
652
+ if (!useFlowBus) registerClient()
618
653
  if (node.nodeMode === 'homeassistant') {
619
654
  node.startMqttBridge()
620
655
  } else {
@@ -78,6 +78,12 @@ function createMqttBridge (options) {
78
78
  const exposeFilter = Array.isArray(opts.exposedGAs)
79
79
  ? new Set(opts.exposedGAs.map((g) => normalizeGa(g)).filter((g) => g !== ''))
80
80
  : null
81
+ // GAs the user marked read-only: exposed as read-only HA entities (no command topic).
82
+ const readOnlyFilter = new Set(
83
+ (Array.isArray(opts.readOnlyGAs) ? opts.readOnlyGAs : [])
84
+ .map((g) => normalizeGa(g))
85
+ .filter((g) => g !== '')
86
+ )
81
87
 
82
88
  const gatewayName = (node && node.name) || 'KNX Gateway'
83
89
  const gatewaySlug = slugify(node && node.name, '') || slugify(node && node.id, 'knx')
@@ -311,6 +317,12 @@ function createMqttBridge (options) {
311
317
  if (baseSlug === '' || simpleSeen.has(ga)) return
312
318
  simpleSeen.add(ga)
313
319
  const map = ha.mapDptToHa(entry.dpt)
320
+ // Read-only GAs are downgraded to a state-only domain and never get a command topic,
321
+ // so Home Assistant can display them but can't write back to the KNX bus.
322
+ const isReadOnly = readOnlyFilter.has(ga)
323
+ const domain = isReadOnly
324
+ ? (map.domain === 'switch' ? 'binary_sensor' : (map.domain === 'number' || map.domain === 'text' ? 'sensor' : map.domain))
325
+ : map.domain
314
326
  const slug = uniqueSlug(baseSlug)
315
327
  const uniqueId = `${deviceId}_${slug}`
316
328
  const name = (typeof entry.devicename === 'string' && entry.devicename.trim()) ? entry.devicename.trim() : ga
@@ -327,7 +339,7 @@ function createMqttBridge (options) {
327
339
  payload_not_available: 'offline',
328
340
  device: deviceBlock
329
341
  }
330
- switch (map.domain) {
342
+ switch (domain) {
331
343
  case 'switch':
332
344
  config.command_topic = commandTopic
333
345
  config.payload_on = 'true'
@@ -357,13 +369,13 @@ function createMqttBridge (options) {
357
369
  if (map.deviceClass) config.device_class = map.deviceClass
358
370
  break
359
371
  }
360
- addDiscovery(map.domain, slug, config)
372
+ addDiscovery(domain, slug, config)
361
373
 
362
374
  // KNX -> MQTT: publish the decoded value to the state topic.
363
375
  addPublisher(ga, (value) => pub(stateTopic, ha.formatValueForMqtt(value), true))
364
376
 
365
- // MQTT -> KNX: writable domains accept commands.
366
- if (map.writable === true) {
377
+ // MQTT -> KNX: writable domains accept commands (unless the GA is read-only).
378
+ if (map.writable === true && !isReadOnly) {
367
379
  const dpt = entry.dpt
368
380
  commandHandlers.set(commandTopic, (text) => {
369
381
  const value = ha.parseCommandFromMqtt(text, dpt)
@@ -11,9 +11,12 @@ Erstellen Sie bidirektionale Brücken zwischen KNX-Gruppenadressen und IoT-Kanä
11
11
 
12
12
  Eine native MQTT-Bridge mit Home-Assistant-Discovery. Benötigt einen MQTT-Broker, der sowohl von Node-RED als auch von Home Assistant erreichbar ist, mit aktivierter MQTT-Integration in HA. Alle Entitäten erscheinen unter einem einzigen Gerät mit dem Namen dieses Nodes.
13
13
 
14
+ - **KNX-Bus-Verbindung** — wie der Knoten Telegramme mit KNX austauscht:
15
+ - *Eigenständig* (Standard) — der Knoten kommuniziert direkt mit dem KNX-Gateway und hat keine Ein-/Ausgangs-Pins.
16
+ - *Flow-Nachrichten* — der Knoten zeigt einen Eingangs- und einen Ausgangs-Pin. Verbinden Sie den Ausgang eines KNXUltimate-Knotens im **Universal**-Modus mit dem Eingangs-Pin (KNX-Bus → MQTT) und den Ausgangs-Pin mit dem Eingang eines weiteren KNXUltimate-Knotens im **Universal**-Modus (MQTT → KNX-Bus).
14
17
  - **Broker-URL / Benutzername / Passwort** — Verbindung zu Ihrem MQTT-Broker.
15
18
  - **Basis-Topic / Discovery-Präfix** — Root-Topic für Status-/Befehls-Topics und das HA-Discovery-Präfix (Standard `homeassistant`).
16
- - **Bereitzustellende Gruppenadressen** — jede im KNX-Gateway importierte Adresse (ETS-Liste) wird mit einem Kontrollkästchen angezeigt. Angehakte Adressen werden als Home-Assistant-Entitäten veröffentlicht, automatisch nach dem DPT typisiert (switch, sensor, binary_sensor, number, text). Verwenden Sie das Filterfeld und die Schaltflächen *Alle auswählen* / *Keine auswählen*. Standardmäßig sind alle Adressen ausgewählt.
19
+ - **Bereitzustellende Gruppenadressen** — jede im KNX-Gateway importierte Adresse (ETS-Liste) wird mit einem Kontrollkästchen angezeigt. Angehakte Adressen werden als Home-Assistant-Entitäten veröffentlicht, automatisch nach dem DPT typisiert (switch, sensor, binary_sensor, number, text). Verwenden Sie das Filterfeld und die Schaltflächen *Alle auswählen* / *Keine auswählen*. Standardmäßig sind alle Adressen ausgewählt. Jede Zeile hat zudem eine Option **Nur lesen**: eine schreibgeschützte Adresse wird weiterhin an Home Assistant veröffentlicht (ihr Status bleibt sichtbar), akzeptiert aber nie Befehle zurück auf den KNX-Bus (switch werden zu binary_sensor, number zu sensor). Die Schaltflächen *Nur lesen setzen* / *Nur lesen entfernen* wenden dies auf alle aktuell angezeigten Adressen an.
17
20
  - **Rollläden & Thermostate** — fassen mehrere Adressen zu einer Entität zusammen:
18
21
  - *Rollladen*: Auf/Ab-GA (1.008), optionale Stopp-GA (1.007), optionale Positions-GA Befehl/Status (5.001). *Position invertieren* bildet die KNX-Konvention (0% = offen) auf Home Assistant (100% = offen) ab.
19
22
  - *Thermostat*: GA Ist-Temperatur (9.001), GA Sollwert Befehl/Status (9.001), optionale Ein/Aus-GA (1.001 → off/heat), plus Min-/Max-Temperatur und Schrittweite.
@@ -29,6 +29,14 @@
29
29
  "select_all": "Alle auswählen",
30
30
  "select_none": "Keine auswählen",
31
31
  "exposed_count": "ausgewählt",
32
+ "read_only": "Nur lesen",
33
+ "read_only_bulk": "Nur lesen für angezeigte Adressen:",
34
+ "read_only_all": "Nur lesen setzen",
35
+ "read_only_none": "Nur lesen entfernen",
36
+ "bus_mode": "KNX-Bus-Verbindung",
37
+ "bus_standalone": "Eigenständig (über das Gateway)",
38
+ "bus_flow": "Flow-Nachrichten (Ein-/Ausgangs-Pins)",
39
+ "bus_flow_hint": "Verbinde einen KNXUltimate-Knoten im Universal-Modus mit dem Eingangs-Pin (KNX → MQTT) und einen weiteren mit dem Ausgangs-Pin (MQTT → KNX).",
32
40
  "no_gateway": "Wähle zuerst ein KNX-Gateway aus.",
33
41
  "no_ga": "Keine Gruppenadressen gefunden. Importiere die ETS-Liste im KNX-Gateway.",
34
42
  "csv_error": "Die Gruppenadressliste konnte nicht vom Gateway geladen werden.",
@@ -113,7 +121,9 @@
113
121
  },
114
122
  "labels": {
115
123
  "outputKnxToIoT": "Strom KNX → IoT",
116
- "outputIoTToKnx": "Bestätigungen IoT → KNX"
124
+ "outputIoTToKnx": "Bestätigungen IoT → KNX",
125
+ "inputKnxBus": "KNX-Bus-Eingang (von einem Universal-Knoten)",
126
+ "outputKnxWrite": "KNX-Schreibausgang (zu einem Universal-Knoten)"
117
127
  }
118
128
  }
119
129
  }
@@ -11,9 +11,12 @@ Create bidirectional bridges between KNX group addresses and IoT channels (MQTT
11
11
 
12
12
  A native MQTT bridge with Home Assistant discovery. It needs an MQTT broker reachable by both Node-RED and Home Assistant, with the MQTT integration enabled in HA. All entities appear under a single device named after this node.
13
13
 
14
+ - **KNX bus connection** — how the node exchanges telegrams with KNX:
15
+ - *Stand-alone* (default) — the node talks to the KNX gateway directly and has no input/output pins.
16
+ - *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).
14
17
  - **Broker URL / Username / Password** — connection to your MQTT broker.
15
18
  - **Base topic / Discovery prefix** — root topic for state/command topics and the HA discovery prefix (default `homeassistant`).
16
- - **Group addresses to expose** — every address imported in the KNX gateway (ETS list) is shown with a checkbox. Ticked addresses are published as Home Assistant entities, typed automatically from the DPT (switch, sensor, binary_sensor, number, text). Use the filter box and the *Select all* / *Select none* buttons. By default all addresses are selected.
19
+ - **Group addresses to expose** — every address imported in the KNX gateway (ETS list) is shown with a checkbox. Ticked addresses are published as Home Assistant entities, typed automatically from the DPT (switch, sensor, binary_sensor, number, text). Use the filter box and the *Select all* / *Select none* buttons. By default all addresses are selected. 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.
17
20
  - **Covers & Thermostats** — group several addresses into one entity:
18
21
  - *Cover*: Up/Down GA (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).
19
22
  - *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.
@@ -29,6 +29,14 @@
29
29
  "select_all": "Select all",
30
30
  "select_none": "Select none",
31
31
  "exposed_count": "selected",
32
+ "read_only": "Read only",
33
+ "read_only_bulk": "Read only for shown addresses:",
34
+ "read_only_all": "Set read only",
35
+ "read_only_none": "Clear read only",
36
+ "bus_mode": "KNX bus connection",
37
+ "bus_standalone": "Stand-alone (via the gateway)",
38
+ "bus_flow": "Flow messages (input/output pins)",
39
+ "bus_flow_hint": "Wire a KNXUltimate node in Universal mode to the input pin (KNX → MQTT) and another one to the output pin (MQTT → KNX).",
32
40
  "no_gateway": "Select a KNX gateway first.",
33
41
  "no_ga": "No group addresses found. Import the ETS list in the KNX gateway.",
34
42
  "csv_error": "Unable to load the group address list from the gateway.",
@@ -113,7 +121,9 @@
113
121
  },
114
122
  "labels": {
115
123
  "outputKnxToIoT": "KNX → IoT stream",
116
- "outputIoTToKnx": "IoT → KNX acknowledgements"
124
+ "outputIoTToKnx": "IoT → KNX acknowledgements",
125
+ "inputKnxBus": "KNX bus in (from a Universal node)",
126
+ "outputKnxWrite": "KNX write out (to a Universal node)"
117
127
  }
118
128
  }
119
129
  }
@@ -11,9 +11,12 @@ Crea puentes bidireccionales entre direcciones de grupo KNX y canales IoT (topic
11
11
 
12
12
  Un puente MQTT nativo con descubrimiento de Home Assistant. Requiere un broker MQTT accesible tanto por Node-RED como por Home Assistant, con la integración MQTT activada en HA. Todas las entidades aparecen bajo un único dispositivo con el nombre de este nodo.
13
13
 
14
+ - **Conexión al bus KNX** — cómo el nodo intercambia telegramas con KNX:
15
+ - *Autónomo* (predeterminado) — el nodo habla directamente con la pasarela KNX y no tiene pines de entrada/salida.
16
+ - *Mensajes de flujo* — el nodo muestra un pin de entrada y uno de salida. Conecta la salida de un nodo KNXUltimate en modo **Universal** al pin de entrada (bus KNX → MQTT) y el pin de salida a la entrada de otro nodo KNXUltimate en modo **Universal** (MQTT → bus KNX).
14
17
  - **URL del broker / Usuario / Contraseña** — conexión a tu broker MQTT.
15
18
  - **Topic base / Prefijo de descubrimiento** — topic raíz para los topics de estado/comando y el prefijo de descubrimiento de HA (predeterminado `homeassistant`).
16
- - **Direcciones de grupo a exponer** — cada dirección importada en la pasarela KNX (lista ETS) se muestra con una casilla. Las direcciones marcadas se publican como entidades de Home Assistant, tipadas automáticamente a partir del DPT (switch, sensor, binary_sensor, number, text). Usa el filtro y los botones *Seleccionar todo* / *Deseleccionar todo*. Por defecto están todas seleccionadas.
19
+ - **Direcciones de grupo a exponer** — cada dirección importada en la pasarela KNX (lista ETS) se muestra con una casilla. Las direcciones marcadas se publican como entidades de Home Assistant, tipadas automáticamente a partir del DPT (switch, sensor, binary_sensor, number, text). Usa el filtro y los botones *Seleccionar todo* / *Deseleccionar todo*. Por defecto están todas seleccionadas. Cada fila tiene además una opción **Solo lectura**: una dirección de solo lectura se sigue publicando en Home Assistant (su estado permanece visible) pero nunca acepta comandos de vuelta al bus KNX (los switch pasan a binary_sensor y los number a sensor). Los botones *Marcar solo lectura* / *Quitar solo lectura* la aplican a todas las direcciones mostradas actualmente.
17
20
  - **Persianas y termostatos** — agrupan varias direcciones en una sola entidad:
18
21
  - *Persiana*: GA subir/bajar (1.008), GA stop opcional (1.007), GA posición comando/estado opcional (5.001). *Invertir posición* asigna la convención KNX (0% = abierto) a Home Assistant (100% = abierto).
19
22
  - *Termostato*: GA temperatura actual (9.001), GA consigna comando/estado (9.001), GA on/off opcional (1.001 → off/heat), además de temperatura mín/máx y paso.
@@ -29,6 +29,14 @@
29
29
  "select_all": "Seleccionar todo",
30
30
  "select_none": "Deseleccionar todo",
31
31
  "exposed_count": "seleccionadas",
32
+ "read_only": "Solo lectura",
33
+ "read_only_bulk": "Solo lectura para las direcciones mostradas:",
34
+ "read_only_all": "Marcar solo lectura",
35
+ "read_only_none": "Quitar solo lectura",
36
+ "bus_mode": "Conexión al bus KNX",
37
+ "bus_standalone": "Autónomo (a través del gateway)",
38
+ "bus_flow": "Mensajes de flujo (pines entrada/salida)",
39
+ "bus_flow_hint": "Conecta un nodo KNXUltimate en modo Universal al pin de entrada (KNX → MQTT) y otro al pin de salida (MQTT → KNX).",
32
40
  "no_gateway": "Selecciona primero una pasarela KNX.",
33
41
  "no_ga": "No se encontraron direcciones de grupo. Importa la lista ETS en la pasarela KNX.",
34
42
  "csv_error": "No se pudo cargar la lista de direcciones de grupo desde la pasarela.",
@@ -113,7 +121,9 @@
113
121
  },
114
122
  "labels": {
115
123
  "outputKnxToIoT": "Flujo KNX → IoT",
116
- "outputIoTToKnx": "Confirmaciones IoT → KNX"
124
+ "outputIoTToKnx": "Confirmaciones IoT → KNX",
125
+ "inputKnxBus": "Entrada bus KNX (desde un nodo Universal)",
126
+ "outputKnxWrite": "Salida escritura KNX (hacia un nodo Universal)"
117
127
  }
118
128
  }
119
129
  }
@@ -11,9 +11,12 @@ Créez des ponts bidirectionnels entre des adresses de groupe KNX et des canaux
11
11
 
12
12
  Une passerelle MQTT native avec découverte Home Assistant. Nécessite un broker MQTT accessible à la fois par Node-RED et Home Assistant, avec l'intégration MQTT activée dans HA. Toutes les entités apparaissent sous un seul appareil portant le nom de ce nœud.
13
13
 
14
+ - **Connexion au bus KNX** — comment le nœud échange les télégrammes avec KNX :
15
+ - *Autonome* (par défaut) — le nœud communique directement avec la passerelle KNX et n'a pas de broches d'entrée/sortie.
16
+ - *Messages de flux* — le nœud affiche une broche d'entrée et une de sortie. Reliez la sortie d'un nœud KNXUltimate en mode **Universel** à la broche d'entrée (bus KNX → MQTT) et la broche de sortie à l'entrée d'un autre nœud KNXUltimate en mode **Universel** (MQTT → bus KNX).
14
17
  - **URL du broker / Nom d'utilisateur / Mot de passe** — connexion à votre broker MQTT.
15
18
  - **Topic de base / Préfixe de découverte** — topic racine pour les topics d'état/commande et le préfixe de découverte HA (par défaut `homeassistant`).
16
- - **Adresses de groupe à exposer** — chaque adresse importée dans la passerelle KNX (liste ETS) est affichée avec une case à cocher. Les adresses cochées sont publiées comme entités Home Assistant, typées automatiquement d'après le DPT (switch, sensor, binary_sensor, number, text). Utilisez le champ de filtre et les boutons *Tout sélectionner* / *Tout désélectionner*. Par défaut, toutes les adresses sont sélectionnées.
19
+ - **Adresses de groupe à exposer** — chaque adresse importée dans la passerelle KNX (liste ETS) est affichée avec une case à cocher. Les adresses cochées sont publiées comme entités Home Assistant, typées automatiquement d'après le DPT (switch, sensor, binary_sensor, number, text). Utilisez le champ de filtre et les boutons *Tout sélectionner* / *Tout désélectionner*. Par défaut, toutes les adresses sont sélectionnées. Chaque ligne dispose aussi d'une option **Lecture seule** : une adresse en lecture seule est toujours publiée vers Home Assistant (son état reste visible) mais n'accepte jamais de commandes vers le bus KNX (les switch deviennent des binary_sensor et les number des sensor). Les boutons *Activer lecture seule* / *Retirer lecture seule* l'appliquent à toutes les adresses actuellement affichées.
17
20
  - **Volets et thermostats** — regroupent plusieurs adresses en une seule entité :
18
21
  - *Volet* : GA montée/descente (1.008), GA stop optionnelle (1.007), GA position commande/état optionnelle (5.001). *Inverser la position* fait correspondre la convention KNX (0% = ouvert) à Home Assistant (100% = ouvert).
19
22
  - *Thermostat* : GA température actuelle (9.001), GA consigne commande/état (9.001), GA marche/arrêt optionnelle (1.001 → off/heat), plus températures min/max et pas.
@@ -29,6 +29,14 @@
29
29
  "select_all": "Tout sélectionner",
30
30
  "select_none": "Tout désélectionner",
31
31
  "exposed_count": "sélectionnées",
32
+ "read_only": "Lecture seule",
33
+ "read_only_bulk": "Lecture seule pour les adresses affichées :",
34
+ "read_only_all": "Activer lecture seule",
35
+ "read_only_none": "Retirer lecture seule",
36
+ "bus_mode": "Connexion au bus KNX",
37
+ "bus_standalone": "Autonome (via la passerelle)",
38
+ "bus_flow": "Messages de flux (broches entrée/sortie)",
39
+ "bus_flow_hint": "Reliez un nœud KNXUltimate en mode Universel à la broche d'entrée (KNX → MQTT) et un autre à la broche de sortie (MQTT → KNX).",
32
40
  "no_gateway": "Sélectionnez d'abord une passerelle KNX.",
33
41
  "no_ga": "Aucune adresse de groupe trouvée. Importez la liste ETS dans la passerelle KNX.",
34
42
  "csv_error": "Impossible de charger la liste des adresses de groupe depuis la passerelle.",
@@ -113,7 +121,9 @@
113
121
  },
114
122
  "labels": {
115
123
  "outputKnxToIoT": "Flux KNX → IoT",
116
- "outputIoTToKnx": "Accusés IoT → KNX"
124
+ "outputIoTToKnx": "Accusés IoT → KNX",
125
+ "inputKnxBus": "Entrée bus KNX (depuis un nœud Universel)",
126
+ "outputKnxWrite": "Sortie écriture KNX (vers un nœud Universel)"
117
127
  }
118
128
  }
119
129
  }
@@ -11,9 +11,12 @@ Crea collegamenti bidirezionali tra indirizzi di gruppo KNX e canali IoT (topic
11
11
 
12
12
  Un bridge MQTT nativo con discovery di Home Assistant. Richiede un broker MQTT raggiungibile sia da Node-RED che da Home Assistant, con l'integrazione MQTT attiva in HA. Tutte le entità compaiono sotto un unico dispositivo con il nome di questo nodo.
13
13
 
14
+ - **Connessione al bus KNX** — come il nodo scambia telegrammi con KNX:
15
+ - *Stand-alone* (predefinito) — il nodo parla direttamente col gateway KNX e non ha PIN di ingresso/uscita.
16
+ - *Messaggi di flow* — il nodo mostra un PIN di ingresso e uno di uscita. Collega l'uscita di un nodo KNXUltimate in modalità **Universale** al PIN di ingresso (bus KNX → MQTT) e il PIN di uscita all'ingresso di un altro nodo KNXUltimate in modalità **Universale** (MQTT → bus KNX).
14
17
  - **URL broker / Nome utente / Password** — connessione al tuo broker MQTT.
15
18
  - **Topic di base / Prefisso discovery** — topic radice per i topic di stato/comando e il prefisso discovery di HA (predefinito `homeassistant`).
16
- - **Indirizzi di gruppo da esporre** — ogni indirizzo importato nel gateway KNX (lista ETS) è elencato con una checkbox. Gli indirizzi spuntati vengono pubblicati come entità Home Assistant, tipizzate automaticamente dal DPT (switch, sensor, binary_sensor, number, text). Usa il filtro e i pulsanti *Seleziona tutti* / *Deseleziona tutti*. Per impostazione predefinita sono tutti selezionati.
19
+ - **Indirizzi di gruppo da esporre** — ogni indirizzo importato nel gateway KNX (lista ETS) è elencato con una checkbox. Gli indirizzi spuntati vengono pubblicati come entità Home Assistant, tipizzate automaticamente dal DPT (switch, sensor, binary_sensor, number, text). Usa il filtro e i pulsanti *Seleziona tutti* / *Deseleziona tutti*. Per impostazione predefinita sono tutti selezionati. Ogni riga ha inoltre un'opzione **Sola lettura**: un indirizzo in sola lettura viene comunque pubblicato su Home Assistant (lo stato resta visibile) ma non accetta mai comandi verso il bus KNX (gli switch diventano binary_sensor, i number diventano sensor). I pulsanti *Imposta sola lettura* / *Togli sola lettura* la applicano a tutti gli indirizzi attualmente mostrati.
17
20
  - **Tapparelle e Termostati** — raggruppano più indirizzi in un'unica entità:
18
21
  - *Tapparella*: GA su/giù (1.008), GA stop opzionale (1.007), GA posizione comando/stato opzionale (5.001). *Inverti posizione* mappa la convenzione KNX (0% = aperto) su Home Assistant (100% = aperto).
19
22
  - *Termostato*: GA temperatura attuale (9.001), GA setpoint comando/stato (9.001), GA on/off opzionale (1.001 → off/heat), più temperatura min/max e step.
@@ -29,6 +29,14 @@
29
29
  "select_all": "Seleziona tutti",
30
30
  "select_none": "Deseleziona tutti",
31
31
  "exposed_count": "selezionati",
32
+ "read_only": "Sola lettura",
33
+ "read_only_bulk": "Sola lettura per gli indirizzi mostrati:",
34
+ "read_only_all": "Imposta sola lettura",
35
+ "read_only_none": "Togli sola lettura",
36
+ "bus_mode": "Connessione al bus KNX",
37
+ "bus_standalone": "Stand-alone (tramite il gateway)",
38
+ "bus_flow": "Messaggi di flow (PIN ingresso/uscita)",
39
+ "bus_flow_hint": "Collega un nodo KNXUltimate in modalità Universale al PIN di ingresso (KNX → MQTT) e un altro al PIN di uscita (MQTT → KNX).",
32
40
  "no_gateway": "Seleziona prima un gateway KNX.",
33
41
  "no_ga": "Nessun indirizzo di gruppo trovato. Importa la lista ETS nel gateway KNX.",
34
42
  "csv_error": "Impossibile caricare la lista degli indirizzi di gruppo dal gateway.",
@@ -113,7 +121,9 @@
113
121
  },
114
122
  "labels": {
115
123
  "outputKnxToIoT": "Flusso KNX → IoT",
116
- "outputIoTToKnx": "Ack IoT → KNX"
124
+ "outputIoTToKnx": "Ack IoT → KNX",
125
+ "inputKnxBus": "Ingresso bus KNX (da un nodo Universale)",
126
+ "outputKnxWrite": "Uscita scrittura KNX (verso un nodo Universale)"
117
127
  }
118
128
  }
119
129
  }
@@ -11,9 +11,12 @@
11
11
 
12
12
  带 Home Assistant 自动发现的原生 MQTT 桥接。需要一个 Node-RED 和 Home Assistant 都能访问的 MQTT broker,并在 HA 中启用 MQTT 集成。所有实体都出现在以该节点命名的同一个设备下。
13
13
 
14
+ - **KNX 总线连接** — 节点如何与 KNX 交换报文:
15
+ - *独立*(默认)— 节点直接与 KNX 网关通信,没有输入/输出端口。
16
+ - *流消息* — 节点显示一个输入端口和一个输出端口。将一个**通用**模式的 KNXUltimate 节点的输出连接到输入端口(KNX 总线 → MQTT),并将输出端口连接到另一个**通用**模式 KNXUltimate 节点的输入(MQTT → KNX 总线)。
14
17
  - **Broker URL / 用户名 / 密码** — 连接到你的 MQTT broker。
15
18
  - **基础主题 / 自动发现前缀** — 状态/命令主题的根主题以及 HA 自动发现前缀(默认 `homeassistant`)。
16
- - **要暴露的组地址** — KNX 网关中导入的每个地址(ETS 列表)都带有一个复选框。勾选的地址会作为 Home Assistant 实体发布,并根据 DPT 自动分类(switch、sensor、binary_sensor、number、text)。可使用筛选框和*全选* / *全不选*按钮。默认全部选中。
19
+ - **要暴露的组地址** — KNX 网关中导入的每个地址(ETS 列表)都带有一个复选框。勾选的地址会作为 Home Assistant 实体发布,并根据 DPT 自动分类(switch、sensor、binary_sensor、number、text)。可使用筛选框和*全选* / *全不选*按钮。默认全部选中。每一行还有一个**只读**开关:只读地址仍会发布到 Home Assistant(状态保持可见),但绝不接受回写到 KNX 总线的命令(switch 变为 binary_sensor,number 变为 sensor)。*设为只读* / *取消只读*按钮会将其应用到当前显示的所有地址。
17
20
  - **卷帘与温控器** — 将多个地址组合成一个实体:
18
21
  - *卷帘*:上/下 GA (1.008)、可选停止 GA (1.007)、可选位置 设置/状态 GA (5.001)。*反转位置* 将 KNX 约定(0% = 打开)映射到 Home Assistant(100% = 打开)。
19
22
  - *温控器*:当前温度 GA (9.001)、设定值 设置/状态 GA (9.001)、可选开/关 GA (1.001 → off/heat),以及最低/最高温度和步长。
@@ -29,6 +29,14 @@
29
29
  "select_all": "全选",
30
30
  "select_none": "全不选",
31
31
  "exposed_count": "已选择",
32
+ "read_only": "只读",
33
+ "read_only_bulk": "对显示的地址设为只读:",
34
+ "read_only_all": "设为只读",
35
+ "read_only_none": "取消只读",
36
+ "bus_mode": "KNX 总线连接",
37
+ "bus_standalone": "独立(通过网关)",
38
+ "bus_flow": "流消息(输入/输出端口)",
39
+ "bus_flow_hint": "将一个通用模式的 KNXUltimate 节点接到输入端口(KNX → MQTT),另一个接到输出端口(MQTT → KNX)。",
32
40
  "no_gateway": "请先选择一个 KNX 网关。",
33
41
  "no_ga": "未找到组地址。请在 KNX 网关中导入 ETS 列表。",
34
42
  "csv_error": "无法从网关加载组地址列表。",
@@ -113,7 +121,9 @@
113
121
  },
114
122
  "labels": {
115
123
  "outputKnxToIoT": "KNX → IoT 流",
116
- "outputIoTToKnx": "IoT → KNX 确认"
124
+ "outputIoTToKnx": "IoT → KNX 确认",
125
+ "inputKnxBus": "KNX 总线输入(来自通用节点)",
126
+ "outputKnxWrite": "KNX 写入输出(发送到通用节点)"
117
127
  }
118
128
  }
119
129
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "5.0.1",
6
+ "version": "5.0.2",
7
7
  "description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.",
8
8
  "files": [
9
9
  "nodes/",