node-red-contrib-knx-ultimate 6.0.4 → 6.0.5

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.
@@ -71,27 +71,31 @@ class classMatterBridge extends EventEmitter {
71
71
  try {
72
72
  api.Logger.level = api.LogLevel.ERROR
73
73
  } catch (error) { /* empty */ }
74
- const environment = api.Environment.default
75
- // Same storage root as the Matter controller engine: instances are separated by their id.
76
- environment.vars.set('storage.path', this.storagePath)
77
-
78
- this.server = await api.ServerNode.create({
79
- id: this.instanceId,
80
- network: { port: this.options.port },
81
- productDescription: {
82
- name: this.options.deviceName,
83
- deviceType: api.AggregatorEndpoint.deviceType
84
- },
85
- basicInformation: {
86
- vendorName: 'KNX-Ultimate',
87
- vendorId: api.VendorId(0xFFF1), // Test vendor ID
88
- nodeLabel: this.options.deviceName,
89
- productName: this.options.deviceName,
90
- productLabel: this.options.deviceName,
91
- productId: 0x8000,
92
- serialNumber: `knxu-${this.instanceId}`.slice(0, 32),
93
- uniqueId: this.instanceId.slice(-32)
94
- }
74
+ const { withEnvironmentLock } = await import('./matterEnvironmentLock.mjs')
75
+ // Environment.default (and its 'storage.path' var) is a process-wide singleton shared
76
+ // with the Matter Controller engine: serialize the set+create critical section so a
77
+ // concurrent startup can never observe another engine's storage path mid-flight.
78
+ this.server = await withEnvironmentLock(async () => {
79
+ const environment = api.Environment.default
80
+ environment.vars.set('storage.path', this.storagePath)
81
+ return api.ServerNode.create({
82
+ id: this.instanceId,
83
+ network: { port: this.options.port },
84
+ productDescription: {
85
+ name: this.options.deviceName,
86
+ deviceType: api.AggregatorEndpoint.deviceType
87
+ },
88
+ basicInformation: {
89
+ vendorName: 'KNX-Ultimate',
90
+ vendorId: api.VendorId(0xFFF1), // Test vendor ID
91
+ nodeLabel: this.options.deviceName,
92
+ productName: this.options.deviceName,
93
+ productLabel: this.options.deviceName,
94
+ productId: 0x8000,
95
+ serialNumber: `knxu-${this.instanceId}`.slice(0, 32),
96
+ uniqueId: this.instanceId.slice(-32)
97
+ }
98
+ })
95
99
  })
96
100
 
97
101
  this.aggregator = new api.Endpoint(api.AggregatorEndpoint, { id: 'aggregator' })
@@ -171,7 +175,8 @@ class classMatterBridge extends EventEmitter {
171
175
  const needsRecreate = newDef !== undefined && oldDef !== undefined &&
172
176
  (newDef.type !== oldDef.type ||
173
177
  (newDef.invertPosition === true) !== (oldDef.invertPosition === true) ||
174
- (newDef.coverExposeAsDimmableLight === true) !== (oldDef.coverExposeAsDimmableLight === true))
178
+ (newDef.hasHeatingSetpoint !== false) !== (oldDef.hasHeatingSetpoint !== false) ||
179
+ (newDef.hasCoolingSetpoint === true) !== (oldDef.hasCoolingSetpoint === true))
175
180
  if (newDef === undefined || needsRecreate) {
176
181
  await this.removeDeviceEndpoint(id)
177
182
  }
@@ -175,14 +175,20 @@ class classMatter extends EventEmitter {
175
175
  // Quiet down the very verbose default matter.js console logging.
176
176
  api.Logger.level = api.LogLevel.ERROR
177
177
  } catch (error) { /* empty */ }
178
- const environment = api.Environment.default
179
- environment.vars.set('storage.path', this.storagePath)
180
- this.controller = new api.CommissioningController({
181
- environment: { environment, id: this.instanceId },
182
- autoConnect: false,
183
- adminFabricLabel: this.fabricLabel
178
+ const { withEnvironmentLock } = await import('./matterEnvironmentLock.mjs')
179
+ // Serialize with the Matter Bridge engine: both write Environment.default.vars
180
+ // ('storage.path') before creating their matter.js node, and that Environment is a
181
+ // process-wide singleton. See matterEnvironmentLock.mjs for why this matters.
182
+ await withEnvironmentLock(async () => {
183
+ const environment = api.Environment.default
184
+ environment.vars.set('storage.path', this.storagePath)
185
+ this.controller = new api.CommissioningController({
186
+ environment: { environment, id: this.instanceId },
187
+ autoConnect: false,
188
+ adminFabricLabel: this.fabricLabel
189
+ })
190
+ await this.controller.start()
184
191
  })
185
- await this.controller.start()
186
192
  this.matterConnectionStatus = 'connected'
187
193
  this.emit('connected')
188
194
  // Connect all already commissioned nodes and hook their events.
@@ -0,0 +1,32 @@
1
+ /* eslint-disable max-len */
2
+ // matter.js keeps its runtime configuration (including 'storage.path') on
3
+ // Environment.default, a SINGLE process-wide object shared by every ServerNode /
4
+ // CommissioningController instance (the KNX Matter Bridge engine AND the Matter
5
+ // Controller engine both read/write it). Today every engine instance in this
6
+ // package happens to set the same 'storage.path' value, so the two engines
7
+ // racing on Environment.default.vars.set() before their own create() call has
8
+ // historically been harmless in practice. It is still a shared-mutable-global
9
+ // access pattern: if a future change ever gives two engines different paths
10
+ // (e.g. per-instance storage isolation), an unguarded interleaving could make
11
+ // engine A's ServerNode.create() observe engine B's path.
12
+ //
13
+ // This module offers a tiny FIFO async mutex so callers can wrap
14
+ // "set the env var(s) then create the node" as one atomic critical section,
15
+ // regardless of how many bridge/controller config nodes start concurrently.
16
+ let queue = Promise.resolve()
17
+
18
+ /**
19
+ * Runs `fn` exclusively with respect to every other call to withEnvironmentLock
20
+ * in this process. Calls are queued FIFO; a throwing/rejecting `fn` does not
21
+ * block subsequent queued calls.
22
+ * @param {() => Promise<any>} fn
23
+ * @returns {Promise<any>} whatever `fn` resolves to
24
+ */
25
+ function withEnvironmentLock (fn) {
26
+ const run = queue.then(fn, fn)
27
+ // Keep the chain alive even if `fn` rejects, so later callers still run.
28
+ queue = run.then(() => undefined, () => undefined)
29
+ return run
30
+ }
31
+
32
+ export { withEnvironmentLock }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "6.0.4",
6
+ "version": "6.0.5",
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/",