node-red-contrib-knx-ultimate 6.0.12 → 6.1.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 (42) hide show
  1. package/CHANGELOG.md +6 -2
  2. package/nodes/knxUltimateMatterControllerDevice.html +273 -13
  3. package/nodes/knxUltimateMatterControllerDevice.js +37 -0
  4. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +13 -1
  5. package/nodes/locales/de/knxUltimateMatterControllerDevice.json +22 -0
  6. package/nodes/locales/de/matter-config.html +11 -1
  7. package/nodes/locales/de/matter-config.json +12 -0
  8. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +13 -1
  9. package/nodes/locales/en/knxUltimateMatterControllerDevice.json +22 -0
  10. package/nodes/locales/en/matter-config.html +11 -1
  11. package/nodes/locales/en/matter-config.json +12 -0
  12. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +13 -1
  13. package/nodes/locales/es/knxUltimateMatterControllerDevice.json +22 -0
  14. package/nodes/locales/es/matter-config.html +11 -1
  15. package/nodes/locales/es/matter-config.json +12 -0
  16. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +13 -1
  17. package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +30 -0
  18. package/nodes/locales/fr/matter-config.html +11 -1
  19. package/nodes/locales/fr/matter-config.json +12 -0
  20. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +13 -1
  21. package/nodes/locales/it/knxUltimateMatterControllerDevice.json +22 -0
  22. package/nodes/locales/it/matter-config.html +11 -1
  23. package/nodes/locales/it/matter-config.json +12 -0
  24. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +13 -1
  25. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +30 -0
  26. package/nodes/locales/zh-CN/matter-config.html +11 -1
  27. package/nodes/locales/zh-CN/matter-config.json +12 -0
  28. package/nodes/matter-config.html +215 -23
  29. package/nodes/matter-config.js +48 -2
  30. package/nodes/utils/matterControllerCommandGate.js +47 -0
  31. package/nodes/utils/matterControllerProfiles/doorLock.js +19 -4
  32. package/nodes/utils/matterControllerProfiles/fan.js +0 -1
  33. package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +32 -15
  34. package/nodes/utils/matterControllerProfiles/switch.js +0 -1
  35. package/nodes/utils/matterControllerProfiles/thermostat.js +0 -1
  36. package/nodes/utils/matterControllerProfiles/windowCovering.js +0 -1
  37. package/nodes/utils/matterControllerSemanticInput.js +212 -0
  38. package/nodes/utils/matterEngine.mjs +118 -16
  39. package/package.json +1 -1
  40. package/resources/matterQrScanner.js +98 -0
  41. package/resources/zxing-browser-0.2.0.min.js +1 -0
  42. package/resources/zxing-browser-LICENSE.txt +21 -0
package/CHANGELOG.md CHANGED
@@ -6,14 +6,18 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 6.0.12** - July 2026<br/>
9
+ **Version 6.1.0** - July 2026<br/>
10
10
 
11
+ - **Matter Controller — QR pairing-code reader**: the pairing editor can now fill the existing `MT:…` field by scanning a QR code with the local webcam or reading it from a selected image, including inverted white-on-dark QR codes used by some device web interfaces. Decoding remains entirely in the browser and never uploads the image or video; after a valid QR is read, pairing starts automatically, while manually typed codes still require **PAIR**. The optional device name is positioned before the scanner, the decoded code remains visible underneath it and fabric backup controls have moved to the bottom of the editor. A prominent blocking progress overlay prevents further editor interactions until commissioning succeeds or fails. When live camera access is unavailable because the editor is not open over HTTPS/`localhost`, permission was denied or the browser lacks camera support, the editor explains the reason and keeps image loading available.<br/>
12
+ - **Matter Controller — commissioned-device status**: fixed the connection-state import for the installed matter.js API, so the associated-devices table now reports `connected`, `disconnected`, `reconnecting` or `waitingfordiscovery` instead of misleading values such as `unknown (0)`. The table has also been regression-tested to preserve every row returned by the controller without collapsing equal Node IDs.<br/>
13
+ - **Matter Controller — per-device command isolation**: commands now remain ordered inside an independent execution lane for each commissioned Node ID instead of sharing one blocking serial queue. An offline, timing-out or removed device can no longer delay other Matter Controller nodes that react to the same KNX group address. Removed Node IDs are rejected before queueing, every Matter operation has a bounded timeout and pending commands are purged on removal. An unavailable Controller node latches its red status and ignores subsequent KNX/flow commands instead of accepting them optimistically; it resumes automatically when that specific Matter device reports `connected`, while opening the node editor explicitly enables a manual retry.<br/>
11
14
  - **Control Matter from KNX — Universal Mode**: a dedicated **Mode** selector now switches between a single Matter device and controller-wide Universal Mode. Its **Universal Battery Monitor** scans every commissioned node and endpoint for battery Power Source clusters, maintains a normalized cache, publishes an optional aggregate KNX alarm as DPT 1.005, cycles low-battery device names every 2 seconds as DPT 16.001, and answers `getAllBatteries` flow queries. The alternative **All Matter events** service preserves the generic attribute/event stream and dynamic read/write/command input.<br/>
12
15
  - **Universal Battery Monitor — KNX editor and diagnostics**: battery options now use the same vertical-tab and ETS/CSV group-address selectors as single-device profiles. Alarm and text mappings are aligned as Function/GA/DPT/Name rows, support editable names and KNX read responses, ignore wired Power Source endpoints, and show an aggregate battery status instead of per-device `unknown` states.<br/>
13
16
  - **Matter light capability refresh**: Color Control `featureMap` and the endpoint device type now drive the Tunable White and RGB/HSV tabs. New selections, changed devices and previously saved nodes normalize stale capabilities so Tunable White lights no longer retain unsupported colour mappings.<br/>
14
17
  - **Matter editor/help cleanup**: decorative animated light icons and duplicate localized help headings were removed from the Matter Controller, Matter Bridge and shared light editor where applicable.<br/>
15
18
  - **Matter Bridge — editor tab orientation**: the device editor now uses the same left-hand vertical tab layout as Matter Controller, keeping Mappings and Advanced options visually consistent without changing saved configuration.<br/>
16
- - **Control Matter from KNX — dedicated controller profiles**: Window Covering (`0x0102`), Thermostat (`0x0201`), Fan Control (`0x0202`) and Switch (`0x003B`) endpoints are now selected into explicit capability-driven profiles instead of the anonymous mapped fallback. They share the guarded mapping lifecycle while enforcing their native input/event cluster boundary and presenting semantic canvas status; Switch initial/long/multi-press events are filtered to the selected endpoint and exposed on the optional flow output. Simple actuators and sensors remain on the generic mapped profile.<br/>
19
+ - **Control Matter from KNX — simple flow input and capability help**: non-light endpoints now accept the same human-readable `msg.payload = { function, value }` contract used by Matter Bridge. The selected node and endpoint are implicit; percentages, temperatures and measurements use human units, while omitting `value` reads a supported state. A capability-driven **Flow input** tab, consistent with the node's existing editor tabs, replaces the modal help button and provides copyable simple examples plus every advertised readable/writable attribute, command, cluster ID and advanced message. It remains available for flow-only nodes without a KNX gateway. Existing top-level `clusterId`/`command`/`attribute` inputs remain compatible, and Door Lock also accepts `{function:"lock",value:true|false}`.<br/>
20
+ - **Control Matter from KNX — dedicated controller profiles**: Window Covering (`0x0102`), Thermostat (`0x0201`), Fan Control (`0x0202`) and Switch (`0x003B`) endpoints are now selected into explicit capability-driven profiles instead of the anonymous mapped fallback. They share the guarded mapping lifecycle while presenting semantic canvas status; Switch initial/long/multi-press events are filtered to the selected endpoint and exposed on the optional flow output. Simple actuators and sensors remain on the generic mapped profile.<br/>
17
21
  - **Control Matter from KNX — flow attribute reads and persistent Door Lock status**: mapped endpoints now use an unambiguous top-level flow contract: `msg.clusterId` plus `msg.attribute` reads an attribute, `msg.value` makes it an attribute write, and `msg.command` plus `msg.args` invokes a command. Reads return the value in `msg.payload`, may force a remote request with `msg.requestFromRemote = true`, and accept numeric attribute ID `0`. Door Lock nodes now retain their last `locked`/`unlocked` canvas status when generic Matter or KNX notifications arrive.<br/>
18
22
 
19
23
  **Version 6.0.10** - July 2026<br/>
@@ -152,6 +152,14 @@
152
152
 
153
153
 
154
154
  var node = this; // Starting from v 4.1.31
155
+ if (node.id) {
156
+ $.ajax({
157
+ url: "KNXUltimateMatterControllerEnable",
158
+ type: "POST",
159
+ contentType: "application/json",
160
+ data: JSON.stringify({ nodeId: node.id })
161
+ });
162
+ }
155
163
  $("#node-input-updateLocalStateFromKNXWrite").prop("checked", node.updateLocalStateFromKNXWrite === true || node.updateLocalStateFromKNXWrite === "true"); // Starting from v 4.1.31
156
164
  const ensureConfigSelection = (selector) => {
157
165
  if ($(selector).val() !== "_ADD_") return;
@@ -221,6 +229,31 @@
221
229
  border-top: 1px solid #ccc;
222
230
  margin: 8px 0;
223
231
  }
232
+ .matter-input-help-table {
233
+ width: 100%;
234
+ border-collapse: collapse;
235
+ margin: 8px 0 18px;
236
+ }
237
+ .matter-input-help-table th,
238
+ .matter-input-help-table td {
239
+ border-bottom: 1px solid #ddd;
240
+ padding: 7px 6px;
241
+ text-align: left;
242
+ vertical-align: top;
243
+ }
244
+ .matter-input-help-table code {
245
+ white-space: pre-wrap;
246
+ word-break: break-word;
247
+ }
248
+ .matter-input-help-copy {
249
+ white-space: nowrap;
250
+ }
251
+ .matter-input-help-advanced {
252
+ max-height: 310px;
253
+ overflow: auto;
254
+ border: 1px solid #ddd;
255
+ padding: 0 8px;
256
+ }
224
257
  </style>`;
225
258
  $('head').append(style);
226
259
  }
@@ -241,6 +274,7 @@
241
274
  const $refreshDevicesButton = $(".hue-refresh-devices");
242
275
  const $locateDeviceButton = $(".hue-locate-device");
243
276
  const $hueDevicesLoading = $(".hue-devices-loading");
277
+ const $inputHelpPanel = $("#tabs-input");
244
278
  const UNIVERSAL_NODE_ID = '__UNIVERSAL__';
245
279
  const isUniversalMode = () => $matterControllerModeInput.val() === 'universal';
246
280
  $matterControllerModeInput.val(node.matterControllerMode || 'single');
@@ -549,6 +583,19 @@
549
583
  return !!cluster && (cluster.commands || []).some((cmd) => wanted.has(normalizeMatterToken(cmd.name)));
550
584
  };
551
585
  const matterCapabilitiesFromEndpoint = (ep) => {
586
+ const inputStructure = {
587
+ endpointId: Number(ep.endpointId),
588
+ clusters: (ep.clusters || []).map((cluster) => ({
589
+ id: Number(cluster.id),
590
+ name: cluster.name || '',
591
+ attributes: (cluster.attributes || []).map((attribute) => ({
592
+ id: Number(attribute.id),
593
+ name: attribute.name,
594
+ writable: attribute.writable === true
595
+ })),
596
+ commands: (cluster.commands || []).map((command) => ({ name: command.name }))
597
+ }))
598
+ };
552
599
  const deviceTypes = (ep.deviceTypes || []).map(normalizeMatterToken);
553
600
  const primaryDeviceType = (ep.deviceTypes || []).find((value) => !/bridgednode|aggregator|controlbridge/i.test(String(value))) || ep.name;
554
601
  const deviceTypeDisplay = matterPrettyType(primaryDeviceType);
@@ -568,6 +615,7 @@
568
615
  color: false,
569
616
  lockDoor: hasMatterCommand(doorLockCluster, ['lockDoor']),
570
617
  unlockDoor: hasMatterCommand(doorLockCluster, ['unlockDoor']),
618
+ inputStructure,
571
619
  requirePinForRemoteOperation: (() => {
572
620
  const attribute = (doorLockCluster.attributes || []).find((item) => normalizeMatterToken(item.name) === 'requirepinforremoteoperation');
573
621
  return attribute ? attribute.value === true : false;
@@ -611,7 +659,7 @@
611
659
  addAttribute(47, 'batPercentRemaining', 'battery', '5.001', false);
612
660
  addAttribute(144, 'activePower', 'active_power', '14.056', false);
613
661
  addAttribute(145, 'cumulativeEnergyImported', 'imported_energy', '13.013', false);
614
- return { known: true, profile: specializedProfile, deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
662
+ return { known: true, profile: specializedProfile, deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets, inputStructure };
615
663
  }
616
664
  const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
617
665
  const hasColorType = /extendedcolor/.test(typeText);
@@ -804,8 +852,144 @@
804
852
  $matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
805
853
  node.matterDeviceCapabilities = $matterCapabilitiesInput.val();
806
854
  }
855
+
856
+ const semanticInputDefinitions = [
857
+ {
858
+ fn: 'onoff',
859
+ label: 'On / Off',
860
+ read: [6, ['onOff']],
861
+ writes: [
862
+ { descriptor: [6, ['on']], value: true },
863
+ { descriptor: [6, ['off']], value: false },
864
+ { descriptor: [6, ['toggle']], value: 'toggle' }
865
+ ]
866
+ },
867
+ { fn: 'level', label: 'Level', read: [8, ['currentLevel']], write: [8, ['moveToLevelWithOnOff', 'moveToLevel']], value: 50 },
868
+ { fn: 'position', label: 'Cover position', read: [258, ['currentPositionLiftPercent100ths', 'currentPositionLiftPercentage']], write: [258, ['goToLiftPercentage']], value: 35 },
869
+ { fn: 'tiltposition', label: 'Cover tilt', read: [258, ['currentPositionTiltPercent100ths', 'currentPositionTiltPercentage']], write: [258, ['goToTiltPercentage']], value: 35 },
870
+ { fn: 'open', label: 'Open', write: [258, ['upOrOpen']], omitValue: true },
871
+ { fn: 'close', label: 'Close', write: [258, ['downOrClose']], omitValue: true },
872
+ { fn: 'stop', label: 'Stop', write: [258, ['stopMotion']], omitValue: true },
873
+ { fn: 'setpoint', label: 'Heating setpoint', read: [513, ['occupiedHeatingSetpoint']], writeAttribute: [513, ['occupiedHeatingSetpoint']], value: 21.5 },
874
+ { fn: 'coolingsetpoint', label: 'Cooling setpoint', read: [513, ['occupiedCoolingSetpoint']], writeAttribute: [513, ['occupiedCoolingSetpoint']], value: 24 },
875
+ { fn: 'currenttemp', label: 'Current temperature', read: [513, ['localTemperature']] },
876
+ { fn: 'fanspeed', label: 'Fan speed', read: [514, ['percentCurrent', 'percentSetting']], writeAttribute: [514, ['percentSetting']], value: 60 },
877
+ { fn: 'temperature', label: 'Temperature', read: [1026, ['measuredValue']] },
878
+ { fn: 'humidity', label: 'Humidity', read: [1029, ['measuredValue']] },
879
+ { fn: 'illuminance', label: 'Illuminance', read: [1024, ['measuredValue']] },
880
+ { fn: 'occupancy', label: 'Occupancy', read: [1030, ['occupancy']] },
881
+ { fn: 'contact', label: 'Contact', read: [69, ['stateValue']] },
882
+ { fn: 'battery', label: 'Battery', read: [47, ['batPercentRemaining']] },
883
+ { fn: 'activepower', label: 'Active power', read: [144, ['activePower']] },
884
+ { fn: 'importedenergy', label: 'Imported energy', read: [145, ['cumulativeEnergyImported']] },
885
+ { fn: 'identify', label: 'Identify', write: [3, ['identify']], omitValue: true }
886
+ ];
887
+ const inputStructureFor = (caps) => caps && caps.inputStructure && Array.isArray(caps.inputStructure.clusters)
888
+ ? caps.inputStructure
889
+ : null;
890
+ const structureMember = (structure, targetKind, descriptor, writableOnly = false) => {
891
+ if (!structure || !descriptor) return null;
892
+ const cluster = structure.clusters.find((item) => Number(item.id) === Number(descriptor[0]));
893
+ if (!cluster) return null;
894
+ const collection = targetKind === 'command' ? cluster.commands : cluster.attributes;
895
+ if (!Array.isArray(collection)) return null;
896
+ const wanted = descriptor[1].map(normalizeMatterToken);
897
+ return collection.find((item) =>
898
+ wanted.includes(normalizeMatterToken(item.name)) &&
899
+ (!writableOnly || item.writable === true)
900
+ ) || null;
901
+ };
902
+ const functionCode = (payload) => `msg.payload = ${JSON.stringify(payload, null, 2)};\nreturn msg;`;
903
+ const advancedCode = (kind, clusterId, name) => {
904
+ if (kind === 'read') return `msg.clusterId = ${Number(clusterId)};\nmsg.attribute = ${JSON.stringify(name)};\nreturn msg;`;
905
+ if (kind === 'write') return `msg.clusterId = ${Number(clusterId)};\nmsg.attribute = ${JSON.stringify(name)};\nmsg.value = 0;\nreturn msg;`;
906
+ return `msg.clusterId = ${Number(clusterId)};\nmsg.command = ${JSON.stringify(name)};\nmsg.args = {};\nreturn msg;`;
907
+ };
908
+ const appendInputHelpRow = ($body, operation, label, code) => {
909
+ const $row = $('<tr></tr>');
910
+ $('<td></td>').text(operation).appendTo($row);
911
+ $('<td></td>').text(label).appendTo($row);
912
+ $('<td><code></code></td>').find('code').text(code).end().appendTo($row);
913
+ $('<td></td>').append(
914
+ $('<button type="button" class="red-ui-button matter-input-help-copy"><i class="fa fa-copy"></i></button>')
915
+ .attr('title', node._('knxUltimateMatterControllerDevice.input_help_copy') || 'Copy')
916
+ .data('copy-text', code)
917
+ ).appendTo($row);
918
+ $body.append($row);
919
+ };
920
+ const renderInputHelp = () => {
921
+ const caps = getEffectiveMatterCapabilities();
922
+ const structure = inputStructureFor(caps);
923
+ const endpointId = structure ? structure.endpointId : Number($matterEndpointInput.val() || node.matterEndpointId || 1);
924
+ $('#matter-input-help-endpoint').text(String(endpointId));
925
+ const $simpleBody = $('#matter-input-help-simple-body').empty();
926
+ if (caps.profile === 'doorLock') {
927
+ if (caps.lockDoor !== false) appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', node._('knxUltimateMatterControllerDevice.input_functions.lock') || 'Lock', functionCode({ function: 'lock', value: true }));
928
+ if (caps.unlockDoor !== false) appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', node._('knxUltimateMatterControllerDevice.input_functions.unlock') || 'Unlock', functionCode({ function: 'lock', value: false }));
929
+ } else {
930
+ semanticInputDefinitions.forEach((definition) => {
931
+ const readTarget = structureMember(structure, 'attribute', definition.read);
932
+ const writeVariants = definition.writes || [{
933
+ descriptor: definition.write || definition.writeAttribute,
934
+ targetKind: definition.write ? 'command' : 'attribute',
935
+ value: definition.value,
936
+ omitValue: definition.omitValue
937
+ }];
938
+ writeVariants.forEach((variant) => {
939
+ const targetKind = variant.targetKind || 'command';
940
+ const writeTarget = structureMember(structure, targetKind, variant.descriptor, targetKind === 'attribute');
941
+ if (!writeTarget) return;
942
+ const payload = { function: definition.fn };
943
+ if (!variant.omitValue) payload.value = variant.value;
944
+ appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', node._(`knxUltimateMatterControllerDevice.input_functions.${definition.fn}`) || definition.label, functionCode(payload));
945
+ });
946
+ if (readTarget) {
947
+ appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_read') || 'Read', node._(`knxUltimateMatterControllerDevice.input_functions.${definition.fn}`) || definition.label, functionCode({ function: definition.fn }));
948
+ }
949
+ });
950
+ }
951
+ $('#matter-input-help-simple-empty').toggle($simpleBody.children().length === 0);
952
+
953
+ const $advancedBody = $('#matter-input-help-advanced-body').empty();
954
+ if (structure) {
955
+ structure.clusters.forEach((cluster) => {
956
+ (cluster.attributes || []).forEach((attribute) => {
957
+ const prefix = `${cluster.name || 'Cluster'} (${cluster.id}) · ${attribute.name} (${attribute.id})`;
958
+ appendInputHelpRow($advancedBody, node._('knxUltimateMatterControllerDevice.input_help_read') || 'Read', prefix, advancedCode('read', cluster.id, attribute.name));
959
+ if (attribute.writable === true) {
960
+ appendInputHelpRow($advancedBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', prefix, advancedCode('write', cluster.id, attribute.name));
961
+ }
962
+ });
963
+ (cluster.commands || []).forEach((command) => {
964
+ const prefix = `${cluster.name || 'Cluster'} (${cluster.id}) · ${command.name}`;
965
+ appendInputHelpRow($advancedBody, node._('knxUltimateMatterControllerDevice.input_help_command') || 'Command', prefix, advancedCode('command', cluster.id, command.name));
966
+ });
967
+ });
968
+ }
969
+ $('#matter-input-help-advanced-empty').toggle($advancedBody.children().length === 0);
970
+ };
971
+ const copyInputHelpText = (text) => {
972
+ const copied = () => RED.notify(node._('knxUltimateMatterControllerDevice.input_help_copied') || 'Message copied', 'success');
973
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
974
+ navigator.clipboard.writeText(text).then(copied).catch(() => {
975
+ const $temporary = $('<textarea></textarea>').val(text).appendTo('body').select();
976
+ try { document.execCommand('copy'); copied(); } finally { $temporary.remove(); }
977
+ });
978
+ return;
979
+ }
980
+ const $temporary = $('<textarea></textarea>').val(text).appendTo('body').select();
981
+ try { document.execCommand('copy'); copied(); } finally { $temporary.remove(); }
982
+ };
983
+ $inputHelpPanel.off('click.matterInputHelp').on('click.matterInputHelp', '.matter-input-help-copy', function () {
984
+ copyInputHelpText($(this).data('copy-text') || '');
985
+ });
807
986
  $tabs.addClass('hue-vertical-tabs');
808
987
  $tabs.tabs(); // Tabs gestione KNX
988
+ $tabs.off('tabsactivate.matterInputHelp').on('tabsactivate.matterInputHelp', (event, ui) => {
989
+ if (ui && ui.newPanel && ui.newPanel.attr('id') === 'tabs-input') {
990
+ renderInputHelp();
991
+ }
992
+ });
809
993
  $tabs.find('ul').addClass('ui-tabs-nav');
810
994
  $tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
811
995
  $universalTabs.addClass('hue-vertical-tabs');
@@ -1044,8 +1228,9 @@
1044
1228
 
1045
1229
  const ensureVisibleMatterTabSelected = () => {
1046
1230
  const $active = $tabs.find('ul.ui-tabs-nav li.ui-tabs-active');
1047
- if ($active.length && $active.is(':visible')) return;
1048
- const $firstVisible = $tabs.find('ul.ui-tabs-nav li:visible').first();
1231
+ const isTabAvailable = function () { return $(this).css('display') !== 'none'; };
1232
+ if ($active.length && $active.filter(isTabAvailable).length) return;
1233
+ const $firstVisible = $tabs.find('ul.ui-tabs-nav li').filter(isTabAvailable).first();
1049
1234
  if (!$firstVisible.length) return;
1050
1235
  const visibleIndex = $tabs.find('ul.ui-tabs-nav li').index($firstVisible);
1051
1236
  if (visibleIndex >= 0) {
@@ -1107,14 +1292,16 @@
1107
1292
  const isDoorLock = caps.profile === 'doorLock';
1108
1293
  const isMapped = ['mapped', 'windowCovering', 'thermostat', 'fan', 'switch'].includes(caps.profile);
1109
1294
  const isUniversal = caps.profile === 'universal' || isUniversalMode();
1295
+ const knxSelected = hasKnxServerSelected();
1110
1296
  const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
1111
1297
 
1112
- setMatterTabVisible('tabs-1', !isUniversal && supportsSwitch);
1113
- setMatterTabVisible('tabs-2', !isUniversal && supportsDim);
1114
- setMatterTabVisible('tabs-3', !isUniversal && supportsTemperature);
1115
- setMatterTabVisible('tabs-4', !isUniversal && supportsColor);
1116
- setMatterTabVisible('tabs-mapped', !isUniversal && isMapped);
1117
- setMatterTabVisible('tabs-6', !isUniversal && hasHueDevice);
1298
+ setMatterTabVisible('tabs-1', !isUniversal && knxSelected && supportsSwitch);
1299
+ setMatterTabVisible('tabs-2', !isUniversal && knxSelected && supportsDim);
1300
+ setMatterTabVisible('tabs-3', !isUniversal && knxSelected && supportsTemperature);
1301
+ setMatterTabVisible('tabs-4', !isUniversal && knxSelected && supportsColor);
1302
+ setMatterTabVisible('tabs-mapped', !isUniversal && knxSelected && isMapped);
1303
+ setMatterTabVisible('tabs-input', !isUniversal && hasHueDevice && (isMapped || isDoorLock));
1304
+ setMatterTabVisible('tabs-6', !isUniversal && knxSelected && hasHueDevice);
1118
1305
  $('#matter-universal-description').toggle(isUniversal);
1119
1306
  $pinSelect.val(isUniversal ? 'yes' : $pinSelect.val()).prop('disabled', isUniversal);
1120
1307
  updateLocateButtonState(false);
@@ -1129,6 +1316,7 @@
1129
1316
  $('#matter-door-lock-pin-required').toggle(isDoorLock && caps.requirePinForRemoteOperation === true);
1130
1317
  $('#matter-mapped-profile').toggle(isMapped);
1131
1318
  if (isMapped) renderMappedProfile(caps.mappedTargets || []);
1319
+ if (hasHueDevice && !isUniversal && (isMapped || isDoorLock)) renderInputHelp();
1132
1320
  $('#matter-selected-device-type-value').text(deviceTypeDisplay);
1133
1321
  $('#matter-selected-device-type').toggle(hasHueDevice && deviceTypeDisplay !== '');
1134
1322
  try { $tabs.tabs('refresh'); } catch (error) { /* empty */ }
@@ -1186,9 +1374,12 @@
1186
1374
  };
1187
1375
 
1188
1376
  const updateTabsVisibility = () => {
1189
- const knxSelected = hasKnxServerSelected();
1190
1377
  const hueDeviceSelected = getHueDeviceValue() !== '';
1191
- const shouldShowTabs = !isUniversalMode() && knxSelected && hueDeviceSelected;
1378
+ applyMatterCapabilities(currentMatterCapabilities);
1379
+ const hasVisibleTab = $tabs.find('ul.ui-tabs-nav li').filter(function () {
1380
+ return $(this).css('display') !== 'none';
1381
+ }).length > 0;
1382
+ const shouldShowTabs = !isUniversalMode() && hueDeviceSelected && hasVisibleTab;
1192
1383
 
1193
1384
  if (shouldShowTabs) {
1194
1385
  $tabs.show();
@@ -1196,7 +1387,6 @@
1196
1387
  } else {
1197
1388
  $tabs.hide();
1198
1389
  }
1199
- applyMatterCapabilities(currentMatterCapabilities);
1200
1390
 
1201
1391
  if ($pinSectionRow.length) {
1202
1392
  $pinSectionRow.show();
@@ -1944,6 +2134,7 @@
1944
2134
  <li><a href="#tabs-3"><i class="fa-solid fa-temperature-quarter"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.tunable_white"></span></a></li>
1945
2135
  <li><a href="#tabs-4"><i class="fa-solid fa-palette"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.rgb_hsv"></span></a></li>
1946
2136
  <li><a href="#tabs-mapped"><i class="fa-solid fa-list-check"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.mappings"></span></a></li>
2137
+ <li><a href="#tabs-input"><i class="fa-solid fa-code"></i> <span data-i18n="knxUltimateMatterControllerDevice.input_help_label"></span></a></li>
1947
2138
  <li><a href="#tabs-6"><i class="fa-solid fa-code-merge"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.behaviour"></span></a></li>
1948
2139
  </ul>
1949
2140
  <div id="tabs-mapped">
@@ -1951,6 +2142,46 @@
1951
2142
  <div id="matter-mapped-profile-body"></div>
1952
2143
  </div>
1953
2144
  </div>
2145
+ <div id="tabs-input">
2146
+ <p class="form-tips" data-i18n="knxUltimateMatterControllerDevice.input_help_pin_hint"></p>
2147
+ <p>
2148
+ <strong data-i18n="knxUltimateMatterControllerDevice.input_help_endpoint"></strong>:
2149
+ <code id="matter-input-help-endpoint"></code>.
2150
+ <span data-i18n="knxUltimateMatterControllerDevice.input_help_endpoint_hint"></span>
2151
+ </p>
2152
+ <h4 data-i18n="knxUltimateMatterControllerDevice.input_help_simple_title"></h4>
2153
+ <p data-i18n="knxUltimateMatterControllerDevice.input_help_simple_hint"></p>
2154
+ <table class="matter-input-help-table">
2155
+ <thead>
2156
+ <tr>
2157
+ <th data-i18n="knxUltimateMatterControllerDevice.input_help_operation"></th>
2158
+ <th data-i18n="knxUltimateMatterControllerDevice.input_help_function"></th>
2159
+ <th data-i18n="knxUltimateMatterControllerDevice.input_help_message"></th>
2160
+ <th></th>
2161
+ </tr>
2162
+ </thead>
2163
+ <tbody id="matter-input-help-simple-body"></tbody>
2164
+ </table>
2165
+ <p id="matter-input-help-simple-empty" style="display:none;" data-i18n="knxUltimateMatterControllerDevice.input_help_no_simple"></p>
2166
+ <details>
2167
+ <summary><strong data-i18n="knxUltimateMatterControllerDevice.input_help_advanced_title"></strong></summary>
2168
+ <p data-i18n="knxUltimateMatterControllerDevice.input_help_advanced_hint"></p>
2169
+ <div class="matter-input-help-advanced">
2170
+ <table class="matter-input-help-table">
2171
+ <thead>
2172
+ <tr>
2173
+ <th data-i18n="knxUltimateMatterControllerDevice.input_help_operation"></th>
2174
+ <th data-i18n="knxUltimateMatterControllerDevice.input_help_target"></th>
2175
+ <th data-i18n="knxUltimateMatterControllerDevice.input_help_message"></th>
2176
+ <th></th>
2177
+ </tr>
2178
+ </thead>
2179
+ <tbody id="matter-input-help-advanced-body"></tbody>
2180
+ </table>
2181
+ <p id="matter-input-help-advanced-empty" style="display:none;" data-i18n="knxUltimateMatterControllerDevice.input_help_no_structure"></p>
2182
+ </div>
2183
+ </details>
2184
+ </div>
1954
2185
  <div id="tabs-1">
1955
2186
  <p>
1956
2187
  <div class="form-row">
@@ -2414,6 +2645,35 @@ the device picker. Door Lock, Window Covering, Thermostat, Fan and Switch endpoi
2414
2645
  dedicated profiles. Switch press, long-press and multi-press events are emitted on the
2415
2646
  optional flow output; simpler endpoints continue through the generic mapped fallback.
2416
2647
 
2648
+ **Flow input**
2649
+
2650
+ For a selected non-light endpoint, open the **Flow input** tab in the editor.
2651
+ The capability-driven tab shows copyable simple examples plus the selected endpoint,
2652
+ all advertised readable/writable attributes and all accepted commands.
2653
+
2654
+ Use `msg.payload = {function:"position", value:35}` for a simple write. Omit `value`
2655
+ to read a supported state, for example `{function:"temperature"}`. Values use human
2656
+ units and read output includes raw Matter details in `msg.matter`. The available
2657
+ functions are filtered by the endpoint and can include `onoff`, `level`, `position`,
2658
+ `tiltposition`, `open`, `close`, `stop`, `setpoint`, `coolingsetpoint`, `currenttemp`,
2659
+ `fanspeed`, sensor readings and `identify`. Door Lock accepts
2660
+ `{function:"lock", value:true|false}`.
2661
+
2662
+ The advanced top-level fields remain supported: `msg.clusterId` with
2663
+ `msg.command`/`msg.args`, or `msg.attribute` with optional `msg.value`. Node ID and
2664
+ Endpoint ID are already selected by this node; `msg.endpointId` is only an override.
2665
+
2666
+ **Device isolation**
2667
+
2668
+ Commands use a separate ordered queue for every commissioned device. An offline,
2669
+ timing-out or removed device cannot delay other Matter devices sharing the same KNX
2670
+ group address. A Controller node that still points to a removed device rejects the
2671
+ command immediately and shows **Device no longer commissioned** in red.
2672
+
2673
+ An unavailable-device error is latched. Further KNX and flow commands are ignored and
2674
+ cannot repaint the red status. The node resumes automatically when its Matter device
2675
+ reports `connected`; opening the node editor also clears the latch for a manual retry.
2676
+
2417
2677
  **Universal Mode**
2418
2678
 
2419
2679
  Select **Universal Mode** in the **Mode** selector to listen to every attribute report,
@@ -2519,7 +2779,7 @@ _Basic Matter effects_
2519
2779
  | Read status at startup | Read the Matter endpoint status at Node-RED startup or full deploy and send it to the KNX bus. |
2520
2780
  | KNX Brightness Status | Updates the KNX brightness status whenever the Matter light switches ON/OFF. It can send 0% while OFF and restore the previous value when ON, or leave the brightness status unchanged. |
2521
2781
  | Update local cached Matter state from KNX bus writes | When enabled, KNX writes immediately update the local Matter-state cache without waiting for the endpoint report. Disable it if the cache should follow only real Matter reports. |
2522
- | Node Input/Output PINs | For mapped endpoints, all Matter selectors belong directly to `msg`. Read an attribute with `msg.clusterId` and `msg.attribute`; add `msg.requestFromRemote = true` to force a device read instead of using the subscribed value. The result is emitted as `msg.payload`. Add `msg.value` to write an attribute, or use `msg.clusterId`, `msg.command` and `msg.args` to invoke a command. Attribute IDs such as `0` are accepted. Door Lock input accepts `msg.payload = true` to lock and `false` to unlock. |
2782
+ | Node Input/Output PINs | For mapped endpoints, use the simple `msg.payload = {function,value}` contract shown in the **Flow input** tab, or the advanced top-level Matter selectors. Read an attribute with `msg.clusterId` and `msg.attribute`; add `msg.requestFromRemote = true` to force a device read. Add `msg.value` to write, or use `msg.command` and `msg.args`. Door Lock accepts both `msg.payload = true/false` and `{function:"lock",value:true/false}`. |
2523
2783
  | Switch on behaviour | It sets the behaviour of your lights when switched on. You can choose from differents behaviours.<br/> **Select color: ** the light will be switched on with the color of your choice. To change color, just CLICK on the color selector (under the _Select color_ control).<br/>**Select temperature and brightness: ** the light will be switched on with the temperature (Kelvin) and brightness (0-100) of your choice.<br/>**None:** the light will retain its last status. In case you've enable the night lighting, after the night time ends, the lamp will resume the color/temperature/brightness state set at day time. |
2524
2784
  | Night Lighting | It allows to set a particular light color/brightness at nighttime. The options are the same as the daytime. You could select either a temperature/brightness or color. A cozy temperature of 2700 Kelvin, with a brightness of 10% or 20%, is a good choice for bathroom's night light.|
2525
2785
  | Day/Night | Select the group address used to set the day/night behaviour. The group address value is _true_ if daytime, _false_ if nighttime. |
@@ -10,8 +10,34 @@ const hueColorConverter = require('./utils/colorManipulators/hueColorConverter')
10
10
  const { createLightEngine } = require('./utils/lightEngines')
11
11
  const { hueStateToCanonical, matterEventToHuePatch } = require('./utils/lightEngines/matterHueShim')
12
12
  const { setupMatterControllerProfile } = require('./utils/matterControllerProfiles')
13
+ const { setupMatterControllerCommandGate } = require('./utils/matterControllerCommandGate')
14
+
15
+ let matterCommandGateEndpointRegistered = false
13
16
 
14
17
  module.exports = function (RED) {
18
+ if (!matterCommandGateEndpointRegistered) {
19
+ RED.httpAdmin.post('/KNXUltimateMatterControllerEnable', RED.auth.needsPermission('matter-config.write'), (req, res) => {
20
+ try {
21
+ const nodeId = String(req.body?.nodeId || '').trim()
22
+ if (nodeId === '') {
23
+ res.status(400).json({ error: 'Missing Matter Controller node id' })
24
+ return
25
+ }
26
+ const targetNode = RED.nodes.getNode(nodeId)
27
+ if (!targetNode || typeof targetNode.clearMatterCommandBlock !== 'function') {
28
+ // New or not-yet-deployed editor nodes have no runtime counterpart.
29
+ res.json({ status: 'not-deployed', cleared: false })
30
+ return
31
+ }
32
+ const cleared = targetNode.clearMatterCommandBlock('editor')
33
+ res.json({ status: 'ok', cleared })
34
+ } catch (error) {
35
+ res.status(500).json({ error: error.message || 'Unable to enable Matter commands' })
36
+ }
37
+ })
38
+ matterCommandGateEndpointRegistered = true
39
+ }
40
+
15
41
  function knxUltimateMatterControllerDevice (config) {
16
42
  RED.nodes.createNode(this, config)
17
43
  const node = this
@@ -20,6 +46,7 @@ module.exports = function (RED) {
20
46
  node.matterNodeId = config.matterNodeId !== undefined ? String(config.matterNodeId) : ''
21
47
  node.matterEndpointId = config.matterEndpointId !== undefined ? Number(config.matterEndpointId) : 1
22
48
  node.matterDeviceName = config.matterDeviceName !== undefined ? String(config.matterDeviceName) : ''
49
+ setupMatterControllerCommandGate(node)
23
50
 
24
51
  let matterCapabilities = {}
25
52
  try { matterCapabilities = JSON.parse(config.matterDeviceCapabilities || '{}') } catch (error) { /* empty */ }
@@ -45,6 +72,7 @@ module.exports = function (RED) {
45
72
  hueManager: {
46
73
  writeHueQueueAdd: (_lightID, _state, _operation) => {
47
74
  try {
75
+ if (node.matterCommandBlocked === true) return
48
76
  if (!node.lightEngine) return
49
77
  const patch = hueStateToCanonical(_state)
50
78
  if (!patch) return
@@ -217,6 +245,7 @@ module.exports = function (RED) {
217
245
  fill, shape, text, payload
218
246
  }) => {
219
247
  try {
248
+ if (node.matterCommandBlocked === true) return
220
249
  if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
221
250
  if (payload === undefined) payload = ''
222
251
  const dDate = new Date()
@@ -228,6 +257,7 @@ module.exports = function (RED) {
228
257
  // Compatibility callback used to update the Matter node status.
229
258
  node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
230
259
  try {
260
+ if (node.matterCommandBlocked === true) return
231
261
  if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
232
262
  if (payload === undefined) payload = ''
233
263
  const dDate = new Date()
@@ -238,6 +268,7 @@ module.exports = function (RED) {
238
268
  }
239
269
 
240
270
  node.writeHueState = function writeHueState (_state) {
271
+ if (node.matterCommandBlocked === true) return
241
272
  const defaultOperation = node.isGrouped_light === false ? 'setLight' : 'setGroupedLight'
242
273
  const isGroupedLightOff = node.isGrouped_light === true && node.currentHUEDevice?.on?.on === false
243
274
  const stateKeys = _state && typeof _state === 'object' ? Object.keys(_state) : []
@@ -330,6 +361,7 @@ module.exports = function (RED) {
330
361
  // Compatibility callback invoked by the Matter controller adapter.
331
362
  node.handleSend = (msg) => {
332
363
  if (!msg || !msg.knx || !isConfiguredKNXGA(msg.knx.destination)) return
364
+ if (node.matterCommandBlocked === true) return
333
365
  if (node.currentHUEDevice === undefined && node.serverHue.linkStatus === 'connected') {
334
366
  node.setNodeStatusHue({
335
367
  fill: 'yellow',
@@ -1671,6 +1703,11 @@ module.exports = function (RED) {
1671
1703
  }
1672
1704
 
1673
1705
  node.on('input', (msg, send, done) => {
1706
+ if (node.matterCommandBlocked === true) {
1707
+ const error = new Error(node.matterCommandBlockReason || 'Matter device unavailable')
1708
+ if (done) done(error); else node.error(error, msg)
1709
+ return
1710
+ }
1674
1711
  try {
1675
1712
  const state = RED.util.cloneMessage(msg)
1676
1713
  node.writeHueState(state)
@@ -19,7 +19,15 @@ Er ersetzt die unveröffentlichten getrennten Matter-Controller-Nodes und behäl
19
19
  | Sensoren | Sensor-Endpunkte zeigen ihre Mess-/Status-GA nur bei Unterstützung: Temperatur, Feuchte, Helligkeit, Präsenz, Kontakt und Batterie. |
20
20
  | Read at startup | Veröffentlicht den gecachten Matter-Wert beim Deploy/Start oder wenn sich das Gerät erneut verbindet. |
21
21
  | Update local state from KNX write | Aktualisiert den lokalen Matter/KNX-Cache, wenn ein Telegramm auf eine konfigurierte KNX-GA geschrieben wird. |
22
- | Node Input/Output PINs | Zeigt Node-RED-Eingangs-/Ausgangspins. Der Eingang akzeptiert boolesche Payloads sowie Matter-ähnliche `msg.payload` oder `msg.on.on`; der Ausgang sendet Statusupdates. Die Auswahl bleibt beim erneuten Öffnen des Editors erhalten. |
22
+ | Node Input/Output PINs | Zeigt Node-RED-Eingangs-/Ausgangspins. Nicht-Licht-Endpunkte akzeptieren das einfache `{function,value}`-Format und die erweiterten Matter-Felder; der Ausgang sendet Statusupdates. |
23
+
24
+ ## Flow-Eingang
25
+
26
+ Öffne bei einem ausgewählten Nicht-Licht-Endpunkt den Tab **Flow-Eingang**. Der Tab wird aus der angekündigten Endpunktstruktur aufgebaut und zeigt kopierbare Beispiele, die Endpoint ID sowie alle lesbaren/schreibbaren Attribute und akzeptierten Befehle. Er bleibt auch verfügbar, wenn der Knoten ohne KNX-Gateway nur im Flow verwendet wird.
27
+
28
+ Einfache Schreibvorgänge verwenden `msg.payload = {function:"position",value:35}`. Ohne `value` wird ein unterstützter Zustand gelesen, zum Beispiel `{function:"temperature"}`; die Ausgabe verwendet verständliche Einheiten und enthält Rohdetails in `msg.matter`. Funktionen wie `onoff`, `level`, `position`, `open`, `close`, `stop`, Sollwerte, Lüfter und Sensoren erscheinen nur bei Unterstützung. Türschlösser akzeptieren `{function:"lock",value:true|false}`.
29
+
30
+ Die bisherigen erweiterten Eingaben bleiben gültig: `msg.clusterId` mit `msg.command`/`msg.args` oder `msg.attribute` und optional `msg.value`. Node ID und Endpoint ID sind bereits im Knoten ausgewählt.
23
31
 
24
32
  ## Universeller Modus
25
33
 
@@ -34,4 +42,8 @@ Beispiele: `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` schalt
34
42
  ## Verhalten
35
43
 
36
44
  Der Knoten pflegt einen lokalen Cache aus Matter-Updates und KNX-Schreibtelegrammen, beantwortet KNX-Leseanfragen aus diesem Cache und kann Werte beim Start lesen/ausgeben. Er hört nur auf konfigurierte Gruppenadressen, daher wird fremder KNX-Verkehr ignoriert.
45
+
46
+ Befehle laufen pro gekoppeltem Gerät in einer eigenen geordneten Warteschlange. Ein offline befindliches, zeitüberschreitendes oder entferntes Gerät kann daher andere Matter-Geräte mit derselben KNX-Gruppenadresse nicht verzögern. Verweist ein Node weiterhin auf ein entferntes Gerät, wird der Befehl sofort abgewiesen und **Device no longer commissioned** rot angezeigt; wähle ein gültiges Matter-Gerät oder entferne den verwaisten Controller-Node.
47
+
48
+ Ein Fehler wegen eines nicht verfügbaren Geräts bleibt verriegelt: weitere KNX- und Flow-Befehle werden ignoriert und können den roten Status nicht überschreiben. Der Node wird automatisch wieder aktiv, sobald dieses Matter-Gerät `connected` meldet; auch das Öffnen des Node-Editors löst die Verriegelung für einen manuellen Neuversuch.
37
49
  </script>
@@ -11,6 +11,28 @@
11
11
  "node-input-hueLight": "Matter-Gerät",
12
12
  "paletteLabel": "Control Matter from KNX",
13
13
  "no_devices": "Keine Matter-Geräte gefunden. Matter-Controller prüfen und aktualisieren.",
14
+ "input_help_label": "Flow-Eingang",
15
+ "input_help_pin_hint": "Aktiviere die Ein-/Ausgangs-PINs des Knotens, bevor du diese Nachrichten sendest.",
16
+ "input_help_endpoint": "Ausgewählter Endpunkt",
17
+ "input_help_endpoint_hint": "Der Knoten kennt Node ID und Endpoint ID bereits; einfache Nachrichten benötigen sie nicht.",
18
+ "input_help_simple_title": "Einfache Nachrichten",
19
+ "input_help_simple_hint": "Diese Funktionen verwenden verständliche Einheiten und werden in den Matter-Cluster, Befehl oder das Attribut des ausgewählten Endpunkts übersetzt.",
20
+ "input_help_advanced_title": "Erweiterte Matter-Details",
21
+ "input_help_advanced_hint": "Es werden nur angekündigte Attribute und Befehle aufgelistet. Schreibbeispiele verwenden 0 als Platzhalter; Befehlsargumente müssen eventuell an den Matter-Datentyp angepasst werden.",
22
+ "input_help_operation": "Operation",
23
+ "input_help_function": "Funktion",
24
+ "input_help_target": "Matter-Ziel",
25
+ "input_help_message": "Nachricht",
26
+ "input_help_read": "Lesen",
27
+ "input_help_write": "Schreiben",
28
+ "input_help_command": "Befehl",
29
+ "input_help_copy": "Kopieren",
30
+ "input_help_copied": "Nachricht kopiert",
31
+ "input_help_no_simple": "Dieser Endpunkt besitzt keine bekannte einfache Eingangsfunktion.",
32
+ "input_help_no_structure": "Die Matter-Struktur ist nicht verfügbar. Aktualisiere die Geräteliste, während der Endpunkt online ist.",
33
+ "input_functions": {
34
+ "onoff": "Ein / Aus", "level": "Stufe", "position": "Rollladenposition", "tiltposition": "Lamellenposition", "open": "Öffnen", "close": "Schließen", "stop": "Stopp", "setpoint": "Heizsollwert", "coolingsetpoint": "Kühlsollwert", "currenttemp": "Aktuelle Temperatur", "fanspeed": "Lüftergeschwindigkeit", "temperature": "Temperatur", "humidity": "Luftfeuchtigkeit", "illuminance": "Beleuchtungsstärke", "occupancy": "Belegung", "contact": "Kontakt", "battery": "Batterie", "activepower": "Wirkleistung", "importedenergy": "Bezogene Energie", "identify": "Identifizieren", "lock": "Verriegeln", "unlock": "Entriegeln"
35
+ },
14
36
  "tabs": {
15
37
  "switch": "Schalten",
16
38
  "dim": "Dimmen",
@@ -11,7 +11,11 @@ Der Controller kommuniziert mit den Geräten über das **IP-Netzwerk** (WLAN, Et
11
11
 
12
12
  1. Zuerst diesen Konfigurations-Node **deployen** (der Controller muss laufen).
13
13
  2. Den Node erneut öffnen und den **Kopplungscode** eingeben: entweder den 11-stelligen manuellen Code (z.B. `3497-011-2332`) oder den QR-Code-Inhalt (`MT:....`).
14
- 3. Auf **KOPPELN** klicken. Die Kommissionierung kann bis zu einer Minute dauern.
14
+ 3. Bei einem manuell eingegebenen Code auf **KOPPELN** klicken. Ein mit **Webcam** oder **Bild** gelesener QR-Code startet die Kopplung automatisch. Die Kommissionierung kann bis zu einer Minute dauern.
15
+
16
+ Statt den QR-Payload einzutippen, klicke auf **Webcam**, um ihn live zu scannen, oder auf **Bild**, um ihn aus einem lokalen Foto zu lesen. Sowohl normale dunkle QR-Codes auf hellem Grund als auch invertierte weiße Codes auf dunklem Grund werden unterstützt. Die Dekodierung erfolgt vollständig im Browser; nach dem Lesen eines gültigen Matter-QR-Codes füllt der Editor das Kopplungscode-Feld und startet die Kopplung sofort. Gib den optionalen Gerätenamen vor dem Scannen ein. Ein manuell eingegebener Code startet weiterhin erst mit **KOPPELN**. Der Live-Zugriff auf die Webcam setzt voraus, dass der Editor über HTTPS oder von `localhost` geöffnet wurde; andernfalls erklärt der Editor die Einschränkung und das Laden eines Bildes bleibt verfügbar.
17
+
18
+ Während der Kommissionierung überdeckt ein blockierender Wartebildschirm den Editor und verhindert weitere Klicks, bis der Vorgang erfolgreich abgeschlossen wird oder fehlschlägt.
15
19
 
16
20
  Wenn das Gerät fabrikneu ist und nur Bluetooth-Kommissionierung unterstützt, kopple es zuerst mit der Hersteller-App oder einem anderen Matter-Controller (Alexa, Google Home, Apple Home) und nutze dann dessen Funktion **"mit weiterem Hub teilen"**, um einen neuen Kopplungscode für KNX-Ultimate zu erzeugen. So tritt das Gerät mehreren Fabrics gleichzeitig bei.
17
21
 
@@ -26,4 +30,10 @@ Mit **Exportieren** wird eine vollständige Sicherung dieser Controller-Instanz
26
30
  ## Ein Gerät entfernen
27
31
 
28
32
  Nutze den Papierkorb-Button in der Liste der gekoppelten Geräte. Der Controller versucht, das Gerät sauber zu dekommissionieren; ist es nicht erreichbar, wird es trotzdem aus der Fabric entfernt (danach kann ein Werksreset des Geräts nötig sein).
33
+
34
+ Die Liste enthält eine Zeile für jeden Node, der derzeit in der Matter-Fabric dieses Controllers gespeichert ist. Node-IDs sind innerhalb dieser Fabric eindeutig; Endpunkte eines einzelnen kommissionierten Bridges werden nicht als separate Geräte aufgeführt. Die Statusspalte zeigt an, ob ein Node verbunden, getrennt, in Wiederverbindung oder auf Geräteerkennung wartend ist.
35
+
36
+ Der Controller hält die Befehlsreihenfolge für jedes gekoppelte Gerät getrennt. Ein langsames, offline befindliches oder entferntes Gerät kann Befehle für andere Geräte nicht blockieren. Controller-Gerätenodes, die weiterhin auf eine entfernte Node ID verweisen, weisen neue Befehle sofort ab und zeigen **Device no longer commissioned** an.
37
+
38
+ Wird ein Gerät nicht verfügbar, bleiben seine Controller-Nodes gesperrt und ignorieren weitere Befehle, bis dieses Gerät erneut `connected` meldet. Die Wiederherstellung erfolgt automatisch; auch das Öffnen des Geräte-Node-Editors löst die Sperre für einen manuellen Neuversuch.
29
39
  </script>