node-red-contrib-knx-ultimate 6.0.11 → 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 (51) hide show
  1. package/CHANGELOG.md +11 -3
  2. package/nodes/knxUltimateHueLight.html +1 -5
  3. package/nodes/knxUltimateMatterControllerDevice.html +584 -47
  4. package/nodes/knxUltimateMatterControllerDevice.js +37 -0
  5. package/nodes/locales/de/knxUltimateMatterBridge.html +0 -2
  6. package/nodes/locales/de/knxUltimateMatterControllerDevice.html +23 -3
  7. package/nodes/locales/de/knxUltimateMatterControllerDevice.json +39 -1
  8. package/nodes/locales/de/matter-config.html +11 -1
  9. package/nodes/locales/de/matter-config.json +12 -0
  10. package/nodes/locales/en/knxUltimateMatterBridge.html +0 -2
  11. package/nodes/locales/en/knxUltimateMatterControllerDevice.html +23 -3
  12. package/nodes/locales/en/knxUltimateMatterControllerDevice.json +39 -1
  13. package/nodes/locales/en/matter-config.html +11 -1
  14. package/nodes/locales/en/matter-config.json +12 -0
  15. package/nodes/locales/es/knxUltimateMatterBridge.html +0 -2
  16. package/nodes/locales/es/knxUltimateMatterControllerDevice.html +23 -3
  17. package/nodes/locales/es/knxUltimateMatterControllerDevice.json +39 -1
  18. package/nodes/locales/es/matter-config.html +11 -1
  19. package/nodes/locales/es/matter-config.json +12 -0
  20. package/nodes/locales/fr/knxUltimateMatterBridge.html +0 -2
  21. package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +23 -3
  22. package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +47 -1
  23. package/nodes/locales/fr/matter-config.html +11 -1
  24. package/nodes/locales/fr/matter-config.json +12 -0
  25. package/nodes/locales/it/knxUltimateMatterBridge.html +0 -2
  26. package/nodes/locales/it/knxUltimateMatterControllerDevice.html +23 -3
  27. package/nodes/locales/it/knxUltimateMatterControllerDevice.json +39 -1
  28. package/nodes/locales/it/matter-config.html +11 -1
  29. package/nodes/locales/it/matter-config.json +12 -0
  30. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +0 -2
  31. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +23 -3
  32. package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +47 -1
  33. package/nodes/locales/zh-CN/matter-config.html +11 -1
  34. package/nodes/locales/zh-CN/matter-config.json +12 -0
  35. package/nodes/matter-config.html +215 -23
  36. package/nodes/matter-config.js +53 -6
  37. package/nodes/utils/matterControllerCommandGate.js +47 -0
  38. package/nodes/utils/matterControllerProfiles/doorLock.js +19 -4
  39. package/nodes/utils/matterControllerProfiles/fan.js +0 -1
  40. package/nodes/utils/matterControllerProfiles/index.js +3 -1
  41. package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +32 -15
  42. package/nodes/utils/matterControllerProfiles/switch.js +0 -1
  43. package/nodes/utils/matterControllerProfiles/thermostat.js +0 -1
  44. package/nodes/utils/matterControllerProfiles/universal.js +462 -0
  45. package/nodes/utils/matterControllerProfiles/windowCovering.js +0 -1
  46. package/nodes/utils/matterControllerSemanticInput.js +212 -0
  47. package/nodes/utils/matterEngine.mjs +120 -17
  48. package/package.json +1 -1
  49. package/resources/matterQrScanner.js +98 -0
  50. package/resources/zxing-browser-0.2.0.min.js +1 -0
  51. package/resources/zxing-browser-LICENSE.txt +21 -0
@@ -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: "" },
@@ -142,6 +152,14 @@
142
152
 
143
153
 
144
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
+ }
145
163
  $("#node-input-updateLocalStateFromKNXWrite").prop("checked", node.updateLocalStateFromKNXWrite === true || node.updateLocalStateFromKNXWrite === "true"); // Starting from v 4.1.31
146
164
  const ensureConfigSelection = (selector) => {
147
165
  if ($(selector).val() !== "_ADD_") return;
@@ -211,6 +229,31 @@
211
229
  border-top: 1px solid #ccc;
212
230
  margin: 8px 0;
213
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
+ }
214
257
  </style>`;
215
258
  $('head').append(style);
216
259
  }
@@ -221,6 +264,8 @@
221
264
  const KNX_EMPTY_VALUES = new Set(['', 'none', '_ADD_', '__NONE__']);
222
265
  // Historical variable names are retained for saved-flow compatibility; they point to Matter widgets.
223
266
  const $hueServerInput = $("#node-input-serverMatter");
267
+ const $matterControllerModeInput = $("#node-input-matterControllerMode");
268
+ const $universalServiceInput = $("#node-input-universalService");
224
269
  const $hueDeviceInput = $("#node-input-matterNodeId");
225
270
  const $deviceNameInput = $("#node-input-matterDeviceName");
226
271
  const $matterEndpointInput = $("#node-input-matterEndpointId");
@@ -229,6 +274,10 @@
229
274
  const $refreshDevicesButton = $(".hue-refresh-devices");
230
275
  const $locateDeviceButton = $(".hue-locate-device");
231
276
  const $hueDevicesLoading = $(".hue-devices-loading");
277
+ const $inputHelpPanel = $("#tabs-input");
278
+ const UNIVERSAL_NODE_ID = '__UNIVERSAL__';
279
+ const isUniversalMode = () => $matterControllerModeInput.val() === 'universal';
280
+ $matterControllerModeInput.val(node.matterControllerMode || 'single');
232
281
  let cachedHueDevices = Array.isArray(node._cachedHueLightDevices) ? node._cachedHueLightDevices : [];
233
282
  node._cachedHueLightDevices = cachedHueDevices;
234
283
  const defaultHueDevicePlaceholder = $deviceNameInput.attr('placeholder') || '';
@@ -289,7 +338,10 @@
289
338
  const title = locateSessionActive
290
339
  ? (node._('knxUltimateMatterControllerDevice.locate_stop_title') || 'Stop Matter locate')
291
340
  : (node._('knxUltimateMatterControllerDevice.locate_start_title') || 'Locate selected Matter device');
292
- $locateDeviceButton.attr('title', title);
341
+ $locateDeviceButton.attr('title', isUniversalMode()
342
+ ? (node._('knxUltimateMatterControllerDevice.universal_mode_locate_disabled') || 'Locate is unavailable in Universal Mode.')
343
+ : title);
344
+ $locateDeviceButton.prop('disabled', isUniversalMode());
293
345
  };
294
346
 
295
347
  const removeNodeRemovalListener = () => {
@@ -340,6 +392,10 @@
340
392
  };
341
393
 
342
394
  const performLocateRequest = ({ silent = false, allowStoredContext = false, action } = {}) => {
395
+ if (isUniversalMode()) {
396
+ if (!silent) RED.notify(node._('knxUltimateMatterControllerDevice.universal_mode_locate_disabled') || 'Locate is unavailable in Universal Mode.', 'warning');
397
+ return null;
398
+ }
343
399
  const context = buildLocateContext({ allowStored: allowStoredContext });
344
400
  if (!context) {
345
401
  if (!silent) {
@@ -419,7 +475,7 @@
419
475
  }).always(() => {
420
476
  locatePendingRequest = null;
421
477
  if ($locateDeviceButton.length) {
422
- $locateDeviceButton.prop('disabled', false);
478
+ $locateDeviceButton.prop('disabled', isUniversalMode());
423
479
  }
424
480
  });
425
481
  return request;
@@ -527,6 +583,19 @@
527
583
  return !!cluster && (cluster.commands || []).some((cmd) => wanted.has(normalizeMatterToken(cmd.name)));
528
584
  };
529
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
+ };
530
599
  const deviceTypes = (ep.deviceTypes || []).map(normalizeMatterToken);
531
600
  const primaryDeviceType = (ep.deviceTypes || []).find((value) => !/bridgednode|aggregator|controlbridge/i.test(String(value))) || ep.name;
532
601
  const deviceTypeDisplay = matterPrettyType(primaryDeviceType);
@@ -546,6 +615,7 @@
546
615
  color: false,
547
616
  lockDoor: hasMatterCommand(doorLockCluster, ['lockDoor']),
548
617
  unlockDoor: hasMatterCommand(doorLockCluster, ['unlockDoor']),
618
+ inputStructure,
549
619
  requirePinForRemoteOperation: (() => {
550
620
  const attribute = (doorLockCluster.attributes || []).find((item) => normalizeMatterToken(item.name) === 'requirepinforremoteoperation');
551
621
  return attribute ? attribute.value === true : false;
@@ -589,47 +659,79 @@
589
659
  addAttribute(47, 'batPercentRemaining', 'battery', '5.001', false);
590
660
  addAttribute(144, 'activePower', 'active_power', '14.056', false);
591
661
  addAttribute(145, 'cumulativeEnergyImported', 'imported_energy', '13.013', false);
592
- 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 };
593
663
  }
594
664
  const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
595
665
  const hasColorType = /extendedcolor/.test(typeText);
666
+ const isTunableWhiteOnly = hasColorTemperatureType && !hasColorType;
596
667
  const hasLevelType = /dimmable|colortemperature|extendedcolor/.test(typeText);
668
+ const colorFeatureMapAttribute = (colorCluster?.attributes || []).find((attribute) => normalizeMatterToken(attribute.name) === 'featuremap');
669
+ const colorFeatureMap = Number(colorFeatureMapAttribute?.value);
670
+ const hasColorFeatureMap = Number.isFinite(colorFeatureMap);
597
671
  const hasColorTemperature = !!colorCluster && (
598
- hasColorTemperatureType ||
599
- hasMatterAttribute(colorCluster, ['colorTemperatureMireds', 'colorTempPhysicalMinMireds', 'colorTempPhysicalMaxMireds']) ||
600
- hasMatterCommand(colorCluster, ['moveToColorTemperature'])
672
+ hasColorFeatureMap
673
+ ? (colorFeatureMap & 0x10) !== 0
674
+ : (
675
+ hasColorTemperatureType ||
676
+ hasMatterAttribute(colorCluster, ['colorTemperatureMireds', 'colorTempPhysicalMinMireds', 'colorTempPhysicalMaxMireds']) ||
677
+ hasMatterCommand(colorCluster, ['moveToColorTemperature'])
678
+ )
601
679
  );
602
680
  const hasColor = !!colorCluster && (
603
- hasColorType ||
604
- hasMatterCommand(colorCluster, ['moveToColor', 'moveToHue', 'moveToSaturation', 'moveToHueAndSaturation'])
681
+ !isTunableWhiteOnly && (
682
+ hasColorFeatureMap
683
+ ? (colorFeatureMap & 0x09) !== 0
684
+ : (
685
+ hasColorType ||
686
+ hasMatterCommand(colorCluster, ['moveToColor', 'moveToHue', 'moveToSaturation', 'moveToHueAndSaturation'])
687
+ )
688
+ )
605
689
  );
606
690
  return {
607
691
  known: true,
608
692
  deviceTypeDisplay,
609
- colorDetectionVersion: 2,
693
+ matterLightType: isTunableWhiteOnly ? 'colorTemperature' : (hasColorType ? 'extendedColor' : 'other'),
694
+ colorDetectionVersion: 3,
610
695
  onOff: !!onOffCluster || /onoff|dimmable|colortemperature|extendedcolor/.test(typeText),
611
696
  level: !!levelCluster || hasLevelType,
612
697
  colorTemperature: hasColorTemperature,
613
698
  color: hasColor
614
699
  };
615
700
  };
701
+ const normalizeMatterCapabilities = (capabilities) => {
702
+ if (!capabilities || typeof capabilities !== 'object') return capabilities;
703
+ const normalized = Object.assign({}, capabilities);
704
+ const displayType = normalizeMatterToken(normalized.deviceTypeDisplay);
705
+ const tunableWhiteOnly = normalized.matterLightType === 'colorTemperature' ||
706
+ displayType === 'tunablewhite' ||
707
+ displayType === 'colortemperaturelight';
708
+ if (tunableWhiteOnly) {
709
+ normalized.colorTemperature = true;
710
+ normalized.color = false;
711
+ normalized.colorDetectionVersion = 3;
712
+ } else if (normalized.known === true && normalized.color === true &&
713
+ normalized.colorDetectionVersion !== 2 && normalized.colorDetectionVersion !== 3) {
714
+ normalized.color = false;
715
+ }
716
+ return normalized;
717
+ };
616
718
  const parseMatterCapabilities = (raw) => {
617
719
  if (raw && typeof raw === 'object') {
618
- if (raw.known === true && raw.color === true && raw.colorDetectionVersion !== 2) raw.color = false;
619
- return raw;
720
+ return normalizeMatterCapabilities(raw);
620
721
  }
621
722
  if (typeof raw !== 'string' || raw.trim() === '') return null;
622
723
  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;
724
+ return normalizeMatterCapabilities(JSON.parse(raw));
628
725
  } catch (error) {
629
726
  return null;
630
727
  }
631
728
  };
632
729
  const serializeMatterCapabilities = (capabilities) => JSON.stringify(capabilities || {});
730
+ const universalMatterCapabilities = () => ({
731
+ known: true,
732
+ profile: 'universal',
733
+ deviceTypeDisplay: node._('knxUltimateMatterControllerDevice.universal_mode') || 'Universal Mode'
734
+ });
633
735
  const getJSONPromise = (url) => new Promise((resolve, reject) => { $.getJSON(url, resolve).fail(reject); });
634
736
 
635
737
  // Filters Matter items shaped as { value, matterNodeId, matterEndpointId }.
@@ -742,13 +844,158 @@
742
844
 
743
845
 
744
846
  const $tabs = $("#tabs");
847
+ const $universalTabs = $("#matter-universal-tabs");
745
848
  const $pinSectionRow = $("#node-input-enableNodePINS").closest('.form-row');
746
849
  const $pinSelect = $("#node-input-enableNodePINS");
747
850
  let currentMatterCapabilities = parseMatterCapabilities($matterCapabilitiesInput.val() || node.matterDeviceCapabilities);
851
+ if (currentMatterCapabilities) {
852
+ $matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
853
+ node.matterDeviceCapabilities = $matterCapabilitiesInput.val();
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
+ });
748
986
  $tabs.addClass('hue-vertical-tabs');
749
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
+ });
750
993
  $tabs.find('ul').addClass('ui-tabs-nav');
751
994
  $tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
995
+ $universalTabs.addClass('hue-vertical-tabs');
996
+ $universalTabs.tabs();
997
+ $universalTabs.find('ul').addClass('ui-tabs-nav');
998
+ $universalTabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
752
999
 
753
1000
  function getDPT(_dpt, _destinationWidget) {
754
1001
  // DPT Switch command
@@ -835,7 +1082,49 @@
835
1082
  } catch (e) { }
836
1083
  }
837
1084
 
1085
+ function getFixedDptGroupAddress(_sourceWidgetAutocomplete, _destinationWidgetName, _dptPrefix) {
1086
+ const $source = $(_sourceWidgetAutocomplete);
1087
+ if ($source.data('ui-autocomplete')) {
1088
+ try { $source.autocomplete('destroy'); } catch (error) { /* empty */ }
1089
+ }
1090
+ $source.autocomplete({
1091
+ minLength: 0,
1092
+ source: function (request, response) {
1093
+ if (!hasKnxServerSelected()) {
1094
+ response([]);
1095
+ return;
1096
+ }
1097
+ const serverId = resolveKnxServerValue();
1098
+ $.getJSON(`knxUltimatecsv?nodeID=${serverId}&_=${Date.now()}`, (data) => {
1099
+ response($.map(data, function (value) {
1100
+ const searchable = `${value.ga} (${value.devicename}) DPT${value.dpt}`;
1101
+ if (!htmlUtilsfullCSVSearch(searchable, `${request.term || ''} ${_dptPrefix}`)) return null;
1102
+ return {
1103
+ label: `${value.ga} # ${value.devicename} # ${value.dpt}`,
1104
+ value: value.ga,
1105
+ deviceName: value.devicename
1106
+ };
1107
+ }));
1108
+ });
1109
+ },
1110
+ select: function (event, ui) {
1111
+ $(_destinationWidgetName).val(ui.item.deviceName || '');
1112
+ }
1113
+ }).focus(function () {
1114
+ $(this).autocomplete('search', `${$(this).val()}exactmatch`);
1115
+ });
1116
+ try {
1117
+ if (hasKnxServerSelected()) {
1118
+ const srv = RED.nodes.node(resolveKnxServerValue());
1119
+ if (srv && srv.id) KNX_enableSecureFormatting($source, srv.id);
1120
+ }
1121
+ } catch (error) { /* empty */ }
1122
+ }
1123
+
838
1124
  const refreshKnxBindings = () => {
1125
+ getFixedDptGroupAddress("#node-input-universalLowBatteryGA", "#node-input-universalLowBatteryGAName", "1.");
1126
+ getFixedDptGroupAddress("#node-input-universalLowBatteryTextGA", "#node-input-universalLowBatteryTextGAName", "16.");
1127
+
839
1128
  getDPT("1.", "#node-input-dptLightSwitch");
840
1129
  getGroupAddress("#node-input-GALightSwitch", "#node-input-nameLightSwitch", "#node-input-dptLightSwitch", ["1."]);
841
1130
 
@@ -939,8 +1228,9 @@
939
1228
 
940
1229
  const ensureVisibleMatterTabSelected = () => {
941
1230
  const $active = $tabs.find('ul.ui-tabs-nav li.ui-tabs-active');
942
- if ($active.length && $active.is(':visible')) return;
943
- 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();
944
1234
  if (!$firstVisible.length) return;
945
1235
  const visibleIndex = $tabs.find('ul.ui-tabs-nav li').index($firstVisible);
946
1236
  if (visibleIndex >= 0) {
@@ -991,7 +1281,7 @@
991
1281
  };
992
1282
 
993
1283
  const applyMatterCapabilities = (capabilities) => {
994
- currentMatterCapabilities = capabilities || null;
1284
+ currentMatterCapabilities = normalizeMatterCapabilities(capabilities) || null;
995
1285
  const caps = getEffectiveMatterCapabilities();
996
1286
  const hasHueDevice = getHueDeviceValue() !== '';
997
1287
  const supportsSwitch = hasHueDevice && caps.onOff !== false;
@@ -1001,14 +1291,20 @@
1001
1291
  const hasRichBehaviour = supportsDim && (supportsTemperature || supportsColor);
1002
1292
  const isDoorLock = caps.profile === 'doorLock';
1003
1293
  const isMapped = ['mapped', 'windowCovering', 'thermostat', 'fan', 'switch'].includes(caps.profile);
1294
+ const isUniversal = caps.profile === 'universal' || isUniversalMode();
1295
+ const knxSelected = hasKnxServerSelected();
1004
1296
  const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
1005
1297
 
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);
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);
1305
+ $('#matter-universal-description').toggle(isUniversal);
1306
+ $pinSelect.val(isUniversal ? 'yes' : $pinSelect.val()).prop('disabled', isUniversal);
1307
+ updateLocateButtonState(false);
1012
1308
  $('#matter-door-lock-options').toggle(isDoorLock);
1013
1309
  $('#matter-switch-command-label').text(isDoorLock
1014
1310
  ? (node._('knxUltimateMatterControllerDevice.door_lock_command') || 'Lock / unlock')
@@ -1020,6 +1316,7 @@
1020
1316
  $('#matter-door-lock-pin-required').toggle(isDoorLock && caps.requirePinForRemoteOperation === true);
1021
1317
  $('#matter-mapped-profile').toggle(isMapped);
1022
1318
  if (isMapped) renderMappedProfile(caps.mappedTargets || []);
1319
+ if (hasHueDevice && !isUniversal && (isMapped || isDoorLock)) renderInputHelp();
1023
1320
  $('#matter-selected-device-type-value').text(deviceTypeDisplay);
1024
1321
  $('#matter-selected-device-type').toggle(hasHueDevice && deviceTypeDisplay !== '');
1025
1322
  try { $tabs.tabs('refresh'); } catch (error) { /* empty */ }
@@ -1077,9 +1374,12 @@
1077
1374
  };
1078
1375
 
1079
1376
  const updateTabsVisibility = () => {
1080
- const knxSelected = hasKnxServerSelected();
1081
1377
  const hueDeviceSelected = getHueDeviceValue() !== '';
1082
- const shouldShowTabs = 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;
1083
1383
 
1084
1384
  if (shouldShowTabs) {
1085
1385
  $tabs.show();
@@ -1087,13 +1387,52 @@
1087
1387
  } else {
1088
1388
  $tabs.hide();
1089
1389
  }
1090
- applyMatterCapabilities(currentMatterCapabilities);
1091
1390
 
1092
1391
  if ($pinSectionRow.length) {
1093
1392
  $pinSectionRow.show();
1094
1393
  }
1095
1394
  };
1395
+
1396
+ const refreshUniversalServiceVisibility = () => {
1397
+ const universal = $matterControllerModeInput.val() === 'universal';
1398
+ $('#matter-universal-options').toggle(universal);
1399
+ $universalTabs.toggle(universal && $universalServiceInput.val() === 'batteryMonitor');
1400
+ if ($universalTabs.is(':visible')) {
1401
+ try { $universalTabs.tabs('refresh'); } catch (error) { /* empty */ }
1402
+ }
1403
+ };
1404
+
1405
+ const applyControllerMode = () => {
1406
+ const universal = $matterControllerModeInput.val() === 'universal';
1407
+ $('#matter-device-section-title, #matter-device-row').toggle(!universal);
1408
+ if (universal) {
1409
+ const universalName = node._('knxUltimateMatterControllerDevice.universal_mode') || 'Universal Mode';
1410
+ currentMatterCapabilities = universalMatterCapabilities();
1411
+ selectedMatterDeviceName = universalName;
1412
+ $deviceNameInput.val(universalName);
1413
+ $hueDeviceInput.val(UNIVERSAL_NODE_ID);
1414
+ $matterEndpointInput.val('');
1415
+ $matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
1416
+ $nodeNameInput.val(node._('knxUltimateMatterControllerDevice.universal_controller_name') || 'Matter Universal Controller');
1417
+ $pinSelect.val('yes');
1418
+ } else if (String($hueDeviceInput.val() || '') === UNIVERSAL_NODE_ID) {
1419
+ currentMatterCapabilities = null;
1420
+ selectedMatterDeviceName = '';
1421
+ $deviceNameInput.val('');
1422
+ $hueDeviceInput.val('');
1423
+ $matterEndpointInput.val('');
1424
+ $matterCapabilitiesInput.val('');
1425
+ }
1426
+ refreshUniversalServiceVisibility();
1427
+ applyMatterCapabilities(currentMatterCapabilities);
1428
+ updateTabsVisibility();
1429
+ };
1430
+
1096
1431
  updateTabsVisibility();
1432
+ applyControllerMode();
1433
+
1434
+ $matterControllerModeInput.on('change.knxUltimateMatterControllerDevice', applyControllerMode);
1435
+ $universalServiceInput.on('change.knxUltimateMatterControllerDevice', refreshUniversalServiceVisibility);
1097
1436
 
1098
1437
  $knxServerInput.on('change.knxUltimateMatterControllerDevice', () => {
1099
1438
  refreshKnxBindings();
@@ -1417,9 +1756,12 @@
1417
1756
  } catch (error) { }
1418
1757
 
1419
1758
  //RED.sidebar.removeTab("tabNRColor");
1420
- if ($("#node-input-enableNodePINS").val() === "yes") {
1759
+ const universalMode = $("#node-input-matterControllerMode").val() === 'universal';
1760
+ this.matterControllerMode = universalMode ? 'universal' : 'single';
1761
+ if (universalMode || $("#node-input-enableNodePINS").val() === "yes") {
1421
1762
  this.outputs = 1;
1422
1763
  this.inputs = 1;
1764
+ this.enableNodePINS = 'yes';
1423
1765
  } else {
1424
1766
  this.outputs = 0;
1425
1767
  this.inputs = 0;
@@ -1428,7 +1770,7 @@
1428
1770
  this.updateLocalStateFromKNXWrite = $("#node-input-updateLocalStateFromKNXWrite").is(":checked"); // Starting from v 4.1.31
1429
1771
  this.matterDeviceName = ($("#node-input-matterDeviceName").val() || '').trim();
1430
1772
  this.matterNodeId = ($("#node-input-matterNodeId").val() || '').trim();
1431
- this.matterEndpointId = Number($("#node-input-matterEndpointId").val() || 1);
1773
+ this.matterEndpointId = universalMode ? '' : Number($("#node-input-matterEndpointId").val() || 1);
1432
1774
  this.matterDeviceCapabilities = ($("#node-input-matterDeviceCapabilities").val() || '').trim();
1433
1775
  const mappedMappings = [];
1434
1776
  const $mappedRows = $('#matter-mapped-profile-body .matter-mapped-row');
@@ -1444,7 +1786,9 @@
1444
1786
  ? JSON.stringify(mappedMappings)
1445
1787
  : ($('#node-input-matterMappings').val() || this.matterMappings || '[]');
1446
1788
  $('#node-input-matterMappings').val(this.matterMappings);
1447
- this.name = this.matterDeviceName || this.name || '';
1789
+ this.name = universalMode
1790
+ ? ($("#node-input-name").val() || this._('knxUltimateMatterControllerDevice.universal_controller_name') || 'Matter Universal Controller')
1791
+ : (this.matterDeviceName || this.name || '');
1448
1792
  $("#node-input-name").val(this.name);
1449
1793
  const parseSavedMatterCapabilities = (raw) => {
1450
1794
  if (typeof raw !== 'string' || raw.trim() === '') return null;
@@ -1631,10 +1975,6 @@
1631
1975
  target="_blank" href="https://youtu.be/jjEUI1J8bkA"><u><span data-i18n="common.youtube_sample"></span></u></a>
1632
1976
  <br />
1633
1977
  <br />
1634
- <p align="center">
1635
- <i class="fa-regular fa-lightbulb fa-bounce fa-2x"></i>
1636
- </p>
1637
- <br />
1638
1978
  <label for="node-input-server">
1639
1979
  <img
1640
1980
  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 +1991,116 @@
1651
1991
  <input type="text" id="node-input-serverMatter" />
1652
1992
  </div>
1653
1993
 
1994
+ <div class="form-row">
1995
+ <label for="node-input-matterControllerMode">
1996
+ <i class="fa fa-list"></i>
1997
+ <span data-i18n="knxUltimateMatterControllerDevice.controller_mode"></span>
1998
+ </label>
1999
+ <select id="node-input-matterControllerMode">
2000
+ <option value="single" data-i18n="knxUltimateMatterControllerDevice.single_mode"></option>
2001
+ <option value="universal" data-i18n="knxUltimateMatterControllerDevice.universal_mode"></option>
2002
+ </select>
2003
+ </div>
2004
+
2005
+ <div id="matter-universal-description" class="form-tips" style="display:none; margin:8px 0;"
2006
+ data-i18n="knxUltimateMatterControllerDevice.universal_mode_description"></div>
2007
+
2008
+ <div id="matter-universal-options" style="display:none;">
2009
+ <div class="form-row">
2010
+ <label for="node-input-universalService">
2011
+ <i class="fa fa-bell"></i>
2012
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_service"></span>
2013
+ </label>
2014
+ <select id="node-input-universalService">
2015
+ <option value="batteryMonitor" data-i18n="knxUltimateMatterControllerDevice.universal_service_battery_monitor"></option>
2016
+ <option value="allEvents" data-i18n="knxUltimateMatterControllerDevice.universal_service_all_events"></option>
2017
+ </select>
2018
+ </div>
2019
+ <div id="matter-universal-tabs" style="display:none; width:900px;">
2020
+ <ul>
2021
+ <li>
2022
+ <a href="#matter-universal-battery-options">
2023
+ <i class="fa fa-battery-quarter"></i>
2024
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_service_battery_monitor"></span>
2025
+ </a>
2026
+ </li>
2027
+ </ul>
2028
+ <div id="matter-universal-battery-options">
2029
+ <div class="form-row">
2030
+ <label for="node-input-universalBatteryThreshold">
2031
+ <i class="fa fa-battery-quarter"></i>
2032
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_battery_threshold"></span>
2033
+ </label>
2034
+ <input type="number" id="node-input-universalBatteryThreshold" min="0" max="100" step="1" style="width:100px;" />
2035
+ <span style="margin-left:5px;">%</span>
2036
+ </div>
2037
+ <div class="form-row" style="display:flex; align-items:center; flex-wrap:nowrap; gap:5px;">
2038
+ <label for="node-input-universalLowBatteryGA" style="width:155px; flex:0 0 155px; margin:0;">
2039
+ <i class="fa fa-bell"></i>
2040
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_low_battery_ga"></span>
2041
+ </label>
2042
+
2043
+ <label for="node-input-universalLowBatteryGA" style="width:25px; flex:0 0 25px; margin:0;"><span data-i18n="common.ga"></span></label>
2044
+ <input type="text" id="node-input-universalLowBatteryGA" placeholder="Ex: 1/1/1"
2045
+ style="width:70px; flex:0 0 70px; margin:0; text-align:left;" />
2046
+
2047
+ <label for="node-input-universalLowBatteryDPT" style="width:38px; flex:0 0 38px; margin:0; text-align:right;"><span data-i18n="common.dpt"></span></label>
2048
+ <input type="text" id="node-input-universalLowBatteryDPT" value="1.005" readonly
2049
+ style="width:75px; flex:0 0 75px; margin:0;" />
2050
+
2051
+ <label for="node-input-universalLowBatteryGAName" style="width:45px; flex:0 0 45px; margin:0; text-align:right;"><span data-i18n="common.name"></span></label>
2052
+ <input type="text" id="node-input-universalLowBatteryGAName"
2053
+ style="width:auto; min-width:90px; flex:1 1 auto; margin:0; text-align:left;" />
2054
+ </div>
2055
+ <div class="form-row" style="display:flex; align-items:center; flex-wrap:nowrap; gap:5px;">
2056
+ <label for="node-input-universalLowBatteryTextGA" style="width:155px; flex:0 0 155px; margin:0;">
2057
+ <i class="fa fa-font"></i>
2058
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_low_battery_text_ga"></span>
2059
+ </label>
2060
+
2061
+ <label for="node-input-universalLowBatteryTextGA" style="width:25px; flex:0 0 25px; margin:0;"><span data-i18n="common.ga"></span></label>
2062
+ <input type="text" id="node-input-universalLowBatteryTextGA" placeholder="Ex: 1/1/1"
2063
+ style="width:70px; flex:0 0 70px; margin:0; text-align:left;" />
2064
+
2065
+ <label for="node-input-universalLowBatteryTextDPT" style="width:38px; flex:0 0 38px; margin:0; text-align:right;"><span data-i18n="common.dpt"></span></label>
2066
+ <input type="text" id="node-input-universalLowBatteryTextDPT" value="16.001" readonly
2067
+ style="width:75px; flex:0 0 75px; margin:0;" />
2068
+
2069
+ <label for="node-input-universalLowBatteryTextGAName" style="width:45px; flex:0 0 45px; margin:0; text-align:right;"><span data-i18n="common.name"></span></label>
2070
+ <input type="text" id="node-input-universalLowBatteryTextGAName"
2071
+ style="width:auto; min-width:90px; flex:1 1 auto; margin:0; text-align:left;" />
2072
+ </div>
2073
+ <div class="form-row">
2074
+ <label for="node-input-universalBatteryOnlyLow" style="width:260px;">
2075
+ <i class="fa fa-filter"></i>
2076
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_battery_only_low"></span>
2077
+ </label>
2078
+ <input type="checkbox" id="node-input-universalBatteryOnlyLow" style="display:inline-block; width:auto;" />
2079
+ </div>
2080
+ <div class="form-row">
2081
+ <label for="node-input-universalEmitInitialState" style="width:260px;">
2082
+ <i class="fa fa-camera"></i>
2083
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_emit_initial"></span>
2084
+ </label>
2085
+ <input type="checkbox" id="node-input-universalEmitInitialState" style="display:inline-block; width:auto;" />
2086
+ </div>
2087
+ <div class="form-row">
2088
+ <label for="node-input-universalIncludeRawEvent" style="width:260px;">
2089
+ <i class="fa fa-code"></i>
2090
+ <span data-i18n="knxUltimateMatterControllerDevice.universal_include_raw"></span>
2091
+ </label>
2092
+ <input type="checkbox" id="node-input-universalIncludeRawEvent" style="display:inline-block; width:auto;" />
2093
+ </div>
2094
+ </div>
2095
+ </div>
2096
+ </div>
2097
+
1654
2098
  <br />
1655
- <p>
2099
+ <p id="matter-device-section-title">
1656
2100
  <b><span data-i18n="knxUltimateMatterControllerDevice.matter_section"></span></b>
1657
2101
  </p>
1658
2102
 
1659
- <div class="form-row">
2103
+ <div class="form-row" id="matter-device-row">
1660
2104
  <label for="node-input-matterDeviceName">
1661
2105
  <i class="fa fa-play-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.matter_device"></span>
1662
2106
  </label>
@@ -1690,6 +2134,7 @@
1690
2134
  <li><a href="#tabs-3"><i class="fa-solid fa-temperature-quarter"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.tunable_white"></span></a></li>
1691
2135
  <li><a href="#tabs-4"><i class="fa-solid fa-palette"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.rgb_hsv"></span></a></li>
1692
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>
1693
2138
  <li><a href="#tabs-6"><i class="fa-solid fa-code-merge"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.behaviour"></span></a></li>
1694
2139
  </ul>
1695
2140
  <div id="tabs-mapped">
@@ -1697,6 +2142,46 @@
1697
2142
  <div id="matter-mapped-profile-body"></div>
1698
2143
  </div>
1699
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>
1700
2185
  <div id="tabs-1">
1701
2186
  <p>
1702
2187
  <div class="form-row">
@@ -2125,18 +2610,19 @@
2125
2610
  </div>
2126
2611
  <br/>
2127
2612
  </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
2613
  </p>
2138
2614
  </div>
2139
2615
  </div>
2616
+
2617
+ <div class="form-row" style="margin-top:12px;">
2618
+ <label for="node-input-enableNodePINS" style="width:260px;">
2619
+ <i class="fa fa-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.node_pins"></span>
2620
+ </label>
2621
+ <select id="node-input-enableNodePINS">
2622
+ <option value="no" data-i18n="knxUltimateMatterControllerDevice.node_pins_hide"></option>
2623
+ <option value="yes" data-i18n="knxUltimateMatterControllerDevice.node_pins_show"></option>
2624
+ </select>
2625
+ </div>
2140
2626
  </div>
2141
2627
 
2142
2628
  <br/>
@@ -2159,6 +2645,57 @@ the device picker. Door Lock, Window Covering, Thermostat, Fan and Switch endpoi
2159
2645
  dedicated profiles. Switch press, long-press and multi-press events are emitted on the
2160
2646
  optional flow output; simpler endpoints continue through the generic mapped fallback.
2161
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
+
2677
+ **Universal Mode**
2678
+
2679
+ Select **Universal Mode** in the **Mode** selector to listen to every attribute report,
2680
+ cluster event and device initialization from this controller. It always exposes one input
2681
+ and one output, does not require a KNX gateway, and does not use endpoint KNX mappings.
2682
+ Output values are in `msg.payload`; routing metadata is in `msg.matter`, including
2683
+ `source`, `nodeId`, `endpointId`, `clusterId` and the attribute/event name.
2684
+ The **Universal Battery Monitor** scans every node and endpoint for Power Source,
2685
+ emits an initial snapshot, and maintains complete battery state. It can emit only
2686
+ batteries below the configured threshold or every battery update. `msg.payload`
2687
+ contains normalized percent, charge level, replacement state, replaceability and
2688
+ voltage; raw Matter metadata remains in `msg.matter`. Send
2689
+ `{payload:{action:"getAllBatteries"}}` to retrieve the cached inventory.
2690
+
2691
+ Use `msg.nodeId`, `msg.endpointId`, `msg.clusterId` and either `msg.command` plus
2692
+ `msg.args`, or `msg.attribute`. Add `msg.value` for an attribute write; omit it for a
2693
+ read. The same selectors may be placed under `msg.matter`. For example, read battery
2694
+ with `{nodeId:"100", endpointId:1, clusterId:47, attribute:"batPercentRemaining"}` or
2695
+ switch on with `{nodeId:"100", endpointId:1, clusterId:6, command:"on", args:{}}`.
2696
+ Battery is emitted in Matter's raw half-percent units and can be divided by two in a
2697
+ Function node.
2698
+
2162
2699
  |Property|Description|
2163
2700
  |--|--|
2164
2701
  | KNX GW | Select the KNX gateway to be used |
@@ -2242,7 +2779,7 @@ _Basic Matter effects_
2242
2779
  | Read status at startup | Read the Matter endpoint status at Node-RED startup or full deploy and send it to the KNX bus. |
2243
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. |
2244
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. |
2245
- | 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}`. |
2246
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. |
2247
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.|
2248
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. |