node-red-contrib-knx-ultimate 5.0.4 → 5.2.2

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/nodes/commonFunctions.js +93 -0
  3. package/nodes/icons/node-matter-icon.svg +12 -0
  4. package/nodes/knxUltimateMatterBridge.html +342 -0
  5. package/nodes/knxUltimateMatterBridge.js +313 -0
  6. package/nodes/knxUltimateMatterDevice.html +632 -0
  7. package/nodes/knxUltimateMatterDevice.js +308 -0
  8. package/nodes/locales/de/knxUltimateMatterBridge.html +53 -0
  9. package/nodes/locales/de/knxUltimateMatterBridge.json +83 -0
  10. package/nodes/locales/de/knxUltimateMatterDevice.html +56 -0
  11. package/nodes/locales/de/knxUltimateMatterDevice.json +109 -0
  12. package/nodes/locales/de/matter-config.html +25 -0
  13. package/nodes/locales/de/matter-config.json +26 -0
  14. package/nodes/locales/de/matterbridge-config.html +34 -0
  15. package/nodes/locales/de/matterbridge-config.json +20 -0
  16. package/nodes/locales/en/knxUltimateMatterBridge.html +53 -0
  17. package/nodes/locales/en/knxUltimateMatterBridge.json +83 -0
  18. package/nodes/locales/en/knxUltimateMatterDevice.html +56 -0
  19. package/nodes/locales/en/knxUltimateMatterDevice.json +109 -0
  20. package/nodes/locales/en/matter-config.html +25 -0
  21. package/nodes/locales/en/matter-config.json +26 -0
  22. package/nodes/locales/en/matterbridge-config.html +34 -0
  23. package/nodes/locales/en/matterbridge-config.json +20 -0
  24. package/nodes/locales/es/knxUltimateMatterBridge.html +53 -0
  25. package/nodes/locales/es/knxUltimateMatterBridge.json +83 -0
  26. package/nodes/locales/es/knxUltimateMatterDevice.html +56 -0
  27. package/nodes/locales/es/knxUltimateMatterDevice.json +109 -0
  28. package/nodes/locales/es/matter-config.html +25 -0
  29. package/nodes/locales/es/matter-config.json +26 -0
  30. package/nodes/locales/es/matterbridge-config.html +34 -0
  31. package/nodes/locales/es/matterbridge-config.json +20 -0
  32. package/nodes/locales/fr/knxUltimateMatterBridge.html +53 -0
  33. package/nodes/locales/fr/knxUltimateMatterBridge.json +83 -0
  34. package/nodes/locales/fr/knxUltimateMatterDevice.html +56 -0
  35. package/nodes/locales/fr/knxUltimateMatterDevice.json +109 -0
  36. package/nodes/locales/fr/matter-config.html +25 -0
  37. package/nodes/locales/fr/matter-config.json +26 -0
  38. package/nodes/locales/fr/matterbridge-config.html +34 -0
  39. package/nodes/locales/fr/matterbridge-config.json +20 -0
  40. package/nodes/locales/it/knxUltimateMatterBridge.html +53 -0
  41. package/nodes/locales/it/knxUltimateMatterBridge.json +83 -0
  42. package/nodes/locales/it/knxUltimateMatterDevice.html +56 -0
  43. package/nodes/locales/it/knxUltimateMatterDevice.json +109 -0
  44. package/nodes/locales/it/matter-config.html +25 -0
  45. package/nodes/locales/it/matter-config.json +26 -0
  46. package/nodes/locales/it/matterbridge-config.html +34 -0
  47. package/nodes/locales/it/matterbridge-config.json +20 -0
  48. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +53 -0
  49. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +83 -0
  50. package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +56 -0
  51. package/nodes/locales/zh-CN/knxUltimateMatterDevice.json +109 -0
  52. package/nodes/locales/zh-CN/matter-config.html +25 -0
  53. package/nodes/locales/zh-CN/matter-config.json +26 -0
  54. package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
  55. package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
  56. package/nodes/matter-config.html +226 -0
  57. package/nodes/matter-config.js +242 -0
  58. package/nodes/matterbridge-config.html +121 -0
  59. package/nodes/matterbridge-config.js +220 -0
  60. package/nodes/utils/matterBridgeDeviceFactory.mjs +511 -0
  61. package/nodes/utils/matterBridgeEngine.mjs +290 -0
  62. package/nodes/utils/matterEngine.mjs +475 -0
  63. package/nodes/utils/matterKnxConverter.js +204 -0
  64. package/package.json +8 -2
@@ -0,0 +1,242 @@
1
+ /* eslint-disable max-len */
2
+ const path = require('path')
3
+
4
+ // 10/09/2024 Setup the color logger
5
+ const loggerSetup = (options) => {
6
+ const clog = require('node-color-log').createNamedLogger(options.setPrefix)
7
+ clog.setLevel(options.loglevel)
8
+ clog.setDate(() => (new Date()).toLocaleString())
9
+ return clog
10
+ }
11
+
12
+ module.exports = (RED) => {
13
+ function matterConfig (config) {
14
+ RED.nodes.createNode(this, config)
15
+ const node = this
16
+ node.nodeClients = [] // Stores the registered clients
17
+ node.loglevel = config.loglevel !== undefined ? config.loglevel : 'error'
18
+ node.sysLogger = null
19
+ node.matterManager = null
20
+ node.timerMatterConfigCheckState = null
21
+ try {
22
+ node.sysLogger = loggerSetup({ loglevel: node.loglevel, setPrefix: 'matter-config.js' })
23
+ } catch (error) { console.log(error.stack) }
24
+ node.name = config.name === undefined || config.name === '' ? 'Matter Controller' : config.name
25
+ node.fabricLabel = config.fabricLabel === undefined || config.fabricLabel === '' ? 'KNX-Ultimate' : config.fabricLabel
26
+ // Stable, filesystem-safe identity of the controller (fabric), bound to this config node.
27
+ node.matterInstanceId = `knxultimate-matter-${node.id.replace(/[^a-zA-Z0-9]/g, '')}`
28
+ node.matterStoragePath = path.join(RED.settings.userDir || '.', 'knxultimatestorage', 'matter')
29
+
30
+ // Helper like the hue-config one
31
+ Object.defineProperty(node, 'linkStatus', {
32
+ get: function () {
33
+ return node.matterManager?.matterConnectionStatus ?? 'disconnected'
34
+ }
35
+ })
36
+
37
+ const safeClientCall = (client, fn, label) => {
38
+ try {
39
+ if (!client || typeof fn !== 'function') return
40
+ fn()
41
+ } catch (error) {
42
+ node.sysLogger?.warn(`Matter client ${label} error: ${error.message}`)
43
+ }
44
+ }
45
+
46
+ node.initMatterConnection = async () => {
47
+ try {
48
+ if (node.matterManager !== null) await node.matterManager.close()
49
+ } catch (error) { /* empty */ }
50
+ try {
51
+ const { classMatter } = await import('./utils/matterEngine.mjs')
52
+ node.matterManager = new classMatter(node.matterStoragePath, node.matterInstanceId, node.fabricLabel, node.sysLogger)
53
+ } catch (error) {
54
+ node.sysLogger?.error(`Errore matter-config: node.initMatterConnection: ${error.message}`)
55
+ throw (error)
56
+ }
57
+
58
+ // Attribute changes coming from the Matter devices
59
+ node.matterManager.on('event', (_event) => {
60
+ node.nodeClients.forEach((_oClient) => {
61
+ try {
62
+ if (_oClient.handleSendMatter !== undefined) _oClient.handleSendMatter(_event)
63
+ } catch (error) {
64
+ node.sysLogger?.error(`Errore node.matterManager.on(event): ${error.message}`)
65
+ }
66
+ })
67
+ })
68
+
69
+ // Cluster events (button presses etc.)
70
+ node.matterManager.on('matterEvent', (_event) => {
71
+ node.nodeClients.forEach((_oClient) => {
72
+ try {
73
+ if (_oClient.handleMatterClusterEvent !== undefined) _oClient.handleMatterClusterEvent(_event)
74
+ } catch (error) {
75
+ node.sysLogger?.error(`Errore node.matterManager.on(matterEvent): ${error.message}`)
76
+ }
77
+ })
78
+ })
79
+
80
+ // A paired node completed the initial sync: let the clients do their initial read
81
+ node.matterManager.on('nodeInitialized', (_nodeId) => {
82
+ node.nodeClients.forEach((_oClient) => {
83
+ if (_oClient.matterNodeId === _nodeId) {
84
+ safeClientCall(_oClient, () => _oClient.handleMatterNodeInitialized(), 'handleMatterNodeInitialized')
85
+ }
86
+ })
87
+ })
88
+
89
+ // Connection state of a single paired device changed
90
+ node.matterManager.on('nodeState', (_nodeId, _state) => {
91
+ node.nodeClients.forEach((_oClient) => {
92
+ if (_oClient.matterNodeId === _nodeId) {
93
+ safeClientCall(_oClient, () => _oClient.setNodeStatusMatter({
94
+ fill: _state === 'connected' ? 'green' : 'yellow',
95
+ shape: _state === 'connected' ? 'dot' : 'ring',
96
+ text: _state,
97
+ payload: ''
98
+ }), 'setNodeStatusMatter')
99
+ if (_state === 'connected') {
100
+ safeClientCall(_oClient, () => _oClient.handleMatterNodeInitialized(), 'handleMatterNodeInitialized')
101
+ }
102
+ }
103
+ })
104
+ })
105
+
106
+ node.matterManager.on('connected', () => {
107
+ node.nodeClients.forEach((_oClient) => {
108
+ safeClientCall(_oClient, () => _oClient.setNodeStatusMatter({
109
+ fill: 'green',
110
+ shape: 'ring',
111
+ text: 'Controller ready',
112
+ payload: ''
113
+ }), 'setNodeStatusMatter')
114
+ })
115
+ })
116
+
117
+ node.matterManager.on('disconnected', () => {
118
+ node.nodeClients.forEach((_oClient) => {
119
+ safeClientCall(_oClient, () => _oClient.setNodeStatusMatter({
120
+ fill: 'red',
121
+ shape: 'ring',
122
+ text: 'Matter Disconnected',
123
+ payload: ''
124
+ }), 'setNodeStatusMatter')
125
+ })
126
+ })
127
+
128
+ try {
129
+ await node.matterManager.Connect()
130
+ } catch (error) {
131
+ node.sysLogger?.error(`Errore matter-config: Connect: ${error.message}`)
132
+ }
133
+ }
134
+
135
+ node.startWatchdogTimer = async () => {
136
+ if (node.timerMatterConfigCheckState !== null) clearTimeout(node.timerMatterConfigCheckState)
137
+ node.timerMatterConfigCheckState = setTimeout(() => {
138
+ (async () => {
139
+ if (node.linkStatus === 'disconnected') {
140
+ try {
141
+ await node.initMatterConnection()
142
+ } catch (error) {
143
+ node.sysLogger?.error(`Errore matter-config: node.startWatchdogTimer: ${error.message}`)
144
+ }
145
+ }
146
+ await node.startWatchdogTimer()
147
+ })()
148
+ }, 60000)
149
+ }
150
+
151
+ setTimeout(() => {
152
+ (async () => {
153
+ try {
154
+ await node.initMatterConnection()
155
+ } catch (error) {
156
+ // Never let a failed startup (missing dependency, corrupted storage...) become
157
+ // an unhandled rejection: the watchdog will retry the connection later.
158
+ node.sysLogger?.error(`matter-config: startup error: ${error.message}`)
159
+ }
160
+ try {
161
+ node.startWatchdogTimer()
162
+ } catch (error) {
163
+ node.sysLogger?.error(`matter-config: watchdog start error: ${error.message}`)
164
+ }
165
+ })()
166
+ }, 5000)
167
+
168
+ // Functions called from the nodes and the admin endpoints ----------------------------------------
169
+ node.getCommissionedNodesDetails = () => {
170
+ if (node.matterManager === null) return []
171
+ return node.matterManager.getCommissionedNodesDetails()
172
+ }
173
+
174
+ node.getNodeStructure = (_nodeId) => {
175
+ if (node.matterManager === null) throw new Error('Matter controller not started')
176
+ return node.matterManager.getNodeStructure(_nodeId)
177
+ }
178
+
179
+ node.commission = async (_pairingCode) => {
180
+ if (node.matterManager === null) throw new Error('Matter controller not started')
181
+ return node.matterManager.commission(_pairingCode)
182
+ }
183
+
184
+ node.removeCommissionedNode = async (_nodeId) => {
185
+ if (node.matterManager === null) throw new Error('Matter controller not started')
186
+ return node.matterManager.removeNode(_nodeId)
187
+ }
188
+ // END functions called from the nodes -------------------------------------------------------------
189
+
190
+ node.addClient = (_Node) => {
191
+ try {
192
+ if (node.nodeClients.filter((x) => x.id === _Node.id).length !== 0) return
193
+ node.nodeClients.push(_Node)
194
+ if (node.linkStatus !== 'connected') {
195
+ safeClientCall(_Node, () => _Node.setNodeStatusMatter({
196
+ fill: 'grey',
197
+ shape: 'ring',
198
+ text: 'Waiting for Matter controller'
199
+ }), 'setNodeStatusMatter')
200
+ return
201
+ }
202
+ safeClientCall(_Node, () => _Node.setNodeStatusMatter({
203
+ fill: 'green',
204
+ shape: 'ring',
205
+ text: 'Ready'
206
+ }), 'setNodeStatusMatter')
207
+ // If the paired node already has values in cache, do the initial read straight away
208
+ safeClientCall(_Node, () => _Node.handleMatterNodeInitialized(), 'handleMatterNodeInitialized')
209
+ } catch (error) {
210
+ node.sysLogger?.error(`matter-config: addClient error: ${error.message}`)
211
+ }
212
+ }
213
+
214
+ node.removeClient = (_Node) => {
215
+ try {
216
+ node.nodeClients = node.nodeClients.filter((x) => x.id !== _Node.id)
217
+ } catch (error) {
218
+ /* empty */
219
+ }
220
+ }
221
+
222
+ node.on('close', (done) => {
223
+ try {
224
+ node.nodeClients = []
225
+ if (node.timerMatterConfigCheckState !== null) clearTimeout(node.timerMatterConfigCheckState);
226
+ (async () => {
227
+ try {
228
+ if (node.matterManager !== null) await node.matterManager.close()
229
+ node.matterManager = null
230
+ node.sysLogger = null
231
+ done()
232
+ } catch (error) {
233
+ done()
234
+ }
235
+ })()
236
+ } catch (error) {
237
+ done()
238
+ }
239
+ })
240
+ }
241
+ RED.nodes.registerType('matter-config', matterConfig)
242
+ }
@@ -0,0 +1,121 @@
1
+ <script type="text/javascript" src="resources/node-red-contrib-knx-ultimate/htmlUtils.js"></script>
2
+
3
+ <script type="text/javascript">
4
+ RED.nodes.registerType('matterbridge-config', {
5
+ category: 'config',
6
+ defaults: {
7
+ name: { value: '' },
8
+ bridgeDeviceName: { value: 'KNX-Ultimate Bridge' },
9
+ bridgePort: { value: 5540, required: true, validate: RED.validators.number() }
10
+ },
11
+ oneditprepare: function () {
12
+ try { RED.sidebar.show('help'); } catch (error) { }
13
+
14
+ var node = this;
15
+ var $pairingBody = $('#mbc-pairing-body');
16
+ var $refreshPairing = $('#mbc-refresh-pairing');
17
+ var $resetButton = $('#mbc-factory-reset');
18
+
19
+ var t = function (key) {
20
+ return RED._('node-red-contrib-knx-ultimate/matterbridge-config:matterbridge-config.properties.' + key);
21
+ };
22
+
23
+ function loadPairingInfo() {
24
+ $pairingBody.html('<i class="fa fa-circle-notch fa-spin"></i>');
25
+ $.getJSON('KNXUltimateMatterBridgeInfo?configId=' + node.id + '&_=' + Date.now(), function (info) {
26
+ $pairingBody.empty();
27
+ if (info.error) {
28
+ $pairingBody.append($('<p style="color:#999;"></p>').text(info.error));
29
+ return;
30
+ }
31
+ if (!info.running) {
32
+ $pairingBody.append($('<p style="color:#999;"></p>').text(t('not_running')));
33
+ return;
34
+ }
35
+ if (info.commissioned) {
36
+ $pairingBody.append($('<p style="color:#1b7d33;"></p>').text(t('commissioned') + ' (' + info.fabrics.length + ')'));
37
+ info.fabrics.forEach(function (f) {
38
+ $pairingBody.append($('<div style="margin-left:10px;"></div>').text('• ' + (f.label || 'Controller') + ' (vendor ' + f.vendorId + ')'));
39
+ });
40
+ $resetButton.show();
41
+ return;
42
+ }
43
+ $pairingBody.append($('<p style="color:#b8860b;"></p>').text(t('awaiting')));
44
+ if (info.manualPairingCode) {
45
+ $pairingBody.append($('<p></p>').append($('<b style="font-size:18px; letter-spacing:2px;"></b>').text(info.manualPairingCode)));
46
+ }
47
+ if (info.qrAscii) {
48
+ $pairingBody.append($('<pre style="font-family:monospace; line-height:1.05; font-size:7px; display:inline-block; padding:6px; background:#fff; color:#000;"></pre>').text(info.qrAscii));
49
+ }
50
+ if (info.qrPairingCode) {
51
+ var url = 'https://project-chip.github.io/connectedhomeip/qrcode.html?data=' + encodeURIComponent(info.qrPairingCode);
52
+ $pairingBody.append($('<p></p>').append($('<a target="_blank"></a>').attr('href', url).text(t('qr_link'))));
53
+ }
54
+ $resetButton.hide();
55
+ }).fail(function () {
56
+ $pairingBody.empty();
57
+ $pairingBody.append($('<p style="color:#999;"></p>').text(t('deploy_first')));
58
+ });
59
+ }
60
+
61
+ $refreshPairing.on('click', function () { loadPairingInfo(); });
62
+ $resetButton.on('click', function () {
63
+ if (!window.confirm(t('reset_confirm'))) return;
64
+ $resetButton.prop('disabled', true);
65
+ $.getJSON('KNXUltimateMatterBridgeReset?configId=' + node.id, function (data) {
66
+ if (data && data.error) RED.notify(data.error, { type: 'error' });
67
+ else RED.notify(t('reset_ok'), { type: 'success' });
68
+ loadPairingInfo();
69
+ }).always(function () { $resetButton.prop('disabled', false); });
70
+ });
71
+
72
+ loadPairingInfo();
73
+ },
74
+ oneditsave: function () {
75
+ try { RED.sidebar.show('info'); } catch (error) { }
76
+ },
77
+ label: function () {
78
+ return this.name || this.bridgeDeviceName || 'Matter Bridge';
79
+ }
80
+ });
81
+ </script>
82
+
83
+ <script type="text/html" data-template-name="matterbridge-config">
84
+ <div class="form-row" style="margin-bottom:10px;">
85
+ <span style="color:#ff0000"><i class="fa fa-youtube"></i></span>&nbsp;<a target="_blank" href="https://www.youtube.com/playlist?list=PL9Yh1bjbLAYrU8PsVhW4xzEug2WtVFv3E"><b>KNX-Ultimate video tutorials (YouTube playlist)</b></a>
86
+ </div>
87
+
88
+ <div class="form-row">
89
+ <b><span data-i18n="matterbridge-config.properties.title"></span></b>
90
+ <p><span data-i18n="matterbridge-config.properties.intro"></span></p>
91
+ </div>
92
+
93
+ <div class="form-row">
94
+ <label for="node-config-input-name">
95
+ <i class="fa fa-tag"></i> <span data-i18n="matterbridge-config.properties.node-config-input-name"></span>
96
+ </label>
97
+ <input type="text" id="node-config-input-name">
98
+ </div>
99
+
100
+ <div class="form-row">
101
+ <label for="node-config-input-bridgeDeviceName" style="width:180px;">
102
+ <i class="fa fa-share-alt"></i> <span data-i18n="matterbridge-config.properties.bridge_name"></span>
103
+ </label>
104
+ <input type="text" id="node-config-input-bridgeDeviceName" style="width:220px;">
105
+ <label for="node-config-input-bridgePort" style="width:60px; text-align:right;"><span data-i18n="matterbridge-config.properties.port"></span></label>
106
+ <input type="number" id="node-config-input-bridgePort" style="width:90px;">
107
+ </div>
108
+
109
+ <hr/>
110
+
111
+ <div class="form-row">
112
+ <b><i class="fa fa-qrcode"></i>&nbsp;<span data-i18n="matterbridge-config.properties.pairing_title"></span></b>
113
+ <button type="button" id="mbc-refresh-pairing" class="red-ui-button" style="margin-left:6px; color:#1b7d33; border-color:#1b7d33;">
114
+ <i class="fa fa-refresh"></i>
115
+ </button>
116
+ <button type="button" id="mbc-factory-reset" class="red-ui-button" style="margin-left:6px; color:#c00; border-color:#c00; display:none;">
117
+ <i class="fa fa-trash"></i>&nbsp;<span data-i18n="matterbridge-config.properties.reset_button"></span>
118
+ </button>
119
+ </div>
120
+ <div class="form-row" id="mbc-pairing-body" style="text-align:center;"></div>
121
+ </script>
@@ -0,0 +1,220 @@
1
+ /* eslint-disable max-len */
2
+ const path = require('path')
3
+
4
+ // 10/09/2024 Setup the color logger
5
+ const loggerSetup = (options) => {
6
+ const clog = require('node-color-log').createNamedLogger(options.setPrefix)
7
+ clog.setLevel(options.loglevel)
8
+ clog.setDate(() => (new Date()).toLocaleString())
9
+ return clog
10
+ }
11
+
12
+ // The running Matter servers, keyed by config node id. Kept OUTSIDE the node lifecycle:
13
+ // a re-deploy must NOT restart the Matter server, otherwise the paired controllers
14
+ // resume their session without re-reading the structure and never see device changes.
15
+ // The devices are reconciled live instead; the server is closed only when the config
16
+ // node is deleted/disabled.
17
+ const runningBridges = new Map()
18
+
19
+ module.exports = (RED) => {
20
+ function matterBridgeConfig (config) {
21
+ RED.nodes.createNode(this, config)
22
+ const node = this
23
+ node.loglevel = config.loglevel !== undefined ? config.loglevel : 'error'
24
+ node.sysLogger = null
25
+ try {
26
+ node.sysLogger = loggerSetup({ loglevel: node.loglevel, setPrefix: 'matterbridge-config.js' })
27
+ } catch (error) { console.log(error.stack) }
28
+
29
+ node.name = config.name === undefined || config.name === '' ? 'Matter Bridge' : config.name
30
+ node.bridgePort = Number(config.bridgePort) || 5540
31
+ node.bridgeDeviceName = config.bridgeDeviceName === undefined || config.bridgeDeviceName === '' ? 'KNX-Ultimate Bridge' : config.bridgeDeviceName
32
+ node.matterInstanceId = `knxultimate-bridge-${node.id.replace(/[^a-zA-Z0-9]/g, '')}`
33
+ node.matterStoragePath = path.join(RED.settings.userDir || '.', 'knxultimatestorage', 'matter')
34
+
35
+ node.matterBridge = null
36
+ node.deviceClients = new Map() // matterDeviceId -> device node
37
+ let startupTimer = null
38
+ let reconcileTimer = null
39
+ let closing = false
40
+
41
+ Object.defineProperty(node, 'linkStatus', {
42
+ get: function () {
43
+ return node.matterBridge?.bridgeStatus === 'running' ? 'running' : 'stopped'
44
+ }
45
+ })
46
+
47
+ const safeClientCall = (client, fn, label) => {
48
+ try {
49
+ if (!client || typeof fn !== 'function') return
50
+ fn()
51
+ } catch (error) {
52
+ node.sysLogger?.warn(`Matter bridge client ${label} error: ${error.message}`)
53
+ }
54
+ }
55
+
56
+ // Desired device definitions built from the currently registered device nodes.
57
+ const buildDeviceDefs = () => {
58
+ const defs = []
59
+ node.deviceClients.forEach((client) => {
60
+ try {
61
+ const def = typeof client.getMatterDef === 'function' ? client.getMatterDef() : undefined
62
+ if (def && def.id && def.type) defs.push(def)
63
+ } catch (error) { /* empty */ }
64
+ })
65
+ return defs
66
+ }
67
+
68
+ // Notify every registered device node that the bridge status changed.
69
+ const notifyClientsStatus = () => {
70
+ node.deviceClients.forEach((client) => {
71
+ safeClientCall(client, () => client.handleBridgeStatus(), 'handleBridgeStatus')
72
+ })
73
+ }
74
+
75
+ const bindEngineEvents = (engine) => {
76
+ engine.removeAllListeners()
77
+ // Matter -> KNX: a controller (Alexa...) sent a command to a bridged device.
78
+ // Route it to the device node that owns that Matter device.
79
+ engine.on('command', (command) => {
80
+ try {
81
+ const client = node.deviceClients.get(command.deviceId)
82
+ if (client === undefined) return
83
+ safeClientCall(client, () => client.handleMatterCommand(command), 'handleMatterCommand')
84
+ } catch (error) {
85
+ node.sysLogger?.warn(`matterbridge-config: command routing error: ${error.message}`)
86
+ }
87
+ })
88
+ engine.on('commissioned', () => notifyClientsStatus())
89
+ engine.on('decommissioned', () => notifyClientsStatus())
90
+ engine.on('fabricsChanged', () => notifyClientsStatus())
91
+ engine.on('online', () => notifyClientsStatus())
92
+ }
93
+
94
+ // Reconcile the exposed devices with the currently registered device nodes.
95
+ const scheduleReconcile = () => {
96
+ if (reconcileTimer !== null) clearTimeout(reconcileTimer)
97
+ reconcileTimer = setTimeout(() => {
98
+ (async () => {
99
+ try {
100
+ if (node.matterBridge === null || node.matterBridge.bridgeStatus !== 'running') return
101
+ await node.matterBridge.reconcileDevices(buildDeviceDefs())
102
+ notifyClientsStatus()
103
+ } catch (error) {
104
+ node.sysLogger?.warn(`matterbridge-config: reconcile error: ${error.message}`)
105
+ }
106
+ })()
107
+ }, 1500)
108
+ }
109
+
110
+ const ensureEngineStarted = async () => {
111
+ try {
112
+ const { classMatterBridge } = await import('./utils/matterBridgeEngine.mjs')
113
+ const existing = runningBridges.get(node.id)
114
+
115
+ if (existing !== undefined && existing.bridgeStatus === 'running' &&
116
+ Number(existing.options.port) === node.bridgePort &&
117
+ existing.options.deviceName === node.bridgeDeviceName) {
118
+ // Re-deploy: reuse the running Matter server and reconcile the devices live.
119
+ node.matterBridge = existing
120
+ bindEngineEvents(existing)
121
+ await existing.reconcileDevices(buildDeviceDefs())
122
+ notifyClientsStatus()
123
+ return
124
+ }
125
+
126
+ if (existing !== undefined) {
127
+ // Port or bridge name changed: a full restart is unavoidable.
128
+ try {
129
+ existing.removeAllListeners()
130
+ await existing.close()
131
+ } catch (error) { /* empty */ }
132
+ runningBridges.delete(node.id)
133
+ }
134
+
135
+ const engine = new classMatterBridge(node.matterStoragePath, node.matterInstanceId, {
136
+ info: (m) => node.sysLogger?.info(m),
137
+ warn: (m) => node.sysLogger?.warn(m),
138
+ error: (m) => node.sysLogger?.error(m),
139
+ debug: (m) => node.sysLogger?.debug(m)
140
+ }, { port: node.bridgePort, deviceName: node.bridgeDeviceName })
141
+ runningBridges.set(node.id, engine)
142
+ node.matterBridge = engine
143
+ bindEngineEvents(engine)
144
+ await engine.start(buildDeviceDefs())
145
+ notifyClientsStatus()
146
+ } catch (error) {
147
+ node.sysLogger?.error(`matterbridge-config: ensureEngineStarted error: ${error.message}`)
148
+ }
149
+ }
150
+
151
+ // The Matter server is started shortly after boot, giving the device nodes the
152
+ // time to register themselves during flow initialization.
153
+ startupTimer = setTimeout(() => {
154
+ (async () => {
155
+ if (closing) return
156
+ await ensureEngineStarted()
157
+ })()
158
+ }, 5000)
159
+
160
+ // Functions called from the device nodes -----------------------------------------
161
+ node.registerDevice = (client) => {
162
+ try {
163
+ if (!client || !client.matterDeviceId) return
164
+ node.deviceClients.set(client.matterDeviceId, client)
165
+ scheduleReconcile()
166
+ } catch (error) {
167
+ node.sysLogger?.warn(`matterbridge-config: registerDevice error: ${error.message}`)
168
+ }
169
+ }
170
+
171
+ node.unregisterDevice = (client) => {
172
+ try {
173
+ if (!client || !client.matterDeviceId) return
174
+ node.deviceClients.delete(client.matterDeviceId)
175
+ scheduleReconcile()
176
+ } catch (error) { /* empty */ }
177
+ }
178
+
179
+ node.setDeviceState = async (deviceId, fn, value) => {
180
+ if (node.matterBridge === null) return false
181
+ return node.matterBridge.setDeviceState(deviceId, fn, value)
182
+ }
183
+
184
+ node.getPairingInfo = () => {
185
+ if (node.matterBridge === null) return { running: false }
186
+ return node.matterBridge.getPairingInfo()
187
+ }
188
+
189
+ node.factoryResetBridge = async () => {
190
+ if (node.matterBridge === null) throw new Error('Matter bridge not started')
191
+ await node.matterBridge.factoryReset()
192
+ notifyClientsStatus()
193
+ }
194
+ // END functions called from the device nodes -------------------------------------
195
+
196
+ node.on('close', (removed, done) => {
197
+ closing = true
198
+ try {
199
+ if (startupTimer !== null) clearTimeout(startupTimer)
200
+ if (reconcileTimer !== null) clearTimeout(reconcileTimer)
201
+ } catch (error) { /* empty */ }
202
+ try {
203
+ if (node.matterBridge !== null) node.matterBridge.removeAllListeners()
204
+ } catch (error) { /* empty */ }
205
+ (async () => {
206
+ try {
207
+ if (removed === true && node.matterBridge !== null) {
208
+ await node.matterBridge.close()
209
+ runningBridges.delete(node.id)
210
+ }
211
+ } catch (error) { /* empty */ }
212
+ node.matterBridge = null
213
+ node.deviceClients.clear()
214
+ node.sysLogger = null
215
+ done()
216
+ })()
217
+ })
218
+ }
219
+ RED.nodes.registerType('matterbridge-config', matterBridgeConfig)
220
+ }