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.
- package/CHANGELOG.md +13 -10
- package/nodes/commonFunctions.js +9 -9
- package/nodes/knxUltimateMatterBridge.html +212 -299
- package/nodes/knxUltimateMatterBridge.js +166 -309
- package/nodes/knxUltimateMatterDevice.html +46 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/de/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/de/matterbridge-config.html +34 -0
- package/nodes/locales/de/matterbridge-config.json +20 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +14 -25
- package/nodes/locales/en/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/en/matterbridge-config.html +34 -0
- package/nodes/locales/en/matterbridge-config.json +20 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/es/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/es/matterbridge-config.html +34 -0
- package/nodes/locales/es/matterbridge-config.json +20 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/fr/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/fr/matterbridge-config.html +34 -0
- package/nodes/locales/fr/matterbridge-config.json +20 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/it/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/it/matterbridge-config.html +34 -0
- package/nodes/locales/it/matterbridge-config.json +20 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
- package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
- package/nodes/matterbridge-config.html +121 -0
- package/nodes/matterbridge-config.js +220 -0
- 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
//
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
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,
|
|
143
|
+
function createLabeledField(row, element, options = {}) {
|
|
100
144
|
const wrapper = $('<div/>', {
|
|
101
|
-
style: 'display:flex; flex-direction:column; margin-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
return element;
|
|
153
|
+
return { wrapper, $labelSpan };
|
|
113
154
|
}
|
|
114
155
|
|
|
115
|
-
function attachGaAutocomplete($ga, $
|
|
156
|
+
function attachGaAutocomplete($ga, $dpt, fieldDef) {
|
|
116
157
|
$ga.autocomplete({
|
|
117
158
|
minLength: 0,
|
|
118
159
|
source(request, response) {
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
293
|
-
node.
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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="
|
|
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-
|
|
352
|
-
<i class="fa fa-
|
|
285
|
+
<label for="node-input-deviceType">
|
|
286
|
+
<i class="fa fa-microchip"></i> <span data-i18n="knxUltimateMatterBridge.fields.type"></span>
|
|
353
287
|
</label>
|
|
354
|
-
<
|
|
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.
|
|
293
|
+
<span data-i18n="knxUltimateMatterBridge.device_help"></span>
|
|
362
294
|
</div>
|
|
363
295
|
|
|
364
|
-
<div
|
|
365
|
-
|
|
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> <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> <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
|
|
329
|
+
Exposes **one KNX device as a Matter device**. This node is in **BETA**.
|
|
417
330
|
|
|
418
|
-
|
|
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
|
|
334
|
+
- **KNX → Matter**: telegrams on the *status* group addresses update the Matter attribute (and the app UI).
|
|
422
335
|
|
|
423
|
-
The
|
|
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
|
|
338
|
+
If you enable the **node PINs**:
|
|
426
339
|
|
|
427
|
-
- input: `msg.payload = {
|
|
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>
|