node-red-contrib-knx-ultimate 5.2.1 → 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 (32) hide show
  1. package/CHANGELOG.md +13 -10
  2. package/nodes/commonFunctions.js +9 -9
  3. package/nodes/knxUltimateMatterBridge.html +212 -299
  4. package/nodes/knxUltimateMatterBridge.js +166 -309
  5. package/nodes/knxUltimateMatterDevice.html +46 -0
  6. package/nodes/locales/de/knxUltimateMatterBridge.html +13 -24
  7. package/nodes/locales/de/knxUltimateMatterBridge.json +4 -1
  8. package/nodes/locales/de/matterbridge-config.html +34 -0
  9. package/nodes/locales/de/matterbridge-config.json +20 -0
  10. package/nodes/locales/en/knxUltimateMatterBridge.html +14 -25
  11. package/nodes/locales/en/knxUltimateMatterBridge.json +4 -1
  12. package/nodes/locales/en/matterbridge-config.html +34 -0
  13. package/nodes/locales/en/matterbridge-config.json +20 -0
  14. package/nodes/locales/es/knxUltimateMatterBridge.html +13 -24
  15. package/nodes/locales/es/knxUltimateMatterBridge.json +4 -1
  16. package/nodes/locales/es/matterbridge-config.html +34 -0
  17. package/nodes/locales/es/matterbridge-config.json +20 -0
  18. package/nodes/locales/fr/knxUltimateMatterBridge.html +13 -24
  19. package/nodes/locales/fr/knxUltimateMatterBridge.json +4 -1
  20. package/nodes/locales/fr/matterbridge-config.html +34 -0
  21. package/nodes/locales/fr/matterbridge-config.json +20 -0
  22. package/nodes/locales/it/knxUltimateMatterBridge.html +13 -24
  23. package/nodes/locales/it/knxUltimateMatterBridge.json +4 -1
  24. package/nodes/locales/it/matterbridge-config.html +34 -0
  25. package/nodes/locales/it/matterbridge-config.json +20 -0
  26. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +13 -24
  27. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +4 -1
  28. package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
  29. package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
  30. package/nodes/matterbridge-config.html +121 -0
  31. package/nodes/matterbridge-config.js +220 -0
  32. package/package.json +2 -1
@@ -3,325 +3,252 @@
3
3
 
4
4
  <script type="text/javascript">
5
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
+
6
88
  RED.nodes.registerType('knxUltimateMatterBridge', {
7
89
  category: 'KNX Ultimate Matter',
8
90
  color: '#D7C7E9',
9
- defaults: {
10
- server: { type: 'knxUltimate-config', required: false },
11
- name: { value: '' },
12
- bridgeDeviceName: { value: 'KNX-Ultimate Bridge' },
13
- bridgePort: { value: 5540, validate: RED.validators.number() },
14
- readStatusAtStartup: { value: 'yes' },
15
- enableNodePINS: { value: 'no' },
16
- devices: { value: [] },
17
- outputs: { value: 0 },
18
- inputs: { value: 0 }
19
- },
91
+ defaults: gaDefaults,
20
92
  inputs: 0,
21
93
  outputs: 0,
22
94
  icon: 'node-matter-icon.svg',
23
95
  label() {
24
- return this.name || 'Matter Bridge (BETA)';
96
+ return this.name || 'Matter Bridge device (BETA)';
25
97
  },
26
- paletteLabel: 'Matter Bridge (BETA)',
98
+ paletteLabel: 'Matter Bridge device (BETA)',
27
99
  oneditprepare() {
28
100
  try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
29
101
  const node = this;
30
102
 
31
- let oNodeServer = RED.nodes.node($('#node-input-server').val());
32
- $('#node-input-server').on('change.knxUltimateMatterBridge', function () {
33
- try { oNodeServer = RED.nodes.node($(this).val()); } catch (error) { /* empty */ }
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)));
34
134
  });
135
+ $type.val(node.deviceType || 'onofflight');
35
136
 
36
- // GA fields of each virtual device type.
37
- // dir 'command' = Matter -> KNX (GA written on Alexa commands), 'status' = KNX -> Matter.
38
- const TYPE_FIELDS = {
39
- onofflight: [
40
- { key: 'OnOff', labelKey: 'fn_onoff_cmd', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.001' },
41
- { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.001' }
42
- ],
43
- onoffplug: [
44
- { key: 'OnOff', labelKey: 'fn_onoff_cmd', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.001' },
45
- { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.001' }
46
- ],
47
- dimmablelight: [
48
- { key: 'OnOff', labelKey: 'fn_onoff_cmd', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.001' },
49
- { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.001' },
50
- { key: 'Level', labelKey: 'fn_level_cmd', dir: 'command', dptPrefix: '5.', fallbackDpt: '5.001' },
51
- { key: 'LevelStatus', labelKey: 'fn_level_status', dir: 'status', dptPrefix: '5.', fallbackDpt: '5.001' }
52
- ],
53
- rgblight: [
54
- { key: 'OnOff', labelKey: 'fn_onoff_cmd', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.001' },
55
- { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.001' },
56
- { key: 'Level', labelKey: 'fn_level_cmd', dir: 'command', dptPrefix: '5.', fallbackDpt: '5.001' },
57
- { key: 'LevelStatus', labelKey: 'fn_level_status', dir: 'status', dptPrefix: '5.', fallbackDpt: '5.001' },
58
- { key: 'RGB', labelKey: 'fn_rgb_cmd', dir: 'command', dptPrefix: '232.', fallbackDpt: '232.600' },
59
- { key: 'RGBStatus', labelKey: 'fn_rgb_status', dir: 'status', dptPrefix: '232.', fallbackDpt: '232.600' }
60
- ],
61
- colortemperaturelight: [
62
- { key: 'OnOff', labelKey: 'fn_onoff_cmd', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.001' },
63
- { key: 'OnOffStatus', labelKey: 'fn_onoff_status', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.001' },
64
- { key: 'Level', labelKey: 'fn_level_cmd', dir: 'command', dptPrefix: '5.', fallbackDpt: '5.001' },
65
- { key: 'LevelStatus', labelKey: 'fn_level_status', dir: 'status', dptPrefix: '5.', fallbackDpt: '5.001' },
66
- { key: 'CT', labelKey: 'fn_ct_cmd', dir: 'command', dptPrefix: '7.', fallbackDpt: '7.600' },
67
- { key: 'CTStatus', labelKey: 'fn_ct_status', dir: 'status', dptPrefix: '7.', fallbackDpt: '7.600' }
68
- ],
69
- temperaturesensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dir: 'status', dptPrefix: '9.', fallbackDpt: '9.001' }],
70
- humiditysensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dir: 'status', dptPrefix: '9.', fallbackDpt: '9.007' }],
71
- lightsensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dir: 'status', dptPrefix: '9.', fallbackDpt: '9.004' }],
72
- occupancysensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.011' }],
73
- contactsensor: [{ key: 'Status', labelKey: 'fn_sensor_ga', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.002' }],
74
- windowcovering: [
75
- { key: 'UpDown', labelKey: 'fn_updown', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.008' },
76
- { key: 'Stop', labelKey: 'fn_stop', dir: 'command', dptPrefix: '1.', fallbackDpt: '1.017' },
77
- { key: 'Position', labelKey: 'fn_position_cmd', dir: 'command', dptPrefix: '5.', fallbackDpt: '5.001' },
78
- { key: 'PositionStatus', labelKey: 'fn_position_status', dir: 'status', dptPrefix: '5.', fallbackDpt: '5.001' }
79
- ],
80
- thermostat: [
81
- { key: 'CurrentTemp', labelKey: 'fn_currenttemp', dir: 'status', dptPrefix: '9.', fallbackDpt: '9.001' },
82
- { key: 'Setpoint', labelKey: 'fn_setpoint_cmd', dir: 'command', dptPrefix: '9.', fallbackDpt: '9.001' },
83
- { key: 'SetpointStatus', labelKey: 'fn_setpoint_status', dir: 'status', dptPrefix: '9.', fallbackDpt: '9.001' }
84
- ],
85
- smokecoalarm: [
86
- { key: 'Smoke', labelKey: 'fn_smoke_ga', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.005' },
87
- { key: 'CO', labelKey: 'fn_co_ga', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.005' }
88
- ],
89
- waterleakdetector: [{ key: 'Status', labelKey: 'fn_leak_ga', dir: 'status', dptPrefix: '1.', fallbackDpt: '1.005' }],
90
- airqualitysensor: [{ key: 'Status', labelKey: 'fn_co2_ga', dir: 'status', dptPrefix: '9.', fallbackDpt: '9.008' }],
91
- fan: [
92
- { key: 'FanSpeed', labelKey: 'fn_fanspeed_cmd', dir: 'command', dptPrefix: '5.', fallbackDpt: '5.001' },
93
- { key: 'FanSpeedStatus', labelKey: 'fn_fanspeed_status', dir: 'status', dptPrefix: '5.', fallbackDpt: '5.001' }
94
- ],
95
- robotvacuum: [] // Flow-only: no GA fields, controlled via the node PINs
96
- };
97
- const TYPE_ORDER = ['onofflight', 'dimmablelight', 'rgblight', 'colortemperaturelight', 'onoffplug', 'windowcovering', 'thermostat', 'fan', 'temperaturesensor', 'humiditysensor', 'lightsensor', 'occupancysensor', 'contactsensor', 'smokecoalarm', 'waterleakdetector', 'airqualitysensor', 'robotvacuum'];
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 = {};
98
142
 
99
- function createLabeledField(row, labelText, element, options = {}) {
143
+ function createLabeledField(row, element, options = {}) {
100
144
  const wrapper = $('<div/>', {
101
- style: 'display:flex; flex-direction:column; margin-right:10px;'
145
+ style: 'display:flex; flex-direction:column; margin-bottom:8px;'
102
146
  }).appendTo(row);
103
147
  if (options.width) wrapper.css('width', options.width);
104
- if (options.flex) wrapper.css('flex', options.flex);
105
- $('<span/>', {
106
- text: labelText,
107
- style: 'font-size:11px; color:#666; margin-bottom:2px;'
148
+ const $labelSpan = $('<span/>', {
149
+ style: 'font-size:11px; color:#666; margin-bottom:2px; white-space:nowrap;'
108
150
  }).appendTo(wrapper);
109
151
  element.css('width', '100%');
110
152
  element.appendTo(wrapper);
111
- element.data('fieldWrapper', wrapper);
112
- return element;
153
+ return { wrapper, $labelSpan };
113
154
  }
114
155
 
115
- function attachGaAutocomplete($ga, $dptHidden, fieldDef) {
156
+ function attachGaAutocomplete($ga, $dpt, fieldDef) {
116
157
  $ga.autocomplete({
117
158
  minLength: 0,
118
159
  source(request, response) {
119
- if (!oNodeServer) { response([]); return; }
120
- $.getJSON(`knxUltimatecsv?nodeID=${oNodeServer.id}`, (data) => {
160
+ const gw = findKnxGateway();
161
+ if (!gw) { response([]); return; }
162
+ $.getJSON(`knxUltimatecsv?nodeID=${gw.id}`, (data) => {
121
163
  response($.map(data, (value) => {
122
164
  const search = `${value.ga} (${value.devicename}) DPT${value.dpt}`;
123
165
  if (htmlUtilsfullCSVSearch(search, `${request.term} ${fieldDef.dptPrefix}`)) {
124
- return {
125
- label: `${value.ga} # ${value.devicename} # ${value.dpt}`,
126
- value: value.ga,
127
- dpt: value.dpt
128
- };
166
+ return { label: `${value.ga} # ${value.devicename} # ${value.dpt}`, value: value.ga, dpt: value.dpt };
129
167
  }
130
168
  return null;
131
169
  }));
132
170
  });
133
171
  },
134
- select(event, ui) {
135
- $dptHidden.val(ui.item.dpt || fieldDef.fallbackDpt);
136
- }
172
+ select(event, ui) { $dpt.val(ui.item.dpt || fieldDef.fallbackDpt); }
137
173
  });
138
174
  $ga.on('focus.knxUltimateMatterBridge click.knxUltimateMatterBridge', function () {
139
175
  try { $(this).autocomplete('search', ''); } catch (error) { /* empty */ }
140
176
  });
141
177
  try {
142
- if (oNodeServer && oNodeServer.id) KNX_enableSecureFormatting($ga, oNodeServer.id);
178
+ const gw = findKnxGateway();
179
+ if (gw && gw.id) KNX_enableSecureFormatting($ga, gw.id);
143
180
  } catch (error) { /* empty */ }
144
181
  }
145
182
 
146
- // ------------------------------------------------------------------
147
- // Devices editable list
148
- // ------------------------------------------------------------------
149
- const container = $('#node-input-devices-container');
150
- container.css('min-height', '260px').css('min-width', '640px').editableList({
151
- sortable: true,
152
- removable: true,
153
- addItem: function (row, index, data) {
154
- const device = $.extend(true, { id: '', type: 'onofflight', name: '' }, data.device);
155
- if (!device.id) {
156
- try { device.id = RED.util.generateId(); } catch (error) { device.id = Math.random().toString(16).slice(2); }
157
- }
158
- row.data('deviceId', device.id);
159
-
160
- const block = $('<div/>').appendTo(row);
161
- const topRow = $('<div/>').addClass('form-row').css({ display: 'flex', alignItems: 'flex-end', flexWrap: 'wrap', gap: '10px' }).appendTo(block);
162
- const gaRow = $('<div/>').addClass('form-row').css({ display: 'flex', alignItems: 'flex-end', flexWrap: 'wrap', gap: '10px', marginTop: '6px' }).appendTo(block);
163
-
164
- // Type
165
- const $type = $('<select/>', { class: 'mb-type' });
166
- TYPE_ORDER.forEach((t) => {
167
- $type.append($('<option></option>').attr('value', t).text(node._('knxUltimateMatterBridge.types.' + t)));
168
- });
169
- createLabeledField(topRow, node._('knxUltimateMatterBridge.fields.type'), $type, { width: '190px' });
170
- $type.val(device.type || 'onofflight');
171
-
172
- // Name (the one Alexa & Co. show and use)
173
- const $name = $('<input/>', { type: 'text', class: 'mb-name' }).attr('placeholder', node._('knxUltimateMatterBridge.fields.name_placeholder')).val(device.name);
174
- createLabeledField(topRow, node._('knxUltimateMatterBridge.fields.name'), $name, { flex: '1 1 220px' });
175
-
176
- // Position inversion (covers only)
177
- const $invertWrapper = $('<label/>', { style: 'display:flex; align-items:center; gap:6px; padding:4px 8px; border:1px solid #ced4da; border-radius:4px; cursor:pointer;' }).appendTo(topRow);
178
- const $invert = $('<input/>', { type: 'checkbox', class: 'mb-invert', style: 'width:auto; margin:0;' }).appendTo($invertWrapper);
179
- $('<span/>', { text: node._('knxUltimateMatterBridge.fields.invert'), style: 'font-size:12px;' }).appendTo($invertWrapper);
180
- $invert.prop('checked', device.invertPosition === true);
181
-
182
- // GA fields: created for every possible function; visibility follows the selected type
183
- const allFieldKeys = {};
184
- Object.keys(TYPE_FIELDS).forEach((t) => {
185
- TYPE_FIELDS[t].forEach((f) => { allFieldKeys[f.key] = f; });
186
- });
187
- const gaInputs = {};
188
- Object.keys(allFieldKeys).forEach((key) => {
189
- const fieldDef = allFieldKeys[key];
190
- // The "corrupted key" fallbacks recover values saved by versions where the
191
- // jQuery UI autocomplete class polluted the field key extraction.
192
- const gaValue = device[`ga${key}`] !== undefined ? device[`ga${key}`] : (device[`ga${key} ui-autocomplete-input`] || '');
193
- const dptValue = device[`dpt${key}`] !== undefined ? device[`dpt${key}`] : (device[`dpt${key} ui-autocomplete-input`] || '');
194
- const $ga = $('<input/>', { type: 'text', class: `mb-ga-${key}`, placeholder: '1/1/1' }).val(gaValue);
195
- const $dpt = $('<input/>', { type: 'hidden', class: `mb-dpt-${key}` }).val(dptValue);
196
- createLabeledField(gaRow, '', $ga, { width: '120px' });
197
- gaRow.append($dpt);
198
- attachGaAutocomplete($ga, $dpt, fieldDef);
199
- gaInputs[key] = { $ga, $dpt, fieldDef };
200
- });
201
-
202
- const updateTypePresentation = () => {
203
- const selected = $type.val();
204
- const active = TYPE_FIELDS[selected] || [];
205
- Object.keys(gaInputs).forEach((key) => {
206
- const entry = gaInputs[key];
207
- const fieldForType = active.find((f) => f.key === key);
208
- const wrapper = entry.$ga.data('fieldWrapper');
209
- if (fieldForType) {
210
- wrapper.show();
211
- wrapper.find('span').first().text(node._('knxUltimateMatterBridge.functions.' + fieldForType.labelKey));
212
- } else {
213
- wrapper.hide();
214
- }
215
- });
216
- if (selected === 'windowcovering') $invertWrapper.show();
217
- else $invertWrapper.hide();
218
- };
219
- $type.on('change', updateTypePresentation);
220
- updateTypePresentation();
221
- }
222
- });
223
-
224
- (node.devices || []).forEach((d) => {
225
- container.editableList('addItem', { device: d });
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 };
226
194
  });
227
195
 
228
- // ------------------------------------------------------------------
229
- // Pairing panel
230
- // ------------------------------------------------------------------
231
- const $pairingBody = $('#mb-pairing-body');
232
- const $refreshPairing = $('#mb-refresh-pairing');
233
- const $resetButton = $('#mb-factory-reset');
234
-
235
- function loadPairingInfo() {
236
- $pairingBody.html('<i class="fa fa-circle-notch fa-spin"></i>');
237
- $.getJSON(`KNXUltimateMatterBridgeInfo?nodeId=${node.id}&_=${Date.now()}`, (info) => {
238
- $pairingBody.empty();
239
- if (info.error) {
240
- $pairingBody.append($('<p style="color:#999;"></p>').text(info.error));
241
- return;
242
- }
243
- if (!info.running) {
244
- $pairingBody.append($('<p style="color:#999;"></p>').text(node._('knxUltimateMatterBridge.pairing.not_running')));
245
- return;
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();
246
207
  }
247
- if (info.commissioned) {
248
- $pairingBody.append($('<p style="color:#1b7d33;"></p>').text(`${node._('knxUltimateMatterBridge.pairing.commissioned')} (${info.fabrics.length})`));
249
- info.fabrics.forEach((f) => {
250
- $pairingBody.append($('<div style="margin-left:10px;"></div>').text(`• ${f.label || 'Controller'} (vendor ${f.vendorId})`));
251
- });
252
- $resetButton.show();
253
- return;
254
- }
255
- $pairingBody.append($('<p style="color:#b8860b;"></p>').text(node._('knxUltimateMatterBridge.pairing.awaiting')));
256
- if (info.manualPairingCode) {
257
- $pairingBody.append($('<p></p>').append($('<b style="font-size:18px; letter-spacing:2px;"></b>').text(info.manualPairingCode)));
258
- }
259
- if (info.qrAscii) {
260
- $pairingBody.append($('<pre style="font-family:monospace; line-height:1.05; font-size:7px; display:inline-block; padding:6px; background:#fff; color:#000;"></pre>').text(info.qrAscii));
261
- }
262
- if (info.qrPairingCode) {
263
- const url = `https://project-chip.github.io/connectedhomeip/qrcode.html?data=${encodeURIComponent(info.qrPairingCode)}`;
264
- $pairingBody.append($('<p></p>').append($('<a target="_blank"></a>').attr('href', url).text(node._('knxUltimateMatterBridge.pairing.qr_link'))));
265
- }
266
- $resetButton.hide();
267
- }).fail(() => {
268
- $pairingBody.empty();
269
- $pairingBody.append($('<p style="color:#999;"></p>').text(node._('knxUltimateMatterBridge.pairing.deploy_first')));
270
208
  });
209
+ $invertRow.toggle(selected === 'windowcovering');
271
210
  }
211
+ $type.on('change', updateTypePresentation);
212
+ updateTypePresentation();
272
213
 
273
- $refreshPairing.on('click.knxUltimateMatterBridge', () => loadPairingInfo());
274
- $resetButton.on('click.knxUltimateMatterBridge', () => {
275
- if (!window.confirm(node._('knxUltimateMatterBridge.pairing.reset_confirm'))) return;
276
- $resetButton.prop('disabled', true);
277
- $.getJSON(`KNXUltimateMatterBridgeReset?nodeId=${node.id}`, (data) => {
278
- if (data && data.error) RED.notify(data.error, { type: 'error' });
279
- else RED.notify(node._('knxUltimateMatterBridge.pairing.reset_ok'), { type: 'success' });
280
- loadPairingInfo();
281
- }).always(() => $resetButton.prop('disabled', false));
282
- });
283
-
284
- loadPairingInfo();
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;
285
217
 
218
+ $('#node-input-invertPosition').prop('checked', node.invertPosition === true);
286
219
  $('#node-input-readStatusAtStartup').val(node.readStatusAtStartup || 'yes');
287
220
  $('#node-input-enableNodePINS').val(node.enableNodePINS || 'no');
288
221
  },
289
222
  oneditsave() {
290
223
  try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
291
224
  const node = this;
292
- const items = $('#node-input-devices-container').editableList('items');
293
- node.devices = [];
294
- items.each(function () {
295
- const row = $(this);
296
- const type = row.find('.mb-type').val();
297
- const device = {
298
- id: row.data('deviceId') || '',
299
- type,
300
- name: (row.find('.mb-name').val() || '').trim()
301
- };
302
- if (!device.name) return; // Skip incomplete rows
303
- if (type === 'windowcovering') device.invertPosition = row.find('.mb-invert').is(':checked');
304
- row.find('input[class*="mb-ga-"]').each(function () {
305
- const $ga = $(this);
306
- // The class attribute may contain extra classes added by jQuery UI
307
- // (e.g. "mb-ga-OnOff ui-autocomplete-input"): extract the key safely.
308
- const match = ($ga.attr('class') || '').match(/mb-ga-([A-Za-z0-9]+)/);
309
- if (!match) return;
310
- const key = match[1];
311
- const ga = ($ga.val() || '').trim();
312
- if (ga !== '') {
313
- device[`ga${key}`] = ga;
314
- device[`dpt${key}`] = (row.find(`.mb-dpt-${key}`).val() || '').trim();
315
- }
316
- });
317
- node.devices.push(device);
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
+ }
318
243
  });
244
+ delete node._matterGaInputs;
319
245
  this.enableNodePINS = $('#node-input-enableNodePINS').val() || 'no';
320
246
  this.outputs = this.enableNodePINS === 'yes' ? 1 : 0;
321
247
  this.inputs = this.enableNodePINS === 'yes' ? 1 : 0;
322
248
  },
323
249
  oneditcancel() {
324
250
  try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
251
+ delete this._matterGaInputs;
325
252
  }
326
253
  });
327
254
  }());
@@ -340,31 +267,43 @@
340
267
  <input type="text" id="node-input-server">
341
268
  </div>
342
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
+
343
277
  <div class="form-row">
344
278
  <label for="node-input-name">
345
- <i class="fa fa-tag"></i> <span data-i18n="common.name"></span>
279
+ <i class="fa fa-tag"></i> <span data-i18n="knxUltimateMatterBridge.device_name"></span>
346
280
  </label>
347
- <input type="text" id="node-input-name">
281
+ <input type="text" id="node-input-name" data-i18n="[placeholder]knxUltimateMatterBridge.fields.name_placeholder">
348
282
  </div>
349
283
 
350
284
  <div class="form-row">
351
- <label for="node-input-bridgeDeviceName" style="width:180px;">
352
- <i class="fa fa-share-alt"></i> <span data-i18n="knxUltimateMatterBridge.bridge_name"></span>
285
+ <label for="node-input-deviceType">
286
+ <i class="fa fa-microchip"></i> <span data-i18n="knxUltimateMatterBridge.fields.type"></span>
353
287
  </label>
354
- <input type="text" id="node-input-bridgeDeviceName" style="width:220px;">
355
- <label for="node-input-bridgePort" style="width:60px; text-align:right;"><span data-i18n="knxUltimateMatterBridge.port"></span></label>
356
- <input type="number" id="node-input-bridgePort" style="width:90px;">
288
+ <select id="node-input-deviceType" style="width:260px;"></select>
357
289
  </div>
358
290
 
359
291
  <div class="form-tips" style="margin-bottom:8px;">
360
292
  <i class="fa fa-circle-info"></i>
361
- <span data-i18n="knxUltimateMatterBridge.devices_help"></span>
293
+ <span data-i18n="knxUltimateMatterBridge.device_help"></span>
362
294
  </div>
363
295
 
364
- <div class="form-row node-input-devices-container-row">
365
- <ol id="node-input-devices-container"></ol>
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>
366
303
  </div>
367
304
 
305
+ <hr/>
306
+
368
307
  <div class="form-row">
369
308
  <label for="node-input-readStatusAtStartup" style="width:260px;">
370
309
  <i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterBridge.read_status_startup"></span>
@@ -384,46 +323,20 @@
384
323
  <option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
385
324
  </select>
386
325
  </div>
387
-
388
- <hr/>
389
-
390
- <div class="form-row">
391
- <b><i class="fa fa-qrcode"></i>&nbsp;<span data-i18n="knxUltimateMatterBridge.pairing.title"></span></b>
392
- <button type="button" id="mb-refresh-pairing" class="red-ui-button" style="margin-left:6px; color:#1b7d33; border-color:#1b7d33;">
393
- <i class="fa fa-refresh"></i>
394
- </button>
395
- <button type="button" id="mb-factory-reset" class="red-ui-button" style="margin-left:6px; color:#c00; border-color:#c00; display:none;">
396
- <i class="fa fa-trash"></i>&nbsp;<span data-i18n="knxUltimateMatterBridge.pairing.reset_button"></span>
397
- </button>
398
- </div>
399
- <div class="form-row" id="mb-pairing-body" style="text-align:center;"></div>
400
-
401
- <br/>
402
- <br/>
403
- <br/>
404
- <br/>
405
- <br/>
406
- <br/>
407
- <br/>
408
- <br/>
409
- <br/>
410
- <br/>
411
- <br/>
412
- <br/>
413
326
  </script>
414
327
 
415
328
  <script type="text/markdown" data-help-name="knxUltimateMatterBridge">
416
- Exposes **KNX group addresses as Matter devices** (bridge). This node is in **BETA**.
329
+ Exposes **one KNX device as a Matter device**. This node is in **BETA**.
417
330
 
418
- Add your KNX virtual devices (lights, plugs, dimmers, sensors), deploy, then open the node again: it shows the **pairing code and QR code**. Scan it with Alexa, Google Home or Apple Home: the app sees a Matter bridge with all your KNX devices inside, using the names you typed.
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.
419
332
 
420
333
  - **Matter → KNX**: commands from the voice assistant write to the *command* group addresses.
421
- - **KNX → Matter**: telegrams on the *status* group addresses update the Matter attributes (and the app UI).
334
+ - **KNX → Matter**: telegrams on the *status* group addresses update the Matter attribute (and the app UI).
422
335
 
423
- The device must be reachable over the IP network; IPv6 link-local must be enabled on the Node-RED host (standard Matter requirement).
336
+ The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
424
337
 
425
- If you enable the **node PINs** you can also talk to the bridge from the flow:
338
+ If you enable the **node PINs**:
426
339
 
427
- - input: `msg.payload = { device: "Kitchen light", function: "onoff", value: true }` updates the Matter state without the KNX bus
340
+ - input: `msg.payload = { function: "onoff", value: true }` updates the Matter state without the KNX bus
428
341
  - output: every command from a Matter controller is forwarded as `msg` (topic = device name, payload = value, `msg.matter` = raw command)
429
342
  </script>