node-red-contrib-knx-ultimate 6.0.11 → 6.0.12

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 (26) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/nodes/knxUltimateHueLight.html +1 -5
  3. package/nodes/knxUltimateMatterControllerDevice.html +318 -41
  4. package/nodes/locales/de/knxUltimateMatterBridge.html +0 -2
  5. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +10 -2
  6. package/nodes/locales/de/knxUltimateMatterControllerDevice.json +17 -1
  7. package/nodes/locales/en/knxUltimateMatterBridge.html +0 -2
  8. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +10 -2
  9. package/nodes/locales/en/knxUltimateMatterControllerDevice.json +17 -1
  10. package/nodes/locales/es/knxUltimateMatterBridge.html +0 -2
  11. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +10 -2
  12. package/nodes/locales/es/knxUltimateMatterControllerDevice.json +17 -1
  13. package/nodes/locales/fr/knxUltimateMatterBridge.html +0 -2
  14. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +10 -2
  15. package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +17 -1
  16. package/nodes/locales/it/knxUltimateMatterBridge.html +0 -2
  17. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +10 -2
  18. package/nodes/locales/it/knxUltimateMatterControllerDevice.json +17 -1
  19. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +0 -2
  20. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +10 -2
  21. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +17 -1
  22. package/nodes/matter-config.js +5 -4
  23. package/nodes/utils/matterControllerProfiles/index.js +3 -1
  24. package/nodes/utils/matterControllerProfiles/universal.js +462 -0
  25. package/nodes/utils/matterEngine.mjs +2 -1
  26. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -6,8 +6,12 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 6.0.11** - July 2026<br/>
9
+ **Version 6.0.12** - July 2026<br/>
10
10
 
11
+ - **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
+ - **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
+ - **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
+ - **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/>
11
15
  - **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/>
12
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/>
13
17
  - **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/>
@@ -1777,10 +1777,6 @@
1777
1777
  target="_blank" href="https://youtu.be/jjEUI1J8bkA"><u><span data-i18n="common.youtube_sample"></span></u></a>
1778
1778
  <br />
1779
1779
  <br />
1780
- <p align="center">
1781
- <i class="fa-regular fa-lightbulb fa-bounce fa-2x"></i>
1782
- </p>
1783
- <br />
1784
1780
  <label for="node-input-server">
1785
1781
  <img
1786
1782
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKnRFWHRDcmVhdGlvbiBUaW1lAEZyIDYgQXVnIDIwMTAgMjE6NTI6MTkgKzAxMDD84aS8AAAAB3RJTUUH3gYYCicNV+4WIQAAAAlwSFlzAAALEgAACxIB0t1+/AAAAARnQU1BAACxjwv8YQUAAACUSURBVHjaY2CgFZg5c+Z/ZEyWAZ8+f/6/ZsWs/xoamqMGkGrA6Wla/1+fVARjEBuGsSoGmY4eZSCNL59d/g8DIDbIAHR14OgFGQByKjIGKX5+6/T///8gGMQGiV1+/B0Fg70GIkD+RMYgxf/O5/7//2MSmAZhkBi6OrgB6Bg5DGB4ajr3f2xqsYYLSDE2THJUDg0AAAqyDVd4tp4YAAAAAElFTkSuQmCC"></img>
@@ -2487,4 +2483,4 @@ Use the **Hue native effects** table to map your KNX values to the effects suppo
2487
2483
  The Dimming function works in **KNX mode `start` and`stop` ** . To start dimming, send only one "start" KNX telegram. To stop dimming, send a "stop" KNX telegram. Please**remember that** , when you set your wall swiches properties.
2488
2484
 
2489
2485
  <br/>
2490
- </script>
2486
+ </script>
@@ -14,6 +14,16 @@
14
14
  //buttonState: {value: true},
15
15
  server: { type: "knxUltimate-config", required: false },
16
16
  serverMatter: { type: "matter-config", required: true },
17
+ matterControllerMode: { value: "single" },
18
+ universalService: { value: "batteryMonitor" },
19
+ universalBatteryThreshold: { value: 20, validate: RED.validators.number() },
20
+ universalLowBatteryGA: { value: "" },
21
+ universalLowBatteryGAName: { value: "" },
22
+ universalLowBatteryTextGA: { value: "" },
23
+ universalLowBatteryTextGAName: { value: "" },
24
+ universalBatteryOnlyLow: { value: true },
25
+ universalEmitInitialState: { value: true },
26
+ universalIncludeRawEvent: { value: true },
17
27
  matterNodeId: { value: "" },
18
28
  matterEndpointId: { value: 1 },
19
29
  matterDeviceName: { value: "" },
@@ -221,6 +231,8 @@
221
231
  const KNX_EMPTY_VALUES = new Set(['', 'none', '_ADD_', '__NONE__']);
222
232
  // Historical variable names are retained for saved-flow compatibility; they point to Matter widgets.
223
233
  const $hueServerInput = $("#node-input-serverMatter");
234
+ const $matterControllerModeInput = $("#node-input-matterControllerMode");
235
+ const $universalServiceInput = $("#node-input-universalService");
224
236
  const $hueDeviceInput = $("#node-input-matterNodeId");
225
237
  const $deviceNameInput = $("#node-input-matterDeviceName");
226
238
  const $matterEndpointInput = $("#node-input-matterEndpointId");
@@ -229,6 +241,9 @@
229
241
  const $refreshDevicesButton = $(".hue-refresh-devices");
230
242
  const $locateDeviceButton = $(".hue-locate-device");
231
243
  const $hueDevicesLoading = $(".hue-devices-loading");
244
+ const UNIVERSAL_NODE_ID = '__UNIVERSAL__';
245
+ const isUniversalMode = () => $matterControllerModeInput.val() === 'universal';
246
+ $matterControllerModeInput.val(node.matterControllerMode || 'single');
232
247
  let cachedHueDevices = Array.isArray(node._cachedHueLightDevices) ? node._cachedHueLightDevices : [];
233
248
  node._cachedHueLightDevices = cachedHueDevices;
234
249
  const defaultHueDevicePlaceholder = $deviceNameInput.attr('placeholder') || '';
@@ -289,7 +304,10 @@
289
304
  const title = locateSessionActive
290
305
  ? (node._('knxUltimateMatterControllerDevice.locate_stop_title') || 'Stop Matter locate')
291
306
  : (node._('knxUltimateMatterControllerDevice.locate_start_title') || 'Locate selected Matter device');
292
- $locateDeviceButton.attr('title', title);
307
+ $locateDeviceButton.attr('title', isUniversalMode()
308
+ ? (node._('knxUltimateMatterControllerDevice.universal_mode_locate_disabled') || 'Locate is unavailable in Universal Mode.')
309
+ : title);
310
+ $locateDeviceButton.prop('disabled', isUniversalMode());
293
311
  };
294
312
 
295
313
  const removeNodeRemovalListener = () => {
@@ -340,6 +358,10 @@
340
358
  };
341
359
 
342
360
  const performLocateRequest = ({ silent = false, allowStoredContext = false, action } = {}) => {
361
+ if (isUniversalMode()) {
362
+ if (!silent) RED.notify(node._('knxUltimateMatterControllerDevice.universal_mode_locate_disabled') || 'Locate is unavailable in Universal Mode.', 'warning');
363
+ return null;
364
+ }
343
365
  const context = buildLocateContext({ allowStored: allowStoredContext });
344
366
  if (!context) {
345
367
  if (!silent) {
@@ -419,7 +441,7 @@
419
441
  }).always(() => {
420
442
  locatePendingRequest = null;
421
443
  if ($locateDeviceButton.length) {
422
- $locateDeviceButton.prop('disabled', false);
444
+ $locateDeviceButton.prop('disabled', isUniversalMode());
423
445
  }
424
446
  });
425
447
  return request;
@@ -593,43 +615,75 @@
593
615
  }
594
616
  const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
595
617
  const hasColorType = /extendedcolor/.test(typeText);
618
+ const isTunableWhiteOnly = hasColorTemperatureType && !hasColorType;
596
619
  const hasLevelType = /dimmable|colortemperature|extendedcolor/.test(typeText);
620
+ const colorFeatureMapAttribute = (colorCluster?.attributes || []).find((attribute) => normalizeMatterToken(attribute.name) === 'featuremap');
621
+ const colorFeatureMap = Number(colorFeatureMapAttribute?.value);
622
+ const hasColorFeatureMap = Number.isFinite(colorFeatureMap);
597
623
  const hasColorTemperature = !!colorCluster && (
598
- hasColorTemperatureType ||
599
- hasMatterAttribute(colorCluster, ['colorTemperatureMireds', 'colorTempPhysicalMinMireds', 'colorTempPhysicalMaxMireds']) ||
600
- hasMatterCommand(colorCluster, ['moveToColorTemperature'])
624
+ hasColorFeatureMap
625
+ ? (colorFeatureMap & 0x10) !== 0
626
+ : (
627
+ hasColorTemperatureType ||
628
+ hasMatterAttribute(colorCluster, ['colorTemperatureMireds', 'colorTempPhysicalMinMireds', 'colorTempPhysicalMaxMireds']) ||
629
+ hasMatterCommand(colorCluster, ['moveToColorTemperature'])
630
+ )
601
631
  );
602
632
  const hasColor = !!colorCluster && (
603
- hasColorType ||
604
- hasMatterCommand(colorCluster, ['moveToColor', 'moveToHue', 'moveToSaturation', 'moveToHueAndSaturation'])
633
+ !isTunableWhiteOnly && (
634
+ hasColorFeatureMap
635
+ ? (colorFeatureMap & 0x09) !== 0
636
+ : (
637
+ hasColorType ||
638
+ hasMatterCommand(colorCluster, ['moveToColor', 'moveToHue', 'moveToSaturation', 'moveToHueAndSaturation'])
639
+ )
640
+ )
605
641
  );
606
642
  return {
607
643
  known: true,
608
644
  deviceTypeDisplay,
609
- colorDetectionVersion: 2,
645
+ matterLightType: isTunableWhiteOnly ? 'colorTemperature' : (hasColorType ? 'extendedColor' : 'other'),
646
+ colorDetectionVersion: 3,
610
647
  onOff: !!onOffCluster || /onoff|dimmable|colortemperature|extendedcolor/.test(typeText),
611
648
  level: !!levelCluster || hasLevelType,
612
649
  colorTemperature: hasColorTemperature,
613
650
  color: hasColor
614
651
  };
615
652
  };
653
+ const normalizeMatterCapabilities = (capabilities) => {
654
+ if (!capabilities || typeof capabilities !== 'object') return capabilities;
655
+ const normalized = Object.assign({}, capabilities);
656
+ const displayType = normalizeMatterToken(normalized.deviceTypeDisplay);
657
+ const tunableWhiteOnly = normalized.matterLightType === 'colorTemperature' ||
658
+ displayType === 'tunablewhite' ||
659
+ displayType === 'colortemperaturelight';
660
+ if (tunableWhiteOnly) {
661
+ normalized.colorTemperature = true;
662
+ normalized.color = false;
663
+ normalized.colorDetectionVersion = 3;
664
+ } else if (normalized.known === true && normalized.color === true &&
665
+ normalized.colorDetectionVersion !== 2 && normalized.colorDetectionVersion !== 3) {
666
+ normalized.color = false;
667
+ }
668
+ return normalized;
669
+ };
616
670
  const parseMatterCapabilities = (raw) => {
617
671
  if (raw && typeof raw === 'object') {
618
- if (raw.known === true && raw.color === true && raw.colorDetectionVersion !== 2) raw.color = false;
619
- return raw;
672
+ return normalizeMatterCapabilities(raw);
620
673
  }
621
674
  if (typeof raw !== 'string' || raw.trim() === '') return null;
622
675
  try {
623
- const parsed = JSON.parse(raw);
624
- if (parsed && parsed.known === true && parsed.color === true && parsed.colorDetectionVersion !== 2) {
625
- parsed.color = false;
626
- }
627
- return parsed;
676
+ return normalizeMatterCapabilities(JSON.parse(raw));
628
677
  } catch (error) {
629
678
  return null;
630
679
  }
631
680
  };
632
681
  const serializeMatterCapabilities = (capabilities) => JSON.stringify(capabilities || {});
682
+ const universalMatterCapabilities = () => ({
683
+ known: true,
684
+ profile: 'universal',
685
+ deviceTypeDisplay: node._('knxUltimateMatterControllerDevice.universal_mode') || 'Universal Mode'
686
+ });
633
687
  const getJSONPromise = (url) => new Promise((resolve, reject) => { $.getJSON(url, resolve).fail(reject); });
634
688
 
635
689
  // Filters Matter items shaped as { value, matterNodeId, matterEndpointId }.
@@ -742,13 +796,22 @@
742
796
 
743
797
 
744
798
  const $tabs = $("#tabs");
799
+ const $universalTabs = $("#matter-universal-tabs");
745
800
  const $pinSectionRow = $("#node-input-enableNodePINS").closest('.form-row');
746
801
  const $pinSelect = $("#node-input-enableNodePINS");
747
802
  let currentMatterCapabilities = parseMatterCapabilities($matterCapabilitiesInput.val() || node.matterDeviceCapabilities);
803
+ if (currentMatterCapabilities) {
804
+ $matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
805
+ node.matterDeviceCapabilities = $matterCapabilitiesInput.val();
806
+ }
748
807
  $tabs.addClass('hue-vertical-tabs');
749
808
  $tabs.tabs(); // Tabs gestione KNX
750
809
  $tabs.find('ul').addClass('ui-tabs-nav');
751
810
  $tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
811
+ $universalTabs.addClass('hue-vertical-tabs');
812
+ $universalTabs.tabs();
813
+ $universalTabs.find('ul').addClass('ui-tabs-nav');
814
+ $universalTabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
752
815
 
753
816
  function getDPT(_dpt, _destinationWidget) {
754
817
  // DPT Switch command
@@ -835,7 +898,49 @@
835
898
  } catch (e) { }
836
899
  }
837
900
 
901
+ function getFixedDptGroupAddress(_sourceWidgetAutocomplete, _destinationWidgetName, _dptPrefix) {
902
+ const $source = $(_sourceWidgetAutocomplete);
903
+ if ($source.data('ui-autocomplete')) {
904
+ try { $source.autocomplete('destroy'); } catch (error) { /* empty */ }
905
+ }
906
+ $source.autocomplete({
907
+ minLength: 0,
908
+ source: function (request, response) {
909
+ if (!hasKnxServerSelected()) {
910
+ response([]);
911
+ return;
912
+ }
913
+ const serverId = resolveKnxServerValue();
914
+ $.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
915
+ response($.map(data, function (value) {
916
+ const searchable = `${value.ga} (${value.devicename}) DPT${value.dpt}`;
917
+ if (!htmlUtilsfullCSVSearch(searchable, `${request.term || ''} ${_dptPrefix}`)) return null;
918
+ return {
919
+ label: `${value.ga} # ${value.devicename} # ${value.dpt}`,
920
+ value: value.ga,
921
+ deviceName: value.devicename
922
+ };
923
+ }));
924
+ });
925
+ },
926
+ select: function (event, ui) {
927
+ $(_destinationWidgetName).val(ui.item.deviceName || '');
928
+ }
929
+ }).focus(function () {
930
+ $(this).autocomplete('search', `${$(this).val()}exactmatch`);
931
+ });
932
+ try {
933
+ if (hasKnxServerSelected()) {
934
+ const srv = RED.nodes.node(resolveKnxServerValue());
935
+ if (srv && srv.id) KNX_enableSecureFormatting($source, srv.id);
936
+ }
937
+ } catch (error) { /* empty */ }
938
+ }
939
+
838
940
  const refreshKnxBindings = () => {
941
+ getFixedDptGroupAddress("#node-input-universalLowBatteryGA", "#node-input-universalLowBatteryGAName", "1.");
942
+ getFixedDptGroupAddress("#node-input-universalLowBatteryTextGA", "#node-input-universalLowBatteryTextGAName", "16.");
943
+
839
944
  getDPT("1.", "#node-input-dptLightSwitch");
840
945
  getGroupAddress("#node-input-GALightSwitch", "#node-input-nameLightSwitch", "#node-input-dptLightSwitch", ["1."]);
841
946
 
@@ -991,7 +1096,7 @@
991
1096
  };
992
1097
 
993
1098
  const applyMatterCapabilities = (capabilities) => {
994
- currentMatterCapabilities = capabilities || null;
1099
+ currentMatterCapabilities = normalizeMatterCapabilities(capabilities) || null;
995
1100
  const caps = getEffectiveMatterCapabilities();
996
1101
  const hasHueDevice = getHueDeviceValue() !== '';
997
1102
  const supportsSwitch = hasHueDevice && caps.onOff !== false;
@@ -1001,14 +1106,18 @@
1001
1106
  const hasRichBehaviour = supportsDim && (supportsTemperature || supportsColor);
1002
1107
  const isDoorLock = caps.profile === 'doorLock';
1003
1108
  const isMapped = ['mapped', 'windowCovering', 'thermostat', 'fan', 'switch'].includes(caps.profile);
1109
+ const isUniversal = caps.profile === 'universal' || isUniversalMode();
1004
1110
  const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
1005
1111
 
1006
- setMatterTabVisible('tabs-1', supportsSwitch);
1007
- setMatterTabVisible('tabs-2', supportsDim);
1008
- setMatterTabVisible('tabs-3', supportsTemperature);
1009
- setMatterTabVisible('tabs-4', supportsColor);
1010
- setMatterTabVisible('tabs-mapped', isMapped);
1011
- setMatterTabVisible('tabs-6', hasHueDevice);
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);
1118
+ $('#matter-universal-description').toggle(isUniversal);
1119
+ $pinSelect.val(isUniversal ? 'yes' : $pinSelect.val()).prop('disabled', isUniversal);
1120
+ updateLocateButtonState(false);
1012
1121
  $('#matter-door-lock-options').toggle(isDoorLock);
1013
1122
  $('#matter-switch-command-label').text(isDoorLock
1014
1123
  ? (node._('knxUltimateMatterControllerDevice.door_lock_command') || 'Lock / unlock')
@@ -1079,7 +1188,7 @@
1079
1188
  const updateTabsVisibility = () => {
1080
1189
  const knxSelected = hasKnxServerSelected();
1081
1190
  const hueDeviceSelected = getHueDeviceValue() !== '';
1082
- const shouldShowTabs = knxSelected && hueDeviceSelected;
1191
+ const shouldShowTabs = !isUniversalMode() && knxSelected && hueDeviceSelected;
1083
1192
 
1084
1193
  if (shouldShowTabs) {
1085
1194
  $tabs.show();
@@ -1093,7 +1202,47 @@
1093
1202
  $pinSectionRow.show();
1094
1203
  }
1095
1204
  };
1205
+
1206
+ const refreshUniversalServiceVisibility = () => {
1207
+ const universal = $matterControllerModeInput.val() === 'universal';
1208
+ $('#matter-universal-options').toggle(universal);
1209
+ $universalTabs.toggle(universal && $universalServiceInput.val() === 'batteryMonitor');
1210
+ if ($universalTabs.is(':visible')) {
1211
+ try { $universalTabs.tabs('refresh'); } catch (error) { /* empty */ }
1212
+ }
1213
+ };
1214
+
1215
+ const applyControllerMode = () => {
1216
+ const universal = $matterControllerModeInput.val() === 'universal';
1217
+ $('#matter-device-section-title, #matter-device-row').toggle(!universal);
1218
+ if (universal) {
1219
+ const universalName = node._('knxUltimateMatterControllerDevice.universal_mode') || 'Universal Mode';
1220
+ currentMatterCapabilities = universalMatterCapabilities();
1221
+ selectedMatterDeviceName = universalName;
1222
+ $deviceNameInput.val(universalName);
1223
+ $hueDeviceInput.val(UNIVERSAL_NODE_ID);
1224
+ $matterEndpointInput.val('');
1225
+ $matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
1226
+ $nodeNameInput.val(node._('knxUltimateMatterControllerDevice.universal_controller_name') || 'Matter Universal Controller');
1227
+ $pinSelect.val('yes');
1228
+ } else if (String($hueDeviceInput.val() || '') === UNIVERSAL_NODE_ID) {
1229
+ currentMatterCapabilities = null;
1230
+ selectedMatterDeviceName = '';
1231
+ $deviceNameInput.val('');
1232
+ $hueDeviceInput.val('');
1233
+ $matterEndpointInput.val('');
1234
+ $matterCapabilitiesInput.val('');
1235
+ }
1236
+ refreshUniversalServiceVisibility();
1237
+ applyMatterCapabilities(currentMatterCapabilities);
1238
+ updateTabsVisibility();
1239
+ };
1240
+
1096
1241
  updateTabsVisibility();
1242
+ applyControllerMode();
1243
+
1244
+ $matterControllerModeInput.on('change.knxUltimateMatterControllerDevice', applyControllerMode);
1245
+ $universalServiceInput.on('change.knxUltimateMatterControllerDevice', refreshUniversalServiceVisibility);
1097
1246
 
1098
1247
  $knxServerInput.on('change.knxUltimateMatterControllerDevice', () => {
1099
1248
  refreshKnxBindings();
@@ -1417,9 +1566,12 @@
1417
1566
  } catch (error) { }
1418
1567
 
1419
1568
  //RED.sidebar.removeTab("tabNRColor");
1420
- if ($("#node-input-enableNodePINS").val() === "yes") {
1569
+ const universalMode = $("#node-input-matterControllerMode").val() === 'universal';
1570
+ this.matterControllerMode = universalMode ? 'universal' : 'single';
1571
+ if (universalMode || $("#node-input-enableNodePINS").val() === "yes") {
1421
1572
  this.outputs = 1;
1422
1573
  this.inputs = 1;
1574
+ this.enableNodePINS = 'yes';
1423
1575
  } else {
1424
1576
  this.outputs = 0;
1425
1577
  this.inputs = 0;
@@ -1428,7 +1580,7 @@
1428
1580
  this.updateLocalStateFromKNXWrite = $("#node-input-updateLocalStateFromKNXWrite").is(":checked"); // Starting from v 4.1.31
1429
1581
  this.matterDeviceName = ($("#node-input-matterDeviceName").val() || '').trim();
1430
1582
  this.matterNodeId = ($("#node-input-matterNodeId").val() || '').trim();
1431
- this.matterEndpointId = Number($("#node-input-matterEndpointId").val() || 1);
1583
+ this.matterEndpointId = universalMode ? '' : Number($("#node-input-matterEndpointId").val() || 1);
1432
1584
  this.matterDeviceCapabilities = ($("#node-input-matterDeviceCapabilities").val() || '').trim();
1433
1585
  const mappedMappings = [];
1434
1586
  const $mappedRows = $('#matter-mapped-profile-body .matter-mapped-row');
@@ -1444,7 +1596,9 @@
1444
1596
  ? JSON.stringify(mappedMappings)
1445
1597
  : ($('#node-input-matterMappings').val() || this.matterMappings || '[]');
1446
1598
  $('#node-input-matterMappings').val(this.matterMappings);
1447
- this.name = this.matterDeviceName || this.name || '';
1599
+ this.name = universalMode
1600
+ ? ($("#node-input-name").val() || this._('knxUltimateMatterControllerDevice.universal_controller_name') || 'Matter Universal Controller')
1601
+ : (this.matterDeviceName || this.name || '');
1448
1602
  $("#node-input-name").val(this.name);
1449
1603
  const parseSavedMatterCapabilities = (raw) => {
1450
1604
  if (typeof raw !== 'string' || raw.trim() === '') return null;
@@ -1631,10 +1785,6 @@
1631
1785
  target="_blank" href="https://youtu.be/jjEUI1J8bkA"><u><span data-i18n="common.youtube_sample"></span></u></a>
1632
1786
  <br />
1633
1787
  <br />
1634
- <p align="center">
1635
- <i class="fa-regular fa-lightbulb fa-bounce fa-2x"></i>
1636
- </p>
1637
- <br />
1638
1788
  <label for="node-input-server">
1639
1789
  <img
1640
1790
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAKnRFWHRDcmVhdGlvbiBUaW1lAEZyIDYgQXVnIDIwMTAgMjE6NTI6MTkgKzAxMDD84aS8AAAAB3RJTUUH3gYYCicNV+4WIQAAAAlwSFlzAAALEgAACxIB0t1+/AAAAARnQU1BAACxjwv8YQUAAACUSURBVHjaY2CgFZg5c+Z/ZEyWAZ8+f/6/ZsWs/xoamqMGkGrA6Wla/1+fVARjEBuGsSoGmY4eZSCNL59d/g8DIDbIAHR14OgFGQByKjIGKX5+6/T///8gGMQGiV1+/B0Fg70GIkD+RMYgxf/O5/7//2MSmAZhkBi6OrgB6Bg5DGB4ajr3f2xqsYYLSDE2THJUDg0AAAqyDVd4tp4YAAAAAElFTkSuQmCC"></img>
@@ -1651,12 +1801,116 @@
1651
1801
  <input type="text" id="node-input-serverMatter" />
1652
1802
  </div>
1653
1803
 
1804
+ <div class="form-row">
1805
+ <label for="node-input-matterControllerMode">
1806
+ <i class="fa fa-list"></i>
1807
+ <span data-i18n="knxUltimateMatterControllerDevice.controller_mode"></span>
1808
+ </label>
1809
+ <select id="node-input-matterControllerMode">
1810
+ <option value="single" data-i18n="knxUltimateMatterControllerDevice.single_mode"></option>
1811
+ <option value="universal" data-i18n="knxUltimateMatterControllerDevice.universal_mode"></option>
1812
+ </select>
1813
+ </div>
1814
+
1815
+ <div id="matter-universal-description" class="form-tips" style="display:none; margin:8px 0;"
1816
+ data-i18n="knxUltimateMatterControllerDevice.universal_mode_description"></div>
1817
+
1818
+ <div id="matter-universal-options" style="display:none;">
1819
+ <div class="form-row">
1820
+ <label for="node-input-universalService">
1821
+ <i class="fa fa-bell"></i>
1822
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_service"></span>
1823
+ </label>
1824
+ <select id="node-input-universalService">
1825
+ <option value="batteryMonitor" data-i18n="knxUltimateMatterControllerDevice.universal_service_battery_monitor"></option>
1826
+ <option value="allEvents" data-i18n="knxUltimateMatterControllerDevice.universal_service_all_events"></option>
1827
+ </select>
1828
+ </div>
1829
+ <div id="matter-universal-tabs" style="display:none; width:900px;">
1830
+ <ul>
1831
+ <li>
1832
+ <a href="#matter-universal-battery-options">
1833
+ <i class="fa fa-battery-quarter"></i>
1834
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_service_battery_monitor"></span>
1835
+ </a>
1836
+ </li>
1837
+ </ul>
1838
+ <div id="matter-universal-battery-options">
1839
+ <div class="form-row">
1840
+ <label for="node-input-universalBatteryThreshold">
1841
+ <i class="fa fa-battery-quarter"></i>
1842
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_battery_threshold"></span>
1843
+ </label>
1844
+ <input type="number" id="node-input-universalBatteryThreshold" min="0" max="100" step="1" style="width:100px;" />
1845
+ <span style="margin-left:5px;">%</span>
1846
+ </div>
1847
+ <div class="form-row" style="display:flex; align-items:center; flex-wrap:nowrap; gap:5px;">
1848
+ <label for="node-input-universalLowBatteryGA" style="width:155px; flex:0 0 155px; margin:0;">
1849
+ <i class="fa fa-bell"></i>
1850
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_low_battery_ga"></span>
1851
+ </label>
1852
+
1853
+ <label for="node-input-universalLowBatteryGA" style="width:25px; flex:0 0 25px; margin:0;"><span data-i18n="common.ga"></span></label>
1854
+ <input type="text" id="node-input-universalLowBatteryGA" placeholder="Ex: 1/1/1"
1855
+ style="width:70px; flex:0 0 70px; margin:0; text-align:left;" />
1856
+
1857
+ <label for="node-input-universalLowBatteryDPT" style="width:38px; flex:0 0 38px; margin:0; text-align:right;"><span data-i18n="common.dpt"></span></label>
1858
+ <input type="text" id="node-input-universalLowBatteryDPT" value="1.005" readonly
1859
+ style="width:75px; flex:0 0 75px; margin:0;" />
1860
+
1861
+ <label for="node-input-universalLowBatteryGAName" style="width:45px; flex:0 0 45px; margin:0; text-align:right;"><span data-i18n="common.name"></span></label>
1862
+ <input type="text" id="node-input-universalLowBatteryGAName"
1863
+ style="width:auto; min-width:90px; flex:1 1 auto; margin:0; text-align:left;" />
1864
+ </div>
1865
+ <div class="form-row" style="display:flex; align-items:center; flex-wrap:nowrap; gap:5px;">
1866
+ <label for="node-input-universalLowBatteryTextGA" style="width:155px; flex:0 0 155px; margin:0;">
1867
+ <i class="fa fa-font"></i>
1868
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_low_battery_text_ga"></span>
1869
+ </label>
1870
+
1871
+ <label for="node-input-universalLowBatteryTextGA" style="width:25px; flex:0 0 25px; margin:0;"><span data-i18n="common.ga"></span></label>
1872
+ <input type="text" id="node-input-universalLowBatteryTextGA" placeholder="Ex: 1/1/1"
1873
+ style="width:70px; flex:0 0 70px; margin:0; text-align:left;" />
1874
+
1875
+ <label for="node-input-universalLowBatteryTextDPT" style="width:38px; flex:0 0 38px; margin:0; text-align:right;"><span data-i18n="common.dpt"></span></label>
1876
+ <input type="text" id="node-input-universalLowBatteryTextDPT" value="16.001" readonly
1877
+ style="width:75px; flex:0 0 75px; margin:0;" />
1878
+
1879
+ <label for="node-input-universalLowBatteryTextGAName" style="width:45px; flex:0 0 45px; margin:0; text-align:right;"><span data-i18n="common.name"></span></label>
1880
+ <input type="text" id="node-input-universalLowBatteryTextGAName"
1881
+ style="width:auto; min-width:90px; flex:1 1 auto; margin:0; text-align:left;" />
1882
+ </div>
1883
+ <div class="form-row">
1884
+ <label for="node-input-universalBatteryOnlyLow" style="width:260px;">
1885
+ <i class="fa fa-filter"></i>
1886
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_battery_only_low"></span>
1887
+ </label>
1888
+ <input type="checkbox" id="node-input-universalBatteryOnlyLow" style="display:inline-block; width:auto;" />
1889
+ </div>
1890
+ <div class="form-row">
1891
+ <label for="node-input-universalEmitInitialState" style="width:260px;">
1892
+ <i class="fa fa-camera"></i>
1893
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_emit_initial"></span>
1894
+ </label>
1895
+ <input type="checkbox" id="node-input-universalEmitInitialState" style="display:inline-block; width:auto;" />
1896
+ </div>
1897
+ <div class="form-row">
1898
+ <label for="node-input-universalIncludeRawEvent" style="width:260px;">
1899
+ <i class="fa fa-code"></i>
1900
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_include_raw"></span>
1901
+ </label>
1902
+ <input type="checkbox" id="node-input-universalIncludeRawEvent" style="display:inline-block; width:auto;" />
1903
+ </div>
1904
+ </div>
1905
+ </div>
1906
+ </div>
1907
+
1654
1908
  <br />
1655
- <p>
1909
+ <p id="matter-device-section-title">
1656
1910
  <b><span data-i18n="knxUltimateMatterControllerDevice.matter_section"></span></b>
1657
1911
  </p>
1658
1912
 
1659
- <div class="form-row">
1913
+ <div class="form-row" id="matter-device-row">
1660
1914
  <label for="node-input-matterDeviceName">
1661
1915
  <i class="fa fa-play-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.matter_device"></span>
1662
1916
  </label>
@@ -2125,18 +2379,19 @@
2125
2379
  </div>
2126
2380
  <br/>
2127
2381
  </div>
2128
- <div class="form-row">
2129
- <label for="node-input-enableNodePINS" style="width:260px;">
2130
- <i class="fa fa-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.node_pins"></span>
2131
- </label>
2132
- <select id="node-input-enableNodePINS">
2133
- <option value="no" data-i18n="knxUltimateMatterControllerDevice.node_pins_hide"></option>
2134
- <option value="yes" data-i18n="knxUltimateMatterControllerDevice.node_pins_show"></option>
2135
- </select>
2136
- </div>
2137
2382
  </p>
2138
2383
  </div>
2139
2384
  </div>
2385
+
2386
+ <div class="form-row" style="margin-top:12px;">
2387
+ <label for="node-input-enableNodePINS" style="width:260px;">
2388
+ <i class="fa fa-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.node_pins"></span>
2389
+ </label>
2390
+ <select id="node-input-enableNodePINS">
2391
+ <option value="no" data-i18n="knxUltimateMatterControllerDevice.node_pins_hide"></option>
2392
+ <option value="yes" data-i18n="knxUltimateMatterControllerDevice.node_pins_show"></option>
2393
+ </select>
2394
+ </div>
2140
2395
  </div>
2141
2396
 
2142
2397
  <br/>
@@ -2159,6 +2414,28 @@ the device picker. Door Lock, Window Covering, Thermostat, Fan and Switch endpoi
2159
2414
  dedicated profiles. Switch press, long-press and multi-press events are emitted on the
2160
2415
  optional flow output; simpler endpoints continue through the generic mapped fallback.
2161
2416
 
2417
+ **Universal Mode**
2418
+
2419
+ Select **Universal Mode** in the **Mode** selector to listen to every attribute report,
2420
+ cluster event and device initialization from this controller. It always exposes one input
2421
+ and one output, does not require a KNX gateway, and does not use endpoint KNX mappings.
2422
+ Output values are in `msg.payload`; routing metadata is in `msg.matter`, including
2423
+ `source`, `nodeId`, `endpointId`, `clusterId` and the attribute/event name.
2424
+ The **Universal Battery Monitor** scans every node and endpoint for Power Source,
2425
+ emits an initial snapshot, and maintains complete battery state. It can emit only
2426
+ batteries below the configured threshold or every battery update. `msg.payload`
2427
+ contains normalized percent, charge level, replacement state, replaceability and
2428
+ voltage; raw Matter metadata remains in `msg.matter`. Send
2429
+ `{payload:{action:"getAllBatteries"}}` to retrieve the cached inventory.
2430
+
2431
+ Use `msg.nodeId`, `msg.endpointId`, `msg.clusterId` and either `msg.command` plus
2432
+ `msg.args`, or `msg.attribute`. Add `msg.value` for an attribute write; omit it for a
2433
+ read. The same selectors may be placed under `msg.matter`. For example, read battery
2434
+ with `{nodeId:"100", endpointId:1, clusterId:47, attribute:"batPercentRemaining"}` or
2435
+ switch on with `{nodeId:"100", endpointId:1, clusterId:6, command:"on", args:{}}`.
2436
+ Battery is emitted in Matter's raw half-percent units and can be divided by two in a
2437
+ Function node.
2438
+
2162
2439
  |Property|Description|
2163
2440
  |--|--|
2164
2441
  | KNX GW | Select the KNX gateway to be used |
@@ -1,6 +1,4 @@
1
1
  <script type="text/markdown" data-help-name="knxUltimateMatterBridge">
2
- # Expose KNX to Matter (BETA)
3
-
4
2
  > Dieser Node ist in **BETA**: Er funktioniert, aber Details können sich zwischen Releases noch ändern.
5
3
 
6
4
  ## Übersicht
@@ -1,6 +1,4 @@
1
1
  <script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
2
- # Control Matter from KNX (BETA)
3
-
4
2
  > Dieser Knoten ist **BETA**: das Verhalten kann sich ändern, während die Matter-Implementierung weiter verfeinert wird.
5
3
 
6
4
  Dieser Knoten steuert einen bereits gekoppelten Matter-Endpunkt über KNX. Wähle das Matter-Gerät aus; der Editor erkennt die Fähigkeiten des Endpunkts und zeigt nur die passenden KNX-Zuordnungen.
@@ -23,6 +21,16 @@ Er ersetzt die unveröffentlichten getrennten Matter-Controller-Nodes und behäl
23
21
  | Update local state from KNX write | Aktualisiert den lokalen Matter/KNX-Cache, wenn ein Telegramm auf eine konfigurierte KNX-GA geschrieben wird. |
24
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. |
25
23
 
24
+ ## Universeller Modus
25
+
26
+ Wähle **Universeller Modus**, um alle Geräte zu überwachen. Der Node hat immer einen Eingang und einen Ausgang und verwendet keine Endpunkt-Zuordnungen. Ein KNX-Gateway ist optional und wird nur für die Alarm-/Text-GAs des Batteriemonitors benötigt.
27
+
28
+ Der **Universelle Batteriemonitor** speichert den vollständigen Batteriestatus. Optionale KNX-GAs senden den Sammelalarm als DPT 1.005 und wechseln alle 2 Sekunden die Gerätenamen als DPT 16.001. `{payload:{action:"getAllBatteries"}}` liefert das Cache-Inventar.
29
+
30
+ Ausgaben enthalten den Rohwert in `msg.payload` und Routingdaten in `msg.matter` (`source`, `nodeId`, `endpointId`, `clusterId`, Attribut-/Ereignisname). Eingaben verwenden `msg.nodeId`, `msg.endpointId`, `msg.clusterId` und entweder `msg.command` mit `msg.args` oder `msg.attribute`; `msg.value` schreibt, ohne `msg.value` wird gelesen. Die Felder können auch unter `msg.matter` stehen.
31
+
32
+ Beispiele: `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` schaltet ein; `{nodeId:"100",endpointId:1,clusterId:47,attribute:"batPercentRemaining"}` liest die Batterie. Batteriewerte bleiben in Matter-Halbprozenteinheiten und können in einem Function-Node durch zwei geteilt werden.
33
+
26
34
  ## Verhalten
27
35
 
28
36
  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.
@@ -92,7 +92,23 @@
92
92
  "matter_controller": "Matter-Controller",
93
93
  "matter_device": "Matter-Gerät",
94
94
  "device_type": "Gerätetyp",
95
- "matter_section": "Matter-Gerät"
95
+ "matter_section": "Matter-Gerät",
96
+ "controller_mode": "Modus",
97
+ "single_mode": "Einzelnes Gerät",
98
+ "universal_mode": "Universeller Modus (alle Matter-Geräte)",
99
+ "universal_mode_description": "Alle mit diesem Controller verbundenen Matter-Geräte überwachen und steuern. Der universelle Modus funktioniert ohne KNX-Zuordnungen.",
100
+ "universal_service": "Dienst",
101
+ "universal_service_battery_monitor": "Universeller Batteriemonitor",
102
+ "universal_service_all_events": "Alle Matter-Ereignisse",
103
+ "universal_battery_threshold": "Schwelle für niedrigen Batteriestand",
104
+ "universal_low_battery_ga": "GA Batteriealarm",
105
+ "universal_low_battery_text_ga": "GA Gerätename",
106
+ "universal_battery_only_low": "Nur niedrige Batteriestände ausgeben",
107
+ "universal_emit_initial": "Anfänglichen Batterie-Snapshot ausgeben",
108
+ "universal_include_raw": "Rohes Matter-Ereignis einschließen",
109
+ "universal_mode_status": "Universeller Matter-Modus",
110
+ "universal_mode_locate_disabled": "„Suchen“ ist im universellen Modus nicht verfügbar.",
111
+ "universal_controller_name": "Universeller Matter-Controller"
96
112
  },
97
113
  "common": {
98
114
  "ga": "GA",
@@ -1,6 +1,4 @@
1
1
  <script type="text/markdown" data-help-name="knxUltimateMatterBridge">
2
- # Expose KNX to Matter (BETA)
3
-
4
2
  > This node is in **BETA**: it works, but details may still change between releases.
5
3
 
6
4
  ## Overview