node-red-contrib-knx-ultimate 5.2.5 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/nodes/commonFunctions.js +40 -3
  3. package/nodes/knxUltimate-config.js +379 -335
  4. package/nodes/knxUltimate.html +8 -9
  5. package/nodes/knxUltimate.js +5 -5
  6. package/nodes/knxUltimateAI.js +1 -1
  7. package/nodes/knxUltimateHueLight.html +3 -3
  8. package/nodes/knxUltimateHueLight.js +1 -1
  9. package/nodes/knxUltimateMatterBridge.html +321 -247
  10. package/nodes/knxUltimateMatterBridge.js +48 -1
  11. package/nodes/knxUltimateMatterControllerDevice.html +2119 -0
  12. package/nodes/knxUltimateMatterControllerDevice.js +1701 -0
  13. package/nodes/locales/de/knxUltimateMatterBridge.html +6 -2
  14. package/nodes/locales/de/knxUltimateMatterBridge.json +14 -1
  15. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +27 -0
  16. package/nodes/locales/de/knxUltimateMatterControllerDevice.json +97 -0
  17. package/nodes/locales/de/matter-config.json +5 -1
  18. package/nodes/locales/de/matterbridge-config.json +1 -0
  19. package/nodes/locales/en/knxUltimateMatterBridge.html +6 -2
  20. package/nodes/locales/en/knxUltimateMatterBridge.json +14 -1
  21. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +27 -0
  22. package/nodes/locales/en/knxUltimateMatterControllerDevice.json +97 -0
  23. package/nodes/locales/en/matter-config.json +6 -1
  24. package/nodes/locales/en/matterbridge-config.json +1 -0
  25. package/nodes/locales/es/knxUltimateMatterBridge.html +6 -2
  26. package/nodes/locales/es/knxUltimateMatterBridge.json +14 -1
  27. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +27 -0
  28. package/nodes/locales/es/knxUltimateMatterControllerDevice.json +97 -0
  29. package/nodes/locales/es/matter-config.json +5 -1
  30. package/nodes/locales/es/matterbridge-config.json +1 -0
  31. package/nodes/locales/fr/knxUltimateMatterBridge.html +6 -2
  32. package/nodes/locales/fr/knxUltimateMatterBridge.json +14 -1
  33. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +27 -0
  34. package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +89 -0
  35. package/nodes/locales/fr/matter-config.json +5 -1
  36. package/nodes/locales/fr/matterbridge-config.json +1 -0
  37. package/nodes/locales/it/knxUltimateMatterBridge.html +6 -2
  38. package/nodes/locales/it/knxUltimateMatterBridge.json +14 -1
  39. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +27 -0
  40. package/nodes/locales/it/knxUltimateMatterControllerDevice.json +97 -0
  41. package/nodes/locales/it/matter-config.json +6 -1
  42. package/nodes/locales/it/matterbridge-config.json +1 -0
  43. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +6 -2
  44. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +14 -1
  45. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +27 -0
  46. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +89 -0
  47. package/nodes/locales/zh-CN/matter-config.json +5 -1
  48. package/nodes/locales/zh-CN/matterbridge-config.json +1 -0
  49. package/nodes/matter-config.html +58 -6
  50. package/nodes/matter-config.js +7 -2
  51. package/nodes/matterbridge-config.html +1 -0
  52. package/nodes/utils/lightEngines/canonicalLight.js +75 -0
  53. package/nodes/utils/lightEngines/hueLightEngine.js +51 -0
  54. package/nodes/utils/lightEngines/index.js +25 -0
  55. package/nodes/utils/lightEngines/knxLightCommand.js +79 -0
  56. package/nodes/utils/lightEngines/lightEngine.js +46 -0
  57. package/nodes/utils/lightEngines/matterHueShim.js +59 -0
  58. package/nodes/utils/lightEngines/matterLightEngine.js +117 -0
  59. package/nodes/utils/matterEngine.mjs +185 -14
  60. package/nodes/utils/matterKnxConverter.js +1 -1
  61. package/package.json +11 -3
  62. package/resources/KNXSendSnippets.js +53 -53
  63. package/resources/htmlUtils.js +6 -4
  64. package/nodes/knxUltimateMatterDevice.html +0 -632
  65. package/nodes/knxUltimateMatterDevice.js +0 -308
  66. package/nodes/locales/de/knxUltimateMatterDevice.html +0 -56
  67. package/nodes/locales/de/knxUltimateMatterDevice.json +0 -109
  68. package/nodes/locales/en/knxUltimateMatterDevice.html +0 -56
  69. package/nodes/locales/en/knxUltimateMatterDevice.json +0 -109
  70. package/nodes/locales/es/knxUltimateMatterDevice.html +0 -56
  71. package/nodes/locales/es/knxUltimateMatterDevice.json +0 -109
  72. package/nodes/locales/fr/knxUltimateMatterDevice.html +0 -56
  73. package/nodes/locales/fr/knxUltimateMatterDevice.json +0 -109
  74. package/nodes/locales/it/knxUltimateMatterDevice.html +0 -56
  75. package/nodes/locales/it/knxUltimateMatterDevice.json +0 -109
  76. package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +0 -56
  77. package/nodes/locales/zh-CN/knxUltimateMatterDevice.json +0 -109
@@ -2,256 +2,276 @@
2
2
  <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
3
3
 
4
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 }
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
+ turnOnBehavior: { value: 'ignoreLevelAfterOn' },
79
+ ignoreLevelAfterOnMs: { value: 800 },
80
+ coverUpdateMode: { value: 'optimistic' },
81
+ coverStatusTimeoutMs: { value: 3000 },
82
+ readStatusAtStartup: { value: 'yes' },
83
+ enableNodePINS: { value: 'no' },
84
+ outputs: { value: 0 },
85
+ inputs: { value: 0 }
86
+ };
87
+ ALL_KEYS.forEach((key) => {
88
+ gaDefaults['ga' + key] = { value: '' };
89
+ gaDefaults['dpt' + key] = { value: '' };
90
+ });
91
+
92
+ RED.nodes.registerType('knxUltimateMatterBridge', {
93
+ category: 'KNX Ultimate Matter',
94
+ color: '#8FE3D4',
95
+ defaults: gaDefaults,
96
+ inputs: 0,
97
+ outputs: 0,
98
+ icon: 'node-matter-icon.svg',
99
+ label() {
100
+ return this.name || 'Expose KNX to Matter (BETA)';
101
+ },
102
+ paletteLabel: 'Expose KNX to Matter (BETA)',
103
+ oneditprepare() {
104
+ try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
105
+ const node = this;
106
+
107
+ // Resolves the KNX gateway at query time: the selected one, or - as a
108
+ // fallback - the first knxUltimate-config available, so the GA list works
109
+ // even before the user picks the gateway on this device node.
110
+ const findKnxGateway = () => {
111
+ let srv = null;
112
+ try {
113
+ const val = $('#node-input-server').val();
114
+ if (val && val !== '_ADD_') srv = RED.nodes.node(val);
115
+ } catch (error) { /* empty */ }
116
+ if (!srv) {
117
+ try {
118
+ RED.nodes.eachConfig((c) => { if (!srv && c.type === 'knxUltimate-config') srv = c; });
119
+ } catch (error) { /* empty */ }
120
+ }
121
+ return srv;
82
122
  };
83
- ALL_KEYS.forEach((key) => {
84
- gaDefaults['ga' + key] = { value: '' };
85
- gaDefaults['dpt' + key] = { value: '' };
123
+
124
+ // Auto-select the KNX gateway when there is only one, sparing the user a click per device node
125
+ try {
126
+ const cur = $('#node-input-server').val();
127
+ if (!cur || cur === '_ADD_') {
128
+ const gws = [];
129
+ RED.nodes.eachConfig((c) => { if (c.type === 'knxUltimate-config') gws.push(c); });
130
+ if (gws.length === 1) $('#node-input-server').val(gws[0].id).trigger('change');
131
+ }
132
+ } catch (error) { /* empty */ }
133
+
134
+ // Device type dropdown
135
+ const $type = $('#node-input-deviceType');
136
+ TYPE_ORDER.forEach((t) => {
137
+ $type.append($('<option></option>').attr('value', t).text(node._('knxUltimateMatterBridge.types.' + t)));
86
138
  });
139
+ $type.val(node.deviceType || 'onofflight');
87
140
 
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;
141
+ // Compact "list" layout: a small gray caption above each input, fields wrapping
142
+ // on one or more rows (same pattern used across the rest of the KNX-Ultimate nodes).
143
+ const $fields = $('#mb-ga-fields').css({ display: 'flex', flexWrap: 'wrap', alignItems: 'flex-end', gap: '10px' });
144
+ const $invertRow = $('#mb-invert-row');
145
+ const $advancedRows = $('#mb-advanced-details');
146
+ const $dimmerAdvancedRows = $('.mb-dimmer-advanced-row');
147
+ const $coverAdvancedRows = $('.mb-cover-advanced-row');
148
+ const gaInputs = {};
149
+
150
+ function createLabeledField(row, element, options = {}) {
151
+ const wrapper = $('<div/>', {
152
+ style: 'display:flex; flex-direction:column; margin-bottom:8px;'
153
+ }).appendTo(row);
154
+ if (options.width) wrapper.css('width', options.width);
155
+ const $labelSpan = $('<span/>', {
156
+ style: 'font-size:11px; color:#666; margin-bottom:2px; white-space:nowrap;'
157
+ }).appendTo(wrapper);
158
+ element.css('width', '100%');
159
+ element.appendTo(wrapper);
160
+ return { wrapper, $labelSpan };
161
+ }
162
+
163
+ function attachGaAutocomplete($ga, $dpt, fieldDef) {
164
+ $ga.autocomplete({
165
+ minLength: 0,
166
+ source(request, response) {
167
+ const gw = findKnxGateway();
168
+ if (!gw) { response([]); return; }
169
+ $.getJSON(`knxUltimatecsv?nodeID=${gw.id}`, (data) => {
170
+ response($.map(data, (value) => {
171
+ const search = `${value.ga} (${value.devicename}) DPT${value.dpt}`;
172
+ if (htmlUtilsfullCSVSearch(search, `${request.term} ${fieldDef.dptPrefix}`)) {
173
+ return { label: `${value.ga} # ${value.devicename} # ${value.dpt}`, value: value.ga, dpt: value.dpt };
174
+ }
175
+ return null;
176
+ }));
177
+ });
248
178
  },
249
- oneditcancel() {
250
- try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
251
- delete this._matterGaInputs;
179
+ select(event, ui) { $dpt.val(ui.item.dpt || fieldDef.fallbackDpt); }
180
+ });
181
+ $ga.on('focus.knxUltimateMatterBridge click.knxUltimateMatterBridge', function () {
182
+ try { $(this).autocomplete('search', ''); } catch (error) { /* empty */ }
183
+ });
184
+ try {
185
+ const gw = findKnxGateway();
186
+ if (gw && gw.id) KNX_enableSecureFormatting($ga, gw.id);
187
+ } catch (error) { /* empty */ }
188
+ }
189
+
190
+ // Build one field per possible GA key; show only the ones of the selected type.
191
+ ALL_KEYS.forEach((key) => {
192
+ const fieldDef = {};
193
+ Object.keys(TYPE_FIELDS).forEach((t) => { const f = TYPE_FIELDS[t].find((x) => x.key === key); if (f) Object.assign(fieldDef, f); });
194
+ const $ga = $('<input/>', { type: 'text', placeholder: '1/1/1' }).val(node['ga' + key] || '');
195
+ const $dpt = $('<input/>', { type: 'hidden' }).val(node['dpt' + key] || '');
196
+ const { wrapper, $labelSpan } = createLabeledField($fields, $ga, { width: '150px' });
197
+ wrapper.append($dpt); // Kept alongside the GA input; never rendered.
198
+ attachGaAutocomplete($ga, $dpt, fieldDef);
199
+ // Keyed by field key (not DOM position), so save is robust regardless of layout.
200
+ gaInputs[key] = { wrapper, $ga, $dpt, $labelSpan };
201
+ });
202
+
203
+ function updateTypePresentation() {
204
+ const selected = $type.val();
205
+ const active = TYPE_FIELDS[selected] || [];
206
+ ALL_KEYS.forEach((key) => {
207
+ const entry = gaInputs[key];
208
+ const fieldForType = active.find((f) => f.key === key);
209
+ if (fieldForType) {
210
+ entry.wrapper.show();
211
+ entry.$labelSpan.text(node._('knxUltimateMatterBridge.functions.' + fieldForType.labelKey));
212
+ } else {
213
+ entry.wrapper.hide();
252
214
  }
215
+ });
216
+ $invertRow.toggle(selected === 'windowcovering');
217
+ const hasLevel = selected === 'dimmablelight' || selected === 'rgblight' || selected === 'colortemperaturelight';
218
+ const isCover = selected === 'windowcovering';
219
+ $advancedRows.toggle(hasLevel || isCover);
220
+ $dimmerAdvancedRows.toggle(hasLevel);
221
+ $coverAdvancedRows.toggle(isCover);
222
+ }
223
+ $type.on('change', updateTypePresentation);
224
+ updateTypePresentation();
225
+
226
+ // oneditsave reads the GA values straight from these references: no DOM
227
+ // position matching, so it cannot break if the layout changes again.
228
+ node._matterGaInputs = gaInputs;
229
+
230
+ $('#node-input-invertPosition').prop('checked', node.invertPosition === true);
231
+ $('#node-input-turnOnBehavior').val(node.turnOnBehavior || 'ignoreLevelAfterOn');
232
+ $('#node-input-ignoreLevelAfterOnMs').val(node.ignoreLevelAfterOnMs === undefined ? 800 : node.ignoreLevelAfterOnMs);
233
+ $('#node-input-coverUpdateMode').val(node.coverUpdateMode || 'optimistic');
234
+ $('#node-input-coverStatusTimeoutMs').val(node.coverStatusTimeoutMs === undefined ? 3000 : node.coverStatusTimeoutMs);
235
+ $('#node-input-readStatusAtStartup').val(node.readStatusAtStartup || 'yes');
236
+ $('#node-input-enableNodePINS').val(node.enableNodePINS || 'no');
237
+ },
238
+ oneditsave() {
239
+ try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
240
+ const node = this;
241
+ const selected = $('#node-input-deviceType').val();
242
+ node.deviceType = selected;
243
+ node.invertPosition = $('#node-input-invertPosition').is(':checked');
244
+ const active = TYPE_FIELDS[selected] || [];
245
+ const gaInputs = node._matterGaInputs || {};
246
+ // Clear every GA/DPT, then write only the ones of the selected type, reading
247
+ // straight from the field references built in oneditprepare (not DOM position).
248
+ ALL_KEYS.forEach((key) => {
249
+ node['ga' + key] = '';
250
+ node['dpt' + key] = '';
251
+ const entry = gaInputs[key];
252
+ if (!entry) return;
253
+ const isActive = active.some((f) => f.key === key);
254
+ const ga = (entry.$ga.val() || '').trim();
255
+ if (isActive && ga !== '') {
256
+ node['ga' + key] = ga;
257
+ node['dpt' + key] = (entry.$dpt.val() || '').trim();
258
+ }
253
259
  });
254
- }());
260
+ delete node._matterGaInputs;
261
+ this.turnOnBehavior = $('#node-input-turnOnBehavior').val() || 'ignoreLevelAfterOn';
262
+ this.ignoreLevelAfterOnMs = Number($('#node-input-ignoreLevelAfterOnMs').val()) || 0;
263
+ this.coverUpdateMode = $('#node-input-coverUpdateMode').val() || 'optimistic';
264
+ this.coverStatusTimeoutMs = Number($('#node-input-coverStatusTimeoutMs').val()) || 0;
265
+ this.enableNodePINS = $('#node-input-enableNodePINS').val() || 'no';
266
+ this.outputs = this.enableNodePINS === 'yes' ? 1 : 0;
267
+ this.inputs = this.enableNodePINS === 'yes' ? 1 : 0;
268
+ },
269
+ oneditcancel() {
270
+ try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
271
+ delete this._matterGaInputs;
272
+ }
273
+ });
274
+ }());
255
275
  </script>
256
276
 
257
277
  <script type="text/html" data-template-name="knxUltimateMatterBridge">
@@ -288,9 +308,9 @@
288
308
  <select id="node-input-deviceType" style="width:260px;"></select>
289
309
  </div>
290
310
 
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>
311
+ <div class="form-tips" style="display:block; width:100%; max-width:none; box-sizing:border-box; margin-bottom:8px; border-left:4px solid #d9822b;">
312
+ <i class="fa fa-triangle-exclamation"></i>
313
+ <span data-i18n="knxUltimateMatterBridge.type_change_warning"></span>
294
314
  </div>
295
315
 
296
316
  <div id="mb-ga-fields"></div>
@@ -302,6 +322,48 @@
302
322
  </label>
303
323
  </div>
304
324
 
325
+ <details id="mb-advanced-details" style="margin:10px 0 8px 0; display:none;">
326
+ <summary style="cursor:pointer;">
327
+ <i class="fa fa-sliders"></i>
328
+ <b><span data-i18n="knxUltimateMatterBridge.advanced.title"></span></b>
329
+ <span data-i18n="knxUltimateMatterBridge.advanced.help"></span>
330
+ </summary>
331
+
332
+ <div class="form-row mb-dimmer-advanced-row" style="display:none; margin-top:8px;">
333
+ <label for="node-input-turnOnBehavior" style="width:260px;">
334
+ <i class="fa fa-lightbulb-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.turn_on_behavior"></span>
335
+ </label>
336
+ <select id="node-input-turnOnBehavior" style="width:280px;">
337
+ <option value="ignoreLevelAfterOn" data-i18n="knxUltimateMatterBridge.advanced.turn_on_ignore_level"></option>
338
+ <option value="forwardAll" data-i18n="knxUltimateMatterBridge.advanced.turn_on_forward_all"></option>
339
+ </select>
340
+ </div>
341
+
342
+ <div class="form-row mb-dimmer-advanced-row" style="display:none;">
343
+ <label for="node-input-ignoreLevelAfterOnMs" style="width:260px;">
344
+ <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.ignore_level_ms"></span>
345
+ </label>
346
+ <input type="number" id="node-input-ignoreLevelAfterOnMs" min="0" step="100" style="width:120px;">
347
+ </div>
348
+
349
+ <div class="form-row mb-cover-advanced-row" style="display:none; margin-top:8px;">
350
+ <label for="node-input-coverUpdateMode" style="width:260px;">
351
+ <i class="fa fa-window-maximize"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_update_mode"></span>
352
+ </label>
353
+ <select id="node-input-coverUpdateMode" style="width:280px;">
354
+ <option value="optimistic" data-i18n="knxUltimateMatterBridge.advanced.cover_optimistic"></option>
355
+ <option value="waitStatus" data-i18n="knxUltimateMatterBridge.advanced.cover_wait_status"></option>
356
+ </select>
357
+ </div>
358
+
359
+ <div class="form-row mb-cover-advanced-row" style="display:none;">
360
+ <label for="node-input-coverStatusTimeoutMs" style="width:260px;">
361
+ <i class="fa fa-clock-o"></i> <span data-i18n="knxUltimateMatterBridge.advanced.cover_status_timeout"></span>
362
+ </label>
363
+ <input type="number" id="node-input-coverStatusTimeoutMs" min="0" step="500" style="width:120px;">
364
+ </div>
365
+ </details>
366
+
305
367
  <hr/>
306
368
 
307
369
  <div class="form-row">
@@ -323,6 +385,13 @@
323
385
  <option value="yes" data-i18n="knxUltimateMatterBridge.node_pins_show"></option>
324
386
  </select>
325
387
  </div>
388
+
389
+ <br/>
390
+ <br/>
391
+ <br/>
392
+ <br/>
393
+ <br/>
394
+
326
395
  </script>
327
396
 
328
397
  <script type="text/markdown" data-help-name="knxUltimateMatterBridge">
@@ -333,6 +402,11 @@ Point it to a **Matter Bridge** configuration node (the actual bridge, paired on
333
402
  - **Matter → KNX**: commands from the voice assistant write to the *command* group addresses.
334
403
  - **KNX → Matter**: telegrams on the *status* group addresses update the Matter attribute (and the app UI).
335
404
 
405
+ The advanced compatibility options are intentionally type-specific:
406
+
407
+ - dimmable devices can ignore the brightness command that some controllers send immediately after an On command
408
+ - covers can optimistically update the Matter position after a command, then correct it when the KNX status GA reports the real position
409
+
336
410
  The **KNX gateway is optional**: without it, enable the node PINs and drive the device from the flow.
337
411
 
338
412
  If you enable the **node PINs**: