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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,8 +6,9 @@
6
6
 
7
7
  # CHANGELOG
8
8
 
9
- **Version 6.0.9** - July 2026<br/>
9
+ **Version 6.0.10** - July 2026<br/>
10
10
 
11
+ - **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/>
11
12
  - **Matter Controller — post-commission naming**: assigning a name immediately after pairing now waits for matter.js to finish exposing the root `BasicInformation` cluster. Root endpoint 0 is resolved explicitly, preventing the misleading `BasicInformation cluster not found` warning seen when re-adding devices such as Shelly Plug.<br/>
12
13
  - **Control Matter from KNX — profile deploy crash**: Door Lock and multi-purpose profiles now expose the KNX status callback before registering with `knxUltimate-config`, preventing `_Node.setNodeStatus is not a function` during deploy.<br/>
13
14
  - **Control Matter from KNX — flow PIN persistence**: the **Node Input/Output PINs** selection is no longer overwritten from the KNX gateway presence when reopening the editor. The saved choice remains authoritative and `inputs`/`outputs` are updated coherently on save.<br/>
@@ -170,6 +170,24 @@ module.exports = function (RED) {
170
170
  }
171
171
  }
172
172
 
173
+ const reportMatterError = (context, error) => {
174
+ const message = error?.message || String(error)
175
+ node.setNodeStatus({ fill: 'red', shape: 'dot', text: `${context} ${message}`, payload: '' })
176
+ try { RED.log.error(`knxUltimateMatterBridge: ${context}: ${message}`) } catch (logError) { /* empty */ }
177
+ }
178
+
179
+ const safeSetDeviceState = async (fn, value, context) => {
180
+ try {
181
+ if (!node.serverMatterBridge || typeof node.serverMatterBridge.setDeviceState !== 'function') {
182
+ throw new Error('Matter bridge unavailable')
183
+ }
184
+ return await node.serverMatterBridge.setDeviceState(node.matterDeviceId, fn, value)
185
+ } catch (error) {
186
+ reportMatterError(context, error)
187
+ return false
188
+ }
189
+ }
190
+
173
191
  const clearCoverStatusTimer = () => {
174
192
  if (coverStatusTimer !== null) {
175
193
  clearTimeout(coverStatusTimer)
@@ -227,7 +245,7 @@ module.exports = function (RED) {
227
245
  clearCoverArrivalTimer()
228
246
 
229
247
  if (node.coverUpdateMode === 'optimistic') {
230
- node.serverMatterBridge.setDeviceState(node.matterDeviceId, 'position', requestedPosition)
248
+ safeSetDeviceState('position', requestedPosition, 'Matter position update error')
231
249
  return
232
250
  }
233
251
 
@@ -241,7 +259,7 @@ module.exports = function (RED) {
241
259
  }
242
260
  coverArrivalTimer = setTimeout(() => {
243
261
  coverArrivalTimer = null
244
- node.serverMatterBridge.setDeviceState(node.matterDeviceId, 'position', requestedPosition)
262
+ safeSetDeviceState('position', requestedPosition, 'Matter position fallback error')
245
263
  node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: 'No exact KNX confirmation in time, assuming arrived at', payload: requestedPosition })
246
264
  }, COVER_ARRIVAL_FALLBACK_MS)
247
265
  }
@@ -341,7 +359,7 @@ module.exports = function (RED) {
341
359
  try {
342
360
  const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(route.dpt))
343
361
  if (value === undefined || value === null) return
344
- node.serverMatterBridge.setDeviceState(node.matterDeviceId, route.fn, value)
362
+ safeSetDeviceState(route.fn, value, 'KNX->Matter error')
345
363
  if (node.deviceType === 'windowcovering' && route.fn === 'position') { clearCoverStatusTimer(); clearCoverArrivalTimer() }
346
364
  node.setNodeStatus({ fill: 'blue', shape: 'dot', text: `KNX->Matter ${route.fn}`, payload: value })
347
365
  } catch (error) {
@@ -391,7 +409,7 @@ module.exports = function (RED) {
391
409
 
392
410
  // Flow input pin: updates the Matter state of this device without going through the KNX bus.
393
411
  // msg.payload = { function: 'onoff'|'level'|'position'|'temperature'|..., value: ... }
394
- node.on('input', (msg, send, done) => {
412
+ node.on('input', async (msg, send, done) => {
395
413
  if (!node.enableNodePINS) {
396
414
  if (done) done()
397
415
  return
@@ -401,7 +419,8 @@ module.exports = function (RED) {
401
419
  const fn = (payload.function || payload.fn || '').toString().trim()
402
420
  if (fn === '' || payload.value === undefined) throw new Error('msg.payload must be { function, value }')
403
421
  if (node.serverMatterBridge === undefined) throw new Error('No Matter bridge selected')
404
- node.serverMatterBridge.setDeviceState(node.matterDeviceId, fn, payload.value)
422
+ const updated = await safeSetDeviceState(fn, payload.value, 'Flow->Matter error')
423
+ if (!updated) throw new Error('Matter state update failed')
405
424
  node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Flow->Matter ${fn}`, payload: payload.value })
406
425
  if (done) done()
407
426
  } catch (error) {
@@ -44,7 +44,10 @@ module.exports = (RED) => {
44
44
  // Matter clients registered on this shared controller connection.
45
45
  try {
46
46
  if (!client || typeof fn !== 'function') return
47
- fn()
47
+ const result = fn()
48
+ if (result && typeof result.catch === 'function') {
49
+ result.catch((error) => node.sysLogger?.warn(`Matter client ${label} async error: ${error.message}`))
50
+ }
48
51
  } catch (error) {
49
52
  node.sysLogger?.warn(`Matter client ${label} error: ${error.message}`)
50
53
  }
@@ -132,6 +135,11 @@ module.exports = (RED) => {
132
135
  })
133
136
  })
134
137
 
138
+ // EventEmitter treats an unhandled "error" event as a process exception.
139
+ node.matterManager.on('error', (error) => {
140
+ node.sysLogger?.error(`Matter controller engine error: ${error?.message || error}`)
141
+ })
142
+
135
143
  try {
136
144
  await node.matterManager.Connect()
137
145
  } catch (error) {
@@ -150,8 +158,12 @@ module.exports = (RED) => {
150
158
  node.sysLogger?.error(`Errore matter-config: node.startWatchdogTimer: ${error.message}`)
151
159
  }
152
160
  }
153
- await node.startWatchdogTimer()
154
- })()
161
+ try {
162
+ await node.startWatchdogTimer()
163
+ } catch (error) {
164
+ node.sysLogger?.error(`matter-config: watchdog reschedule error: ${error.message}`)
165
+ }
166
+ })().catch((error) => node.sysLogger?.error(`matter-config: watchdog callback error: ${error.message}`))
155
167
  }, 60000)
156
168
  }
157
169
 
@@ -165,11 +177,11 @@ module.exports = (RED) => {
165
177
  node.sysLogger?.error(`matter-config: startup error: ${error.message}`)
166
178
  }
167
179
  try {
168
- node.startWatchdogTimer()
180
+ await node.startWatchdogTimer()
169
181
  } catch (error) {
170
182
  node.sysLogger?.error(`matter-config: watchdog start error: ${error.message}`)
171
183
  }
172
- })()
184
+ })().catch((error) => node.sysLogger?.error(`matter-config: startup callback error: ${error.message}`))
173
185
  }, 5000)
174
186
 
175
187
  // Functions called from the nodes and the admin endpoints ----------------------------------------
@@ -53,7 +53,10 @@ module.exports = (RED) => {
53
53
  const safeClientCall = (client, fn, label) => {
54
54
  try {
55
55
  if (!client || typeof fn !== 'function') return
56
- fn()
56
+ const result = fn()
57
+ if (result && typeof result.catch === 'function') {
58
+ result.catch((error) => node.sysLogger?.warn(`Matter bridge client ${label} async error: ${error.message}`))
59
+ }
57
60
  } catch (error) {
58
61
  node.sysLogger?.warn(`Matter bridge client ${label} error: ${error.message}`)
59
62
  }
@@ -80,6 +83,10 @@ module.exports = (RED) => {
80
83
 
81
84
  const bindEngineEvents = (engine) => {
82
85
  engine.removeAllListeners()
86
+ // An EventEmitter "error" without a listener terminates Node-RED.
87
+ engine.on('error', (error) => {
88
+ node.sysLogger?.error(`Matter bridge engine error: ${error?.message || error}`)
89
+ })
83
90
  // Matter -> KNX: a controller (Alexa...) sent a command to a bridged device.
84
91
  // Route it to the device node that owns that Matter device.
85
92
  engine.on('command', (command) => {
@@ -109,7 +116,7 @@ module.exports = (RED) => {
109
116
  } catch (error) {
110
117
  node.sysLogger?.warn(`matterbridge-config: reconcile error: ${error.message}`)
111
118
  }
112
- })()
119
+ })().catch((error) => node.sysLogger?.warn(`matterbridge-config: reconcile callback error: ${error.message}`))
113
120
  }, 1500)
114
121
  }
115
122
 
@@ -160,7 +167,7 @@ module.exports = (RED) => {
160
167
  (async () => {
161
168
  if (closing) return
162
169
  await ensureEngineStarted()
163
- })()
170
+ })().catch((error) => node.sysLogger?.error(`matterbridge-config: startup callback error: ${error.message}`))
164
171
  }, 5000)
165
172
 
166
173
  // Functions called from the device nodes -----------------------------------------
@@ -37,7 +37,11 @@ class classMatter extends EventEmitter {
37
37
  this.exitAllQueues = false
38
38
  this._api = null // Lazy loaded matter.js exports
39
39
  this._logThrottle = new Map()
40
- if (startQueue) this.handleQueue()
40
+ if (startQueue) {
41
+ // The queue is intentionally fire-and-forget, but its Promise must always be
42
+ // observed so an unexpected loop failure cannot become an unhandled rejection.
43
+ this.handleQueue().catch((error) => this._log('error', `classMatter: queue loop: ${error.message}`))
44
+ }
41
45
  }
42
46
 
43
47
  _log = (level, message) => {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "6.0.9",
6
+ "version": "6.0.10",
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/",