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 +3 -1
- package/nodes/knxUltimateMatterControllerDevice.html +154 -9
- package/nodes/knxUltimateMatterControllerDevice.js +5 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +2 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +10 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +2 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +10 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +2 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +10 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +2 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +10 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +2 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +10 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +2 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +10 -0
- package/nodes/utils/matterControllerProfiles/doorLock.js +210 -0
- package/nodes/utils/matterControllerProfiles/index.js +21 -0
- package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +148 -0
- package/package.json +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
-
**Version 6.0.
|
|
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') || '
|
|
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
|
-
//
|
|
582
|
-
//
|
|
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
|
-
|
|
613
|
-
|
|
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
|
+
"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":
|
|
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",
|