node-red-contrib-knx-ultimate 6.0.10 → 6.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,12 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
+ **Version 6.0.11** - July 2026<br/>
10
+
11
+ - **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/>
12
+ - **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 enforcing their native input/event cluster boundary and 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/>
13
+ - **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/>
14
+
9
15
  **Version 6.0.10** - July 2026<br/>
10
16
 
11
17
  - **Matter nodes — crash containment**: asynchronous bridge state updates, controller client callbacks, engine errors, startup/watchdog timers and the controller command queue are now guarded so rejected promises and EventEmitter errors are reported instead of becoming uncaught exceptions or unhandled rejections capable of stopping Node-RED.<br/>
@@ -109,8 +109,71 @@
109
109
  try { RED.sidebar.show('help'); } catch (error) { /* empty */ }
110
110
  const node = this;
111
111
 
112
- // Use the same jQuery UI tab structure as the main KNXUltimate node.
113
- $('#mb-tabs').tabs();
112
+ // Keep the same left-hand vertical tab orientation as Matter Controller.
113
+ if ($('#knxUltimateMatterControllerDeviceVerticalTabs').length === 0) {
114
+ $('head').append(`
115
+ <style id="knxUltimateMatterControllerDeviceVerticalTabs">
116
+ .hue-vertical-tabs.ui-tabs.ui-widget.ui-widget-content.ui-corner-all {
117
+ display: flex;
118
+ border: none;
119
+ padding: 0;
120
+ }
121
+ .hue-vertical-tabs > ul.ui-tabs-nav {
122
+ flex: 0 0 180px;
123
+ border-right: 1px solid #ccc;
124
+ border-left: none;
125
+ border-top: none;
126
+ border-bottom: none;
127
+ padding: 0.5em 0.3em;
128
+ }
129
+ .hue-vertical-tabs > ul.ui-tabs-nav li {
130
+ float: none;
131
+ width: 100%;
132
+ margin: 0 0 2px 0;
133
+ }
134
+ .hue-vertical-tabs > ul.ui-tabs-nav li a {
135
+ display: block;
136
+ width: 100%;
137
+ white-space: normal;
138
+ position: relative;
139
+ border-bottom: none !important;
140
+ }
141
+ .hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active {
142
+ border-bottom: none !important;
143
+ }
144
+ .hue-vertical-tabs > ul.ui-tabs-nav li.ui-tabs-active a::after {
145
+ content: "";
146
+ position: absolute;
147
+ left: 0;
148
+ bottom: 0;
149
+ width: 50%;
150
+ height: 3px;
151
+ background: currentColor;
152
+ }
153
+ .hue-vertical-tabs .ui-tabs-panel {
154
+ flex: 1;
155
+ padding: 0.8em 1em;
156
+ box-sizing: border-box;
157
+ border: none;
158
+ background: transparent;
159
+ }
160
+ .hue-vertical-tabs .form-row > dt {
161
+ flex: 1 1 auto;
162
+ margin: 0;
163
+ }
164
+ .hue-vertical-tabs hr {
165
+ width: 100%;
166
+ border: 0;
167
+ border-top: 1px solid #ccc;
168
+ margin: 8px 0;
169
+ }
170
+ </style>`);
171
+ }
172
+ const $tabs = $('#mb-tabs');
173
+ $tabs.addClass('hue-vertical-tabs');
174
+ $tabs.tabs();
175
+ $tabs.find('ul').addClass('ui-tabs-nav');
176
+ $tabs.find('li').removeClass('ui-corner-top').addClass('ui-corner-left');
114
177
 
115
178
  // Resolves the KNX gateway at query time: the selected one, or - as a
116
179
  // fallback - the first knxUltimate-config available, so the GA list works
@@ -441,6 +504,8 @@ Exposes **one KNX device as a Matter device**. This node is in **BETA**.
441
504
 
442
505
  Point it to a **Matter Bridge** configuration node (the actual bridge, paired once by Alexa/Google Home/Apple Home), pick the device type, give it the name the assistant will use, and fill the group addresses.
443
506
 
507
+ The **Mappings** and **Advanced** tabs are arranged vertically on the left, matching the Matter Controller editor.
508
+
444
509
  - **Matter → KNX**: commands from the voice assistant write to the *command* group addresses.
445
510
  - **KNX → Matter**: telegrams on the *status* group addresses update the Matter attribute (and the app UI).
446
511
 
@@ -554,6 +554,11 @@
554
554
  }
555
555
  const isMatterLight = deviceTypes.some((type) => /(?:onoff|dimmable|colortemperature|extendedcolor)light/.test(type));
556
556
  if (!isMatterLight) {
557
+ const specializedProfile = getMatterCluster(ep, 258) ? 'windowCovering'
558
+ : getMatterCluster(ep, 513) ? 'thermostat'
559
+ : getMatterCluster(ep, 514) ? 'fan'
560
+ : getMatterCluster(ep, 59) ? 'switch'
561
+ : 'mapped';
557
562
  const mappedTargets = [];
558
563
  const addCommand = (clusterId, target, label, dpt) => {
559
564
  const cluster = getMatterCluster(ep, clusterId);
@@ -584,7 +589,7 @@
584
589
  addAttribute(47, 'batPercentRemaining', 'battery', '5.001', false);
585
590
  addAttribute(144, 'activePower', 'active_power', '14.056', false);
586
591
  addAttribute(145, 'cumulativeEnergyImported', 'imported_energy', '13.013', false);
587
- return { known: true, profile: 'mapped', deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
592
+ return { known: true, profile: specializedProfile, deviceTypeDisplay, onOff: false, level: false, colorTemperature: false, color: false, mappedTargets };
588
593
  }
589
594
  const hasColorTemperatureType = /colortemperature|extendedcolor/.test(typeText);
590
595
  const hasColorType = /extendedcolor/.test(typeText);
@@ -995,7 +1000,7 @@
995
1000
  const supportsColor = hasHueDevice && caps.color === true;
996
1001
  const hasRichBehaviour = supportsDim && (supportsTemperature || supportsColor);
997
1002
  const isDoorLock = caps.profile === 'doorLock';
998
- const isMapped = caps.profile === 'mapped';
1003
+ const isMapped = ['mapped', 'windowCovering', 'thermostat', 'fan', 'switch'].includes(caps.profile);
999
1004
  const deviceTypeDisplay = String(caps.deviceTypeDisplay || '').trim();
1000
1005
 
1001
1006
  setMatterTabVisible('tabs-1', supportsSwitch);
@@ -2150,7 +2155,9 @@
2150
2155
 
2151
2156
  The editor detects the selected endpoint capabilities and exposes only the corresponding
2152
2157
  Matter functions. Bridged Matter devices are expanded into their individual endpoints in
2153
- the device picker.
2158
+ the device picker. Door Lock, Window Covering, Thermostat, Fan and Switch endpoints use
2159
+ dedicated profiles. Switch press, long-press and multi-press events are emitted on the
2160
+ optional flow output; simpler endpoints continue through the generic mapped fallback.
2154
2161
 
2155
2162
  |Property|Description|
2156
2163
  |--|--|
@@ -2235,6 +2242,7 @@ _Basic Matter effects_
2235
2242
  | Read status at startup | Read the Matter endpoint status at Node-RED startup or full deploy and send it to the KNX bus. |
2236
2243
  | KNX Brightness Status | Updates the KNX brightness status whenever the Matter light switches ON/OFF. It can send 0% while OFF and restore the previous value when ON, or leave the brightness status unchanged. |
2237
2244
  | Update local cached Matter state from KNX bus writes | When enabled, KNX writes immediately update the local Matter-state cache without waiting for the endpoint report. Disable it if the cache should follow only real Matter reports. |
2245
+ | Node Input/Output PINs | For mapped endpoints, all Matter selectors belong directly to `msg`. Read an attribute with `msg.clusterId` and `msg.attribute`; add `msg.requestFromRemote = true` to force a device read instead of using the subscribed value. The result is emitted as `msg.payload`. Add `msg.value` to write an attribute, or use `msg.clusterId`, `msg.command` and `msg.args` to invoke a command. Attribute IDs such as `0` are accepted. Door Lock input accepts `msg.payload = true` to lock and `false` to unlock. |
2238
2246
  | 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. |
2239
2247
  | 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.|
2240
2248
  | Day/Night | Select the group address used to set the day/night behaviour. The group address value is _true_ if daytime, _false_ if nighttime. |
@@ -52,6 +52,7 @@ const setupDoorLockProfile = (RED, node, config) => {
52
52
  node.inputRBE = 'false'
53
53
  node.passthrough = 'no'
54
54
  node.currentLockState = undefined
55
+ node.matterConnectionStatus = ''
55
56
  node.knxUltimateAcceptedGAs = [config.GALightSwitch, config.GALightState]
56
57
  .map((ga) => String(ga || '').trim())
57
58
  .filter((ga) => ga !== '')
@@ -60,7 +61,15 @@ const setupDoorLockProfile = (RED, node, config) => {
60
61
  // knxUltimate-config invokes this synchronously from addClient(). Profiles return
61
62
  // early from the main light constructor, so they must expose the callback before
62
63
  // registering with the shared KNX configuration node.
63
- node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) => setStatus(fill, shape, text)
64
+ node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) => {
65
+ if (node.currentLockState !== undefined) {
66
+ const state = lockStateToBoolean(node.currentLockState)
67
+ const knxText = text ? ` | KNX: ${text}` : ''
68
+ setStatus(state === undefined ? 'yellow' : 'blue', state === undefined ? 'ring' : 'dot', `Matter: ${lockStateName(node.currentLockState)}${knxText}`)
69
+ return
70
+ }
71
+ setStatus(fill, shape, text)
72
+ }
64
73
 
65
74
  const commandArgs = () => {
66
75
  // Matter represents the remote credential as bytes. Keep the PIN in Node-RED's
@@ -181,7 +190,15 @@ const setupDoorLockProfile = (RED, node, config) => {
181
190
  }
182
191
  }
183
192
  node.setNodeStatusMatter = (status) => {
184
- if (status && status.text) setStatus(status.fill || 'grey', status.shape || 'ring', status.text)
193
+ if (!status || !status.text) return
194
+ node.matterConnectionStatus = status.text
195
+ const isAvailable = /^(connected|ready|controller ready)$/i.test(status.text)
196
+ if (isAvailable && node.currentLockState !== undefined) {
197
+ const state = lockStateToBoolean(node.currentLockState)
198
+ setStatus(state === undefined ? 'yellow' : 'blue', state === undefined ? 'ring' : 'dot', `Matter: ${lockStateName(node.currentLockState)}`)
199
+ return
200
+ }
201
+ setStatus(status.fill || 'grey', status.shape || 'ring', status.text)
185
202
  }
186
203
 
187
204
  if (node.serverKNX) {
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { setupMappedEndpointProfile } = require('./mappedEndpoint')
4
+
5
+ const FAN_CONTROL_CLUSTER_ID = 0x0202
6
+
7
+ const setupFanProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
8
+ profileName: 'fan',
9
+ inputClusterId: FAN_CONTROL_CLUSTER_ID,
10
+ eventClusterId: FAN_CONTROL_CLUSTER_ID,
11
+ formatAttributeStatus: (event) => {
12
+ if (['percentCurrent', 'percentSetting'].includes(event.attributeName) && event.value !== null && event.value !== undefined) {
13
+ return `Matter fan: ${Math.round(Number(event.value))}%`
14
+ }
15
+ if (event.attributeName === 'fanMode') return `Matter fan mode: ${event.value}`
16
+ return ''
17
+ }
18
+ })
19
+
20
+ module.exports = { FAN_CONTROL_CLUSTER_ID, setupFanProfile }
@@ -2,12 +2,20 @@
2
2
 
3
3
  const { setupDoorLockProfile } = require('./doorLock')
4
4
  const { setupMappedEndpointProfile } = require('./mappedEndpoint')
5
+ const { setupWindowCoveringProfile } = require('./windowCovering')
6
+ const { setupThermostatProfile } = require('./thermostat')
7
+ const { setupFanProfile } = require('./fan')
8
+ const { setupSwitchProfile } = require('./switch')
5
9
 
6
10
  // Controller-side Matter device profiles live behind this registry. Keep profile
7
11
  // selection capability-driven: the editor records the profile only after inspecting
8
12
  // the endpoint's actual device types, clusters, attributes and supported commands.
9
13
  const PROFILE_SETUPS = Object.freeze({
10
14
  doorLock: setupDoorLockProfile,
15
+ windowCovering: setupWindowCoveringProfile,
16
+ thermostat: setupThermostatProfile,
17
+ fan: setupFanProfile,
18
+ switch: setupSwitchProfile,
11
19
  mapped: setupMappedEndpointProfile
12
20
  })
13
21
 
@@ -24,7 +24,7 @@ const parseMappings = (value) => {
24
24
  }
25
25
  }
26
26
 
27
- const setupMappedEndpointProfile = (RED, node, config) => {
27
+ const setupMappedEndpointProfile = (RED, node, config, options = {}) => {
28
28
  node.name = config.name || node.matterDeviceName || 'Control Matter from KNX'
29
29
  node.topic = node.name
30
30
  node.mappings = parseMappings(config.matterMappings)
@@ -40,6 +40,7 @@ const setupMappedEndpointProfile = (RED, node, config) => {
40
40
  node.outputRBE = 'false'
41
41
  node.inputRBE = 'false'
42
42
  node.passthrough = 'no'
43
+ node.matterProfile = options.profileName || 'mapped'
43
44
  const enablePins = config.enableNodePINS === 'yes'
44
45
  let lastInitialReadTs = 0
45
46
 
@@ -111,7 +112,8 @@ const setupMappedEndpointProfile = (RED, node, config) => {
111
112
  sendKnx(mapping, matterToKnx(event.clusterId, event.attributeName, event.value))
112
113
  })
113
114
  if (enablePins) node.send({ topic: `${event.clusterId}.${event.attributeName}`, payload: event.value, matter: event })
114
- status('blue', 'dot', `Matter→KNX: ${event.attributeName}`)
115
+ const profileStatus = typeof options.formatAttributeStatus === 'function' ? options.formatAttributeStatus(event) : ''
116
+ status('blue', 'dot', profileStatus || `Matter→KNX: ${event.attributeName}`)
115
117
  } catch (error) {
116
118
  RED.log.error(`knxUltimateMatterControllerDevice mapped Matter: ${error.message}`)
117
119
  status('red', 'ring', error.message)
@@ -120,8 +122,11 @@ const setupMappedEndpointProfile = (RED, node, config) => {
120
122
  node.handleMatterClusterEvent = (event) => {
121
123
  // Cluster events have no implicit KNX mapping. They are exposed as raw flow output
122
124
  // only when the user explicitly enables the node pins.
123
- if (enablePins && String(event?.nodeId) === String(node.matterNodeId) && Number(event?.endpointId) === Number(node.matterEndpointId)) {
125
+ const clusterAccepted = options.eventClusterId === undefined || Number(event?.clusterId) === Number(options.eventClusterId)
126
+ if (enablePins && clusterAccepted && String(event?.nodeId) === String(node.matterNodeId) && Number(event?.endpointId) === Number(node.matterEndpointId)) {
124
127
  node.send({ topic: `${event.clusterId}.${event.eventName}`, payload: event.events, matter: event })
128
+ const profileStatus = typeof options.formatEventStatus === 'function' ? options.formatEventStatus(event) : ''
129
+ if (profileStatus) status('blue', 'dot', profileStatus)
125
130
  }
126
131
  }
127
132
  node.handleMatterNodeInitialized = () => {
@@ -137,20 +142,53 @@ const setupMappedEndpointProfile = (RED, node, config) => {
137
142
  if (node.serverKNX) { node.serverKNX.removeClient(node); node.serverKNX.addClient(node) } else status('yellow', 'ring', 'No KNX gateway selected')
138
143
  if (node.serverMatter) { node.serverMatter.removeClient(node); node.serverMatter.addClient(node) }
139
144
  node.on('input', (msg, send, done) => {
140
- try {
141
- const payload = msg.payload || {}
145
+ const complete = typeof done === 'function' ? done : () => {}
146
+ const output = typeof send === 'function' ? send : node.send.bind(node)
147
+ Promise.resolve().then(async () => {
142
148
  const mapping = {
143
- endpointId: payload.endpointId ?? node.matterEndpointId,
144
- clusterId: payload.clusterId,
145
- targetKind: payload.command !== undefined ? 'command' : 'attribute',
146
- target: payload.command ?? payload.attribute
149
+ endpointId: msg.endpointId ?? node.matterEndpointId,
150
+ clusterId: msg.clusterId,
151
+ targetKind: msg.command !== undefined ? 'command' : 'attribute',
152
+ target: msg.command ?? msg.attribute
147
153
  }
148
- if (!mapping.target || mapping.clusterId === undefined) throw new Error('Matter input requires clusterId and command or attribute')
149
- enqueue(mapping, payload.args ?? payload.value)
150
- if (done) done()
151
- } catch (error) {
152
- if (done) done(error); else node.error(error, msg)
153
- }
154
+ if (mapping.target === undefined || mapping.target === null || mapping.target === '' || mapping.clusterId === undefined || mapping.clusterId === null) {
155
+ throw new Error('Matter input requires clusterId and command or attribute')
156
+ }
157
+ if (options.inputClusterId !== undefined && Number(mapping.clusterId) !== Number(options.inputClusterId)) {
158
+ throw new Error(`${node.matterProfile} input requires clusterId ${options.inputClusterId}`)
159
+ }
160
+ const isAttributeRead = mapping.targetKind === 'attribute' && msg.value === undefined
161
+ if (isAttributeRead) {
162
+ const currentManager = manager()
163
+ if (!currentManager) throw new Error('Matter controller not ready')
164
+ const value = await currentManager.readAttribute(
165
+ node.matterNodeId,
166
+ mapping.endpointId,
167
+ mapping.clusterId,
168
+ mapping.target,
169
+ msg.requestFromRemote === true
170
+ )
171
+ output({
172
+ ...msg,
173
+ payload: value,
174
+ matter: {
175
+ source: 'inputRead',
176
+ nodeId: node.matterNodeId,
177
+ endpointId: mapping.endpointId,
178
+ clusterId: mapping.clusterId,
179
+ attribute: mapping.target
180
+ }
181
+ })
182
+ status('blue', 'dot', `Matter read: ${mapping.target}`)
183
+ complete()
184
+ return
185
+ }
186
+ enqueue(mapping, mapping.targetKind === 'command' ? msg.args : msg.value)
187
+ complete()
188
+ }).catch((error) => {
189
+ status('red', 'ring', error.message)
190
+ if (typeof done === 'function') done(error); else node.error(error, msg)
191
+ })
154
192
  })
155
193
  node.on('close', (done) => {
156
194
  try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ const { setupMappedEndpointProfile } = require('./mappedEndpoint')
4
+
5
+ const SWITCH_CLUSTER_ID = 0x003b
6
+
7
+ const setupSwitchProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
8
+ profileName: 'switch',
9
+ inputClusterId: SWITCH_CLUSTER_ID,
10
+ eventClusterId: SWITCH_CLUSTER_ID,
11
+ formatEventStatus: (event) => `Matter switch: ${event.eventName}`
12
+ })
13
+
14
+ module.exports = { SWITCH_CLUSTER_ID, setupSwitchProfile }
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { setupMappedEndpointProfile } = require('./mappedEndpoint')
4
+
5
+ const THERMOSTAT_CLUSTER_ID = 0x0201
6
+
7
+ const setupThermostatProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
8
+ profileName: 'thermostat',
9
+ inputClusterId: THERMOSTAT_CLUSTER_ID,
10
+ eventClusterId: THERMOSTAT_CLUSTER_ID,
11
+ formatAttributeStatus: (event) => {
12
+ if (['localTemperature', 'occupiedHeatingSetpoint', 'occupiedCoolingSetpoint'].includes(event.attributeName) && event.value !== null && event.value !== undefined) {
13
+ return `Matter thermostat: ${(Number(event.value) / 100).toFixed(1)} °C`
14
+ }
15
+ if (event.attributeName === 'systemMode') return `Matter thermostat mode: ${event.value}`
16
+ return ''
17
+ }
18
+ })
19
+
20
+ module.exports = { THERMOSTAT_CLUSTER_ID, setupThermostatProfile }
@@ -0,0 +1,20 @@
1
+ 'use strict'
2
+
3
+ const { setupMappedEndpointProfile } = require('./mappedEndpoint')
4
+
5
+ const WINDOW_COVERING_CLUSTER_ID = 0x0102
6
+
7
+ const setupWindowCoveringProfile = (RED, node, config) => setupMappedEndpointProfile(RED, node, config, {
8
+ profileName: 'windowCovering',
9
+ inputClusterId: WINDOW_COVERING_CLUSTER_ID,
10
+ eventClusterId: WINDOW_COVERING_CLUSTER_ID,
11
+ formatAttributeStatus: (event) => {
12
+ if (event.attributeName === 'currentPositionLiftPercent100ths' && event.value !== null && event.value !== undefined) {
13
+ return `Matter cover: ${Math.round(Number(event.value) / 100)}%`
14
+ }
15
+ if (event.attributeName === 'operationalStatus') return 'Matter cover: operational status updated'
16
+ return ''
17
+ }
18
+ })
19
+
20
+ module.exports = { WINDOW_COVERING_CLUSTER_ID, setupWindowCoveringProfile }
@@ -470,6 +470,14 @@ class classMatter extends EventEmitter {
470
470
  }
471
471
  }
472
472
 
473
+ _findAttribute = (clusterClient, attributeNameOrId) => {
474
+ if (clusterClient === undefined || attributeNameOrId === undefined || attributeNameOrId === null) return undefined
475
+ if (typeof attributeNameOrId === 'string' && clusterClient.attributes[attributeNameOrId] !== undefined) {
476
+ return clusterClient.attributes[attributeNameOrId]
477
+ }
478
+ return Object.values(clusterClient.attributes).find((attribute) => Number(attribute?.id) === Number(attributeNameOrId))
479
+ }
480
+
473
481
  // Returns the full structure of a commissioned node: endpoints, clusters, attributes (with cached values) and commands.
474
482
  // Used by the editor UI to let the user pick the mapping targets.
475
483
  getNodeStructure = (_nodeIdString) => {
@@ -551,7 +559,7 @@ class classMatter extends EventEmitter {
551
559
  if (node === undefined) throw new Error(`Matter node ${_nodeIdString} unknown or not yet connected`)
552
560
  const clusterClient = this._findClusterClient(node, _endpointId, _clusterId)
553
561
  if (clusterClient === undefined) throw new Error(`Cluster ${_clusterId} not found on endpoint ${_endpointId}`)
554
- const attribute = clusterClient.attributes[_attributeName]
562
+ const attribute = this._findAttribute(clusterClient, _attributeName)
555
563
  if (attribute === undefined) throw new Error(`Attribute ${_attributeName} not found in cluster ${clusterClient.name}`)
556
564
  return attribute.get(_requestFromRemote)
557
565
  }
@@ -563,7 +571,7 @@ class classMatter extends EventEmitter {
563
571
  if (node === undefined) return undefined
564
572
  const clusterClient = this._findClusterClient(node, _endpointId, _clusterId)
565
573
  if (clusterClient === undefined) return undefined
566
- const attribute = clusterClient.attributes[_attributeName]
574
+ const attribute = this._findAttribute(clusterClient, _attributeName)
567
575
  if (attribute === undefined) return undefined
568
576
  return attribute.getLocal()
569
577
  } catch (error) {
package/package.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "6.0.10",
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.",
6
+ "version": "6.0.11",
7
+ "description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
8
8
  "files": [
9
9
  "nodes/",
10
10
  "resources/",
@@ -145,4 +145,4 @@
145
145
  "vite": "^7.1.3",
146
146
  "vue": "^3.5.21"
147
147
  }
148
- }
148
+ }