node-red-contrib-knx-ultimate 5.2.4 → 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.
- package/CHANGELOG.md +21 -8
- package/README.md +10 -3
- package/img/readmemain.png +0 -0
- package/nodes/commonFunctions.js +40 -3
- package/nodes/knxUltimate-config.js +379 -335
- package/nodes/knxUltimate.html +8 -9
- package/nodes/knxUltimate.js +5 -5
- package/nodes/knxUltimateAI.js +1 -1
- package/nodes/knxUltimateHueLight.html +3 -3
- package/nodes/knxUltimateHueLight.js +1 -1
- package/nodes/knxUltimateMatterBridge.html +321 -247
- package/nodes/knxUltimateMatterBridge.js +48 -1
- package/nodes/knxUltimateMatterControllerDevice.html +2119 -0
- package/nodes/knxUltimateMatterControllerDevice.js +1701 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/de/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/de/matter-config.json +5 -1
- package/nodes/locales/de/matterbridge-config.json +1 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/en/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/en/matter-config.json +6 -1
- package/nodes/locales/en/matterbridge-config.json +1 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/es/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/es/matter-config.json +5 -1
- package/nodes/locales/es/matterbridge-config.json +1 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/fr/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +89 -0
- package/nodes/locales/fr/matter-config.json +5 -1
- package/nodes/locales/fr/matterbridge-config.json +1 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/it/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/it/matter-config.json +6 -1
- package/nodes/locales/it/matterbridge-config.json +1 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +89 -0
- package/nodes/locales/zh-CN/matter-config.json +5 -1
- package/nodes/locales/zh-CN/matterbridge-config.json +1 -0
- package/nodes/matter-config.html +58 -6
- package/nodes/matter-config.js +7 -2
- package/nodes/matterbridge-config.html +1 -0
- package/nodes/utils/lightEngines/canonicalLight.js +75 -0
- package/nodes/utils/lightEngines/hueLightEngine.js +51 -0
- package/nodes/utils/lightEngines/index.js +25 -0
- package/nodes/utils/lightEngines/knxLightCommand.js +79 -0
- package/nodes/utils/lightEngines/lightEngine.js +46 -0
- package/nodes/utils/lightEngines/matterHueShim.js +59 -0
- package/nodes/utils/lightEngines/matterLightEngine.js +117 -0
- package/nodes/utils/matterEngine.mjs +185 -14
- package/nodes/utils/matterKnxConverter.js +1 -1
- package/package.json +21 -3
- package/resources/KNXSendSnippets.js +53 -53
- package/resources/htmlUtils.js +6 -4
- package/nodes/knxUltimateMatterDevice.html +0 -632
- package/nodes/knxUltimateMatterDevice.js +0 -308
- package/nodes/locales/de/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/de/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/en/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/en/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/es/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/es/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/fr/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/fr/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/it/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/it/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +0 -56
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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-
|
|
293
|
-
<span data-i18n="knxUltimateMatterBridge.
|
|
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**:
|