node-red-contrib-knx-ultimate 6.0.6 → 6.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,8 +6,13 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 6.0.6** - July 2026<br/>
9
+ **Version 6.0.8** - July 2026<br/>
10
10
 
11
+ - **Control Matter from KNX — profile deploy crash**: Door Lock and multi-purpose profiles now expose the KNX status callback before registering with `knxUltimate-config`, preventing `_Node.setNodeStatus is not a function` during deploy.<br/>
12
+ - **Control Matter from KNX — flow PIN persistence**: the **Node Input/Output PINs** selection is no longer overwritten from the KNX gateway presence when reopening the editor. The saved choice remains authoritative and `inputs`/`outputs` are updated coherently on save.<br/>
13
+ - **Matter terminology cleanup**: removed leftover Philips Hue wording from Matter controller labels, notifications, runtime status, localized strings and help. Backward-compatible saved configuration keys remain unchanged.<br/>
14
+ - **Control Matter from KNX (BETA) — Door Lock support**: commissioned Door Lock endpoints (including locks bridged by a vendor Matter hub) are now detected from their real `0x0101` cluster and expose KNX DPT 1 command/status mappings. `true` invokes `lockDoor`, `false` invokes `unlockDoor`, and subscribed `lockState` feedback updates KNX without command reflection. `NotFullyLocked` and `Unlatched` remain explicit flow states and are never collapsed into an unsafe binary KNX value. The optional remote-operation PIN is stored as a Node-RED credential; endpoints that do not advertise a requested command are rejected instead of receiving an invented operation. This is the first controller-side device profile in the extensible profile architecture requested in [discussion #519](https://github.com/Supergiovane/node-red-contrib-knx-ultimate/discussions/519).<br/>
15
+ - **Control Matter from KNX (BETA) — multi-purpose endpoint profiles**: the controller node now keeps its established light path unchanged while routing non-light endpoints through a separate capability-driven mapped profile. Selecting a plug/On-Off actuator, cover, thermostat, fan, environmental/contact/occupancy sensor, battery, electrical-power or energy endpoint builds only the KNX mappings backed by clusters, attributes and supported commands actually reported by that endpoint. These mappings now live inside a dedicated **Mappings** tab beside **Behaviour**, matching the established light editor layout. Each mapping can be disabled by leaving its GA empty; cached status supports KNX read responses and startup publication, attribute reports never reflect commands back to Matter, and cluster events remain available on the optional flow output. Saved mappings survive an untouched editor save even when the Matter endpoint is temporarily offline.<br/>
11
16
  - **Matter nodes — cleaner editors**: long pairing, storage, cache and endpoint-structure explanations were removed from the Matter node forms and consolidated in the localized HTML help, keeping the editors focused on fields, actions and live operational status.<br/>
12
17
  - **Matter nodes — distinct canvas icons**: Matter Controller and Matter Bridge nodes now use separate compact icons, with `mat` above a large right/left direction arrow, making the two integration directions immediately distinguishable in the palette, workspace and configuration selectors.<br/>
13
18
  - **Matter Controller/Bridge — protocol version and portable storage backups**: the Node-RED startup version line now logs the implemented Matter protocol revision (Matter 1.5.1, separately from the matter.js package version). Both Matter configuration editors can export and import a per-instance JSON backup containing the complete persistent Matter storage required to preserve fabrics, private credentials, sessions and commissioned/paired nodes. Import atomically replaces only the selected instance and restarts its engine; backup files contain secrets and must be protected like passwords.<br/>
@@ -18,6 +18,7 @@
18
18
  matterEndpointId: { value: 1 },
19
19
  matterDeviceName: { value: "" },
20
20
  matterDeviceCapabilities: { value: "" },
21
+ matterMappings: { value: "[]" },
21
22
  name: { value: "" },
22
23
 
23
24
  nameLightSwitch: { value: "" },
@@ -123,6 +124,9 @@
123
124
  restoreDayMode: { value: "no" }, // Starting from v 4.1.31
124
125
  updateLocalStateFromKNXWrite: { value: true } // Starting from v 4.1.31
125
126
  },
127
+ credentials: {
128
+ doorLockPin: { type: "password" }
129
+ },
126
130
  inputs: 0,
127
131
  outputs: 0,
128
132
  icon: "node-matter-controller-icon.svg",
@@ -215,7 +219,7 @@
215
219
  ensureVerticalTabsStyle();
216
220
  const $knxServerInput = $("#node-input-server");
217
221
  const KNX_EMPTY_VALUES = new Set(['', 'none', '_ADD_', '__NONE__']);
218
- // Variable names kept from the Hue-born code; they now point to the Matter widgets.
222
+ // Historical variable names are retained for saved-flow compatibility; they point to Matter widgets.
219
223
  const $hueServerInput = $("#node-input-serverMatter");
220
224
  const $hueDeviceInput = $("#node-input-matterNodeId");
221
225
  const $deviceNameInput = $("#node-input-matterDeviceName");
@@ -283,8 +287,8 @@
283
287
  $icon.removeClass('fa-stop fa-play').addClass(locateSessionActive ? 'fa fa-stop' : 'fa fa-play');
284
288
  }
285
289
  const title = locateSessionActive
286
- ? (node._('knxUltimateMatterControllerDevice.locate_stop_title') || 'Stop Hue locate')
287
- : (node._('knxUltimateMatterControllerDevice.locate_start_title') || 'Locate selected Hue device');
290
+ ? (node._('knxUltimateMatterControllerDevice.locate_stop_title') || 'Stop Matter locate')
291
+ : (node._('knxUltimateMatterControllerDevice.locate_start_title') || 'Locate selected Matter device');
288
292
  $locateDeviceButton.attr('title', title);
289
293
  };
290
294
 
@@ -340,8 +344,8 @@
340
344
  if (!context) {
341
345
  if (!silent) {
342
346
  const message = !resolveHueServerValue({ allowStored: true })
343
- ? (node._('knxUltimateMatterControllerDevice.locate_no_bridge') || 'Select a Hue bridge first')
344
- : (node._('knxUltimateMatterControllerDevice.locate_no_device') || 'Select a Hue device first');
347
+ ? (node._('knxUltimateMatterControllerDevice.locate_no_bridge') || 'Select a Matter controller first')
348
+ : (node._('knxUltimateMatterControllerDevice.locate_no_device') || 'Select a Matter device first');
345
349
  RED.notify(message, 'warning');
346
350
  }
347
351
  updateLocateButtonState(false);
@@ -410,7 +414,7 @@
410
414
  updateLocateButtonState(false);
411
415
  clearLocateAutoReset();
412
416
  if (!silent) {
413
- RED.notify(message || (node._('knxUltimateMatterControllerDevice.locate_error') || 'Unable to locate Hue device'), 'error');
417
+ RED.notify(message || (node._('knxUltimateMatterControllerDevice.locate_error') || 'Unable to locate Matter device'), 'error');
414
418
  }
415
419
  }).always(() => {
416
420
  locatePendingRequest = null;
@@ -506,11 +510,11 @@
506
510
  }
507
511
  return val;
508
512
  };
509
- const matterTypePretty = { 'ma-extendedcolorlight': 'Colour light', 'ma-colortemperaturelight': 'Tunable white', 'ma-dimmablelight': 'Dimmable light', 'ma-onofflight': 'On/off light', 'ma-onoffpluginunit': 'Plug', 'ma-dimmablepluginunit': 'Dimmable plug' };
513
+ const matterTypePretty = { 'ma-extendedcolorlight': 'Colour light', 'ma-colortemperaturelight': 'Tunable white', 'ma-dimmablelight': 'Dimmable light', 'ma-onofflight': 'On/off light', 'ma-onoffpluginunit': 'Plug', 'ma-dimmablepluginunit': 'Dimmable plug', 'ma-doorlock': 'Door lock', 'ma-windowcovering': 'Cover', 'ma-thermostat': 'Thermostat', 'ma-fan': 'Fan', 'ma-temperaturesensor': 'Temperature sensor', 'ma-humiditysensor': 'Humidity sensor', 'ma-lightsensor': 'Light sensor', 'ma-occupancysensor': 'Occupancy sensor', 'ma-contactsensor': 'Contact sensor' };
510
514
  const matterPrettyType = (raw) => {
511
515
  const key = String(raw || '').toLowerCase();
512
516
  if (matterTypePretty[key]) return matterTypePretty[key];
513
- return String(raw || '').replace(/^ma-/i, '').replace(/light$/i, ' light') || 'Light';
517
+ return String(raw || '').replace(/^ma-/i, '').replace(/light$/i, ' light') || 'Matter endpoint';
514
518
  };
515
519
  const normalizeMatterToken = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]/g, '');
516
520
  const getMatterCluster = (ep, clusterId) => (ep.clusters || []).find((c) => Number(c.id) === Number(clusterId));
@@ -524,10 +528,64 @@
524
528
  };
525
529
  const matterCapabilitiesFromEndpoint = (ep) => {
526
530
  const deviceTypes = (ep.deviceTypes || []).map(normalizeMatterToken);
531
+ const primaryDeviceType = (ep.deviceTypes || []).find((value) => !/bridgednode|aggregator|controlbridge/i.test(String(value))) || ep.name;
532
+ const deviceTypeDisplay = matterPrettyType(primaryDeviceType);
527
533
  const typeText = deviceTypes.join(' ');
528
534
  const onOffCluster = getMatterCluster(ep, 6);
529
535
  const levelCluster = getMatterCluster(ep, 8);
530
536
  const colorCluster = getMatterCluster(ep, 768);
537
+ const doorLockCluster = getMatterCluster(ep, 257);
538
+ if (doorLockCluster) {
539
+ return {
540
+ known: true,
541
+ profile: 'doorLock',
542
+ deviceTypeDisplay,
543
+ onOff: true,
544
+ level: false,
545
+ colorTemperature: false,
546
+ color: false,
547
+ lockDoor: hasMatterCommand(doorLockCluster, ['lockDoor']),
548
+ unlockDoor: hasMatterCommand(doorLockCluster, ['unlockDoor']),
549
+ requirePinForRemoteOperation: (() => {
550
+ const attribute = (doorLockCluster.attributes || []).find((item) => normalizeMatterToken(item.name) === 'requirepinforremoteoperation');
551
+ return attribute ? attribute.value === true : false;
552
+ })()
553
+ };
554
+ }
555
+ const isMatterLight = deviceTypes.some((type) => /(?:onoff|dimmable|colortemperature|extendedcolor)light/.test(type));
556
+ if (!isMatterLight) {
557
+ const mappedTargets = [];
558
+ const addCommand = (clusterId, target, label, dpt) => {
559
+ const cluster = getMatterCluster(ep, clusterId);
560
+ if (hasMatterCommand(cluster, [target])) mappedTargets.push({ direction: 'command', endpointId: Number(ep.endpointId), clusterId, targetKind: 'command', target, label, dpt });
561
+ };
562
+ const addAttribute = (clusterId, target, label, dpt, writable) => {
563
+ const cluster = getMatterCluster(ep, clusterId);
564
+ if (hasMatterAttribute(cluster, [target])) mappedTargets.push({ direction: writable ? 'command' : 'status', endpointId: Number(ep.endpointId), clusterId, targetKind: 'attribute', target, label, dpt });
565
+ };
566
+ addCommand(6, 'on', 'onoff_command', '1.001');
567
+ addAttribute(6, 'onOff', 'onoff_state', '1.001', false);
568
+ addCommand(258, 'upOrOpen', 'cover_updown', '1.008');
569
+ addCommand(258, 'stopMotion', 'cover_stop', '1.017');
570
+ addCommand(258, 'goToLiftPercentage', 'cover_position_command', '5.001');
571
+ addAttribute(258, 'currentPositionLiftPercent100ths', 'cover_position_state', '5.001', false);
572
+ addAttribute(513, 'occupiedHeatingSetpoint', 'heating_command', '9.001', true);
573
+ addAttribute(513, 'occupiedHeatingSetpoint', 'heating_state', '9.001', false);
574
+ addAttribute(513, 'occupiedCoolingSetpoint', 'cooling_command', '9.001', true);
575
+ addAttribute(513, 'occupiedCoolingSetpoint', 'cooling_state', '9.001', false);
576
+ addAttribute(513, 'localTemperature', 'local_temperature', '9.001', false);
577
+ addAttribute(514, 'percentSetting', 'fan_command', '5.001', true);
578
+ addAttribute(514, 'percentCurrent', 'fan_state', '5.001', false);
579
+ addAttribute(1026, 'measuredValue', 'temperature', '9.001', false);
580
+ addAttribute(1029, 'measuredValue', 'humidity', '9.007', false);
581
+ addAttribute(1024, 'measuredValue', 'illuminance', '9.004', false);
582
+ addAttribute(1030, 'occupancy', 'occupancy', '1.011', false);
583
+ addAttribute(69, 'stateValue', 'contact', '1.002', false);
584
+ addAttribute(47, 'batPercentRemaining', 'battery', '5.001', false);
585
+ addAttribute(144, 'activePower', 'active_power', '14.056', false);
586
+ addAttribute(145, 'cumulativeEnergyImported', 'imported_energy', '13.013', false);
587
+ return { known: true, profile: 'mapped', deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
588
+ }
531
589
  const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
532
590
  const hasColorType = /extendedcolor/.test(typeText);
533
591
  const hasLevelType = /dimmable|colortemperature|extendedcolor/.test(typeText);
@@ -542,6 +600,7 @@
542
600
  );
543
601
  return {
544
602
  known: true,
603
+ deviceTypeDisplay,
545
604
  colorDetectionVersion: 2,
546
605
  onOff: !!onOffCluster || /onoff|dimmable|colortemperature|extendedcolor/.test(typeText),
547
606
  level: !!levelCluster || hasLevelType,
@@ -568,7 +627,7 @@
568
627
  const serializeMatterCapabilities = (capabilities) => JSON.stringify(capabilities || {});
569
628
  const getJSONPromise = (url) => new Promise((resolve, reject) => { $.getJSON(url, resolve).fail(reject); });
570
629
 
571
- // Same name/contract as the Hue filter, on Matter items { value, matterNodeId, matterEndpointId }.
630
+ // Filters Matter items shaped as { value, matterNodeId, matterEndpointId }.
572
631
  const filterHueDevices = (devices, term) => {
573
632
  const cleaned = (term || '').replace(/exactmatch/gi, '').trim();
574
633
  return $.map(devices, (item) => {
@@ -578,9 +637,8 @@
578
637
  });
579
638
  };
580
639
 
581
- // Matter counterpart of the Hue device fetch: every commissioned node is
582
- // expanded into its light endpoints (clusters OnOff 6 / Level 8 / Color 768),
583
- // so a bridge (e.g. a Hue Bridge paired via Matter) lists each lamp.
640
+ // Expand every commissioned node into its functional endpoints. Lights keep
641
+ // the legacy light UI; other endpoints select a capability-driven profile.
584
642
  const fetchHueDevices = (term, respond, { forceRefresh = false } = {}) => {
585
643
  const matterServer = getHueServerConfig();
586
644
  if (!matterServer) {
@@ -609,8 +667,10 @@
609
667
  const items = [];
610
668
  results.forEach((entry) => {
611
669
  const device = entry.device;
612
- const lightEps = entry.endpoints.filter((ep) => Array.isArray(ep.clusters) && ep.clusters.some((c) => [6, 8, 768].indexOf(Number(c.id)) !== -1));
613
- const eps = lightEps.length ? lightEps : entry.endpoints.filter((ep) => Number(ep.endpointId) !== 0);
670
+ // Multi-purpose controller: list every functional endpoint. Capability
671
+ // detection below decides whether it uses the unchanged light path or
672
+ // a dedicated/mapped non-light profile.
673
+ const eps = entry.endpoints.filter((ep) => Number(ep.endpointId) !== 0);
614
674
  const many = eps.length > 1;
615
675
  eps.forEach((ep) => {
616
676
  const label = matterEndpointLabel(ep);
@@ -697,11 +757,11 @@
697
757
  $.getJSON(`knxUltimateDpts?serverId=${serverId}&_=${Date.now()}`, (data) => {
698
758
  data.forEach((dpt) => {
699
759
  if (prefixes.some((prefix) => prefix === "" || dpt.value.startsWith(prefix))) {
700
- // Adjustment for HUE Temperature
760
+ // Adjustment for Matter color temperature
701
761
  if (dpt.value.startsWith("7.600")) {
702
762
  $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - KNX Kelvin range 2000-6535k (Homeassistant color_temperature_mode: absolute)"));
703
763
  } else if (dpt.value.startsWith("9.002")) {
704
- $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - HUE Kelvin range 2000-6535k (Homeassistant color_temperature_mode: absolute_float)"));
764
+ $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - Matter Kelvin range 2000-6535 K (Home Assistant color_temperature_mode: absolute_float)"));
705
765
  } else if (dpt.value.startsWith("5.001")) {
706
766
  $(_destinationWidget).append($("<option></option>").attr("value", dpt.value).text(dpt.text + " - Homeassistant color_temperature_mode: relative"));
707
767
  } else {
@@ -934,12 +994,29 @@
934
994
  const supportsTemperature = hasHueDevice && caps.colorTemperature === true;
935
995
  const supportsColor = hasHueDevice && caps.color === true;
936
996
  const hasRichBehaviour = supportsDim && (supportsTemperature || supportsColor);
997
+ const isDoorLock = caps.profile === 'doorLock';
998
+ const isMapped = caps.profile === 'mapped';
999
+ const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
937
1000
 
938
1001
  setMatterTabVisible('tabs-1', supportsSwitch);
939
1002
  setMatterTabVisible('tabs-2', supportsDim);
940
1003
  setMatterTabVisible('tabs-3', supportsTemperature);
941
1004
  setMatterTabVisible('tabs-4', supportsColor);
1005
+ setMatterTabVisible('tabs-mapped', isMapped);
942
1006
  setMatterTabVisible('tabs-6', hasHueDevice);
1007
+ $('#matter-door-lock-options').toggle(isDoorLock);
1008
+ $('#matter-switch-command-label').text(isDoorLock
1009
+ ? (node._('knxUltimateMatterControllerDevice.door_lock_command') || 'Lock / unlock')
1010
+ : (node._('knxUltimateMatterControllerDevice.control') || 'Control'));
1011
+ $('#matter-switch-status-label').text(isDoorLock
1012
+ ? (node._('knxUltimateMatterControllerDevice.door_lock_status') || 'Lock state')
1013
+ : (node._('knxUltimateMatterControllerDevice.status') || 'Status'));
1014
+ $('#matter-door-lock-command-warning').toggle(isDoorLock && (caps.lockDoor === false || caps.unlockDoor === false));
1015
+ $('#matter-door-lock-pin-required').toggle(isDoorLock && caps.requirePinForRemoteOperation === true);
1016
+ $('#matter-mapped-profile').toggle(isMapped);
1017
+ if (isMapped) renderMappedProfile(caps.mappedTargets || []);
1018
+ $('#matter-selected-device-type-value').text(deviceTypeDisplay);
1019
+ $('#matter-selected-device-type').toggle(hasHueDevice && deviceTypeDisplay !== '');
943
1020
  try { $tabs.tabs('refresh'); } catch (error) { /* empty */ }
944
1021
  ensureVisibleMatterTabSelected();
945
1022
 
@@ -960,6 +1037,40 @@
960
1037
  refreshBehaviourVisibility();
961
1038
  };
962
1039
 
1040
+ const parseMatterMappings = () => {
1041
+ try { return JSON.parse($('#node-input-matterMappings').val() || node.matterMappings || '[]'); } catch (error) { return []; }
1042
+ };
1043
+ const renderMappedProfile = (targets) => {
1044
+ const saved = parseMatterMappings();
1045
+ const $body = $('#matter-mapped-profile-body').empty();
1046
+ targets.forEach((target) => {
1047
+ const key = `${target.direction}:${target.endpointId}:${target.clusterId}:${target.targetKind}:${target.target}`;
1048
+ const previous = saved.find((item) => `${item.direction}:${item.endpointId}:${item.clusterId}:${item.targetKind}:${item.target}` === key) || {};
1049
+ const $row = $('<div class="form-row matter-mapped-row" style="display:flex;align-items:center;"></div>').data('mapping', target);
1050
+ const translated = node._(`knxUltimateMatterControllerDevice.mapped.${target.label}`);
1051
+ $('<label style="width:220px; margin-right:10px;"></label>').text(translated || target.target).appendTo($row);
1052
+ $('<label style="width:25px; margin:0 5px 0 0;">GA</label>').appendTo($row);
1053
+ const $ga = $('<input class="matter-mapped-ga" type="text" placeholder="Ex: 1/1/1" style="width:100px; margin-right:18px;">').val(previous.ga || '').appendTo($row);
1054
+ $('<label style="width:35px; margin:0 5px 0 0;">DPT</label>').appendTo($row);
1055
+ const $dpt = $('<input class="matter-mapped-dpt" type="text" placeholder="DPT" style="width:140px;">').val(previous.dpt || target.dpt || '').appendTo($row);
1056
+ $ga.autocomplete({
1057
+ minLength: 0,
1058
+ source(request, response) {
1059
+ if (!hasKnxServerSelected()) { response([]); return; }
1060
+ $.getJSON(`knxUltimatecsv?nodeID=${resolveKnxServerValue()}&_=${Date.now()}`, (data) => {
1061
+ response($.map(data, (value) => {
1062
+ const searchable = `${value.ga} ${value.devicename} ${value.dpt}`;
1063
+ if (!htmlUtilsfullCSVSearch(searchable, request.term || '')) return null;
1064
+ return { label: `${value.ga} # ${value.devicename} # ${value.dpt}`, value: value.ga, dpt: value.dpt };
1065
+ }));
1066
+ }).fail(() => response([]));
1067
+ },
1068
+ select(event, ui) { if (ui.item.dpt) $dpt.val(ui.item.dpt); }
1069
+ }).focus(function () { $(this).autocomplete('search', $(this).val()); });
1070
+ $body.append($row);
1071
+ });
1072
+ };
1073
+
963
1074
  const updateTabsVisibility = () => {
964
1075
  const knxSelected = hasKnxServerSelected();
965
1076
  const hueDeviceSelected = getHueDeviceValue() !== '';
@@ -973,13 +1084,6 @@
973
1084
  }
974
1085
  applyMatterCapabilities(currentMatterCapabilities);
975
1086
 
976
- if ($pinSelect.length) {
977
- const desiredPins = knxSelected ? 'no' : 'yes';
978
- if ($pinSelect.val() !== desiredPins) {
979
- $pinSelect.val(desiredPins).trigger('change');
980
- }
981
- }
982
-
983
1087
  if ($pinSectionRow.length) {
984
1088
  $pinSectionRow.show();
985
1089
  }
@@ -1125,7 +1229,7 @@
1125
1229
  .append($("<option>").val("temperature").text(node._("knxUltimateMatterControllerDevice.select_temperature_brightness")));
1126
1230
  $("#node-input-specifySwitchOnBrightness").val(node.specifySwitchOnBrightness);
1127
1231
  $("#node-input-enableDayNightLighting").val(node.enableDayNightLighting);
1128
- // "Get current" reads the state from the Hue bridge: not available on Matter.
1232
+ // Live "Get current" reads are not available on this Matter path.
1129
1233
  $("#getColorAtSwitchOnDayTimeButton").hide();
1130
1234
  $("#getColorAtSwitchOnNightTimeButton").hide();
1131
1235
  applyMatterCapabilities(currentMatterCapabilities);
@@ -1150,8 +1254,8 @@
1150
1254
  if ($("#node-input-enableDayNightLighting").val() === "yes") blinkBackground("#colorPickerNight");
1151
1255
  });
1152
1256
 
1153
- // "Get current" (reading the live state from the Hue bridge) does not exist on
1154
- // Matter: the buttons are hidden by populateSwitchOnCombos above.
1257
+ // Live "Get current" reads do not exist on this Matter path; the buttons are
1258
+ // hidden by populateSwitchOnCombos above.
1155
1259
 
1156
1260
  // Fill options for minDimLevel and maxDimLevel and comboBrightnessAtSwitchOn (for color brightness at switch on, with temperature toghedher)
1157
1261
  for (let index = 100; index >= 0; index -= 5) {
@@ -1321,6 +1425,20 @@
1321
1425
  this.matterNodeId = ($("#node-input-matterNodeId").val() || '').trim();
1322
1426
  this.matterEndpointId = Number($("#node-input-matterEndpointId").val() || 1);
1323
1427
  this.matterDeviceCapabilities = ($("#node-input-matterDeviceCapabilities").val() || '').trim();
1428
+ const mappedMappings = [];
1429
+ const $mappedRows = $('#matter-mapped-profile-body .matter-mapped-row');
1430
+ $mappedRows.each(function () {
1431
+ const base = $(this).data('mapping') || {};
1432
+ const ga = ($(this).find('.matter-mapped-ga').val() || '').trim();
1433
+ const dpt = ($(this).find('.matter-mapped-dpt').val() || '').trim();
1434
+ if (ga && dpt) mappedMappings.push(Object.assign({}, base, { ga, dpt }));
1435
+ });
1436
+ // Preserve saved mappings when the endpoint is offline and its asynchronous
1437
+ // structure could not be rebuilt during an untouched editor round trip.
1438
+ this.matterMappings = $mappedRows.length > 0
1439
+ ? JSON.stringify(mappedMappings)
1440
+ : ($('#node-input-matterMappings').val() || this.matterMappings || '[]');
1441
+ $('#node-input-matterMappings').val(this.matterMappings);
1324
1442
  this.name = this.matterDeviceName || this.name || '';
1325
1443
  $("#node-input-name").val(this.name);
1326
1444
  const parseSavedMatterCapabilities = (raw) => {
@@ -1546,9 +1664,15 @@
1546
1664
  <span class="hue-devices-loading" style="margin-left:6px; display:none; color:#1b7d33;">
1547
1665
  <i class="fa fa-circle-notch fa-spin"></i>
1548
1666
  </span>
1667
+ <span id="matter-selected-device-type" style="display:none; margin-left:10px; color:#666; white-space:nowrap;">
1668
+ <i class="fa fa-microchip"></i>
1669
+ <span data-i18n="knxUltimateMatterControllerDevice.device_type"></span>:
1670
+ <span id="matter-selected-device-type-value" style="font-weight:600;"></span>
1671
+ </span>
1549
1672
  <input type="hidden" id="node-input-matterNodeId" />
1550
1673
  <input type="hidden" id="node-input-matterEndpointId" />
1551
1674
  <input type="hidden" id="node-input-matterDeviceCapabilities" />
1675
+ <input type="hidden" id="node-input-matterMappings" />
1552
1676
  <input type="hidden" id="node-input-name" />
1553
1677
  </div>
1554
1678
 
@@ -1560,12 +1684,18 @@
1560
1684
  <li><a href="#tabs-2"><i class="fa-solid fa-arrow-up-wide-short"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.dim"></span></a></li>
1561
1685
  <li><a href="#tabs-3"><i class="fa-solid fa-temperature-quarter"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.tunable_white"></span></a></li>
1562
1686
  <li><a href="#tabs-4"><i class="fa-solid fa-palette"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.rgb_hsv"></span></a></li>
1687
+ <li><a href="#tabs-mapped"><i class="fa-solid fa-list-check"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.mappings"></span></a></li>
1563
1688
  <li><a href="#tabs-6"><i class="fa-solid fa-code-merge"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.behaviour"></span></a></li>
1564
1689
  </ul>
1690
+ <div id="tabs-mapped">
1691
+ <div id="matter-mapped-profile" style="display:none;">
1692
+ <div id="matter-mapped-profile-body"></div>
1693
+ </div>
1694
+ </div>
1565
1695
  <div id="tabs-1">
1566
1696
  <p>
1567
1697
  <div class="form-row">
1568
- <label for="node-input-nameLightSwitch" style="width:110px;"><i class="fa fa-play-circle-o"></i> <span data-i18n="knxUltimateMatterControllerDevice.control"></span></label>
1698
+ <label for="node-input-nameLightSwitch" style="width:110px;"><i class="fa fa-play-circle-o"></i> <span id="matter-switch-command-label" data-i18n="knxUltimateMatterControllerDevice.control"></span></label>
1569
1699
 
1570
1700
  <label for="node-input-GALightSwitch" style="width:20px;"><span data-i18n="common.ga"></span></label>
1571
1701
  <input type="text" id="node-input-GALightSwitch" placeholder="Ex: 1/1/1"
@@ -1578,7 +1708,7 @@
1578
1708
  <input type="text" id="node-input-nameLightSwitch" style="width:190px;margin-left: 5px; text-align: left;">
1579
1709
  </div>
1580
1710
  <div class="form-row">
1581
- <label for="node-input-nameLightState" style="width:110px;"><i class="fa fa-question-circle"></i> <span data-i18n="knxUltimateMatterControllerDevice.status"></span></label>
1711
+ <label for="node-input-nameLightState" style="width:110px;"><i class="fa fa-question-circle"></i> <span id="matter-switch-status-label" data-i18n="knxUltimateMatterControllerDevice.status"></span></label>
1582
1712
 
1583
1713
  <label for="node-input-GALightState" style="width:20px;"><span data-i18n="common.ga"></span></label>
1584
1714
  <input type="text" id="node-input-GALightState" placeholder="Ex: 1/1/1"
@@ -1591,6 +1721,14 @@
1591
1721
  data-i18n="knxUltimateMatterControllerDevice.node-input-name"></span></label>
1592
1722
  <input type="text" id="node-input-nameLightState" style="width:190px;margin-left: 5px; text-align: left;">
1593
1723
  </div>
1724
+ <div id="matter-door-lock-options" style="display:none;">
1725
+ <div class="form-row">
1726
+ <label for="node-input-doorLockPin" style="width:260px;"><i class="fa fa-key"></i> <span data-i18n="knxUltimateMatterControllerDevice.door_lock_pin"></span></label>
1727
+ <input type="password" id="node-input-doorLockPin" style="width:210px;" autocomplete="new-password">
1728
+ </div>
1729
+ <div id="matter-door-lock-pin-required" class="form-tips" style="display:none;" data-i18n="knxUltimateMatterControllerDevice.door_lock_pin_required"></div>
1730
+ <div id="matter-door-lock-command-warning" class="form-tips" style="display:none; color:#a94442;" data-i18n="knxUltimateMatterControllerDevice.door_lock_commands_missing"></div>
1731
+ </div>
1594
1732
  </p>
1595
1733
  </div>
1596
1734
  <div id="tabs-2">
@@ -2006,14 +2144,13 @@
2006
2144
 
2007
2145
 
2008
2146
  <script type="text/markdown" data-help-name="knxUltimateMatterControllerDevice">
2009
- <p>This node controls Philips Hue lights (single or grouped) and maps their commands/states to KNX.</p>
2147
+ <p>This node controls commissioned Matter endpoints and maps their commands and states to KNX.</p>
2010
2148
 
2011
2149
  **General**
2012
2150
 
2013
- This node was born from the Hue Light node: same KNX group-address model and behaviour,
2014
- with the Philips Hue engine replaced by the **Matter controller** engine. A Matter bridge
2015
- (for example a Hue Bridge paired via Matter) is expanded into its lamps in the picker.
2016
- Hue-only features (native effects, locate) are not available on Matter.
2151
+ The editor detects the selected endpoint capabilities and exposes only the corresponding
2152
+ Matter functions. Bridged Matter devices are expanded into their individual endpoints in
2153
+ the device picker.
2017
2154
 
2018
2155
  |Property|Description|
2019
2156
  |--|--|
@@ -2025,14 +2162,14 @@ Hue-only features (native effects, locate) are not available on Matter.
2025
2162
 
2026
2163
  **OPTIONS**
2027
2164
 
2028
- Here you can link KNX Group Addresses to the available Hue commands/states.<br/>
2165
+ Here you can link KNX Group Addresses to the available Matter commands and states.<br/>
2029
2166
  Start typing in the GA field (name or Group Address); suggestions appear while you type.
2030
2167
 
2031
2168
  **Switch**
2032
2169
 
2033
2170
  |Property|Description|
2034
2171
  |--|--|
2035
- | Control | This GA is used to turn on/off the Hue light via a boolean KNX value true/false|
2172
+ | Control | This GA turns the Matter light on or off via a boolean KNX value true/false. |
2036
2173
  | Status | Link this to the light's switch status group address|
2037
2174
 
2038
2175
  <br/>
@@ -2041,8 +2178,8 @@ Start typing in the GA field (name or Group Address); suggestions appear while y
2041
2178
 
2042
2179
  |Property|Description|
2043
2180
  |--|--|
2044
- | Control dim | Relative DIM of the Hue light. You can set the dimming speed in the **Behaviour** tab. |
2045
- | Control % | Changes the absolute Hue light's brightness (0-100%)|
2181
+ | Control dim | Relative DIM of the Matter light. You can set the dimming speed in the **Behaviour** tab. |
2182
+ | Control % | Changes the absolute Matter light brightness (0-100%). |
2046
2183
  | Status % | Link this to the light's brightness status KNX group address |
2047
2184
  | Dim Speed (ms) | Dimming speed in milliseconds. Applies to both the light brightness and the tunable-white datapoints. Calculated over the 0%→100% range. |
2048
2185
  | Min Dim brightness | Tha Minimum brightness that the lamp can reach. For example, if you are dimming the light down, the light will stop dimming at the specified brightness %. |
@@ -2057,8 +2194,8 @@ Start typing in the GA field (name or Group Address); suggestions appear while y
2057
2194
  | Control dim | Change white temperature using DPT 3.007 dimming. Speed is set in the **Behaviour** tab.|
2058
2195
  | Control % | Change white temperature using DPT 5.001. 0 = full warm, 100 = full cold.|
2059
2196
  | Status %| Temperature status GA. DPT 5.001 absolute value: 0 = full warm, 100 = full cold.|
2060
- | Control kelvin | **DPT 7.600: ** set temperature in Kelvin using the KNX range 2000-6535 (converted to Hue mirek).<br/>**DPT 9.002:** set temperature using Hue range 2000-6535 K (Ambiance starts at 2200 K). Conversions may introduce small deviations.|
2061
- | Status kelvin | **DPT 7.600: ** read temperature in Kelvin using KNX range 2000-6535 (converted from Hue).<br/>**DPT 9.002:** read temperature using Hue range 2000-6535 K (Ambiance starts at 2200 K). Conversions may introduce small deviations. |
2197
+ | Control kelvin | **DPT 7.600:** set the Matter color temperature in Kelvin in the supported endpoint range.<br/>**DPT 9.002:** set the temperature as a KNX floating-point value. Conversions may introduce small deviations. |
2198
+ | Status kelvin | **DPT 7.600:** read the Matter color temperature in Kelvin.<br/>**DPT 9.002:** read the temperature as a KNX floating-point value. |
2062
2199
  | Invert dim direction | Inverts the DIM direction. |
2063
2200
  <br/>
2064
2201
 
@@ -2082,12 +2219,12 @@ For controlling the HSV "V” (brightness), use the standard controls under the
2082
2219
 
2083
2220
  **Effects**
2084
2221
 
2085
- _Non-Hue basic effects_
2222
+ _Basic Matter effects_
2086
2223
 
2087
2224
  |Property|Description|
2088
2225
  |--|--|
2089
- | Blink | _true_ Blink the light, _false_ Stop blinking. Blinks the light on and off. Useful for signalling. Works with all Hue lights. |
2090
- | Color Cycle | _true_ start cycle, _false_ Stop cycle. Randomly changes the Hue light's color at regular interval. Works with all Hue lights having color capabilities. The color effect will start 10 seconds after set. |
2226
+ | Blink | _true_ blinks the light, _false_ stops blinking. Useful for signalling and available on compatible Matter lights. |
2227
+ | Color Cycle | _true_ starts the cycle, _false_ stops it. Randomly changes the Matter light color at regular intervals when color control is supported. |
2091
2228
 
2092
2229
  <br/>
2093
2230
 
@@ -2095,16 +2232,16 @@ _Non-Hue basic effects_
2095
2232
 
2096
2233
  | Property | Description |
2097
2234
  | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2098
- | Read status at startup | Read the Hue light status at node-red's startup or node-red's full deploy, and send that status to the KNX BUS |
2099
- | KNX Brightness Status | Updates the KNX brightness group address status, whenever the Hue lamp is switched ON/OFF. The options are **When Hue light is Off send 0%. When Hue On, restore previous value (Default KNX behaviour) ** and**Leave as is (default Hue behaviour) ** . If you have KNX dimmer with brightness status, like MDT, the suggested option is _**When Hue light is Off send 0%. When Hue On, restore previous value (Default KNX behaviour)** _ |
2100
- | Update local cached Hue state from KNX bus writes | Advanced option, enabled by default. When enabled, writes arriving from the KNX bus also update the node's local cached Hue state immediately, without waiting for feedback/events from the Hue bridge. This gives faster local reactions and more consistent immediate KNX read responses, especially while the light or grouped light is OFF. Disable it if you prefer the cache to follow only real feedback/events from the Hue bridge. |
2235
+ | Read status at startup | Read the Matter endpoint status at Node-RED startup or full deploy and send it to the KNX bus. |
2236
+ | 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
+ | 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. |
2101
2238
  | 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. |
2102
2239
  | 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.|
2103
2240
  | Day/Night | Select the group address used to set the day/night behaviour. The group address value is _true_ if daytime, _false_ if nighttime. |
2104
2241
  | Invert day/night value | Invert the values of _Day/Night_ group address. Default value is **unchecked** . |
2105
2242
  | Read status at startup | Read the status at startup and emit the event to the KNX bus at startup/reconnection. (Default "no")|
2106
2243
  | Force day mode | You can force the day mode by manually switching the light as described here: **Switch to DAY mode by rapid switching the ligth off then on (This light only) ** does what described and acts only on this light.**Switch to DAY mode by rapid switching the ligth off then on (apply yo ALL light nodes)** acts to ALL Light nodes, by setting the Day/Night group address to Day mode. |
2107
- | Node Input/Output PINs | Hide or show the input/output PINs. Input/output PINS allow the node to accept msg input from the flow and send msg output to the flow. Input msg must follow the Hue API v.2 Standards. This is an example msg, that turns on the light: <code>msg.on = {"on":true}</code>. Please refer to the [official Hue Api page](https://developers.meethue.com/develop/hue-api-v2/api-reference/#resource_light__id__put) |
2244
+ | Node Input/Output PINs | Hide or show the flow input/output PINs. The input accepts supported Matter commands and the output emits validated Matter endpoint events. The selection is persisted with the node. |
2108
2245
 
2109
2246
  ### Note
2110
2247
 
@@ -9,6 +9,7 @@ const dptlib = require('knxultimate').dptlib
9
9
  const hueColorConverter = require('./utils/colorManipulators/hueColorConverter')
10
10
  const { createLightEngine } = require('./utils/lightEngines')
11
11
  const { hueStateToCanonical, matterEventToHuePatch } = require('./utils/lightEngines/matterHueShim')
12
+ const { setupMatterControllerProfile } = require('./utils/matterControllerProfiles')
12
13
 
13
14
  module.exports = function (RED) {
14
15
  function knxUltimateMatterControllerDevice (config) {
@@ -20,6 +21,10 @@ module.exports = function (RED) {
20
21
  node.matterEndpointId = config.matterEndpointId !== undefined ? Number(config.matterEndpointId) : 1
21
22
  node.matterDeviceName = config.matterDeviceName !== undefined ? String(config.matterDeviceName) : ''
22
23
 
24
+ let matterCapabilities = {}
25
+ try { matterCapabilities = JSON.parse(config.matterDeviceCapabilities || '{}') } catch (error) { /* empty */ }
26
+ if (setupMatterControllerProfile(matterCapabilities.profile, RED, node, config)) return
27
+
23
28
  // Matter engine (adapter). The manager is resolved lazily at each write, because
24
29
  // matter-config creates its matterManager asynchronously after the deploy.
25
30
  node.lightEngine = createLightEngine('matter', {
@@ -110,8 +115,8 @@ module.exports = function (RED) {
110
115
  config.GADaylightSensor
111
116
  ].map((ga) => String(ga || '').trim()).filter((ga) => ga !== '')
112
117
  const isConfiguredKNXGA = (ga) => node.knxUltimateAcceptedGAs.includes(String(ga || '').trim())
113
- // Synthetic "current device" in Hue shape, kept in sync from the Matter attribute
114
- // reports. It lets the whole Hue-born logic below run untouched on a Matter light.
118
+ // Synthetic current-device compatibility state, kept in sync from Matter attribute
119
+ // reports. It lets the established light logic run untouched on a Matter light.
115
120
  node.currentHUEDevice = {
116
121
  id: node.matterNodeId,
117
122
  type: 'light',
@@ -216,14 +221,14 @@ module.exports = function (RED) {
216
221
  node.status({ fill, shape, text: (node.sHUENodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
217
222
  } catch (error) { }
218
223
  }
219
- // Used to call the status update from the HUE config node.
224
+ // Compatibility callback used to update the Matter node status.
220
225
  node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
221
226
  try {
222
227
  if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
223
228
  if (payload === undefined) payload = ''
224
229
  const dDate = new Date()
225
230
  payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
226
- node.sHUENodeStatusText = `|HUE: ${text} ${payload} (${formatTs(dDate)})`
231
+ node.sHUENodeStatusText = `|Matter: ${text} ${payload} (${formatTs(dDate)})`
227
232
  node.status({ fill, shape, text: node.sHUENodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
228
233
  } catch (error) { }
229
234
  }
@@ -318,7 +323,7 @@ module.exports = function (RED) {
318
323
  return Math.floor(Math.random() * (max - min + 1) + min) // The maximum is inclusive and the minimum is inclusive
319
324
  }
320
325
 
321
- // This function is called by the hue-config.js
326
+ // Compatibility callback invoked by the Matter controller adapter.
322
327
  node.handleSend = (msg) => {
323
328
  if (!msg || !msg.knx || !isConfiguredKNXGA(msg.knx.destination)) return
324
329
  if (node.currentHUEDevice === undefined && node.serverHue.linkStatus === 'connected') {
@@ -1161,7 +1166,7 @@ module.exports = function (RED) {
1161
1166
  node.setNodeStatusHue({
1162
1167
  fill: 'red',
1163
1168
  shape: 'ring',
1164
- text: "Rejected HUE light settings. I'm still not ready...",
1169
+ text: "Rejected Matter light settings. I'm still not ready...",
1165
1170
  payload: ''
1166
1171
  })
1167
1172
  return
@@ -15,11 +15,13 @@ Er ersetzt die unveröffentlichten getrennten Matter-Controller-Nodes und behäl
15
15
  | Matter controller | Matter-Controller-Konfigurationsknoten, in dem das Gerät gekoppelt wurde. |
16
16
  | Matter device | Matter-Endpunkt aus den gekoppelten Geräten. Die UI wird aus den echten Fähigkeiten neu aufgebaut. |
17
17
  | Switch / Steckdose / Licht On-Off | On/Off-Befehls- und Status-Gruppenadressen, normalerweise DPT `1.001`. |
18
+ | Türschloss | Eine DPT-`1.xxx`-Befehls-GA ruft bei `true` `lockDoor` und bei `false` `unlockDoor` auf; eine separate Status-GA erhält nur eindeutige Verriegelt/Entriegelt-Zustände. Falls erforderlich, wird die Fernbedienungs-PIN im Credential-Feld gespeichert. Nicht angebotene Befehle werden abgewiesen. |
19
+ | Andere Endpunkte | Steckdosen, Ein/Aus-Aktoren, Jalousien, Thermostate, Lüfter, Umwelt-/Kontakt-/Belegungssensoren sowie Batterie-, Leistungs- und Energieendpunkte verwenden das Mehrzweckprofil. Der Tab **Zuordnungen** enthält nur tatsächlich angebotene Cluster, Attribute und Befehle. |
18
20
  | Lichtsteuerung | Für Licht-Endpunkte wird die vollständige Licht-UI verwendet: relatives DIM (DPT `3.007`), Helligkeit %, RGB/HSV, Tunable White, Einschalt-Helligkeit/-Temperatur, Tag/Nacht-Licht, Min/Max-Dimmlevel und Dimmgeschwindigkeit. Nicht unterstützte Bereiche bleiben ausgeblendet. |
19
21
  | Sensoren | Sensor-Endpunkte zeigen ihre Mess-/Status-GA nur bei Unterstützung: Temperatur, Feuchte, Helligkeit, Präsenz, Kontakt und Batterie. |
20
22
  | Read at startup | Veröffentlicht den gecachten Matter-Wert beim Deploy/Start oder wenn sich das Gerät erneut verbindet. |
21
23
  | Update local state from KNX write | Aktualisiert den lokalen Matter/KNX-Cache, wenn ein Telegramm auf eine konfigurierte KNX-GA geschrieben wird. |
22
- | Node Input/Output PINs | Zeigt Node-RED-Eingangs-/Ausgangspins. Der Eingang akzeptiert boolesche Payloads sowie Matter-ähnliche `msg.payload` oder `msg.on.on`; der Ausgang sendet Statusupdates. |
24
+ | 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. |
23
25
 
24
26
  ## Verhalten
25
27