node-red-contrib-knx-ultimate 5.0.4 → 5.2.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/nodes/commonFunctions.js +93 -0
  3. package/nodes/icons/node-matter-icon.svg +12 -0
  4. package/nodes/knxUltimateMatterBridge.html +342 -0
  5. package/nodes/knxUltimateMatterBridge.js +313 -0
  6. package/nodes/knxUltimateMatterDevice.html +632 -0
  7. package/nodes/knxUltimateMatterDevice.js +308 -0
  8. package/nodes/locales/de/knxUltimateMatterBridge.html +53 -0
  9. package/nodes/locales/de/knxUltimateMatterBridge.json +83 -0
  10. package/nodes/locales/de/knxUltimateMatterDevice.html +56 -0
  11. package/nodes/locales/de/knxUltimateMatterDevice.json +109 -0
  12. package/nodes/locales/de/matter-config.html +25 -0
  13. package/nodes/locales/de/matter-config.json +26 -0
  14. package/nodes/locales/de/matterbridge-config.html +34 -0
  15. package/nodes/locales/de/matterbridge-config.json +20 -0
  16. package/nodes/locales/en/knxUltimateMatterBridge.html +53 -0
  17. package/nodes/locales/en/knxUltimateMatterBridge.json +83 -0
  18. package/nodes/locales/en/knxUltimateMatterDevice.html +56 -0
  19. package/nodes/locales/en/knxUltimateMatterDevice.json +109 -0
  20. package/nodes/locales/en/matter-config.html +25 -0
  21. package/nodes/locales/en/matter-config.json +26 -0
  22. package/nodes/locales/en/matterbridge-config.html +34 -0
  23. package/nodes/locales/en/matterbridge-config.json +20 -0
  24. package/nodes/locales/es/knxUltimateMatterBridge.html +53 -0
  25. package/nodes/locales/es/knxUltimateMatterBridge.json +83 -0
  26. package/nodes/locales/es/knxUltimateMatterDevice.html +56 -0
  27. package/nodes/locales/es/knxUltimateMatterDevice.json +109 -0
  28. package/nodes/locales/es/matter-config.html +25 -0
  29. package/nodes/locales/es/matter-config.json +26 -0
  30. package/nodes/locales/es/matterbridge-config.html +34 -0
  31. package/nodes/locales/es/matterbridge-config.json +20 -0
  32. package/nodes/locales/fr/knxUltimateMatterBridge.html +53 -0
  33. package/nodes/locales/fr/knxUltimateMatterBridge.json +83 -0
  34. package/nodes/locales/fr/knxUltimateMatterDevice.html +56 -0
  35. package/nodes/locales/fr/knxUltimateMatterDevice.json +109 -0
  36. package/nodes/locales/fr/matter-config.html +25 -0
  37. package/nodes/locales/fr/matter-config.json +26 -0
  38. package/nodes/locales/fr/matterbridge-config.html +34 -0
  39. package/nodes/locales/fr/matterbridge-config.json +20 -0
  40. package/nodes/locales/it/knxUltimateMatterBridge.html +53 -0
  41. package/nodes/locales/it/knxUltimateMatterBridge.json +83 -0
  42. package/nodes/locales/it/knxUltimateMatterDevice.html +56 -0
  43. package/nodes/locales/it/knxUltimateMatterDevice.json +109 -0
  44. package/nodes/locales/it/matter-config.html +25 -0
  45. package/nodes/locales/it/matter-config.json +26 -0
  46. package/nodes/locales/it/matterbridge-config.html +34 -0
  47. package/nodes/locales/it/matterbridge-config.json +20 -0
  48. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +53 -0
  49. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +83 -0
  50. package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +56 -0
  51. package/nodes/locales/zh-CN/knxUltimateMatterDevice.json +109 -0
  52. package/nodes/locales/zh-CN/matter-config.html +25 -0
  53. package/nodes/locales/zh-CN/matter-config.json +26 -0
  54. package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
  55. package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
  56. package/nodes/matter-config.html +226 -0
  57. package/nodes/matter-config.js +242 -0
  58. package/nodes/matterbridge-config.html +121 -0
  59. package/nodes/matterbridge-config.js +220 -0
  60. package/nodes/utils/matterBridgeDeviceFactory.mjs +511 -0
  61. package/nodes/utils/matterBridgeEngine.mjs +290 -0
  62. package/nodes/utils/matterEngine.mjs +475 -0
  63. package/nodes/utils/matterKnxConverter.js +204 -0
  64. package/package.json +8 -2
package/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 5.2.2** - July 2026<br/>
10
+
11
+ - New **Matter Bridge (BETA)**: exposes **KNX group addresses as Matter devices**. Alexa, Google Home, Apple Home (or any Matter controller) commission the bridge once — via QR code or 11-digit manual code shown in the editor — and see all your KNX devices with the names you typed, ready for app and voice control. It is the opposite direction of the Matter Device node introduced in 5.1.0.<br/>
12
+ - Two-node architecture (like the HUE and Matter Controller integrations): a **Matter Bridge** configuration node is the bridge itself (holds the pairing, the QR code and the port), and a **Matter Bridge device** node is added per exposed device and points to it. Spread the device nodes across your flows, each with its own status — no giant list.<br/>
13
+ - **17 device types**: on/off light, dimmable light, **RGB color light** (DPT 232.600, converted to/from the Matter color wheel), **tunable white light** (color temperature in Kelvin, DPT 7.600), plug, cover/shutter (up/down, stop, position % with optional inversion), thermostat (current temperature + heating setpoint, both ways), **fan/ventilation** (speed %), sensors (temperature, humidity, illuminance, occupancy, contact), **smoke/CO alarm**, **water leak detector**, **air quality sensor** (CO2 ppm with automatic air-quality classification) and **robot vacuum** (flow-only: integrate your robot with any Node-RED node and expose it to the voice assistants).<br/>
14
+ - Bidirectional with loop protection: assistant commands write to the KNX *command* group addresses; telegrams on the *status* group addresses update the Matter attributes (and the apps). At startup each device node can send a `GroupValue_Read` to its status GAs so the Matter attributes are populated before the first app query.<br/>
15
+ - **Live device reconciliation**: the Matter server stays alive across re-deploys — added/renamed/removed device nodes appear on the paired controllers within seconds, without restarting Node-RED and without re-pairing.<br/>
16
+ - Optional **node Input/Output PINs**: the output forwards every controller command to the flow; the input updates the Matter state from flow messages (`msg.payload = { function, value }`). Devices without a command GA become flow-only devices.<br/>
17
+ - The **KNX gateway is optional** on the device node: without it the device runs in flow-only mode, useful to expose flow-computed or non-KNX devices to Alexa & Co.<br/>
18
+ - Multi-fabric: several controllers can be paired at the same time; the pairing panel lists them and a **Reset pairing** button decommissions everything. The bridge identity persists across re-deploys (no re-pairing needed). Each configuration node can use a different UDP port, so multiple independent bridges are possible.<br/>
19
+ - Editor with ETS group-address autocomplete filtered per function: the KNX gateway is resolved automatically (auto-selected when only one exists), so the address list is always available. Localized in EN/IT/DE/FR/ES/zh-CN.<br/>
20
+ - Note for early 5.2.x BETA testers: the two-node split changes the Matter identity of the bridge - remove the old bridge from your Matter app and pair the new configuration node once.<br/>
21
+ - **Matter Device** node editor: the mappings list grows to fill the edit panel and has a native resize grip (bottom-right corner) to drag it to any height.<br/>
22
+ - Matter integration hardening: try/catch protection on all critical paths (controller startup, client registration, structure queries, commissioning aftermath) so errors never crash Node-RED. Fixed the devices editor losing the configured group addresses on close/reopen (with automatic recovery of configurations saved by the affected versions).<br/>
23
+ - **Documentation**: node help for the three Matter nodes in all six languages, new **Matter section** in the online docs sidebar with the Matter Controller, Matter Device and Matter Bridge pages (EN/IT/DE/FR/ES/zh-CN), the two new nodes on the homepage node list and the Matter logo alongside Home Assistant and MQTT.<br/>
24
+
25
+ **Version 5.1.0** - July 2026<br/>
26
+
27
+ - New **Matter integration (BETA)**: control any commissioned **Matter device** from the KNX bus and track its states on KNX group addresses.<br/>
28
+ - New **Matter Controller** configuration node: a full Matter controller with its own fabric (based on [matter.js](https://github.com/project-chip/matter.js)). Pair devices directly from the editor using the 11-digit manual pairing code or the QR code content (`MT:...`); commissioning works over the IP network (WiFi/Ethernet or Thread via border router). The commissioned devices list shows name, product, node ID and connection state, with one-click unpair. Fabric credentials are stored under `knxultimatestorage/matter` in the Node-RED user directory.<br/>
29
+ - New **Matter Device (BETA)** node: pick a paired Matter device and map KNX group addresses to its functions with a dynamic mapping list. The function list is **human friendly**: it shows only the functions the device really exposes, with plain-language localized names (e.g. *"On/Off switch"*, *"Instantaneous power (W)"*, *"Room temperature (°C)"*) and the current value in brackets, instead of raw Matter cluster/attribute names. Friendly endpoint names for 34 common device types (smart plug, dimmable light, cover, thermostat, sensors...).<br/>
30
+ - Mappings are bidirectional: **KNX → Matter** (commands/attribute writes, e.g. GA 1/1/1 DPT 1.001 → OnOff) and **Matter → KNX** (attribute changes written to the GA; `GroupValue_Read` requests answered from the cache). Automatic value conversion for the common clusters: OnOff, LevelControl (0-254 ↔ %), WindowCovering (percent100ths ↔ %, up/down), ColorControl (mireds ↔ Kelvin), Thermostat and temperature/humidity sensors (centi-units ↔ units), illuminance (log scale ↔ Lux), battery (half-percent ↔ %), power/energy metering (mW ↔ W, mWh ↔ kWh), door lock, occupancy and contact sensors.<br/>
31
+ - Selecting a function pre-fills the **suggested KNX DPT** and the GA name; the group address field has the usual **ETS autocomplete**. Optional node input/output pins for raw Matter access from the flow. Editor and help localized in EN/IT/DE/FR/ES/zh-CN.<br/>
32
+
9
33
  **Version 5.0.4** - July 2026<br/>
10
34
 
11
35
  - **IoT Bridge** node, **Covers & Thermostats** editor: fixed the group-address autocomplete, which suggested only DPT **1.x** addresses in every field. Each field now filters by the datapoint it expects: cover **Set/Status position** GAs list DPT **5.x** (5.001), and the thermostat **Current temperature** and **Setpoint set/status** GAs list DPT **9.x** (9.001). Up/Down, Stop and On/Off keep their 1.x filter.<br/>
@@ -944,6 +944,99 @@ module.exports = (RED) => {
944
944
  }
945
945
  })
946
946
 
947
+ // MATTER: returns the list of commissioned Matter devices of a matter-config node
948
+ RED.httpAdmin.get('/KNXUltimateMatterGetNodes', RED.auth.needsPermission('matter-config.read'), (req, res) => {
949
+ try {
950
+ const matterServer = RED.nodes.getNode(req.query.serverId)
951
+ if (matterServer === null || matterServer === undefined) {
952
+ res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
953
+ return
954
+ }
955
+ res.json({ devices: matterServer.getCommissionedNodesDetails() })
956
+ } catch (error) {
957
+ RED.log.error(`Err KNXUltimateMatterGetNodes: ${error.message}`)
958
+ res.json({ error: error.message })
959
+ }
960
+ })
961
+
962
+ // MATTER: returns endpoints/clusters/attributes/commands of a commissioned Matter device
963
+ RED.httpAdmin.get('/KNXUltimateMatterGetStructure', RED.auth.needsPermission('matter-config.read'), (req, res) => {
964
+ try {
965
+ const matterServer = RED.nodes.getNode(req.query.serverId)
966
+ if (matterServer === null || matterServer === undefined) {
967
+ res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
968
+ return
969
+ }
970
+ res.json(matterServer.getNodeStructure(req.query.nodeId))
971
+ } catch (error) {
972
+ RED.log.error(`Err KNXUltimateMatterGetStructure: ${error.message}`)
973
+ res.json({ error: error.message })
974
+ }
975
+ })
976
+
977
+ // MATTER: commissions (pairs) a new Matter device using a pairing code or QR code string
978
+ RED.httpAdmin.get('/KNXUltimateMatterPair', RED.auth.needsPermission('matter-config.write'), async (req, res) => {
979
+ try {
980
+ const matterServer = RED.nodes.getNode(req.query.serverId)
981
+ if (matterServer === null || matterServer === undefined) {
982
+ res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
983
+ return
984
+ }
985
+ const nodeId = await matterServer.commission(req.query.code)
986
+ res.json({ nodeId })
987
+ } catch (error) {
988
+ RED.log.error(`Err KNXUltimateMatterPair: ${error.message}`)
989
+ res.json({ error: error.message })
990
+ }
991
+ })
992
+
993
+ // MATTER: decommissions (unpairs) a Matter device
994
+ RED.httpAdmin.get('/KNXUltimateMatterUnpair', RED.auth.needsPermission('matter-config.write'), async (req, res) => {
995
+ try {
996
+ const matterServer = RED.nodes.getNode(req.query.serverId)
997
+ if (matterServer === null || matterServer === undefined) {
998
+ res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
999
+ return
1000
+ }
1001
+ await matterServer.removeCommissionedNode(req.query.nodeId)
1002
+ res.json({ status: 'ok' })
1003
+ } catch (error) {
1004
+ RED.log.error(`Err KNXUltimateMatterUnpair: ${error.message}`)
1005
+ res.json({ error: error.message })
1006
+ }
1007
+ })
1008
+
1009
+ // MATTER BRIDGE: returns the pairing info (QR code, manual code, commissioned fabrics) of a matterbridge-config node
1010
+ RED.httpAdmin.get('/KNXUltimateMatterBridgeInfo', RED.auth.needsPermission('matterbridge-config.read'), (req, res) => {
1011
+ try {
1012
+ const bridgeConfig = RED.nodes.getNode(req.query.configId)
1013
+ if (bridgeConfig === null || bridgeConfig === undefined || typeof bridgeConfig.getPairingInfo !== 'function') {
1014
+ res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
1015
+ return
1016
+ }
1017
+ res.json(bridgeConfig.getPairingInfo())
1018
+ } catch (error) {
1019
+ RED.log.error(`Err KNXUltimateMatterBridgeInfo: ${error.message}`)
1020
+ res.json({ error: error.message })
1021
+ }
1022
+ })
1023
+
1024
+ // MATTER BRIDGE: factory reset (removes all paired controllers, restarts pairing advertising)
1025
+ RED.httpAdmin.get('/KNXUltimateMatterBridgeReset', RED.auth.needsPermission('matterbridge-config.write'), async (req, res) => {
1026
+ try {
1027
+ const bridgeConfig = RED.nodes.getNode(req.query.configId)
1028
+ if (bridgeConfig === null || bridgeConfig === undefined || typeof bridgeConfig.factoryResetBridge !== 'function') {
1029
+ res.json({ error: 'PLEASE DEPLOY FIRST: then try again.' })
1030
+ return
1031
+ }
1032
+ await bridgeConfig.factoryResetBridge()
1033
+ res.json({ status: 'ok' })
1034
+ } catch (error) {
1035
+ RED.log.error(`Err KNXUltimateMatterBridgeReset: ${error.message}`)
1036
+ res.json({ error: error.message })
1037
+ }
1038
+ })
1039
+
947
1040
  RED.httpAdmin.get('/knxUltimateDpts', (req, res) => {
948
1041
  try {
949
1042
  const dpts = Object.entries(dptlib.dpts).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy('base'))
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="40" height="60" viewBox="0 0 40 60" xmlns="http://www.w3.org/2000/svg">
3
+ <g fill="#ffffff" stroke="none">
4
+ <circle cx="20" cy="30" r="4.5"/>
5
+ <g id="matterArrow">
6
+ <rect x="18.4" y="9" width="3.2" height="11"/>
7
+ <path d="M 14.5 18.5 L 25.5 18.5 L 20 25.5 Z"/>
8
+ </g>
9
+ <use href="#matterArrow" transform="rotate(120 20 30)"/>
10
+ <use href="#matterArrow" transform="rotate(240 20 30)"/>
11
+ </g>
12
+ </svg>
@@ -0,0 +1,342 @@
1
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/11f26b4500.js"></script>
2
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
3
+
4
+ <script type="text/javascript">
5
+ (function () {
6
+ // GA fields of each virtual device type.
7
+ // dir 'command' = Matter -> KNX (GA written on assistant commands), 'status' = KNX -> Matter.
8
+ const TYPE_FIELDS = {
9
+ onofflight: [
10
+ { key: 'OnOff', labelKey: 'fn_onoff_cmd', dptPrefix: '1.', fallbackDpt: '1.001' },
11
+ { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dptPrefix: '1.', fallbackDpt: '1.001' }
12
+ ],
13
+ onoffplug: [
14
+ { key: 'OnOff', labelKey: 'fn_onoff_cmd', dptPrefix: '1.', fallbackDpt: '1.001' },
15
+ { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dptPrefix: '1.', fallbackDpt: '1.001' }
16
+ ],
17
+ dimmablelight: [
18
+ { key: 'OnOff', labelKey: 'fn_onoff_cmd', dptPrefix: '1.', fallbackDpt: '1.001' },
19
+ { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dptPrefix: '1.', fallbackDpt: '1.001' },
20
+ { key: 'Level', labelKey: 'fn_level_cmd', dptPrefix: '5.', fallbackDpt: '5.001' },
21
+ { key: 'LevelStatus', labelKey: 'fn_level_status', dptPrefix: '5.', fallbackDpt: '5.001' }
22
+ ],
23
+ rgblight: [
24
+ { key: 'OnOff', labelKey: 'fn_onoff_cmd', dptPrefix: '1.', fallbackDpt: '1.001' },
25
+ { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dptPrefix: '1.', fallbackDpt: '1.001' },
26
+ { key: 'Level', labelKey: 'fn_level_cmd', dptPrefix: '5.', fallbackDpt: '5.001' },
27
+ { key: 'LevelStatus', labelKey: 'fn_level_status', dptPrefix: '5.', fallbackDpt: '5.001' },
28
+ { key: 'RGB', labelKey: 'fn_rgb_cmd', dptPrefix: '232.', fallbackDpt: '232.600' },
29
+ { key: 'RGBStatus', labelKey: 'fn_rgb_status', dptPrefix: '232.', fallbackDpt: '232.600' }
30
+ ],
31
+ colortemperaturelight: [
32
+ { key: 'OnOff', labelKey: 'fn_onoff_cmd', dptPrefix: '1.', fallbackDpt: '1.001' },
33
+ { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dptPrefix: '1.', fallbackDpt: '1.001' },
34
+ { key: 'Level', labelKey: 'fn_level_cmd', dptPrefix: '5.', fallbackDpt: '5.001' },
35
+ { key: 'LevelStatus', labelKey: 'fn_level_status', dptPrefix: '5.', fallbackDpt: '5.001' },
36
+ { key: 'CT', labelKey: 'fn_ct_cmd', dptPrefix: '7.', fallbackDpt: '7.600' },
37
+ { key: 'CTStatus', labelKey: 'fn_ct_status', dptPrefix: '7.', fallbackDpt: '7.600' }
38
+ ],
39
+ temperaturesensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dptPrefix: '9.', fallbackDpt: '9.001' }],
40
+ humiditysensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dptPrefix: '9.', fallbackDpt: '9.007' }],
41
+ lightsensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dptPrefix: '9.', fallbackDpt: '9.004' }],
42
+ occupancysensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dptPrefix: '1.', fallbackDpt: '1.011' }],
43
+ contactsensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dptPrefix: '1.', fallbackDpt: '1.002' }],
44
+ windowcovering: [
45
+ { key: 'UpDown', labelKey: 'fn_updown', dptPrefix: '1.', fallbackDpt: '1.008' },
46
+ { key: 'Stop', labelKey: 'fn_stop', dptPrefix: '1.', fallbackDpt: '1.017' },
47
+ { key: 'Position', labelKey: 'fn_position_cmd', dptPrefix: '5.', fallbackDpt: '5.001' },
48
+ { key: 'PositionStatus', labelKey: 'fn_position_status', dptPrefix: '5.', fallbackDpt: '5.001' }
49
+ ],
50
+ thermostat: [
51
+ { key: 'CurrentTemp', labelKey: 'fn_currenttemp', dptPrefix: '9.', fallbackDpt: '9.001' },
52
+ { key: 'Setpoint', labelKey: 'fn_setpoint_cmd', dptPrefix: '9.', fallbackDpt: '9.001' },
53
+ { key: 'SetpointStatus', labelKey: 'fn_setpoint_status', dptPrefix: '9.', fallbackDpt: '9.001' }
54
+ ],
55
+ smokecoalarm: [
56
+ { key: 'Smoke', labelKey: 'fn_smoke_ga', dptPrefix: '1.', fallbackDpt: '1.005' },
57
+ { key: 'CO', labelKey: 'fn_co_ga', dptPrefix: '1.', fallbackDpt: '1.005' }
58
+ ],
59
+ waterleakdetector: [{ key: 'Status', labelKey: 'fn_leak_ga', dptPrefix: '1.', fallbackDpt: '1.005' }],
60
+ airqualitysensor: [{ key: 'Status', labelKey: 'fn_co2_ga', dptPrefix: '9.', fallbackDpt: '9.008' }],
61
+ fan: [
62
+ { key: 'FanSpeed', labelKey: 'fn_fanspeed_cmd', dptPrefix: '5.', fallbackDpt: '5.001' },
63
+ { key: 'FanSpeedStatus', labelKey: 'fn_fanspeed_status', dptPrefix: '5.', fallbackDpt: '5.001' }
64
+ ],
65
+ robotvacuum: [] // Flow-only: no GA fields, controlled via the node PINs
66
+ };
67
+ const TYPE_ORDER = ['onofflight', 'dimmablelight', 'rgblight', 'colortemperaturelight', 'onoffplug', 'windowcovering', 'thermostat', 'fan', 'temperaturesensor', 'humiditysensor', 'lightsensor', 'occupancysensor', 'contactsensor', 'smokecoalarm', 'waterleakdetector', 'airqualitysensor', 'robotvacuum'];
68
+
69
+ // Every GA/DPT key across all types, used to declare the node defaults.
70
+ const ALL_KEYS = [];
71
+ Object.keys(TYPE_FIELDS).forEach((t) => TYPE_FIELDS[t].forEach((f) => { if (ALL_KEYS.indexOf(f.key) === -1) ALL_KEYS.push(f.key); }));
72
+ const gaDefaults = {
73
+ server: { type: 'knxUltimate-config', required: false },
74
+ serverMatterBridge: { type: 'matterbridge-config', required: true },
75
+ name: { value: '' },
76
+ deviceType: { value: 'onofflight' },
77
+ invertPosition: { value: false },
78
+ readStatusAtStartup: { value: 'yes' },
79
+ enableNodePINS: { value: 'no' },
80
+ outputs: { value: 0 },
81
+ inputs: { value: 0 }
82
+ };
83
+ ALL_KEYS.forEach((key) => {
84
+ gaDefaults['ga' + key] = { value: '' };
85
+ gaDefaults['dpt' + key] = { value: '' };
86
+ });
87
+
88
+ RED.nodes.registerType('knxUltimateMatterBridge', {
89
+ category: 'KNX Ultimate Matter',
90
+ color: '#D7C7E9',
91
+ defaults: gaDefaults,
92
+ inputs: 0,
93
+ outputs: 0,
94
+ icon: 'node-matter-icon.svg',
95
+ label() {
96
+ return this.name || 'Matter Bridge device (BETA)';
97
+ },
98
+ paletteLabel: 'Matter Bridge device (BETA)',
99
+ oneditprepare() {
100
+ try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
101
+ const node = this;
102
+
103
+ // Resolves the KNX gateway at query time: the selected one, or - as a
104
+ // fallback - the first knxUltimate-config available, so the GA list works
105
+ // even before the user picks the gateway on this device node.
106
+ const findKnxGateway = () => {
107
+ let srv = null;
108
+ try {
109
+ const val = $('#node-input-server').val();
110
+ if (val && val !== '_ADD_') srv = RED.nodes.node(val);
111
+ } catch (error) { /* empty */ }
112
+ if (!srv) {
113
+ try {
114
+ RED.nodes.eachConfig((c) => { if (!srv && c.type === 'knxUltimate-config') srv = c; });
115
+ } catch (error) { /* empty */ }
116
+ }
117
+ return srv;
118
+ };
119
+
120
+ // Auto-select the KNX gateway when there is only one, sparing the user a click per device node
121
+ try {
122
+ const cur = $('#node-input-server').val();
123
+ if (!cur || cur === '_ADD_') {
124
+ const gws = [];
125
+ RED.nodes.eachConfig((c) => { if (c.type === 'knxUltimate-config') gws.push(c); });
126
+ if (gws.length === 1) $('#node-input-server').val(gws[0].id).trigger('change');
127
+ }
128
+ } catch (error) { /* empty */ }
129
+
130
+ // Device type dropdown
131
+ const $type = $('#node-input-deviceType');
132
+ TYPE_ORDER.forEach((t) => {
133
+ $type.append($('<option></option>').attr('value', t).text(node._('knxUltimateMatterBridge.types.' + t)));
134
+ });
135
+ $type.val(node.deviceType || 'onofflight');
136
+
137
+ // Compact "list" layout: a small gray caption above each input, fields wrapping
138
+ // on one or more rows (same pattern used across the rest of the KNX-Ultimate nodes).
139
+ const $fields = $('#mb-ga-fields').css({ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: '10px' });
140
+ const $invertRow = $('#mb-invert-row');
141
+ const gaInputs = {};
142
+
143
+ function createLabeledField(row, element, options = {}) {
144
+ const wrapper = $('<div/>', {
145
+ style: 'display:flex; flex-direction:column; margin-bottom:8px;'
146
+ }).appendTo(row);
147
+ if (options.width) wrapper.css('width', options.width);
148
+ const $labelSpan = $('<span/>', {
149
+ style: 'font-size:11px; color:#666; margin-bottom:2px; white-space:nowrap;'
150
+ }).appendTo(wrapper);
151
+ element.css('width', '100%');
152
+ element.appendTo(wrapper);
153
+ return { wrapper, $labelSpan };
154
+ }
155
+
156
+ function attachGaAutocomplete($ga, $dpt, fieldDef) {
157
+ $ga.autocomplete({
158
+ minLength: 0,
159
+ source(request, response) {
160
+ const gw = findKnxGateway();
161
+ if (!gw) { response([]); return; }
162
+ $.getJSON(`knxUltimatecsv?nodeID=${gw.id}`, (data) => {
163
+ response($.map(data, (value) => {
164
+ const search = `${value.ga} (${value.devicename}) DPT${value.dpt}`;
165
+ if (htmlUtilsfullCSVSearch(search, `${request.term} ${fieldDef.dptPrefix}`)) {
166
+ return { label: `${value.ga} # ${value.devicename} # ${value.dpt}`, value: value.ga, dpt: value.dpt };
167
+ }
168
+ return null;
169
+ }));
170
+ });
171
+ },
172
+ select(event, ui) { $dpt.val(ui.item.dpt || fieldDef.fallbackDpt); }
173
+ });
174
+ $ga.on('focus.knxUltimateMatterBridge click.knxUltimateMatterBridge', function () {
175
+ try { $(this).autocomplete('search', ''); } catch (error) { /* empty */ }
176
+ });
177
+ try {
178
+ const gw = findKnxGateway();
179
+ if (gw && gw.id) KNX_enableSecureFormatting($ga, gw.id);
180
+ } catch (error) { /* empty */ }
181
+ }
182
+
183
+ // Build one field per possible GA key; show only the ones of the selected type.
184
+ ALL_KEYS.forEach((key) => {
185
+ const fieldDef = {};
186
+ Object.keys(TYPE_FIELDS).forEach((t) => { const f = TYPE_FIELDS[t].find((x) => x.key === key); if (f) Object.assign(fieldDef, f); });
187
+ const $ga = $('<input/>', { type: 'text', placeholder: '1/1/1' }).val(node['ga' + key] || '');
188
+ const $dpt = $('<input/>', { type: 'hidden' }).val(node['dpt' + key] || '');
189
+ const { wrapper, $labelSpan } = createLabeledField($fields, $ga, { width: '150px' });
190
+ wrapper.append($dpt); // Kept alongside the GA input; never rendered.
191
+ attachGaAutocomplete($ga, $dpt, fieldDef);
192
+ // Keyed by field key (not DOM position), so save is robust regardless of layout.
193
+ gaInputs[key] = { wrapper, $ga, $dpt, $labelSpan };
194
+ });
195
+
196
+ function updateTypePresentation() {
197
+ const selected = $type.val();
198
+ const active = TYPE_FIELDS[selected] || [];
199
+ ALL_KEYS.forEach((key) => {
200
+ const entry = gaInputs[key];
201
+ const fieldForType = active.find((f) => f.key === key);
202
+ if (fieldForType) {
203
+ entry.wrapper.show();
204
+ entry.$labelSpan.text(node._('knxUltimateMatterBridge.functions.' + fieldForType.labelKey));
205
+ } else {
206
+ entry.wrapper.hide();
207
+ }
208
+ });
209
+ $invertRow.toggle(selected === 'windowcovering');
210
+ }
211
+ $type.on('change', updateTypePresentation);
212
+ updateTypePresentation();
213
+
214
+ // oneditsave reads the GA values straight from these references: no DOM
215
+ // position matching, so it cannot break if the layout changes again.
216
+ node._matterGaInputs = gaInputs;
217
+
218
+ $('#node-input-invertPosition').prop('checked', node.invertPosition === true);
219
+ $('#node-input-readStatusAtStartup').val(node.readStatusAtStartup || 'yes');
220
+ $('#node-input-enableNodePINS').val(node.enableNodePINS || 'no');
221
+ },
222
+ oneditsave() {
223
+ try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
224
+ const node = this;
225
+ const selected = $('#node-input-deviceType').val();
226
+ node.deviceType = selected;
227
+ node.invertPosition = $('#node-input-invertPosition').is(':checked');
228
+ const active = TYPE_FIELDS[selected] || [];
229
+ const gaInputs = node._matterGaInputs || {};
230
+ // Clear every GA/DPT, then write only the ones of the selected type, reading
231
+ // straight from the field references built in oneditprepare (not DOM position).
232
+ ALL_KEYS.forEach((key) => {
233
+ node['ga' + key] = '';
234
+ node['dpt' + key] = '';
235
+ const entry = gaInputs[key];
236
+ if (!entry) return;
237
+ const isActive = active.some((f) => f.key === key);
238
+ const ga = (entry.$ga.val() || '').trim();
239
+ if (isActive && ga !== '') {
240
+ node['ga' + key] = ga;
241
+ node['dpt' + key] = (entry.$dpt.val() || '').trim();
242
+ }
243
+ });
244
+ delete node._matterGaInputs;
245
+ this.enableNodePINS = $('#node-input-enableNodePINS').val() || 'no';
246
+ this.outputs = this.enableNodePINS === 'yes' ? 1 : 0;
247
+ this.inputs = this.enableNodePINS === 'yes' ? 1 : 0;
248
+ },
249
+ oneditcancel() {
250
+ try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
251
+ delete this._matterGaInputs;
252
+ }
253
+ });
254
+ }());
255
+ </script>
256
+
257
+ <script type="text/html" data-template-name="knxUltimateMatterBridge">
258
+ <div class="form-row" style="margin-bottom:10px;">
259
+ <span style="color:#ff0000"><i class="fa fa-youtube"></i></span>&nbsp;<a target="_blank" href="https://www.youtube.com/playlist?list=PL9Yh1bjbLAYrU8PsVhW4xzEug2WtVFv3E"><b>KNX-Ultimate video tutorials (YouTube playlist)</b></a>
260
+ </div>
261
+
262
+ <div class="form-row">
263
+ <label for="node-input-server">
264
+ <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKnRFWHRDcmVhdGlvbiBUaW1lAEZyIDYgQXVnIDIwMTAgMjE6NTI6MTkgKzAxMDD84aS8AAAAB3RJTUUH3gYYCicNV+4WIQAAAAlwSFlzAAALEgAACxIB0t1+/AAAAARnQU1BAACxjwv8YQUAAACUSURBVHjaY2CgFZg5c+Z/ZEyWAZ8+f/6/ZsWs/xoamqMGkGrA6Wla/1+fVARjEBuGsSoGmY4eZSCNL59d/g8DIDbIAHR14OgFGQByKjIGKX5+6/T///8gGMQGiV1+/B0Fg70GIkD+RMYgxf/O5/7//2MSmAZhkBi6OrgB6Bg5DGB4ajr3f2xqsYYLSDE2THJUDg0AAAqyDVd4tp4YAAAAAElFTkSuQmCC" />
265
+ <span data-i18n="common.knx_gw"></span>
266
+ </label>
267
+ <input type="text" id="node-input-server">
268
+ </div>
269
+
270
+ <div class="form-row">
271
+ <label for="node-input-serverMatterBridge">
272
+ <i class="fa fa-share-alt"></i> <span data-i18n="knxUltimateMatterBridge.bridge"></span>
273
+ </label>
274
+ <input type="text" id="node-input-serverMatterBridge">
275
+ </div>
276
+
277
+ <div class="form-row">
278
+ <label for="node-input-name">
279
+ <i class="fa fa-tag"></i> <span data-i18n="knxUltimateMatterBridge.device_name"></span>
280
+ </label>
281
+ <input type="text" id="node-input-name" data-i18n="[placeholder]knxUltimateMatterBridge.fields.name_placeholder">
282
+ </div>
283
+
284
+ <div class="form-row">
285
+ <label for="node-input-deviceType">
286
+ <i class="fa fa-microchip"></i> <span data-i18n="knxUltimateMatterBridge.fields.type"></span>
287
+ </label>
288
+ <select id="node-input-deviceType" style="width:260px;"></select>
289
+ </div>
290
+
291
+ <div class="form-tips" style="margin-bottom:8px;">
292
+ <i class="fa fa-circle-info"></i>
293
+ <span data-i18n="knxUltimateMatterBridge.device_help"></span>
294
+ </div>
295
+
296
+ <div id="mb-ga-fields"></div>
297
+
298
+ <div class="form-row" id="mb-invert-row" style="display:none;">
299
+ <label for="node-input-invertPosition" style="width:auto;">
300
+ <input type="checkbox" id="node-input-invertPosition" style="width:auto; display:inline-block; vertical-align:middle;">
301
+ <span data-i18n="knxUltimateMatterBridge.fields.invert"></span>
302
+ </label>
303
+ </div>
304
+
305
+ <hr/>
306
+
307
+ <div class="form-row">
308
+ <label for="node-input-readStatusAtStartup" style="width:260px;">
309
+ <i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterBridge.read_status_startup"></span>
310
+ </label>
311
+ <select id="node-input-readStatusAtStartup" style="width:120px;">
312
+ <option value="yes" data-i18n="knxUltimateMatterBridge.opt_yes"></option>
313
+ <option value="no" data-i18n="knxUltimateMatterBridge.opt_no"></option>
314
+ </select>
315
+ </div>
316
+
317
+ <div class="form-row">
318
+ <label for="node-input-enableNodePINS" style="width:260px;">
319
+ <i class="fa fa-code"></i> <span data-i18n="knxUltimateMatterBridge.node_pins"></span>
320
+ </label>
321
+ <select id="node-input-enableNodePINS" style="width:220px;">
322
+ <option value="no" data-i18n="knxUltimateMatterBridge.node_pins_hide"></option>
323
+ <option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
324
+ </select>
325
+ </div>
326
+ </script>
327
+
328
+ <script type="text/markdown" data-help-name="knxUltimateMatterBridge">
329
+ Exposes **one KNX device as a Matter device**. This node is in **BETA**.
330
+
331
+ Point it to a **Matter Bridge** configuration node (the actual bridge, paired once by Alexa/Google Home/Apple Home), pick the device type, give it the name the assistant will use, and fill the group addresses.
332
+
333
+ - **Matter → KNX**: commands from the voice assistant write to the *command* group addresses.
334
+ - **KNX → Matter**: telegrams on the *status* group addresses update the Matter attribute (and the app UI).
335
+
336
+ The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
337
+
338
+ If you enable the **node PINs**:
339
+
340
+ - input: `msg.payload = { function: "onoff", value: true }` updates the Matter state without the KNX bus
341
+ - output: every command from a Matter controller is forwarded as `msg` (topic = device name, payload = value, `msg.matter` = raw command)
342
+ </script>