node-red-contrib-knx-ultimate 6.0.12 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -2
- package/nodes/knxUltimateMatterControllerDevice.html +273 -13
- package/nodes/knxUltimateMatterControllerDevice.js +37 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +13 -1
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +22 -0
- package/nodes/locales/de/matter-config.html +11 -1
- package/nodes/locales/de/matter-config.json +12 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +13 -1
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +22 -0
- package/nodes/locales/en/matter-config.html +11 -1
- package/nodes/locales/en/matter-config.json +12 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +13 -1
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +22 -0
- package/nodes/locales/es/matter-config.html +11 -1
- package/nodes/locales/es/matter-config.json +12 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +13 -1
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +30 -0
- package/nodes/locales/fr/matter-config.html +11 -1
- package/nodes/locales/fr/matter-config.json +12 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +13 -1
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +22 -0
- package/nodes/locales/it/matter-config.html +11 -1
- package/nodes/locales/it/matter-config.json +12 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +13 -1
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +30 -0
- package/nodes/locales/zh-CN/matter-config.html +11 -1
- package/nodes/locales/zh-CN/matter-config.json +12 -0
- package/nodes/matter-config.html +215 -23
- package/nodes/matter-config.js +48 -2
- package/nodes/utils/matterControllerCommandGate.js +47 -0
- package/nodes/utils/matterControllerProfiles/doorLock.js +19 -4
- package/nodes/utils/matterControllerProfiles/fan.js +0 -1
- package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +32 -15
- package/nodes/utils/matterControllerProfiles/switch.js +0 -1
- package/nodes/utils/matterControllerProfiles/thermostat.js +0 -1
- package/nodes/utils/matterControllerProfiles/windowCovering.js +0 -1
- package/nodes/utils/matterControllerSemanticInput.js +212 -0
- package/nodes/utils/matterEngine.mjs +118 -16
- package/package.json +1 -1
- package/resources/matterQrScanner.js +98 -0
- package/resources/zxing-browser-0.2.0.min.js +1 -0
- package/resources/zxing-browser-LICENSE.txt +21 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
|
|
7
7
|
# CHANGELOG
|
|
8
8
|
|
|
9
|
-
**Version 6.0
|
|
9
|
+
**Version 6.1.0** - July 2026<br/>
|
|
10
10
|
|
|
11
|
+
- **Matter Controller — QR pairing-code reader**: the pairing editor can now fill the existing `MT:…` field by scanning a QR code with the local webcam or reading it from a selected image, including inverted white-on-dark QR codes used by some device web interfaces. Decoding remains entirely in the browser and never uploads the image or video; after a valid QR is read, pairing starts automatically, while manually typed codes still require **PAIR**. The optional device name is positioned before the scanner, the decoded code remains visible underneath it and fabric backup controls have moved to the bottom of the editor. A prominent blocking progress overlay prevents further editor interactions until commissioning succeeds or fails. When live camera access is unavailable because the editor is not open over HTTPS/`localhost`, permission was denied or the browser lacks camera support, the editor explains the reason and keeps image loading available.<br/>
|
|
12
|
+
- **Matter Controller — commissioned-device status**: fixed the connection-state import for the installed matter.js API, so the associated-devices table now reports `connected`, `disconnected`, `reconnecting` or `waitingfordiscovery` instead of misleading values such as `unknown (0)`. The table has also been regression-tested to preserve every row returned by the controller without collapsing equal Node IDs.<br/>
|
|
13
|
+
- **Matter Controller — per-device command isolation**: commands now remain ordered inside an independent execution lane for each commissioned Node ID instead of sharing one blocking serial queue. An offline, timing-out or removed device can no longer delay other Matter Controller nodes that react to the same KNX group address. Removed Node IDs are rejected before queueing, every Matter operation has a bounded timeout and pending commands are purged on removal. An unavailable Controller node latches its red status and ignores subsequent KNX/flow commands instead of accepting them optimistically; it resumes automatically when that specific Matter device reports `connected`, while opening the node editor explicitly enables a manual retry.<br/>
|
|
11
14
|
- **Control Matter from KNX — Universal Mode**: a dedicated **Mode** selector now switches between a single Matter device and controller-wide Universal Mode. Its **Universal Battery Monitor** scans every commissioned node and endpoint for battery Power Source clusters, maintains a normalized cache, publishes an optional aggregate KNX alarm as DPT 1.005, cycles low-battery device names every 2 seconds as DPT 16.001, and answers `getAllBatteries` flow queries. The alternative **All Matter events** service preserves the generic attribute/event stream and dynamic read/write/command input.<br/>
|
|
12
15
|
- **Universal Battery Monitor — KNX editor and diagnostics**: battery options now use the same vertical-tab and ETS/CSV group-address selectors as single-device profiles. Alarm and text mappings are aligned as Function/GA/DPT/Name rows, support editable names and KNX read responses, ignore wired Power Source endpoints, and show an aggregate battery status instead of per-device `unknown` states.<br/>
|
|
13
16
|
- **Matter light capability refresh**: Color Control `featureMap` and the endpoint device type now drive the Tunable White and RGB/HSV tabs. New selections, changed devices and previously saved nodes normalize stale capabilities so Tunable White lights no longer retain unsupported colour mappings.<br/>
|
|
14
17
|
- **Matter editor/help cleanup**: decorative animated light icons and duplicate localized help headings were removed from the Matter Controller, Matter Bridge and shared light editor where applicable.<br/>
|
|
15
18
|
- **Matter Bridge — editor tab orientation**: the device editor now uses the same left-hand vertical tab layout as Matter Controller, keeping Mappings and Advanced options visually consistent without changing saved configuration.<br/>
|
|
16
|
-
- **Control Matter from KNX —
|
|
19
|
+
- **Control Matter from KNX — simple flow input and capability help**: non-light endpoints now accept the same human-readable `msg.payload = { function, value }` contract used by Matter Bridge. The selected node and endpoint are implicit; percentages, temperatures and measurements use human units, while omitting `value` reads a supported state. A capability-driven **Flow input** tab, consistent with the node's existing editor tabs, replaces the modal help button and provides copyable simple examples plus every advertised readable/writable attribute, command, cluster ID and advanced message. It remains available for flow-only nodes without a KNX gateway. Existing top-level `clusterId`/`command`/`attribute` inputs remain compatible, and Door Lock also accepts `{function:"lock",value:true|false}`.<br/>
|
|
20
|
+
- **Control Matter from KNX — dedicated controller profiles**: Window Covering (`0x0102`), Thermostat (`0x0201`), Fan Control (`0x0202`) and Switch (`0x003B`) endpoints are now selected into explicit capability-driven profiles instead of the anonymous mapped fallback. They share the guarded mapping lifecycle while presenting semantic canvas status; Switch initial/long/multi-press events are filtered to the selected endpoint and exposed on the optional flow output. Simple actuators and sensors remain on the generic mapped profile.<br/>
|
|
17
21
|
- **Control Matter from KNX — flow attribute reads and persistent Door Lock status**: mapped endpoints now use an unambiguous top-level flow contract: `msg.clusterId` plus `msg.attribute` reads an attribute, `msg.value` makes it an attribute write, and `msg.command` plus `msg.args` invokes a command. Reads return the value in `msg.payload`, may force a remote request with `msg.requestFromRemote = true`, and accept numeric attribute ID `0`. Door Lock nodes now retain their last `locked`/`unlocked` canvas status when generic Matter or KNX notifications arrive.<br/>
|
|
18
22
|
|
|
19
23
|
**Version 6.0.10** - July 2026<br/>
|
|
@@ -152,6 +152,14 @@
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
var node = this; // Starting from v 4.1.31
|
|
155
|
+
if (node.id) {
|
|
156
|
+
$.ajax({
|
|
157
|
+
url: "KNXUltimateMatterControllerEnable",
|
|
158
|
+
type: "POST",
|
|
159
|
+
contentType: "application/json",
|
|
160
|
+
data: JSON.stringify({ nodeId: node.id })
|
|
161
|
+
});
|
|
162
|
+
}
|
|
155
163
|
$("#node-input-updateLocalStateFromKNXWrite").prop("checked", node.updateLocalStateFromKNXWrite === true || node.updateLocalStateFromKNXWrite === "true"); // Starting from v 4.1.31
|
|
156
164
|
const ensureConfigSelection = (selector) => {
|
|
157
165
|
if ($(selector).val() !== "_ADD_") return;
|
|
@@ -221,6 +229,31 @@
|
|
|
221
229
|
border-top: 1px solid #ccc;
|
|
222
230
|
margin: 8px 0;
|
|
223
231
|
}
|
|
232
|
+
.matter-input-help-table {
|
|
233
|
+
width: 100%;
|
|
234
|
+
border-collapse: collapse;
|
|
235
|
+
margin: 8px 0 18px;
|
|
236
|
+
}
|
|
237
|
+
.matter-input-help-table th,
|
|
238
|
+
.matter-input-help-table td {
|
|
239
|
+
border-bottom: 1px solid #ddd;
|
|
240
|
+
padding: 7px 6px;
|
|
241
|
+
text-align: left;
|
|
242
|
+
vertical-align: top;
|
|
243
|
+
}
|
|
244
|
+
.matter-input-help-table code {
|
|
245
|
+
white-space: pre-wrap;
|
|
246
|
+
word-break: break-word;
|
|
247
|
+
}
|
|
248
|
+
.matter-input-help-copy {
|
|
249
|
+
white-space: nowrap;
|
|
250
|
+
}
|
|
251
|
+
.matter-input-help-advanced {
|
|
252
|
+
max-height: 310px;
|
|
253
|
+
overflow: auto;
|
|
254
|
+
border: 1px solid #ddd;
|
|
255
|
+
padding: 0 8px;
|
|
256
|
+
}
|
|
224
257
|
</style>`;
|
|
225
258
|
$('head').append(style);
|
|
226
259
|
}
|
|
@@ -241,6 +274,7 @@
|
|
|
241
274
|
const $refreshDevicesButton = $(".hue-refresh-devices");
|
|
242
275
|
const $locateDeviceButton = $(".hue-locate-device");
|
|
243
276
|
const $hueDevicesLoading = $(".hue-devices-loading");
|
|
277
|
+
const $inputHelpPanel = $("#tabs-input");
|
|
244
278
|
const UNIVERSAL_NODE_ID = '__UNIVERSAL__';
|
|
245
279
|
const isUniversalMode = () => $matterControllerModeInput.val() === 'universal';
|
|
246
280
|
$matterControllerModeInput.val(node.matterControllerMode || 'single');
|
|
@@ -549,6 +583,19 @@
|
|
|
549
583
|
return !!cluster && (cluster.commands || []).some((cmd) => wanted.has(normalizeMatterToken(cmd.name)));
|
|
550
584
|
};
|
|
551
585
|
const matterCapabilitiesFromEndpoint = (ep) => {
|
|
586
|
+
const inputStructure = {
|
|
587
|
+
endpointId: Number(ep.endpointId),
|
|
588
|
+
clusters: (ep.clusters || []).map((cluster) => ({
|
|
589
|
+
id: Number(cluster.id),
|
|
590
|
+
name: cluster.name || '',
|
|
591
|
+
attributes: (cluster.attributes || []).map((attribute) => ({
|
|
592
|
+
id: Number(attribute.id),
|
|
593
|
+
name: attribute.name,
|
|
594
|
+
writable: attribute.writable === true
|
|
595
|
+
})),
|
|
596
|
+
commands: (cluster.commands || []).map((command) => ({ name: command.name }))
|
|
597
|
+
}))
|
|
598
|
+
};
|
|
552
599
|
const deviceTypes = (ep.deviceTypes || []).map(normalizeMatterToken);
|
|
553
600
|
const primaryDeviceType = (ep.deviceTypes || []).find((value) => !/bridgednode|aggregator|controlbridge/i.test(String(value))) || ep.name;
|
|
554
601
|
const deviceTypeDisplay = matterPrettyType(primaryDeviceType);
|
|
@@ -568,6 +615,7 @@
|
|
|
568
615
|
color: false,
|
|
569
616
|
lockDoor: hasMatterCommand(doorLockCluster, ['lockDoor']),
|
|
570
617
|
unlockDoor: hasMatterCommand(doorLockCluster, ['unlockDoor']),
|
|
618
|
+
inputStructure,
|
|
571
619
|
requirePinForRemoteOperation: (() => {
|
|
572
620
|
const attribute = (doorLockCluster.attributes || []).find((item) => normalizeMatterToken(item.name) === 'requirepinforremoteoperation');
|
|
573
621
|
return attribute ? attribute.value === true : false;
|
|
@@ -611,7 +659,7 @@
|
|
|
611
659
|
addAttribute(47, 'batPercentRemaining', 'battery', '5.001', false);
|
|
612
660
|
addAttribute(144, 'activePower', 'active_power', '14.056', false);
|
|
613
661
|
addAttribute(145, 'cumulativeEnergyImported', 'imported_energy', '13.013', false);
|
|
614
|
-
return { known: true, profile: specializedProfile, deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
|
|
662
|
+
return { known: true, profile: specializedProfile, deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets, inputStructure };
|
|
615
663
|
}
|
|
616
664
|
const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
|
|
617
665
|
const hasColorType = /extendedcolor/.test(typeText);
|
|
@@ -804,8 +852,144 @@
|
|
|
804
852
|
$matterCapabilitiesInput.val(serializeMatterCapabilities(currentMatterCapabilities));
|
|
805
853
|
node.matterDeviceCapabilities = $matterCapabilitiesInput.val();
|
|
806
854
|
}
|
|
855
|
+
|
|
856
|
+
const semanticInputDefinitions = [
|
|
857
|
+
{
|
|
858
|
+
fn: 'onoff',
|
|
859
|
+
label: 'On / Off',
|
|
860
|
+
read: [6, ['onOff']],
|
|
861
|
+
writes: [
|
|
862
|
+
{ descriptor: [6, ['on']], value: true },
|
|
863
|
+
{ descriptor: [6, ['off']], value: false },
|
|
864
|
+
{ descriptor: [6, ['toggle']], value: 'toggle' }
|
|
865
|
+
]
|
|
866
|
+
},
|
|
867
|
+
{ fn: 'level', label: 'Level', read: [8, ['currentLevel']], write: [8, ['moveToLevelWithOnOff', 'moveToLevel']], value: 50 },
|
|
868
|
+
{ fn: 'position', label: 'Cover position', read: [258, ['currentPositionLiftPercent100ths', 'currentPositionLiftPercentage']], write: [258, ['goToLiftPercentage']], value: 35 },
|
|
869
|
+
{ fn: 'tiltposition', label: 'Cover tilt', read: [258, ['currentPositionTiltPercent100ths', 'currentPositionTiltPercentage']], write: [258, ['goToTiltPercentage']], value: 35 },
|
|
870
|
+
{ fn: 'open', label: 'Open', write: [258, ['upOrOpen']], omitValue: true },
|
|
871
|
+
{ fn: 'close', label: 'Close', write: [258, ['downOrClose']], omitValue: true },
|
|
872
|
+
{ fn: 'stop', label: 'Stop', write: [258, ['stopMotion']], omitValue: true },
|
|
873
|
+
{ fn: 'setpoint', label: 'Heating setpoint', read: [513, ['occupiedHeatingSetpoint']], writeAttribute: [513, ['occupiedHeatingSetpoint']], value: 21.5 },
|
|
874
|
+
{ fn: 'coolingsetpoint', label: 'Cooling setpoint', read: [513, ['occupiedCoolingSetpoint']], writeAttribute: [513, ['occupiedCoolingSetpoint']], value: 24 },
|
|
875
|
+
{ fn: 'currenttemp', label: 'Current temperature', read: [513, ['localTemperature']] },
|
|
876
|
+
{ fn: 'fanspeed', label: 'Fan speed', read: [514, ['percentCurrent', 'percentSetting']], writeAttribute: [514, ['percentSetting']], value: 60 },
|
|
877
|
+
{ fn: 'temperature', label: 'Temperature', read: [1026, ['measuredValue']] },
|
|
878
|
+
{ fn: 'humidity', label: 'Humidity', read: [1029, ['measuredValue']] },
|
|
879
|
+
{ fn: 'illuminance', label: 'Illuminance', read: [1024, ['measuredValue']] },
|
|
880
|
+
{ fn: 'occupancy', label: 'Occupancy', read: [1030, ['occupancy']] },
|
|
881
|
+
{ fn: 'contact', label: 'Contact', read: [69, ['stateValue']] },
|
|
882
|
+
{ fn: 'battery', label: 'Battery', read: [47, ['batPercentRemaining']] },
|
|
883
|
+
{ fn: 'activepower', label: 'Active power', read: [144, ['activePower']] },
|
|
884
|
+
{ fn: 'importedenergy', label: 'Imported energy', read: [145, ['cumulativeEnergyImported']] },
|
|
885
|
+
{ fn: 'identify', label: 'Identify', write: [3, ['identify']], omitValue: true }
|
|
886
|
+
];
|
|
887
|
+
const inputStructureFor = (caps) => caps && caps.inputStructure && Array.isArray(caps.inputStructure.clusters)
|
|
888
|
+
? caps.inputStructure
|
|
889
|
+
: null;
|
|
890
|
+
const structureMember = (structure, targetKind, descriptor, writableOnly = false) => {
|
|
891
|
+
if (!structure || !descriptor) return null;
|
|
892
|
+
const cluster = structure.clusters.find((item) => Number(item.id) === Number(descriptor[0]));
|
|
893
|
+
if (!cluster) return null;
|
|
894
|
+
const collection = targetKind === 'command' ? cluster.commands : cluster.attributes;
|
|
895
|
+
if (!Array.isArray(collection)) return null;
|
|
896
|
+
const wanted = descriptor[1].map(normalizeMatterToken);
|
|
897
|
+
return collection.find((item) =>
|
|
898
|
+
wanted.includes(normalizeMatterToken(item.name)) &&
|
|
899
|
+
(!writableOnly || item.writable === true)
|
|
900
|
+
) || null;
|
|
901
|
+
};
|
|
902
|
+
const functionCode = (payload) => `msg.payload = ${JSON.stringify(payload, null, 2)};\nreturn msg;`;
|
|
903
|
+
const advancedCode = (kind, clusterId, name) => {
|
|
904
|
+
if (kind === 'read') return `msg.clusterId = ${Number(clusterId)};\nmsg.attribute = ${JSON.stringify(name)};\nreturn msg;`;
|
|
905
|
+
if (kind === 'write') return `msg.clusterId = ${Number(clusterId)};\nmsg.attribute = ${JSON.stringify(name)};\nmsg.value = 0;\nreturn msg;`;
|
|
906
|
+
return `msg.clusterId = ${Number(clusterId)};\nmsg.command = ${JSON.stringify(name)};\nmsg.args = {};\nreturn msg;`;
|
|
907
|
+
};
|
|
908
|
+
const appendInputHelpRow = ($body, operation, label, code) => {
|
|
909
|
+
const $row = $('<tr></tr>');
|
|
910
|
+
$('<td></td>').text(operation).appendTo($row);
|
|
911
|
+
$('<td></td>').text(label).appendTo($row);
|
|
912
|
+
$('<td><code></code></td>').find('code').text(code).end().appendTo($row);
|
|
913
|
+
$('<td></td>').append(
|
|
914
|
+
$('<button type="button" class="red-ui-button matter-input-help-copy"><i class="fa fa-copy"></i></button>')
|
|
915
|
+
.attr('title', node._('knxUltimateMatterControllerDevice.input_help_copy') || 'Copy')
|
|
916
|
+
.data('copy-text', code)
|
|
917
|
+
).appendTo($row);
|
|
918
|
+
$body.append($row);
|
|
919
|
+
};
|
|
920
|
+
const renderInputHelp = () => {
|
|
921
|
+
const caps = getEffectiveMatterCapabilities();
|
|
922
|
+
const structure = inputStructureFor(caps);
|
|
923
|
+
const endpointId = structure ? structure.endpointId : Number($matterEndpointInput.val() || node.matterEndpointId || 1);
|
|
924
|
+
$('#matter-input-help-endpoint').text(String(endpointId));
|
|
925
|
+
const $simpleBody = $('#matter-input-help-simple-body').empty();
|
|
926
|
+
if (caps.profile === 'doorLock') {
|
|
927
|
+
if (caps.lockDoor !== false) appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', node._('knxUltimateMatterControllerDevice.input_functions.lock') || 'Lock', functionCode({ function: 'lock', value: true }));
|
|
928
|
+
if (caps.unlockDoor !== false) appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', node._('knxUltimateMatterControllerDevice.input_functions.unlock') || 'Unlock', functionCode({ function: 'lock', value: false }));
|
|
929
|
+
} else {
|
|
930
|
+
semanticInputDefinitions.forEach((definition) => {
|
|
931
|
+
const readTarget = structureMember(structure, 'attribute', definition.read);
|
|
932
|
+
const writeVariants = definition.writes || [{
|
|
933
|
+
descriptor: definition.write || definition.writeAttribute,
|
|
934
|
+
targetKind: definition.write ? 'command' : 'attribute',
|
|
935
|
+
value: definition.value,
|
|
936
|
+
omitValue: definition.omitValue
|
|
937
|
+
}];
|
|
938
|
+
writeVariants.forEach((variant) => {
|
|
939
|
+
const targetKind = variant.targetKind || 'command';
|
|
940
|
+
const writeTarget = structureMember(structure, targetKind, variant.descriptor, targetKind === 'attribute');
|
|
941
|
+
if (!writeTarget) return;
|
|
942
|
+
const payload = { function: definition.fn };
|
|
943
|
+
if (!variant.omitValue) payload.value = variant.value;
|
|
944
|
+
appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', node._(`knxUltimateMatterControllerDevice.input_functions.${definition.fn}`) || definition.label, functionCode(payload));
|
|
945
|
+
});
|
|
946
|
+
if (readTarget) {
|
|
947
|
+
appendInputHelpRow($simpleBody, node._('knxUltimateMatterControllerDevice.input_help_read') || 'Read', node._(`knxUltimateMatterControllerDevice.input_functions.${definition.fn}`) || definition.label, functionCode({ function: definition.fn }));
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
$('#matter-input-help-simple-empty').toggle($simpleBody.children().length === 0);
|
|
952
|
+
|
|
953
|
+
const $advancedBody = $('#matter-input-help-advanced-body').empty();
|
|
954
|
+
if (structure) {
|
|
955
|
+
structure.clusters.forEach((cluster) => {
|
|
956
|
+
(cluster.attributes || []).forEach((attribute) => {
|
|
957
|
+
const prefix = `${cluster.name || 'Cluster'} (${cluster.id}) · ${attribute.name} (${attribute.id})`;
|
|
958
|
+
appendInputHelpRow($advancedBody, node._('knxUltimateMatterControllerDevice.input_help_read') || 'Read', prefix, advancedCode('read', cluster.id, attribute.name));
|
|
959
|
+
if (attribute.writable === true) {
|
|
960
|
+
appendInputHelpRow($advancedBody, node._('knxUltimateMatterControllerDevice.input_help_write') || 'Write', prefix, advancedCode('write', cluster.id, attribute.name));
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
(cluster.commands || []).forEach((command) => {
|
|
964
|
+
const prefix = `${cluster.name || 'Cluster'} (${cluster.id}) · ${command.name}`;
|
|
965
|
+
appendInputHelpRow($advancedBody, node._('knxUltimateMatterControllerDevice.input_help_command') || 'Command', prefix, advancedCode('command', cluster.id, command.name));
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
$('#matter-input-help-advanced-empty').toggle($advancedBody.children().length === 0);
|
|
970
|
+
};
|
|
971
|
+
const copyInputHelpText = (text) => {
|
|
972
|
+
const copied = () => RED.notify(node._('knxUltimateMatterControllerDevice.input_help_copied') || 'Message copied', 'success');
|
|
973
|
+
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
974
|
+
navigator.clipboard.writeText(text).then(copied).catch(() => {
|
|
975
|
+
const $temporary = $('<textarea></textarea>').val(text).appendTo('body').select();
|
|
976
|
+
try { document.execCommand('copy'); copied(); } finally { $temporary.remove(); }
|
|
977
|
+
});
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const $temporary = $('<textarea></textarea>').val(text).appendTo('body').select();
|
|
981
|
+
try { document.execCommand('copy'); copied(); } finally { $temporary.remove(); }
|
|
982
|
+
};
|
|
983
|
+
$inputHelpPanel.off('click.matterInputHelp').on('click.matterInputHelp', '.matter-input-help-copy', function () {
|
|
984
|
+
copyInputHelpText($(this).data('copy-text') || '');
|
|
985
|
+
});
|
|
807
986
|
$tabs.addClass('hue-vertical-tabs');
|
|
808
987
|
$tabs.tabs(); // Tabs gestione KNX
|
|
988
|
+
$tabs.off('tabsactivate.matterInputHelp').on('tabsactivate.matterInputHelp', (event, ui) => {
|
|
989
|
+
if (ui && ui.newPanel && ui.newPanel.attr('id') === 'tabs-input') {
|
|
990
|
+
renderInputHelp();
|
|
991
|
+
}
|
|
992
|
+
});
|
|
809
993
|
$tabs.find('ul').addClass('ui-tabs-nav');
|
|
810
994
|
$tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
|
|
811
995
|
$universalTabs.addClass('hue-vertical-tabs');
|
|
@@ -1044,8 +1228,9 @@
|
|
|
1044
1228
|
|
|
1045
1229
|
const ensureVisibleMatterTabSelected = () => {
|
|
1046
1230
|
const $active = $tabs.find('ul.ui-tabs-nav li.ui-tabs-active');
|
|
1047
|
-
|
|
1048
|
-
|
|
1231
|
+
const isTabAvailable = function () { return $(this).css('display') !== 'none'; };
|
|
1232
|
+
if ($active.length && $active.filter(isTabAvailable).length) return;
|
|
1233
|
+
const $firstVisible = $tabs.find('ul.ui-tabs-nav li').filter(isTabAvailable).first();
|
|
1049
1234
|
if (!$firstVisible.length) return;
|
|
1050
1235
|
const visibleIndex = $tabs.find('ul.ui-tabs-nav li').index($firstVisible);
|
|
1051
1236
|
if (visibleIndex >= 0) {
|
|
@@ -1107,14 +1292,16 @@
|
|
|
1107
1292
|
const isDoorLock = caps.profile === 'doorLock';
|
|
1108
1293
|
const isMapped = ['mapped', 'windowCovering', 'thermostat', 'fan', 'switch'].includes(caps.profile);
|
|
1109
1294
|
const isUniversal = caps.profile === 'universal' || isUniversalMode();
|
|
1295
|
+
const knxSelected = hasKnxServerSelected();
|
|
1110
1296
|
const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
|
|
1111
1297
|
|
|
1112
|
-
setMatterTabVisible('tabs-1', !isUniversal && supportsSwitch);
|
|
1113
|
-
setMatterTabVisible('tabs-2', !isUniversal && supportsDim);
|
|
1114
|
-
setMatterTabVisible('tabs-3', !isUniversal && supportsTemperature);
|
|
1115
|
-
setMatterTabVisible('tabs-4', !isUniversal && supportsColor);
|
|
1116
|
-
setMatterTabVisible('tabs-mapped', !isUniversal && isMapped);
|
|
1117
|
-
setMatterTabVisible('tabs-
|
|
1298
|
+
setMatterTabVisible('tabs-1', !isUniversal && knxSelected && supportsSwitch);
|
|
1299
|
+
setMatterTabVisible('tabs-2', !isUniversal && knxSelected && supportsDim);
|
|
1300
|
+
setMatterTabVisible('tabs-3', !isUniversal && knxSelected && supportsTemperature);
|
|
1301
|
+
setMatterTabVisible('tabs-4', !isUniversal && knxSelected && supportsColor);
|
|
1302
|
+
setMatterTabVisible('tabs-mapped', !isUniversal && knxSelected && isMapped);
|
|
1303
|
+
setMatterTabVisible('tabs-input', !isUniversal && hasHueDevice && (isMapped || isDoorLock));
|
|
1304
|
+
setMatterTabVisible('tabs-6', !isUniversal && knxSelected && hasHueDevice);
|
|
1118
1305
|
$('#matter-universal-description').toggle(isUniversal);
|
|
1119
1306
|
$pinSelect.val(isUniversal ? 'yes' : $pinSelect.val()).prop('disabled', isUniversal);
|
|
1120
1307
|
updateLocateButtonState(false);
|
|
@@ -1129,6 +1316,7 @@
|
|
|
1129
1316
|
$('#matter-door-lock-pin-required').toggle(isDoorLock && caps.requirePinForRemoteOperation === true);
|
|
1130
1317
|
$('#matter-mapped-profile').toggle(isMapped);
|
|
1131
1318
|
if (isMapped) renderMappedProfile(caps.mappedTargets || []);
|
|
1319
|
+
if (hasHueDevice && !isUniversal && (isMapped || isDoorLock)) renderInputHelp();
|
|
1132
1320
|
$('#matter-selected-device-type-value').text(deviceTypeDisplay);
|
|
1133
1321
|
$('#matter-selected-device-type').toggle(hasHueDevice && deviceTypeDisplay !== '');
|
|
1134
1322
|
try { $tabs.tabs('refresh'); } catch (error) { /* empty */ }
|
|
@@ -1186,9 +1374,12 @@
|
|
|
1186
1374
|
};
|
|
1187
1375
|
|
|
1188
1376
|
const updateTabsVisibility = () => {
|
|
1189
|
-
const knxSelected = hasKnxServerSelected();
|
|
1190
1377
|
const hueDeviceSelected = getHueDeviceValue() !== '';
|
|
1191
|
-
|
|
1378
|
+
applyMatterCapabilities(currentMatterCapabilities);
|
|
1379
|
+
const hasVisibleTab = $tabs.find('ul.ui-tabs-nav li').filter(function () {
|
|
1380
|
+
return $(this).css('display') !== 'none';
|
|
1381
|
+
}).length > 0;
|
|
1382
|
+
const shouldShowTabs = !isUniversalMode() && hueDeviceSelected && hasVisibleTab;
|
|
1192
1383
|
|
|
1193
1384
|
if (shouldShowTabs) {
|
|
1194
1385
|
$tabs.show();
|
|
@@ -1196,7 +1387,6 @@
|
|
|
1196
1387
|
} else {
|
|
1197
1388
|
$tabs.hide();
|
|
1198
1389
|
}
|
|
1199
|
-
applyMatterCapabilities(currentMatterCapabilities);
|
|
1200
1390
|
|
|
1201
1391
|
if ($pinSectionRow.length) {
|
|
1202
1392
|
$pinSectionRow.show();
|
|
@@ -1944,6 +2134,7 @@
|
|
|
1944
2134
|
<li><a href="#tabs-3"><i class="fa-solid fa-temperature-quarter"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.tunable_white"></span></a></li>
|
|
1945
2135
|
<li><a href="#tabs-4"><i class="fa-solid fa-palette"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.rgb_hsv"></span></a></li>
|
|
1946
2136
|
<li><a href="#tabs-mapped"><i class="fa-solid fa-list-check"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.mappings"></span></a></li>
|
|
2137
|
+
<li><a href="#tabs-input"><i class="fa-solid fa-code"></i> <span data-i18n="knxUltimateMatterControllerDevice.input_help_label"></span></a></li>
|
|
1947
2138
|
<li><a href="#tabs-6"><i class="fa-solid fa-code-merge"></i> <span data-i18n="knxUltimateMatterControllerDevice.tabs.behaviour"></span></a></li>
|
|
1948
2139
|
</ul>
|
|
1949
2140
|
<div id="tabs-mapped">
|
|
@@ -1951,6 +2142,46 @@
|
|
|
1951
2142
|
<div id="matter-mapped-profile-body"></div>
|
|
1952
2143
|
</div>
|
|
1953
2144
|
</div>
|
|
2145
|
+
<div id="tabs-input">
|
|
2146
|
+
<p class="form-tips" data-i18n="knxUltimateMatterControllerDevice.input_help_pin_hint"></p>
|
|
2147
|
+
<p>
|
|
2148
|
+
<strong data-i18n="knxUltimateMatterControllerDevice.input_help_endpoint"></strong>:
|
|
2149
|
+
<code id="matter-input-help-endpoint"></code>.
|
|
2150
|
+
<span data-i18n="knxUltimateMatterControllerDevice.input_help_endpoint_hint"></span>
|
|
2151
|
+
</p>
|
|
2152
|
+
<h4 data-i18n="knxUltimateMatterControllerDevice.input_help_simple_title"></h4>
|
|
2153
|
+
<p data-i18n="knxUltimateMatterControllerDevice.input_help_simple_hint"></p>
|
|
2154
|
+
<table class="matter-input-help-table">
|
|
2155
|
+
<thead>
|
|
2156
|
+
<tr>
|
|
2157
|
+
<th data-i18n="knxUltimateMatterControllerDevice.input_help_operation"></th>
|
|
2158
|
+
<th data-i18n="knxUltimateMatterControllerDevice.input_help_function"></th>
|
|
2159
|
+
<th data-i18n="knxUltimateMatterControllerDevice.input_help_message"></th>
|
|
2160
|
+
<th></th>
|
|
2161
|
+
</tr>
|
|
2162
|
+
</thead>
|
|
2163
|
+
<tbody id="matter-input-help-simple-body"></tbody>
|
|
2164
|
+
</table>
|
|
2165
|
+
<p id="matter-input-help-simple-empty" style="display:none;" data-i18n="knxUltimateMatterControllerDevice.input_help_no_simple"></p>
|
|
2166
|
+
<details>
|
|
2167
|
+
<summary><strong data-i18n="knxUltimateMatterControllerDevice.input_help_advanced_title"></strong></summary>
|
|
2168
|
+
<p data-i18n="knxUltimateMatterControllerDevice.input_help_advanced_hint"></p>
|
|
2169
|
+
<div class="matter-input-help-advanced">
|
|
2170
|
+
<table class="matter-input-help-table">
|
|
2171
|
+
<thead>
|
|
2172
|
+
<tr>
|
|
2173
|
+
<th data-i18n="knxUltimateMatterControllerDevice.input_help_operation"></th>
|
|
2174
|
+
<th data-i18n="knxUltimateMatterControllerDevice.input_help_target"></th>
|
|
2175
|
+
<th data-i18n="knxUltimateMatterControllerDevice.input_help_message"></th>
|
|
2176
|
+
<th></th>
|
|
2177
|
+
</tr>
|
|
2178
|
+
</thead>
|
|
2179
|
+
<tbody id="matter-input-help-advanced-body"></tbody>
|
|
2180
|
+
</table>
|
|
2181
|
+
<p id="matter-input-help-advanced-empty" style="display:none;" data-i18n="knxUltimateMatterControllerDevice.input_help_no_structure"></p>
|
|
2182
|
+
</div>
|
|
2183
|
+
</details>
|
|
2184
|
+
</div>
|
|
1954
2185
|
<div id="tabs-1">
|
|
1955
2186
|
<p>
|
|
1956
2187
|
<div class="form-row">
|
|
@@ -2414,6 +2645,35 @@ the device picker. Door Lock, Window Covering, Thermostat, Fan and Switch endpoi
|
|
|
2414
2645
|
dedicated profiles. Switch press, long-press and multi-press events are emitted on the
|
|
2415
2646
|
optional flow output; simpler endpoints continue through the generic mapped fallback.
|
|
2416
2647
|
|
|
2648
|
+
**Flow input**
|
|
2649
|
+
|
|
2650
|
+
For a selected non-light endpoint, open the **Flow input** tab in the editor.
|
|
2651
|
+
The capability-driven tab shows copyable simple examples plus the selected endpoint,
|
|
2652
|
+
all advertised readable/writable attributes and all accepted commands.
|
|
2653
|
+
|
|
2654
|
+
Use `msg.payload = {function:"position", value:35}` for a simple write. Omit `value`
|
|
2655
|
+
to read a supported state, for example `{function:"temperature"}`. Values use human
|
|
2656
|
+
units and read output includes raw Matter details in `msg.matter`. The available
|
|
2657
|
+
functions are filtered by the endpoint and can include `onoff`, `level`, `position`,
|
|
2658
|
+
`tiltposition`, `open`, `close`, `stop`, `setpoint`, `coolingsetpoint`, `currenttemp`,
|
|
2659
|
+
`fanspeed`, sensor readings and `identify`. Door Lock accepts
|
|
2660
|
+
`{function:"lock", value:true|false}`.
|
|
2661
|
+
|
|
2662
|
+
The advanced top-level fields remain supported: `msg.clusterId` with
|
|
2663
|
+
`msg.command`/`msg.args`, or `msg.attribute` with optional `msg.value`. Node ID and
|
|
2664
|
+
Endpoint ID are already selected by this node; `msg.endpointId` is only an override.
|
|
2665
|
+
|
|
2666
|
+
**Device isolation**
|
|
2667
|
+
|
|
2668
|
+
Commands use a separate ordered queue for every commissioned device. An offline,
|
|
2669
|
+
timing-out or removed device cannot delay other Matter devices sharing the same KNX
|
|
2670
|
+
group address. A Controller node that still points to a removed device rejects the
|
|
2671
|
+
command immediately and shows **Device no longer commissioned** in red.
|
|
2672
|
+
|
|
2673
|
+
An unavailable-device error is latched. Further KNX and flow commands are ignored and
|
|
2674
|
+
cannot repaint the red status. The node resumes automatically when its Matter device
|
|
2675
|
+
reports `connected`; opening the node editor also clears the latch for a manual retry.
|
|
2676
|
+
|
|
2417
2677
|
**Universal Mode**
|
|
2418
2678
|
|
|
2419
2679
|
Select **Universal Mode** in the **Mode** selector to listen to every attribute report,
|
|
@@ -2519,7 +2779,7 @@ _Basic Matter effects_
|
|
|
2519
2779
|
| Read status at startup | Read the Matter endpoint status at Node-RED startup or full deploy and send it to the KNX bus. |
|
|
2520
2780
|
| KNX Brightness Status | Updates the KNX brightness status whenever the Matter light switches ON/OFF. It can send 0% while OFF and restore the previous value when ON, or leave the brightness status unchanged. |
|
|
2521
2781
|
| Update local cached Matter state from KNX bus writes | When enabled, KNX writes immediately update the local Matter-state cache without waiting for the endpoint report. Disable it if the cache should follow only real Matter reports. |
|
|
2522
|
-
| Node Input/Output PINs | For mapped endpoints,
|
|
2782
|
+
| Node Input/Output PINs | For mapped endpoints, use the simple `msg.payload = {function,value}` contract shown in the **Flow input** tab, or the advanced top-level Matter selectors. Read an attribute with `msg.clusterId` and `msg.attribute`; add `msg.requestFromRemote = true` to force a device read. Add `msg.value` to write, or use `msg.command` and `msg.args`. Door Lock accepts both `msg.payload = true/false` and `{function:"lock",value:true/false}`. |
|
|
2523
2783
|
| Switch on behaviour | It sets the behaviour of your lights when switched on. You can choose from differents behaviours.<br/> **Select color: ** the light will be switched on with the color of your choice. To change color, just CLICK on the color selector (under the _Select color_ control).<br/>**Select temperature and brightness: ** the light will be switched on with the temperature (Kelvin) and brightness (0-100) of your choice.<br/>**None:** the light will retain its last status. In case you've enable the night lighting, after the night time ends, the lamp will resume the color/temperature/brightness state set at day time. |
|
|
2524
2784
|
| Night Lighting | It allows to set a particular light color/brightness at nighttime. The options are the same as the daytime. You could select either a temperature/brightness or color. A cozy temperature of 2700 Kelvin, with a brightness of 10% or 20%, is a good choice for bathroom's night light.|
|
|
2525
2785
|
| Day/Night | Select the group address used to set the day/night behaviour. The group address value is _true_ if daytime, _false_ if nighttime. |
|
|
@@ -10,8 +10,34 @@ const hueColorConverter = require('./utils/colorManipulators/hueColorConverter')
|
|
|
10
10
|
const { createLightEngine } = require('./utils/lightEngines')
|
|
11
11
|
const { hueStateToCanonical, matterEventToHuePatch } = require('./utils/lightEngines/matterHueShim')
|
|
12
12
|
const { setupMatterControllerProfile } = require('./utils/matterControllerProfiles')
|
|
13
|
+
const { setupMatterControllerCommandGate } = require('./utils/matterControllerCommandGate')
|
|
14
|
+
|
|
15
|
+
let matterCommandGateEndpointRegistered = false
|
|
13
16
|
|
|
14
17
|
module.exports = function (RED) {
|
|
18
|
+
if (!matterCommandGateEndpointRegistered) {
|
|
19
|
+
RED.httpAdmin.post('/KNXUltimateMatterControllerEnable', RED.auth.needsPermission('matter-config.write'), (req, res) => {
|
|
20
|
+
try {
|
|
21
|
+
const nodeId = String(req.body?.nodeId || '').trim()
|
|
22
|
+
if (nodeId === '') {
|
|
23
|
+
res.status(400).json({ error: 'Missing Matter Controller node id' })
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const targetNode = RED.nodes.getNode(nodeId)
|
|
27
|
+
if (!targetNode || typeof targetNode.clearMatterCommandBlock !== 'function') {
|
|
28
|
+
// New or not-yet-deployed editor nodes have no runtime counterpart.
|
|
29
|
+
res.json({ status: 'not-deployed', cleared: false })
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
const cleared = targetNode.clearMatterCommandBlock('editor')
|
|
33
|
+
res.json({ status: 'ok', cleared })
|
|
34
|
+
} catch (error) {
|
|
35
|
+
res.status(500).json({ error: error.message || 'Unable to enable Matter commands' })
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
matterCommandGateEndpointRegistered = true
|
|
39
|
+
}
|
|
40
|
+
|
|
15
41
|
function knxUltimateMatterControllerDevice (config) {
|
|
16
42
|
RED.nodes.createNode(this, config)
|
|
17
43
|
const node = this
|
|
@@ -20,6 +46,7 @@ module.exports = function (RED) {
|
|
|
20
46
|
node.matterNodeId = config.matterNodeId !== undefined ? String(config.matterNodeId) : ''
|
|
21
47
|
node.matterEndpointId = config.matterEndpointId !== undefined ? Number(config.matterEndpointId) : 1
|
|
22
48
|
node.matterDeviceName = config.matterDeviceName !== undefined ? String(config.matterDeviceName) : ''
|
|
49
|
+
setupMatterControllerCommandGate(node)
|
|
23
50
|
|
|
24
51
|
let matterCapabilities = {}
|
|
25
52
|
try { matterCapabilities = JSON.parse(config.matterDeviceCapabilities || '{}') } catch (error) { /* empty */ }
|
|
@@ -45,6 +72,7 @@ module.exports = function (RED) {
|
|
|
45
72
|
hueManager: {
|
|
46
73
|
writeHueQueueAdd: (_lightID, _state, _operation) => {
|
|
47
74
|
try {
|
|
75
|
+
if (node.matterCommandBlocked === true) return
|
|
48
76
|
if (!node.lightEngine) return
|
|
49
77
|
const patch = hueStateToCanonical(_state)
|
|
50
78
|
if (!patch) return
|
|
@@ -217,6 +245,7 @@ module.exports = function (RED) {
|
|
|
217
245
|
fill, shape, text, payload
|
|
218
246
|
}) => {
|
|
219
247
|
try {
|
|
248
|
+
if (node.matterCommandBlocked === true) return
|
|
220
249
|
if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
|
|
221
250
|
if (payload === undefined) payload = ''
|
|
222
251
|
const dDate = new Date()
|
|
@@ -228,6 +257,7 @@ module.exports = function (RED) {
|
|
|
228
257
|
// Compatibility callback used to update the Matter node status.
|
|
229
258
|
node.setNodeStatusHue = ({ fill, shape, text, payload }) => {
|
|
230
259
|
try {
|
|
260
|
+
if (node.matterCommandBlocked === true) return
|
|
231
261
|
if (node.currentHUEDevice?.on?.on === true) { fill = 'blue'; shape = 'dot' } else { fill = 'blue'; shape = 'ring' };
|
|
232
262
|
if (payload === undefined) payload = ''
|
|
233
263
|
const dDate = new Date()
|
|
@@ -238,6 +268,7 @@ module.exports = function (RED) {
|
|
|
238
268
|
}
|
|
239
269
|
|
|
240
270
|
node.writeHueState = function writeHueState (_state) {
|
|
271
|
+
if (node.matterCommandBlocked === true) return
|
|
241
272
|
const defaultOperation = node.isGrouped_light === false ? 'setLight' : 'setGroupedLight'
|
|
242
273
|
const isGroupedLightOff = node.isGrouped_light === true && node.currentHUEDevice?.on?.on === false
|
|
243
274
|
const stateKeys = _state && typeof _state === 'object' ? Object.keys(_state) : []
|
|
@@ -330,6 +361,7 @@ module.exports = function (RED) {
|
|
|
330
361
|
// Compatibility callback invoked by the Matter controller adapter.
|
|
331
362
|
node.handleSend = (msg) => {
|
|
332
363
|
if (!msg || !msg.knx || !isConfiguredKNXGA(msg.knx.destination)) return
|
|
364
|
+
if (node.matterCommandBlocked === true) return
|
|
333
365
|
if (node.currentHUEDevice === undefined && node.serverHue.linkStatus === 'connected') {
|
|
334
366
|
node.setNodeStatusHue({
|
|
335
367
|
fill: 'yellow',
|
|
@@ -1671,6 +1703,11 @@ module.exports = function (RED) {
|
|
|
1671
1703
|
}
|
|
1672
1704
|
|
|
1673
1705
|
node.on('input', (msg, send, done) => {
|
|
1706
|
+
if (node.matterCommandBlocked === true) {
|
|
1707
|
+
const error = new Error(node.matterCommandBlockReason || 'Matter device unavailable')
|
|
1708
|
+
if (done) done(error); else node.error(error, msg)
|
|
1709
|
+
return
|
|
1710
|
+
}
|
|
1674
1711
|
try {
|
|
1675
1712
|
const state = RED.util.cloneMessage(msg)
|
|
1676
1713
|
node.writeHueState(state)
|
|
@@ -19,7 +19,15 @@ Er ersetzt die unveröffentlichten getrennten Matter-Controller-Nodes und behäl
|
|
|
19
19
|
| Sensoren | Sensor-Endpunkte zeigen ihre Mess-/Status-GA nur bei Unterstützung: Temperatur, Feuchte, Helligkeit, Präsenz, Kontakt und Batterie. |
|
|
20
20
|
| Read at startup | Veröffentlicht den gecachten Matter-Wert beim Deploy/Start oder wenn sich das Gerät erneut verbindet. |
|
|
21
21
|
| Update local state from KNX write | Aktualisiert den lokalen Matter/KNX-Cache, wenn ein Telegramm auf eine konfigurierte KNX-GA geschrieben wird. |
|
|
22
|
-
| Node Input/Output PINs | Zeigt Node-RED-Eingangs-/Ausgangspins.
|
|
22
|
+
| Node Input/Output PINs | Zeigt Node-RED-Eingangs-/Ausgangspins. Nicht-Licht-Endpunkte akzeptieren das einfache `{function,value}`-Format und die erweiterten Matter-Felder; der Ausgang sendet Statusupdates. |
|
|
23
|
+
|
|
24
|
+
## Flow-Eingang
|
|
25
|
+
|
|
26
|
+
Öffne bei einem ausgewählten Nicht-Licht-Endpunkt den Tab **Flow-Eingang**. Der Tab wird aus der angekündigten Endpunktstruktur aufgebaut und zeigt kopierbare Beispiele, die Endpoint ID sowie alle lesbaren/schreibbaren Attribute und akzeptierten Befehle. Er bleibt auch verfügbar, wenn der Knoten ohne KNX-Gateway nur im Flow verwendet wird.
|
|
27
|
+
|
|
28
|
+
Einfache Schreibvorgänge verwenden `msg.payload = {function:"position",value:35}`. Ohne `value` wird ein unterstützter Zustand gelesen, zum Beispiel `{function:"temperature"}`; die Ausgabe verwendet verständliche Einheiten und enthält Rohdetails in `msg.matter`. Funktionen wie `onoff`, `level`, `position`, `open`, `close`, `stop`, Sollwerte, Lüfter und Sensoren erscheinen nur bei Unterstützung. Türschlösser akzeptieren `{function:"lock",value:true|false}`.
|
|
29
|
+
|
|
30
|
+
Die bisherigen erweiterten Eingaben bleiben gültig: `msg.clusterId` mit `msg.command`/`msg.args` oder `msg.attribute` und optional `msg.value`. Node ID und Endpoint ID sind bereits im Knoten ausgewählt.
|
|
23
31
|
|
|
24
32
|
## Universeller Modus
|
|
25
33
|
|
|
@@ -34,4 +42,8 @@ Beispiele: `{nodeId:"100",endpointId:1,clusterId:6,command:"on",args:{}}` schalt
|
|
|
34
42
|
## Verhalten
|
|
35
43
|
|
|
36
44
|
Der Knoten pflegt einen lokalen Cache aus Matter-Updates und KNX-Schreibtelegrammen, beantwortet KNX-Leseanfragen aus diesem Cache und kann Werte beim Start lesen/ausgeben. Er hört nur auf konfigurierte Gruppenadressen, daher wird fremder KNX-Verkehr ignoriert.
|
|
45
|
+
|
|
46
|
+
Befehle laufen pro gekoppeltem Gerät in einer eigenen geordneten Warteschlange. Ein offline befindliches, zeitüberschreitendes oder entferntes Gerät kann daher andere Matter-Geräte mit derselben KNX-Gruppenadresse nicht verzögern. Verweist ein Node weiterhin auf ein entferntes Gerät, wird der Befehl sofort abgewiesen und **Device no longer commissioned** rot angezeigt; wähle ein gültiges Matter-Gerät oder entferne den verwaisten Controller-Node.
|
|
47
|
+
|
|
48
|
+
Ein Fehler wegen eines nicht verfügbaren Geräts bleibt verriegelt: weitere KNX- und Flow-Befehle werden ignoriert und können den roten Status nicht überschreiben. Der Node wird automatisch wieder aktiv, sobald dieses Matter-Gerät `connected` meldet; auch das Öffnen des Node-Editors löst die Verriegelung für einen manuellen Neuversuch.
|
|
37
49
|
</script>
|
|
@@ -11,6 +11,28 @@
|
|
|
11
11
|
"node-input-hueLight": "Matter-Gerät",
|
|
12
12
|
"paletteLabel": "Control Matter from KNX",
|
|
13
13
|
"no_devices": "Keine Matter-Geräte gefunden. Matter-Controller prüfen und aktualisieren.",
|
|
14
|
+
"input_help_label": "Flow-Eingang",
|
|
15
|
+
"input_help_pin_hint": "Aktiviere die Ein-/Ausgangs-PINs des Knotens, bevor du diese Nachrichten sendest.",
|
|
16
|
+
"input_help_endpoint": "Ausgewählter Endpunkt",
|
|
17
|
+
"input_help_endpoint_hint": "Der Knoten kennt Node ID und Endpoint ID bereits; einfache Nachrichten benötigen sie nicht.",
|
|
18
|
+
"input_help_simple_title": "Einfache Nachrichten",
|
|
19
|
+
"input_help_simple_hint": "Diese Funktionen verwenden verständliche Einheiten und werden in den Matter-Cluster, Befehl oder das Attribut des ausgewählten Endpunkts übersetzt.",
|
|
20
|
+
"input_help_advanced_title": "Erweiterte Matter-Details",
|
|
21
|
+
"input_help_advanced_hint": "Es werden nur angekündigte Attribute und Befehle aufgelistet. Schreibbeispiele verwenden 0 als Platzhalter; Befehlsargumente müssen eventuell an den Matter-Datentyp angepasst werden.",
|
|
22
|
+
"input_help_operation": "Operation",
|
|
23
|
+
"input_help_function": "Funktion",
|
|
24
|
+
"input_help_target": "Matter-Ziel",
|
|
25
|
+
"input_help_message": "Nachricht",
|
|
26
|
+
"input_help_read": "Lesen",
|
|
27
|
+
"input_help_write": "Schreiben",
|
|
28
|
+
"input_help_command": "Befehl",
|
|
29
|
+
"input_help_copy": "Kopieren",
|
|
30
|
+
"input_help_copied": "Nachricht kopiert",
|
|
31
|
+
"input_help_no_simple": "Dieser Endpunkt besitzt keine bekannte einfache Eingangsfunktion.",
|
|
32
|
+
"input_help_no_structure": "Die Matter-Struktur ist nicht verfügbar. Aktualisiere die Geräteliste, während der Endpunkt online ist.",
|
|
33
|
+
"input_functions": {
|
|
34
|
+
"onoff": "Ein / Aus", "level": "Stufe", "position": "Rollladenposition", "tiltposition": "Lamellenposition", "open": "Öffnen", "close": "Schließen", "stop": "Stopp", "setpoint": "Heizsollwert", "coolingsetpoint": "Kühlsollwert", "currenttemp": "Aktuelle Temperatur", "fanspeed": "Lüftergeschwindigkeit", "temperature": "Temperatur", "humidity": "Luftfeuchtigkeit", "illuminance": "Beleuchtungsstärke", "occupancy": "Belegung", "contact": "Kontakt", "battery": "Batterie", "activepower": "Wirkleistung", "importedenergy": "Bezogene Energie", "identify": "Identifizieren", "lock": "Verriegeln", "unlock": "Entriegeln"
|
|
35
|
+
},
|
|
14
36
|
"tabs": {
|
|
15
37
|
"switch": "Schalten",
|
|
16
38
|
"dim": "Dimmen",
|
|
@@ -11,7 +11,11 @@ Der Controller kommuniziert mit den Geräten über das **IP-Netzwerk** (WLAN, Et
|
|
|
11
11
|
|
|
12
12
|
1. Zuerst diesen Konfigurations-Node **deployen** (der Controller muss laufen).
|
|
13
13
|
2. Den Node erneut öffnen und den **Kopplungscode** eingeben: entweder den 11-stelligen manuellen Code (z.B. `3497-011-2332`) oder den QR-Code-Inhalt (`MT:....`).
|
|
14
|
-
3.
|
|
14
|
+
3. Bei einem manuell eingegebenen Code auf **KOPPELN** klicken. Ein mit **Webcam** oder **Bild** gelesener QR-Code startet die Kopplung automatisch. Die Kommissionierung kann bis zu einer Minute dauern.
|
|
15
|
+
|
|
16
|
+
Statt den QR-Payload einzutippen, klicke auf **Webcam**, um ihn live zu scannen, oder auf **Bild**, um ihn aus einem lokalen Foto zu lesen. Sowohl normale dunkle QR-Codes auf hellem Grund als auch invertierte weiße Codes auf dunklem Grund werden unterstützt. Die Dekodierung erfolgt vollständig im Browser; nach dem Lesen eines gültigen Matter-QR-Codes füllt der Editor das Kopplungscode-Feld und startet die Kopplung sofort. Gib den optionalen Gerätenamen vor dem Scannen ein. Ein manuell eingegebener Code startet weiterhin erst mit **KOPPELN**. Der Live-Zugriff auf die Webcam setzt voraus, dass der Editor über HTTPS oder von `localhost` geöffnet wurde; andernfalls erklärt der Editor die Einschränkung und das Laden eines Bildes bleibt verfügbar.
|
|
17
|
+
|
|
18
|
+
Während der Kommissionierung überdeckt ein blockierender Wartebildschirm den Editor und verhindert weitere Klicks, bis der Vorgang erfolgreich abgeschlossen wird oder fehlschlägt.
|
|
15
19
|
|
|
16
20
|
Wenn das Gerät fabrikneu ist und nur Bluetooth-Kommissionierung unterstützt, kopple es zuerst mit der Hersteller-App oder einem anderen Matter-Controller (Alexa, Google Home, Apple Home) und nutze dann dessen Funktion **"mit weiterem Hub teilen"**, um einen neuen Kopplungscode für KNX-Ultimate zu erzeugen. So tritt das Gerät mehreren Fabrics gleichzeitig bei.
|
|
17
21
|
|
|
@@ -26,4 +30,10 @@ Mit **Exportieren** wird eine vollständige Sicherung dieser Controller-Instanz
|
|
|
26
30
|
## Ein Gerät entfernen
|
|
27
31
|
|
|
28
32
|
Nutze den Papierkorb-Button in der Liste der gekoppelten Geräte. Der Controller versucht, das Gerät sauber zu dekommissionieren; ist es nicht erreichbar, wird es trotzdem aus der Fabric entfernt (danach kann ein Werksreset des Geräts nötig sein).
|
|
33
|
+
|
|
34
|
+
Die Liste enthält eine Zeile für jeden Node, der derzeit in der Matter-Fabric dieses Controllers gespeichert ist. Node-IDs sind innerhalb dieser Fabric eindeutig; Endpunkte eines einzelnen kommissionierten Bridges werden nicht als separate Geräte aufgeführt. Die Statusspalte zeigt an, ob ein Node verbunden, getrennt, in Wiederverbindung oder auf Geräteerkennung wartend ist.
|
|
35
|
+
|
|
36
|
+
Der Controller hält die Befehlsreihenfolge für jedes gekoppelte Gerät getrennt. Ein langsames, offline befindliches oder entferntes Gerät kann Befehle für andere Geräte nicht blockieren. Controller-Gerätenodes, die weiterhin auf eine entfernte Node ID verweisen, weisen neue Befehle sofort ab und zeigen **Device no longer commissioned** an.
|
|
37
|
+
|
|
38
|
+
Wird ein Gerät nicht verfügbar, bleiben seine Controller-Nodes gesperrt und ignorieren weitere Befehle, bis dieses Gerät erneut `connected` meldet. Die Wiederherstellung erfolgt automatisch; auch das Öffnen des Geräte-Node-Editors löst die Sperre für einen manuellen Neuversuch.
|
|
29
39
|
</script>
|