node-red-contrib-knx-ultimate 6.0.10 → 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 (33) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/nodes/knxUltimateHueLight.html +1 -5
  3. package/nodes/knxUltimateMatterBridge.html +67 -2
  4. package/nodes/knxUltimateMatterControllerDevice.html +329 -44
  5. package/nodes/locales/de/knxUltimateMatterBridge.html +0 -2
  6. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +10 -2
  7. package/nodes/locales/de/knxUltimateMatterControllerDevice.json +17 -1
  8. package/nodes/locales/en/knxUltimateMatterBridge.html +0 -2
  9. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +10 -2
  10. package/nodes/locales/en/knxUltimateMatterControllerDevice.json +17 -1
  11. package/nodes/locales/es/knxUltimateMatterBridge.html +0 -2
  12. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +10 -2
  13. package/nodes/locales/es/knxUltimateMatterControllerDevice.json +17 -1
  14. package/nodes/locales/fr/knxUltimateMatterBridge.html +0 -2
  15. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +10 -2
  16. package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +17 -1
  17. package/nodes/locales/it/knxUltimateMatterBridge.html +0 -2
  18. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +10 -2
  19. package/nodes/locales/it/knxUltimateMatterControllerDevice.json +17 -1
  20. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +0 -2
  21. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +10 -2
  22. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +17 -1
  23. package/nodes/matter-config.js +5 -4
  24. package/nodes/utils/matterControllerProfiles/doorLock.js +19 -2
  25. package/nodes/utils/matterControllerProfiles/fan.js +20 -0
  26. package/nodes/utils/matterControllerProfiles/index.js +11 -1
  27. package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +53 -15
  28. package/nodes/utils/matterControllerProfiles/switch.js +14 -0
  29. package/nodes/utils/matterControllerProfiles/thermostat.js +20 -0
  30. package/nodes/utils/matterControllerProfiles/universal.js +462 -0
  31. package/nodes/utils/matterControllerProfiles/windowCovering.js +20 -0
  32. package/nodes/utils/matterEngine.mjs +12 -3
  33. package/package.json +3 -3
@@ -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;
@@ -554,6 +576,11 @@
554
576
  }
555
577
  const isMatterLight = deviceTypes.some((type) => /(?:onoff|dimmable|colortemperature|extendedcolor)light/.test(type));
556
578
  if (!isMatterLight) {
579
+ const specializedProfile = getMatterCluster(ep, 258) ? 'windowCovering'
580
+ : getMatterCluster(ep, 513) ? 'thermostat'
581
+ : getMatterCluster(ep, 514) ? 'fan'
582
+ : getMatterCluster(ep, 59) ? 'switch'
583
+ : 'mapped';
557
584
  const mappedTargets = [];
558
585
  const addCommand = (clusterId, target, label, dpt) => {
559
586
  const cluster = getMatterCluster(ep, clusterId);
@@ -584,47 +611,79 @@
584
611
  addAttribute(47, 'batPercentRemaining', 'battery', '5.001', false);
585
612
  addAttribute(144, 'activePower', 'active_power', '14.056', false);
586
613
  addAttribute(145, 'cumulativeEnergyImported', 'imported_energy', '13.013', false);
587
- return { known: true, profile: 'mapped', deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
614
+ return { known: true, profile: specializedProfile, deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
588
615
  }
589
616
  const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
590
617
  const hasColorType = /extendedcolor/.test(typeText);
618
+ const isTunableWhiteOnly = hasColorTemperatureType && !hasColorType;
591
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);
592
623
  const hasColorTemperature = !!colorCluster && (
593
- hasColorTemperatureType ||
594
- hasMatterAttribute(colorCluster, ['colorTemperatureMireds', 'colorTempPhysicalMinMireds', 'colorTempPhysicalMaxMireds']) ||
595
- hasMatterCommand(colorCluster, ['moveToColorTemperature'])
624
+ hasColorFeatureMap
625
+ ? (colorFeatureMap & 0x10) !== 0
626
+ : (
627
+ hasColorTemperatureType ||
628
+ hasMatterAttribute(colorCluster, ['colorTemperatureMireds', 'colorTempPhysicalMinMireds', 'colorTempPhysicalMaxMireds']) ||
629
+ hasMatterCommand(colorCluster, ['moveToColorTemperature'])
630
+ )
596
631
  );
597
632
  const hasColor = !!colorCluster && (
598
- hasColorType ||
599
- 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
+ )
600
641
  );
601
642
  return {
602
643
  known: true,
603
644
  deviceTypeDisplay,
604
- colorDetectionVersion: 2,
645
+ matterLightType: isTunableWhiteOnly ? 'colorTemperature' : (hasColorType ? 'extendedColor' : 'other'),
646
+ colorDetectionVersion: 3,
605
647
  onOff: !!onOffCluster || /onoff|dimmable|colortemperature|extendedcolor/.test(typeText),
606
648
  level: !!levelCluster || hasLevelType,
607
649
  colorTemperature: hasColorTemperature,
608
650
  color: hasColor
609
651
  };
610
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
+ };
611
670
  const parseMatterCapabilities = (raw) => {
612
671
  if (raw && typeof raw === 'object') {
613
- if (raw.known === true && raw.color === true && raw.colorDetectionVersion !== 2) raw.color = false;
614
- return raw;
672
+ return normalizeMatterCapabilities(raw);
615
673
  }
616
674
  if (typeof raw !== 'string' || raw.trim() === '') return null;
617
675
  try {
618
- const parsed = JSON.parse(raw);
619
- if (parsed && parsed.known === true && parsed.color === true && parsed.colorDetectionVersion !== 2) {
620
- parsed.color = false;
621
- }
622
- return parsed;
676
+ return normalizeMatterCapabilities(JSON.parse(raw));
623
677
  } catch (error) {
624
678
  return null;
625
679
  }
626
680
  };
627
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
+ });
628
687
  const getJSONPromise = (url) => new Promise((resolve, reject) => { $.getJSON(url, resolve).fail(reject); });
629
688
 
630
689
  // Filters Matter items shaped as { value, matterNodeId, matterEndpointId }.
@@ -737,13 +796,22 @@
737
796
 
738
797
 
739
798
  const $tabs = $("#tabs");
799
+ const $universalTabs = $("#matter-universal-tabs");
740
800
  const $pinSectionRow = $("#node-input-enableNodePINS").closest('.form-row');
741
801
  const $pinSelect = $("#node-input-enableNodePINS");
742
802
  let currentMatterCapabilities = parseMatterCapabilities($matterCapabilitiesInput.val() || node.matterDeviceCapabilities);
803
+ if (currentMatterCapabilities) {
804
+ $matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
805
+ node.matterDeviceCapabilities = $matterCapabilitiesInput.val();
806
+ }
743
807
  $tabs.addClass('hue-vertical-tabs');
744
808
  $tabs.tabs(); // Tabs gestione KNX
745
809
  $tabs.find('ul').addClass('ui-tabs-nav');
746
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');
747
815
 
748
816
  function getDPT(_dpt, _destinationWidget) {
749
817
  // DPT Switch command
@@ -830,7 +898,49 @@
830
898
  } catch (e) { }
831
899
  }
832
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
+
833
940
  const refreshKnxBindings = () => {
941
+ getFixedDptGroupAddress("#node-input-universalLowBatteryGA", "#node-input-universalLowBatteryGAName", "1.");
942
+ getFixedDptGroupAddress("#node-input-universalLowBatteryTextGA", "#node-input-universalLowBatteryTextGAName", "16.");
943
+
834
944
  getDPT("1.", "#node-input-dptLightSwitch");
835
945
  getGroupAddress("#node-input-GALightSwitch", "#node-input-nameLightSwitch", "#node-input-dptLightSwitch", ["1."]);
836
946
 
@@ -986,7 +1096,7 @@
986
1096
  };
987
1097
 
988
1098
  const applyMatterCapabilities = (capabilities) => {
989
- currentMatterCapabilities = capabilities || null;
1099
+ currentMatterCapabilities = normalizeMatterCapabilities(capabilities) || null;
990
1100
  const caps = getEffectiveMatterCapabilities();
991
1101
  const hasHueDevice = getHueDeviceValue() !== '';
992
1102
  const supportsSwitch = hasHueDevice && caps.onOff !== false;
@@ -995,15 +1105,19 @@
995
1105
  const supportsColor = hasHueDevice && caps.color === true;
996
1106
  const hasRichBehaviour = supportsDim && (supportsTemperature || supportsColor);
997
1107
  const isDoorLock = caps.profile === 'doorLock';
998
- const isMapped = caps.profile === 'mapped';
1108
+ const isMapped = ['mapped', 'windowCovering', 'thermostat', 'fan', 'switch'].includes(caps.profile);
1109
+ const isUniversal = caps.profile === 'universal' || isUniversalMode();
999
1110
  const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
1000
1111
 
1001
- setMatterTabVisible('tabs-1', supportsSwitch);
1002
- setMatterTabVisible('tabs-2', supportsDim);
1003
- setMatterTabVisible('tabs-3', supportsTemperature);
1004
- setMatterTabVisible('tabs-4', supportsColor);
1005
- setMatterTabVisible('tabs-mapped', isMapped);
1006
- 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);
1007
1121
  $('#matter-door-lock-options').toggle(isDoorLock);
1008
1122
  $('#matter-switch-command-label').text(isDoorLock
1009
1123
  ? (node._('knxUltimateMatterControllerDevice.door_lock_command') || 'Lock / unlock')
@@ -1074,7 +1188,7 @@
1074
1188
  const updateTabsVisibility = () => {
1075
1189
  const knxSelected = hasKnxServerSelected();
1076
1190
  const hueDeviceSelected = getHueDeviceValue() !== '';
1077
- const shouldShowTabs = knxSelected && hueDeviceSelected;
1191
+ const shouldShowTabs = !isUniversalMode() && knxSelected && hueDeviceSelected;
1078
1192
 
1079
1193
  if (shouldShowTabs) {
1080
1194
  $tabs.show();
@@ -1088,7 +1202,47 @@
1088
1202
  $pinSectionRow.show();
1089
1203
  }
1090
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
+
1091
1241
  updateTabsVisibility();
1242
+ applyControllerMode();
1243
+
1244
+ $matterControllerModeInput.on('change.knxUltimateMatterControllerDevice', applyControllerMode);
1245
+ $universalServiceInput.on('change.knxUltimateMatterControllerDevice', refreshUniversalServiceVisibility);
1092
1246
 
1093
1247
  $knxServerInput.on('change.knxUltimateMatterControllerDevice', () => {
1094
1248
  refreshKnxBindings();
@@ -1412,9 +1566,12 @@
1412
1566
  } catch (error) { }
1413
1567
 
1414
1568
  //RED.sidebar.removeTab("tabNRColor");
1415
- 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") {
1416
1572
  this.outputs = 1;
1417
1573
  this.inputs = 1;
1574
+ this.enableNodePINS = 'yes';
1418
1575
  } else {
1419
1576
  this.outputs = 0;
1420
1577
  this.inputs = 0;
@@ -1423,7 +1580,7 @@
1423
1580
  this.updateLocalStateFromKNXWrite = $("#node-input-updateLocalStateFromKNXWrite").is(":checked"); // Starting from v 4.1.31
1424
1581
  this.matterDeviceName = ($("#node-input-matterDeviceName").val() || '').trim();
1425
1582
  this.matterNodeId = ($("#node-input-matterNodeId").val() || '').trim();
1426
- this.matterEndpointId = Number($("#node-input-matterEndpointId").val() || 1);
1583
+ this.matterEndpointId = universalMode ? '' : Number($("#node-input-matterEndpointId").val() || 1);
1427
1584
  this.matterDeviceCapabilities = ($("#node-input-matterDeviceCapabilities").val() || '').trim();
1428
1585
  const mappedMappings = [];
1429
1586
  const $mappedRows = $('#matter-mapped-profile-body .matter-mapped-row');
@@ -1439,7 +1596,9 @@
1439
1596
  ? JSON.stringify(mappedMappings)
1440
1597
  : ($('#node-input-matterMappings').val() || this.matterMappings || '[]');
1441
1598
  $('#node-input-matterMappings').val(this.matterMappings);
1442
- 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 || '');
1443
1602
  $("#node-input-name").val(this.name);
1444
1603
  const parseSavedMatterCapabilities = (raw) => {
1445
1604
  if (typeof raw !== 'string' || raw.trim() === '') return null;
@@ -1626,10 +1785,6 @@
1626
1785
  target="_blank" href="https://youtu.be/jjEUI1J8bkA"><u><span data-i18n="common.youtube_sample"></span></u></a>
1627
1786
  <br />
1628
1787
  <br />
1629
- <p align="center">
1630
- <i class="fa-regular fa-lightbulb fa-bounce fa-2x"></i>
1631
- </p>
1632
- <br />
1633
1788
  <label for="node-input-server">
1634
1789
  <img
1635
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>
@@ -1646,12 +1801,116 @@
1646
1801
  <input type="text" id="node-input-serverMatter" />
1647
1802
  </div>
1648
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
+
1649
1908
  <br />
1650
- <p>
1909
+ <p id="matter-device-section-title">
1651
1910
  <b><span data-i18n="knxUltimateMatterControllerDevice.matter_section"></span></b>
1652
1911
  </p>
1653
1912
 
1654
- <div class="form-row">
1913
+ <div class="form-row" id="matter-device-row">
1655
1914
  <label for="node-input-matterDeviceName">
1656
1915
  <i class="fa fa-play-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.matter_device"></span>
1657
1916
  </label>
@@ -2120,18 +2379,19 @@
2120
2379
  </div>
2121
2380
  <br/>
2122
2381
  </div>
2123
- <div class="form-row">
2124
- <label for="node-input-enableNodePINS" style="width:260px;">
2125
- <i class="fa fa-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.node_pins"></span>
2126
- </label>
2127
- <select id="node-input-enableNodePINS">
2128
- <option value="no" data-i18n="knxUltimateMatterControllerDevice.node_pins_hide"></option>
2129
- <option value="yes" data-i18n="knxUltimateMatterControllerDevice.node_pins_show"></option>
2130
- </select>
2131
- </div>
2132
2382
  </p>
2133
2383
  </div>
2134
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>
2135
2395
  </div>
2136
2396
 
2137
2397
  <br/>
@@ -2150,7 +2410,31 @@
2150
2410
 
2151
2411
  The editor detects the selected endpoint capabilities and exposes only the corresponding
2152
2412
  Matter functions. Bridged Matter devices are expanded into their individual endpoints in
2153
- the device picker.
2413
+ the device picker. Door Lock, Window Covering, Thermostat, Fan and Switch endpoints use
2414
+ dedicated profiles. Switch press, long-press and multi-press events are emitted on the
2415
+ optional flow output; simpler endpoints continue through the generic mapped fallback.
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.
2154
2438
 
2155
2439
  |Property|Description|
2156
2440
  |--|--|
@@ -2235,6 +2519,7 @@ _Basic Matter effects_
2235
2519
  | Read status at startup | Read the Matter endpoint status at Node-RED startup or full deploy and send it to the KNX bus. |
2236
2520
  | 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. |
2237
2521
  | 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. |
2238
2523
  | 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. |
2239
2524
  | 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.|
2240
2525
  | Day/Night | Select the group address used to set the day/night behaviour. The group address value is _true_ if daytime, _false_ if nighttime. |
@@ -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