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

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,10 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 6.0.6** - July 2026<br/>
9
+ **Version 6.0.7** - July 2026<br/>
10
10
 
11
+ - **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/>
12
+ - **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
13
  - **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
14
  - **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
15
  - **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",
@@ -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,
@@ -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);
@@ -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() !== '';
@@ -1321,6 +1432,20 @@
1321
1432
  this.matterNodeId = ($("#node-input-matterNodeId").val() || '').trim();
1322
1433
  this.matterEndpointId = Number($("#node-input-matterEndpointId").val() || 1);
1323
1434
  this.matterDeviceCapabilities = ($("#node-input-matterDeviceCapabilities").val() || '').trim();
1435
+ const mappedMappings = [];
1436
+ const $mappedRows = $('#matter-mapped-profile-body .matter-mapped-row');
1437
+ $mappedRows.each(function () {
1438
+ const base = $(this).data('mapping') || {};
1439
+ const ga = ($(this).find('.matter-mapped-ga').val() || '').trim();
1440
+ const dpt = ($(this).find('.matter-mapped-dpt').val() || '').trim();
1441
+ if (ga && dpt) mappedMappings.push(Object.assign({}, base, { ga, dpt }));
1442
+ });
1443
+ // Preserve saved mappings when the endpoint is offline and its asynchronous
1444
+ // structure could not be rebuilt during an untouched editor round trip.
1445
+ this.matterMappings = $mappedRows.length > 0
1446
+ ? JSON.stringify(mappedMappings)
1447
+ : ($('#node-input-matterMappings').val() || this.matterMappings || '[]');
1448
+ $('#node-input-matterMappings').val(this.matterMappings);
1324
1449
  this.name = this.matterDeviceName || this.name || '';
1325
1450
  $("#node-input-name").val(this.name);
1326
1451
  const parseSavedMatterCapabilities = (raw) => {
@@ -1546,9 +1671,15 @@
1546
1671
  <span class="hue-devices-loading" style="margin-left:6px; display:none; color:#1b7d33;">
1547
1672
  <i class="fa fa-circle-notch fa-spin"></i>
1548
1673
  </span>
1674
+ <span id="matter-selected-device-type" style="display:none; margin-left:10px; color:#666; white-space:nowrap;">
1675
+ <i class="fa fa-microchip"></i>
1676
+ <span data-i18n="knxUltimateMatterControllerDevice.device_type"></span>:
1677
+ <span id="matter-selected-device-type-value" style="font-weight:600;"></span>
1678
+ </span>
1549
1679
  <input type="hidden" id="node-input-matterNodeId" />
1550
1680
  <input type="hidden" id="node-input-matterEndpointId" />
1551
1681
  <input type="hidden" id="node-input-matterDeviceCapabilities" />
1682
+ <input type="hidden" id="node-input-matterMappings" />
1552
1683
  <input type="hidden" id="node-input-name" />
1553
1684
  </div>
1554
1685
 
@@ -1560,12 +1691,18 @@
1560
1691
  <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
1692
  <li><a href="#tabs-3"><i class="fa-solid fa-temperature-quarter"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.tunable_white"></span></a></li>
1562
1693
  <li><a href="#tabs-4"><i class="fa-solid fa-palette"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.rgb_hsv"></span></a></li>
1694
+ <li><a href="#tabs-mapped"><i class="fa-solid fa-list-check"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.mappings"></span></a></li>
1563
1695
  <li><a href="#tabs-6"><i class="fa-solid fa-code-merge"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.behaviour"></span></a></li>
1564
1696
  </ul>
1697
+ <div id="tabs-mapped">
1698
+ <div id="matter-mapped-profile" style="display:none;">
1699
+ <div id="matter-mapped-profile-body"></div>
1700
+ </div>
1701
+ </div>
1565
1702
  <div id="tabs-1">
1566
1703
  <p>
1567
1704
  <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>
1705
+ <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
1706
 
1570
1707
  <label for="node-input-GALightSwitch" style="width:20px;"><span data-i18n="common.ga"></span></label>
1571
1708
  <input type="text" id="node-input-GALightSwitch" placeholder="Ex: 1/1/1"
@@ -1578,7 +1715,7 @@
1578
1715
  <input type="text" id="node-input-nameLightSwitch" style="width:190px;margin-left: 5px; text-align: left;">
1579
1716
  </div>
1580
1717
  <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>
1718
+ <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
1719
 
1583
1720
  <label for="node-input-GALightState" style="width:20px;"><span data-i18n="common.ga"></span></label>
1584
1721
  <input type="text" id="node-input-GALightState" placeholder="Ex: 1/1/1"
@@ -1591,6 +1728,14 @@
1591
1728
  data-i18n="knxUltimateMatterControllerDevice.node-input-name"></span></label>
1592
1729
  <input type="text" id="node-input-nameLightState" style="width:190px;margin-left: 5px; text-align: left;">
1593
1730
  </div>
1731
+ <div id="matter-door-lock-options" style="display:none;">
1732
+ <div class="form-row">
1733
+ <label for="node-input-doorLockPin" style="width:260px;"><i class="fa fa-key"></i> <span data-i18n="knxUltimateMatterControllerDevice.door_lock_pin"></span></label>
1734
+ <input type="password" id="node-input-doorLockPin" style="width:210px;" autocomplete="new-password">
1735
+ </div>
1736
+ <div id="matter-door-lock-pin-required" class="form-tips" style="display:none;" data-i18n="knxUltimateMatterControllerDevice.door_lock_pin_required"></div>
1737
+ <div id="matter-door-lock-command-warning" class="form-tips" style="display:none; color:#a94442;" data-i18n="knxUltimateMatterControllerDevice.door_lock_commands_missing"></div>
1738
+ </div>
1594
1739
  </p>
1595
1740
  </div>
1596
1741
  <div id="tabs-2">
@@ -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', {
@@ -15,6 +15,8 @@ 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. |
@@ -17,10 +17,19 @@
17
17
  "tunable_white": "Einstellbares Weiß",
18
18
  "rgb_hsv": "RGB/HSV",
19
19
  "effects": "Effekte",
20
+ "mappings": "Zuordnungen",
20
21
  "behaviour": "Verhalten"
21
22
  },
22
23
  "control": "Steuerung",
23
24
  "status": "Status",
25
+ "door_lock_command": "Verriegeln / entriegeln",
26
+ "door_lock_status": "Schlossstatus",
27
+ "door_lock_pin": "PIN für Fernbedienung",
28
+ "door_lock_pin_required": "Dieses Schloss meldet, dass für die Fernbedienung eine PIN erforderlich ist.",
29
+ "door_lock_commands_missing": "Dieser Endpunkt bietet nicht beide Befehle zum Verriegeln und Entriegeln an. Fehlende Befehle werden abgewiesen.",
30
+ "mapped": {
31
+ "onoff_command": "Ein/Aus-Befehl", "onoff_state": "Ein/Aus-Status", "cover_updown": "Jalousie auf/ab", "cover_stop": "Jalousie Stopp", "cover_position_command": "Positionsbefehl", "cover_position_state": "Positionsstatus", "heating_command": "Heizsollwert-Befehl", "heating_state": "Heizsollwert-Status", "cooling_command": "Kühlsollwert-Befehl", "cooling_state": "Kühlsollwert-Status", "local_temperature": "Lokale Temperatur", "fan_command": "Lüfterdrehzahl-Befehl", "fan_state": "Lüfterdrehzahl-Status", "temperature": "Temperatur", "humidity": "Luftfeuchte", "illuminance": "Beleuchtungsstärke", "occupancy": "Belegung", "contact": "Binär-/Kontaktstatus", "battery": "Batterie", "active_power": "Wirkleistung", "imported_energy": "Bezogene Energie"
32
+ },
24
33
  "night_lighting": "Nachtbeleuchtung",
25
34
  "no_night_lighting": "Keine Nachtbeleuchtung",
26
35
  "get_current": "Aktuellen Wert holen",
@@ -82,6 +91,7 @@
82
91
  "effect_native_label": "Hue-native Effekte",
83
92
  "matter_controller": "Matter-Controller",
84
93
  "matter_device": "Matter-Gerät",
94
+ "device_type": "Gerätetyp",
85
95
  "matter_section": "Matter-Gerät"
86
96
  },
87
97
  "common": {
@@ -15,6 +15,8 @@ It replaces the unpublished per-device Matter controller nodes and keeps the ful
15
15
  | Matter controller | Matter controller configuration node where the device has been commissioned. |
16
16
  | Matter device | Matter endpoint selected from commissioned devices. The UI is rebuilt from its real capabilities. |
17
17
  | Switch / Plug / Light On-Off | On/Off command and status group addresses, usually DPT `1.001`. |
18
+ | Door Lock | A DPT `1.xxx` command GA invokes `lockDoor` for `true` and `unlockDoor` for `false`; a separate status GA receives only unambiguous Locked/Unlocked states. If the endpoint requires it, store the remote-operation PIN in the credential field. Commands not advertised by the endpoint are rejected. |
19
+ | Other endpoints | Plugs, On/Off actuators, covers, thermostats, fans, environmental/contact/occupancy sensors, battery, power and energy endpoints use the multi-purpose mapped profile. The dedicated **Mappings** tab contains only mappings backed by clusters, attributes and commands advertised by that endpoint; leave a GA empty to disable it. |
18
20
  | Light controls | For light endpoints, the same light UI is used: relative DIM (DPT `3.007`), brightness %, RGB/HSV, tunable white, switch-on brightness/temperature, day/night lighting, min/max dim level and dim speed. Unsupported sections are hidden. |
19
21
  | Sensors | Sensor endpoints expose their measurement/status GA only when supported: temperature, humidity, illuminance, occupancy, contact and battery. |
20
22
  | Read at startup | Publishes the cached Matter value at deploy/startup or when the device reconnects. |
@@ -17,10 +17,19 @@
17
17
  "tunable_white": "Tunable white",
18
18
  "rgb_hsv": "RGB/HSV",
19
19
  "effects": "Effects",
20
+ "mappings": "Mappings",
20
21
  "behaviour": "Behaviour"
21
22
  },
22
23
  "control": "Control",
23
24
  "status": "Status",
25
+ "door_lock_command": "Lock / unlock",
26
+ "door_lock_status": "Lock state",
27
+ "door_lock_pin": "Remote-operation PIN",
28
+ "door_lock_pin_required": "This lock reports that a PIN is required for remote operation.",
29
+ "door_lock_commands_missing": "This endpoint does not advertise both lock and unlock commands. Missing commands will be rejected.",
30
+ "mapped": {
31
+ "onoff_command": "On / Off command", "onoff_state": "On / Off state", "cover_updown": "Cover up / down", "cover_stop": "Cover stop", "cover_position_command": "Cover position command", "cover_position_state": "Cover position state", "heating_command": "Heating setpoint command", "heating_state": "Heating setpoint state", "cooling_command": "Cooling setpoint command", "cooling_state": "Cooling setpoint state", "local_temperature": "Local temperature", "fan_command": "Fan speed command", "fan_state": "Fan speed state", "temperature": "Temperature", "humidity": "Humidity", "illuminance": "Illuminance", "occupancy": "Occupancy", "contact": "Boolean / contact state", "battery": "Battery", "active_power": "Active power", "imported_energy": "Imported energy"
32
+ },
24
33
  "night_lighting": "Night Lighting",
25
34
  "no_night_lighting": "No night lighting",
26
35
  "get_current": "Get current",
@@ -82,6 +91,7 @@
82
91
  "effect_native_label": "Hue native effects",
83
92
  "matter_controller": "Matter controller",
84
93
  "matter_device": "Matter device",
94
+ "device_type": "Device type",
85
95
  "matter_section": "Matter device"
86
96
  },
87
97
  "common": {
@@ -15,6 +15,8 @@ Sustituye a los nodos Matter separados no publicados y conserva toda la UI de lu
15
15
  | Matter controller | Nodo de configuración Matter Controller donde el dispositivo fue emparejado. |
16
16
  | Dispositivo Matter | Endpoint Matter seleccionado entre los dispositivos emparejados. La UI se reconstruye a partir de sus capacidades reales. |
17
17
  | Switch / Enchufe / Luz On-Off | Direcciones de grupo de comando y estado On/Off, normalmente DPT `1.001`. |
18
+ | Cerradura | Una GA de comando DPT `1.xxx` invoca `lockDoor` con `true` y `unlockDoor` con `false`; una GA de estado separada recibe solo estados Bloqueada/Desbloqueada inequívocos. Si el endpoint lo exige, el PIN remoto se guarda en el campo de credencial. Los comandos no anunciados se rechazan. |
19
+ | Otros endpoints | Enchufes, actuadores On/Off, persianas, termostatos, ventiladores, sensores ambientales/de contacto/de ocupación, batería, potencia y energía usan el perfil multipropósito. La pestaña **Mapeos** contiene únicamente las funciones respaldadas por los clústeres, atributos y comandos anunciados. |
18
20
  | Controles de luz | Para endpoints de luz se usa la UI de luz completa: DIM relativo (DPT `3.007`), brillo %, RGB/HSV, blanco ajustable, brillo/temperatura al encender, modo día/noche, nivel min/max y velocidad de regulación. Las secciones no soportadas quedan ocultas. |
19
21
  | Sensores | Los endpoints de sensor muestran su GA de medida/estado solo cuando está soportado: temperatura, humedad, iluminancia, ocupación, contacto y batería. |
20
22
  | Read at startup | Publica el valor Matter en caché al desplegar/iniciar o cuando el dispositivo se reconecta. |
@@ -17,10 +17,19 @@
17
17
  "tunable_white": "Blanco sintonizable",
18
18
  "rgb_hsv": "RGB/HSV",
19
19
  "effects": "Efectos",
20
+ "mappings": "Mapeos",
20
21
  "behaviour": "Comportamiento"
21
22
  },
22
23
  "control": "Control",
23
24
  "status": "Estado",
25
+ "door_lock_command": "Bloquear / desbloquear",
26
+ "door_lock_status": "Estado de la cerradura",
27
+ "door_lock_pin": "PIN de operación remota",
28
+ "door_lock_pin_required": "La cerradura indica que se requiere un PIN para operaciones remotas.",
29
+ "door_lock_commands_missing": "Este endpoint no anuncia ambos comandos de bloqueo y desbloqueo. Los comandos ausentes serán rechazados.",
30
+ "mapped": {
31
+ "onoff_command": "Comando Encendido / Apagado", "onoff_state": "Estado Encendido / Apagado", "cover_updown": "Persiana subir / bajar", "cover_stop": "Parar persiana", "cover_position_command": "Comando posición persiana", "cover_position_state": "Estado posición persiana", "heating_command": "Comando consigna calefacción", "heating_state": "Estado consigna calefacción", "cooling_command": "Comando consigna refrigeración", "cooling_state": "Estado consigna refrigeración", "local_temperature": "Temperatura local", "fan_command": "Comando velocidad ventilador", "fan_state": "Estado velocidad ventilador", "temperature": "Temperatura", "humidity": "Humedad", "illuminance": "Iluminancia", "occupancy": "Ocupación", "contact": "Estado booleano / contacto", "battery": "Batería", "active_power": "Potencia activa", "imported_energy": "Energía importada"
32
+ },
24
33
  "night_lighting": "Iluminación nocturna",
25
34
  "no_night_lighting": "Sin iluminación nocturna",
26
35
  "get_current": "Obtener",
@@ -82,6 +91,7 @@
82
91
  "effect_native_label": "Efectos nativos de tono",
83
92
  "matter_controller": "Controlador Matter",
84
93
  "matter_device": "Dispositivo Matter",
94
+ "device_type": "Tipo de dispositivo",
85
95
  "matter_section": "Dispositivo Matter"
86
96
  },
87
97
  "common": {
@@ -15,6 +15,8 @@ Il remplace les nœuds Matter séparés non publiés et conserve toute l'UI lumi
15
15
  | Matter controller | Nœud de configuration Matter Controller dans lequel le périphérique a été appairé. |
16
16
  | Appareil Matter | Endpoint Matter choisi parmi les appareils appairés. L'UI est reconstruite à partir de ses capacités réelles. |
17
17
  | Switch / Prise / Lumière On-Off | Adresses de groupe commande et état On/Off, généralement DPT `1.001`. |
18
+ | Serrure | Une AG de commande DPT `1.xxx` appelle `lockDoor` avec `true` et `unlockDoor` avec `false` ; une AG d'état séparée reçoit uniquement les états Verrouillé/Déverrouillé non ambigus. Si nécessaire, le PIN distant est conservé dans le champ d'identification. Les commandes non annoncées sont refusées. |
19
+ | Autres points de terminaison | Prises, actionneurs On/Off, volets, thermostats, ventilateurs, capteurs d'environnement/contact/occupation, batterie, puissance et énergie utilisent le profil polyvalent. L'onglet **Mappages** contient uniquement les fonctions prises en charge par les clusters, attributs et commandes annoncés. |
18
20
  | Contrôles lumière | Pour les endpoints lumière, l'UI lumière complète est utilisée : DIM relatif (DPT `3.007`), luminosité %, RGB/HSV, blanc réglable, luminosité/température à l'allumage, mode jour/nuit, niveau min/max et vitesse de variation. Les sections non supportées restent masquées. |
19
21
  | Capteurs | Les endpoints capteur affichent leur GA de mesure/état uniquement si elle est supportée : température, humidité, éclairement, occupation, contact et batterie. |
20
22
  | Read at startup | Publie la valeur Matter en cache au déploiement/démarrage ou quand le périphérique se reconnecte. |
@@ -17,10 +17,19 @@
17
17
  "tunable_white": "White à réglage réglable",
18
18
  "rgb_hsv": "RVB / HSV",
19
19
  "effects": "Effets",
20
+ "mappings": "Mappages",
20
21
  "behaviour": "Comportement"
21
22
  },
22
23
  "control": "Contrôle",
23
24
  "status": "Statut",
25
+ "door_lock_command": "Verrouiller / déverrouiller",
26
+ "door_lock_status": "État de la serrure",
27
+ "door_lock_pin": "PIN d'opération à distance",
28
+ "door_lock_pin_required": "Cette serrure indique qu'un PIN est requis pour les opérations à distance.",
29
+ "door_lock_commands_missing": "Ce point de terminaison n'annonce pas les deux commandes de verrouillage et déverrouillage. Les commandes absentes seront refusées.",
30
+ "mapped": {
31
+ "onoff_command": "Commande Marche / Arrêt", "onoff_state": "État Marche / Arrêt", "cover_updown": "Volet montée / descente", "cover_stop": "Arrêt volet", "cover_position_command": "Commande position volet", "cover_position_state": "État position volet", "heating_command": "Commande consigne chauffage", "heating_state": "État consigne chauffage", "cooling_command": "Commande consigne refroidissement", "cooling_state": "État consigne refroidissement", "local_temperature": "Température locale", "fan_command": "Commande vitesse ventilateur", "fan_state": "État vitesse ventilateur", "temperature": "Température", "humidity": "Humidité", "illuminance": "Éclairement", "occupancy": "Occupation", "contact": "État booléen / contact", "battery": "Batterie", "active_power": "Puissance active", "imported_energy": "Énergie importée"
32
+ },
24
33
  "night_lighting": "Éclairage nocturne",
25
34
  "no_night_lighting": "Pas d'éclairage de nuit",
26
35
  "get_current": "Prendre le courant",
@@ -74,6 +83,7 @@
74
83
  "effect_native_label": "Effets natifs de la teinte",
75
84
  "matter_controller": "Contrôleur Matter",
76
85
  "matter_device": "Appareil Matter",
86
+ "device_type": "Type d'appareil",
77
87
  "matter_section": "Appareil Matter"
78
88
  },
79
89
  "common": {
@@ -15,6 +15,8 @@ Sostituisce i nodi Matter separati non pubblicati e mantiene tutta la UI luce qu
15
15
  | Matter controller | Nodo di configurazione Matter Controller in cui il dispositivo è stato associato. |
16
16
  | Dispositivo Matter | Endpoint Matter selezionato tra i dispositivi abbinati. La UI viene ricostruita in base alle capability reali. |
17
17
  | Switch / Presa / Luce On-Off | Indirizzi di gruppo di comando e stato On/Off, di solito DPT `1.001`. |
18
+ | Serratura | Un GA comando DPT `1.xxx` invoca `lockDoor` con `true` e `unlockDoor` con `false`; un GA di stato separato riceve soltanto gli stati non ambigui Bloccata/Sbloccata. Se richiesto dall'endpoint, salva il PIN per operazioni remote nel campo credential. I comandi non annunciati dall'endpoint vengono rifiutati. |
19
+ | Altri endpoint | Prese, attuatori On/Off, tapparelle, termostati, ventilatori, sensori ambientali/contatto/presenza, batteria, potenza ed energia usano il profilo multi-purpose. La TAB **Mappature** contiene soltanto le funzioni supportate dai cluster, attributi e comandi annunciati dall'endpoint; lascia vuoto un GA per disabilitarlo. |
18
20
  | Controlli luce | Per gli endpoint luce viene usata la stessa UI luce completa: DIM relativo (DPT `3.007`), luminosità %, RGB/HSV, bianco dinamico, luminosità/temperatura all'accensione, modalità giorno/notte, livello min/max e velocità dimmer. Le sezioni non supportate restano nascoste. |
19
21
  | Sensori | Gli endpoint sensore mostrano il relativo GA di misura/stato solo quando supportato: temperatura, umidità, illuminamento, presenza, contatto e batteria. |
20
22
  | Read at startup | Pubblica il valore Matter in cache al deploy/avvio o quando il dispositivo si riconnette. |
@@ -17,10 +17,19 @@
17
17
  "tunable_white": "Bianco regolabile",
18
18
  "rgb_hsv": "RGB/HSV",
19
19
  "effects": "Effetti",
20
+ "mappings": "Mappature",
20
21
  "behaviour": "Comportamento"
21
22
  },
22
23
  "control": "Comando",
23
24
  "status": "Stato",
25
+ "door_lock_command": "Blocca / sblocca",
26
+ "door_lock_status": "Stato serratura",
27
+ "door_lock_pin": "PIN operazione remota",
28
+ "door_lock_pin_required": "La serratura indica che per le operazioni remote è richiesto un PIN.",
29
+ "door_lock_commands_missing": "L'endpoint non annuncia entrambi i comandi di blocco e sblocco. I comandi mancanti saranno rifiutati.",
30
+ "mapped": {
31
+ "onoff_command": "Comando On / Off", "onoff_state": "Stato On / Off", "cover_updown": "Tapparella su / giù", "cover_stop": "Stop tapparella", "cover_position_command": "Comando posizione tapparella", "cover_position_state": "Stato posizione tapparella", "heating_command": "Comando setpoint riscaldamento", "heating_state": "Stato setpoint riscaldamento", "cooling_command": "Comando setpoint raffrescamento", "cooling_state": "Stato setpoint raffrescamento", "local_temperature": "Temperatura locale", "fan_command": "Comando velocità ventilatore", "fan_state": "Stato velocità ventilatore", "temperature": "Temperatura", "humidity": "Umidità", "illuminance": "Illuminamento", "occupancy": "Presenza", "contact": "Stato booleano / contatto", "battery": "Batteria", "active_power": "Potenza attiva", "imported_energy": "Energia importata"
32
+ },
24
33
  "night_lighting": "Illuminazione notturna",
25
34
  "no_night_lighting": "Nessuna illuminazione notturna",
26
35
  "get_current": "Ottieni corrente",
@@ -82,6 +91,7 @@
82
91
  "effect_native_label": "Effetti nativi HUE",
83
92
  "matter_controller": "Controller Matter",
84
93
  "matter_device": "Dispositivo Matter",
94
+ "device_type": "Tipo dispositivo",
85
95
  "matter_section": "Dispositivo Matter"
86
96
  },
87
97
  "common": {
@@ -15,6 +15,8 @@
15
15
  | Matter controller | 设备已在其中配网的 Matter Controller 配置节点。 |
16
16
  | Matter device | 从已配对设备中选择的 Matter endpoint。UI 会根据真实能力重新构建。 |
17
17
  | Switch / 插座 / 灯 On-Off | On/Off 命令和状态组地址,通常使用 DPT `1.001`。 |
18
+ | 门锁 | DPT `1.xxx` 命令组地址以 `true` 调用 `lockDoor`、以 `false` 调用 `unlockDoor`;独立状态组地址仅接收明确的已上锁/已解锁状态。如端点要求,远程操作 PIN 保存在凭据字段中。端点未声明的命令会被拒绝。 |
19
+ | 其他端点 | 插座、开关执行器、窗帘、恒温器、风扇、环境/接点/占用传感器以及电池、功率和电能端点使用多用途映射配置。专用的 **映射** 选项卡仅包含端点实际声明的集群、属性和命令;组地址留空即可禁用。 |
18
20
  | 灯光控制 | 对灯光 endpoint 使用完整灯光 UI:相对调光(DPT `3.007`)、亮度百分比、RGB/HSV、色温、开灯亮度/温度、日/夜模式、最小/最大调光等级和调光速度。不支持的部分会隐藏。 |
19
21
  | 传感器 | 传感器 endpoint 只在支持时显示对应测量/状态 GA:温度、湿度、照度、占用、接触和电池。 |
20
22
  | Read at startup | 在部署/启动或设备重新连接时发布缓存的 Matter 值。 |
@@ -17,10 +17,19 @@
17
17
  "tunable_white": "可调白光",
18
18
  "rgb_hsv": "RGB/HSV",
19
19
  "effects": "效果",
20
+ "mappings": "映射",
20
21
  "behaviour": "行为"
21
22
  },
22
23
  "control": "控制",
23
24
  "status": "状态",
25
+ "door_lock_command": "上锁 / 解锁",
26
+ "door_lock_status": "门锁状态",
27
+ "door_lock_pin": "远程操作 PIN",
28
+ "door_lock_pin_required": "此门锁报告远程操作需要 PIN。",
29
+ "door_lock_commands_missing": "此端点未声明完整的上锁和解锁命令;缺失的命令将被拒绝。",
30
+ "mapped": {
31
+ "onoff_command": "开/关命令", "onoff_state": "开/关状态", "cover_updown": "窗帘上/下", "cover_stop": "窗帘停止", "cover_position_command": "窗帘位置命令", "cover_position_state": "窗帘位置状态", "heating_command": "制热设定值命令", "heating_state": "制热设定值状态", "cooling_command": "制冷设定值命令", "cooling_state": "制冷设定值状态", "local_temperature": "本地温度", "fan_command": "风扇速度命令", "fan_state": "风扇速度状态", "temperature": "温度", "humidity": "湿度", "illuminance": "照度", "occupancy": "占用", "contact": "布尔/接点状态", "battery": "电池", "active_power": "有功功率", "imported_energy": "输入电能"
32
+ },
24
33
  "night_lighting": "夜间照明",
25
34
  "no_night_lighting": "无夜间照明",
26
35
  "get_current": "获取当前",
@@ -74,6 +83,7 @@
74
83
  "effect_native_label": "Hue 原生效果",
75
84
  "matter_controller": "Matter 控制器",
76
85
  "matter_device": "Matter 设备",
86
+ "device_type": "设备类型",
77
87
  "matter_section": "Matter 设备"
78
88
  },
79
89
  "common": {
@@ -0,0 +1,210 @@
1
+ 'use strict'
2
+
3
+ const dptlib = require('knxultimate').dptlib
4
+
5
+ const DOOR_LOCK_CLUSTER_ID = 0x0101
6
+ const LOCK_STATE = {
7
+ NOT_FULLY_LOCKED: 0,
8
+ LOCKED: 1,
9
+ UNLOCKED: 2,
10
+ UNLATCHED: 3
11
+ }
12
+
13
+ const decodeKnxBoolean = (msg, dpt) => {
14
+ if (msg && msg.knx && Buffer.isBuffer(msg.knx.rawValue)) {
15
+ return !!dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(dpt || '1.001'))
16
+ }
17
+ return !!(msg && msg.payload)
18
+ }
19
+
20
+ const lockStateName = (value) => {
21
+ switch (Number(value)) {
22
+ case LOCK_STATE.LOCKED: return 'locked'
23
+ case LOCK_STATE.UNLOCKED: return 'unlocked'
24
+ case LOCK_STATE.NOT_FULLY_LOCKED: return 'notFullyLocked'
25
+ case LOCK_STATE.UNLATCHED: return 'unlatched'
26
+ default: return 'unknown'
27
+ }
28
+ }
29
+
30
+ const lockStateToBoolean = (value) => {
31
+ if (Number(value) === LOCK_STATE.LOCKED) return true
32
+ if (Number(value) === LOCK_STATE.UNLOCKED) return false
33
+ return undefined
34
+ }
35
+
36
+ const setupDoorLockProfile = (RED, node, config) => {
37
+ node.name = config.name || node.matterDeviceName || 'Control Matter door lock from KNX'
38
+ node.topic = node.name
39
+ node.notifyreadrequest = true
40
+ node.notifyreadrequestalsorespondtobus = 'false'
41
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
42
+ node.notifyresponse = false
43
+ node.notifywrite = true
44
+ node.initialread = true
45
+ node.listenallga = true
46
+ node.outputtype = 'write'
47
+ node.outputRBE = 'false'
48
+ node.inputRBE = 'false'
49
+ node.passthrough = 'no'
50
+ node.currentLockState = undefined
51
+ node.knxUltimateAcceptedGAs = [config.GALightSwitch, config.GALightState]
52
+ .map((ga) => String(ga || '').trim())
53
+ .filter((ga) => ga !== '')
54
+
55
+ const setStatus = (fill, shape, text) => node.status({ fill, shape, text })
56
+
57
+ const commandArgs = () => {
58
+ const pin = String(node.credentials?.doorLockPin || '')
59
+ return pin === '' ? {} : { pinCode: Buffer.from(pin, 'utf8') }
60
+ }
61
+
62
+ const sendFlow = (source, state, rawState) => {
63
+ if (config.enableNodePINS !== 'yes') return
64
+ node.send({
65
+ topic: node.topic,
66
+ payload: state,
67
+ matter: {
68
+ source,
69
+ nodeId: node.matterNodeId,
70
+ endpointId: node.matterEndpointId,
71
+ clusterId: DOOR_LOCK_CLUSTER_ID,
72
+ lockState: rawState,
73
+ lockStateName: rawState === undefined ? (state ? 'locked' : 'unlocked') : lockStateName(rawState)
74
+ }
75
+ })
76
+ }
77
+
78
+ const writeKnxState = (state, outputtype = 'write') => {
79
+ const ga = String(config.GALightState || '').trim()
80
+ if (ga === '' || !node.serverKNX) return
81
+ node.serverKNX.sendKNXTelegramToKNXEngine({
82
+ grpaddr: ga,
83
+ payload: state,
84
+ dpt: config.dptLightState || '1.001',
85
+ outputtype,
86
+ nodecallerid: node.id
87
+ })
88
+ }
89
+
90
+ const publishMatterState = (rawState, source = 'matter') => {
91
+ node.currentLockState = Number(rawState)
92
+ const state = lockStateToBoolean(rawState)
93
+ const name = lockStateName(rawState)
94
+ if (state !== undefined) writeKnxState(state)
95
+ sendFlow(source, state, rawState)
96
+ setStatus(state === undefined ? 'yellow' : 'blue', state === undefined ? 'ring' : 'dot', `Matter: ${name}`)
97
+ }
98
+
99
+ const queueCommand = (locked, source = 'knx') => {
100
+ const manager = node.serverMatter?.matterManager
101
+ if (!manager) throw new Error('Matter controller not ready')
102
+ const capabilities = (() => {
103
+ try { return JSON.parse(config.matterDeviceCapabilities || '{}') } catch (error) { return {} }
104
+ })()
105
+ if (locked && capabilities.lockDoor === false) throw new Error('The Matter endpoint does not expose lockDoor')
106
+ if (!locked && capabilities.unlockDoor === false) throw new Error('The Matter endpoint does not expose unlockDoor')
107
+ const queued = manager.writeMatterQueueAdd({
108
+ nodeId: node.matterNodeId,
109
+ endpointId: node.matterEndpointId,
110
+ clusterId: DOOR_LOCK_CLUSTER_ID,
111
+ kind: 'command',
112
+ name: locked ? 'lockDoor' : 'unlockDoor',
113
+ args: commandArgs()
114
+ })
115
+ if (queued && typeof queued.catch === 'function') {
116
+ queued.catch((error) => {
117
+ RED.log.error(`knxUltimateMatterControllerDevice DoorLock command: ${error.message}`)
118
+ setStatus('red', 'ring', error.message)
119
+ })
120
+ }
121
+ sendFlow(source, locked)
122
+ setStatus('green', 'dot', `KNX→Matter: ${locked ? 'lock' : 'unlock'}`)
123
+ }
124
+
125
+ node.handleSend = (msg) => {
126
+ try {
127
+ if (!msg?.knx || !node.knxUltimateAcceptedGAs.includes(String(msg.knx.destination || '').trim())) return
128
+ if (msg.knx.event === 'GroupValue_Read') {
129
+ if (String(msg.knx.destination) === String(config.GALightState)) {
130
+ const state = lockStateToBoolean(node.currentLockState)
131
+ if (state !== undefined) writeKnxState(state, 'response')
132
+ }
133
+ return
134
+ }
135
+ if (String(msg.knx.destination) !== String(config.GALightSwitch)) return
136
+ queueCommand(decodeKnxBoolean(msg, config.dptLightSwitch), 'knx')
137
+ } catch (error) {
138
+ RED.log.error(`knxUltimateMatterControllerDevice DoorLock KNX: ${error.message}`)
139
+ setStatus('red', 'ring', error.message)
140
+ }
141
+ }
142
+
143
+ node.handleSendMatter = (event) => {
144
+ try {
145
+ if (String(event?.nodeId) !== String(node.matterNodeId)) return
146
+ if (Number(event?.endpointId) !== Number(node.matterEndpointId)) return
147
+ if (Number(event?.clusterId) !== DOOR_LOCK_CLUSTER_ID || event.attributeName !== 'lockState') return
148
+ publishMatterState(event.value)
149
+ } catch (error) {
150
+ RED.log.error(`knxUltimateMatterControllerDevice DoorLock Matter: ${error.message}`)
151
+ }
152
+ }
153
+ node.handleMatterClusterEvent = () => {}
154
+ node.handleMatterNodeInitialized = () => {
155
+ try {
156
+ const value = node.serverMatter?.matterManager?.getCachedAttribute(
157
+ node.matterNodeId,
158
+ node.matterEndpointId,
159
+ DOOR_LOCK_CLUSTER_ID,
160
+ 'lockState'
161
+ )
162
+ if (value !== undefined && value !== null) publishMatterState(value, 'initial')
163
+ } catch (error) {
164
+ RED.log.error(`knxUltimateMatterControllerDevice DoorLock initial state: ${error.message}`)
165
+ }
166
+ }
167
+ node.setNodeStatusMatter = (status) => {
168
+ if (status && status.text) setStatus(status.fill || 'grey', status.shape || 'ring', status.text)
169
+ }
170
+
171
+ if (node.serverKNX) {
172
+ node.serverKNX.removeClient(node)
173
+ node.serverKNX.addClient(node)
174
+ } else {
175
+ setStatus('yellow', 'ring', 'No KNX gateway selected')
176
+ }
177
+ if (node.serverMatter) {
178
+ node.serverMatter.removeClient(node)
179
+ node.serverMatter.addClient(node)
180
+ }
181
+
182
+ node.on('input', (msg, send, done) => {
183
+ try {
184
+ const value = typeof msg.payload === 'object' && msg.payload !== null && msg.payload.locked !== undefined
185
+ ? msg.payload.locked
186
+ : msg.payload
187
+ if (typeof value !== 'boolean') throw new Error('Door Lock input requires msg.payload boolean or { locked: boolean }')
188
+ queueCommand(value, 'flow')
189
+ if (done) done()
190
+ } catch (error) {
191
+ setStatus('red', 'ring', error.message)
192
+ if (done) done(error)
193
+ else node.error(error, msg)
194
+ }
195
+ })
196
+
197
+ node.on('close', (done) => {
198
+ try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
199
+ try { if (node.serverMatter) node.serverMatter.removeClient(node) } catch (error) { /* empty */ }
200
+ done()
201
+ })
202
+ }
203
+
204
+ module.exports = {
205
+ DOOR_LOCK_CLUSTER_ID,
206
+ LOCK_STATE,
207
+ lockStateName,
208
+ lockStateToBoolean,
209
+ setupDoorLockProfile
210
+ }
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const { setupDoorLockProfile } = require('./doorLock')
4
+ const { setupMappedEndpointProfile } = require('./mappedEndpoint')
5
+
6
+ // Controller-side Matter device profiles live behind this registry. Keep profile
7
+ // selection capability-driven: the editor records the profile only after inspecting
8
+ // the endpoint's actual device types, clusters, attributes and supported commands.
9
+ const PROFILE_SETUPS = Object.freeze({
10
+ doorLock: setupDoorLockProfile,
11
+ mapped: setupMappedEndpointProfile
12
+ })
13
+
14
+ const setupMatterControllerProfile = (profile, RED, node, config) => {
15
+ const setup = PROFILE_SETUPS[profile]
16
+ if (typeof setup !== 'function') return false
17
+ setup(RED, node, config)
18
+ return true
19
+ }
20
+
21
+ module.exports = { PROFILE_SETUPS, setupMatterControllerProfile }
@@ -0,0 +1,148 @@
1
+ 'use strict'
2
+
3
+ const dptlib = require('knxultimate').dptlib
4
+ const { knxToMatter, matterToKnx } = require('../matterKnxConverter')
5
+
6
+ const isValidGroupAddress = (value) => {
7
+ const parts = String(value || '').trim().split('/')
8
+ if (parts.length !== 3 || parts.some((part) => !/^\d+$/.test(part))) return false
9
+ const [main, middle, sub] = parts.map(Number)
10
+ return main >= 0 && main <= 31 && middle >= 0 && middle <= 7 && sub >= 0 && sub <= 255
11
+ }
12
+
13
+ const parseMappings = (value) => {
14
+ try {
15
+ const parsed = Array.isArray(value) ? value : JSON.parse(value || '[]')
16
+ return parsed.filter((mapping) => {
17
+ if (!mapping || !isValidGroupAddress(mapping.ga) || !mapping.target || !mapping.dpt) return false
18
+ try { dptlib.resolve(mapping.dpt); return true } catch (error) { return false }
19
+ })
20
+ } catch (error) {
21
+ return []
22
+ }
23
+ }
24
+
25
+ const setupMappedEndpointProfile = (RED, node, config) => {
26
+ node.name = config.name || node.matterDeviceName || 'Control Matter from KNX'
27
+ node.topic = node.name
28
+ node.mappings = parseMappings(config.matterMappings)
29
+ node.knxUltimateAcceptedGAs = [...new Set(node.mappings.map((mapping) => String(mapping.ga).trim()))]
30
+ node.notifyreadrequest = true
31
+ node.notifyreadrequestalsorespondtobus = 'false'
32
+ node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
33
+ node.notifyresponse = false
34
+ node.notifywrite = true
35
+ node.initialread = true
36
+ node.listenallga = true
37
+ node.outputtype = 'write'
38
+ node.outputRBE = 'false'
39
+ node.inputRBE = 'false'
40
+ node.passthrough = 'no'
41
+ const enablePins = config.enableNodePINS === 'yes'
42
+ let lastInitialReadTs = 0
43
+
44
+ const status = (fill, shape, text) => node.status({ fill, shape, text })
45
+ const manager = () => node.serverMatter?.matterManager
46
+ const sendKnx = (mapping, payload, outputtype = 'write') => {
47
+ if (payload === undefined || !node.serverKNX) return false
48
+ node.serverKNX.sendKNXTelegramToKNXEngine({
49
+ grpaddr: mapping.ga,
50
+ payload,
51
+ dpt: mapping.dpt,
52
+ outputtype,
53
+ nodecallerid: node.id
54
+ })
55
+ return true
56
+ }
57
+ const sendCached = (mapping, outputtype) => {
58
+ const currentManager = manager()
59
+ if (!currentManager) return false
60
+ const value = currentManager.getCachedAttribute(node.matterNodeId, mapping.endpointId, mapping.clusterId, mapping.target)
61
+ return sendKnx(mapping, matterToKnx(mapping.clusterId, mapping.target, value), outputtype)
62
+ }
63
+ const enqueue = (mapping, value) => {
64
+ const currentManager = manager()
65
+ if (!currentManager) throw new Error('Matter controller not ready')
66
+ const action = knxToMatter(mapping, value)
67
+ if (!action) return
68
+ const queued = currentManager.writeMatterQueueAdd({
69
+ nodeId: node.matterNodeId,
70
+ endpointId: mapping.endpointId,
71
+ clusterId: mapping.clusterId,
72
+ kind: action.kind,
73
+ name: action.name,
74
+ args: action.args
75
+ })
76
+ if (queued && typeof queued.catch === 'function') queued.catch((error) => status('red', 'ring', error.message))
77
+ }
78
+
79
+ node.handleSend = (msg) => {
80
+ try {
81
+ if (!msg?.knx || !node.knxUltimateAcceptedGAs.includes(String(msg.knx.destination || '').trim())) return
82
+ const matches = node.mappings.filter((mapping) => mapping.ga === msg.knx.destination)
83
+ if (msg.knx.event === 'GroupValue_Read') {
84
+ matches.filter((mapping) => mapping.direction === 'status').forEach((mapping) => sendCached(mapping, 'response'))
85
+ return
86
+ }
87
+ matches.filter((mapping) => mapping.direction === 'command').forEach((mapping) => {
88
+ const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(mapping.dpt))
89
+ enqueue(mapping, value)
90
+ })
91
+ status('green', 'dot', 'KNX→Matter')
92
+ } catch (error) {
93
+ RED.log.error(`knxUltimateMatterControllerDevice mapped KNX: ${error.message}`)
94
+ status('red', 'ring', error.message)
95
+ }
96
+ }
97
+ node.handleSendMatter = (event) => {
98
+ try {
99
+ if (String(event?.nodeId) !== String(node.matterNodeId) || Number(event?.endpointId) !== Number(node.matterEndpointId)) return
100
+ node.mappings.filter((mapping) => mapping.direction === 'status' && Number(mapping.clusterId) === Number(event.clusterId) && mapping.target === event.attributeName).forEach((mapping) => {
101
+ sendKnx(mapping, matterToKnx(event.clusterId, event.attributeName, event.value))
102
+ })
103
+ if (enablePins) node.send({ topic: `${event.clusterId}.${event.attributeName}`, payload: event.value, matter: event })
104
+ status('blue', 'dot', `Matter→KNX: ${event.attributeName}`)
105
+ } catch (error) {
106
+ RED.log.error(`knxUltimateMatterControllerDevice mapped Matter: ${error.message}`)
107
+ status('red', 'ring', error.message)
108
+ }
109
+ }
110
+ node.handleMatterClusterEvent = (event) => {
111
+ if (enablePins && String(event?.nodeId) === String(node.matterNodeId) && Number(event?.endpointId) === Number(node.matterEndpointId)) {
112
+ node.send({ topic: `${event.clusterId}.${event.eventName}`, payload: event.events, matter: event })
113
+ }
114
+ }
115
+ node.handleMatterNodeInitialized = () => {
116
+ if (config.readStatusAtStartup === 'no' || Date.now() - lastInitialReadTs < 5000) return
117
+ let sent = 0
118
+ node.mappings.filter((mapping) => mapping.direction === 'status').forEach((mapping) => { if (sendCached(mapping, 'write')) sent += 1 })
119
+ if (sent > 0) lastInitialReadTs = Date.now()
120
+ }
121
+ node.setNodeStatusMatter = (value) => { if (value?.text) status(value.fill || 'grey', value.shape || 'ring', value.text) }
122
+
123
+ if (node.serverKNX) { node.serverKNX.removeClient(node); node.serverKNX.addClient(node) } else status('yellow', 'ring', 'No KNX gateway selected')
124
+ if (node.serverMatter) { node.serverMatter.removeClient(node); node.serverMatter.addClient(node) }
125
+ node.on('input', (msg, send, done) => {
126
+ try {
127
+ const payload = msg.payload || {}
128
+ const mapping = {
129
+ endpointId: payload.endpointId ?? node.matterEndpointId,
130
+ clusterId: payload.clusterId,
131
+ targetKind: payload.command !== undefined ? 'command' : 'attribute',
132
+ target: payload.command ?? payload.attribute
133
+ }
134
+ if (!mapping.target || mapping.clusterId === undefined) throw new Error('Matter input requires clusterId and command or attribute')
135
+ enqueue(mapping, payload.args ?? payload.value)
136
+ if (done) done()
137
+ } catch (error) {
138
+ if (done) done(error); else node.error(error, msg)
139
+ }
140
+ })
141
+ node.on('close', (done) => {
142
+ try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
143
+ try { if (node.serverMatter) node.serverMatter.removeClient(node) } catch (error) { /* empty */ }
144
+ done()
145
+ })
146
+ }
147
+
148
+ module.exports = { isValidGroupAddress, parseMappings, setupMappedEndpointProfile }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "6.0.6",
6
+ "version": "6.0.7",
7
7
  "description": "Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.",
8
8
  "files": [
9
9
  "nodes/",
@@ -99,7 +99,11 @@
99
99
  "smarthome",
100
100
  "routing"
101
101
  ],
102
- "author": "Massimo Saccani (Supergiovane)",
102
+ "author": {
103
+ "name": "Massimo Saccani",
104
+ "email": "maxsupergiovane@icloud.com",
105
+ "url": "https://youtube.com/@maxsupervibe"
106
+ },
103
107
  "license": "MIT",
104
108
  "scripts": {
105
109
  "build": "npm run knx-ai:build && npm run knx-viewer:build",