node-red-contrib-knx-ultimate 5.2.1 → 5.2.3

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 (47) hide show
  1. package/CHANGELOG.md +20 -10
  2. package/nodes/commonFunctions.js +9 -9
  3. package/nodes/knxUltimateIoTBridge.html +59 -2
  4. package/nodes/knxUltimateIoTBridge.js +4 -0
  5. package/nodes/knxUltimateMatterBridge.html +212 -299
  6. package/nodes/knxUltimateMatterBridge.js +166 -309
  7. package/nodes/knxUltimateMatterDevice.html +46 -0
  8. package/nodes/lib/mqtt-bridge.js +43 -1
  9. package/nodes/locales/de/knxUltimateIoTBridge.html +1 -0
  10. package/nodes/locales/de/knxUltimateIoTBridge.json +6 -0
  11. package/nodes/locales/de/knxUltimateMatterBridge.html +13 -24
  12. package/nodes/locales/de/knxUltimateMatterBridge.json +4 -1
  13. package/nodes/locales/de/matterbridge-config.html +34 -0
  14. package/nodes/locales/de/matterbridge-config.json +20 -0
  15. package/nodes/locales/en/knxUltimateIoTBridge.html +1 -0
  16. package/nodes/locales/en/knxUltimateIoTBridge.json +6 -0
  17. package/nodes/locales/en/knxUltimateMatterBridge.html +14 -25
  18. package/nodes/locales/en/knxUltimateMatterBridge.json +4 -1
  19. package/nodes/locales/en/matterbridge-config.html +34 -0
  20. package/nodes/locales/en/matterbridge-config.json +20 -0
  21. package/nodes/locales/es/knxUltimateIoTBridge.html +1 -0
  22. package/nodes/locales/es/knxUltimateIoTBridge.json +6 -0
  23. package/nodes/locales/es/knxUltimateMatterBridge.html +13 -24
  24. package/nodes/locales/es/knxUltimateMatterBridge.json +4 -1
  25. package/nodes/locales/es/matterbridge-config.html +34 -0
  26. package/nodes/locales/es/matterbridge-config.json +20 -0
  27. package/nodes/locales/fr/knxUltimateIoTBridge.html +1 -0
  28. package/nodes/locales/fr/knxUltimateIoTBridge.json +6 -0
  29. package/nodes/locales/fr/knxUltimateMatterBridge.html +13 -24
  30. package/nodes/locales/fr/knxUltimateMatterBridge.json +4 -1
  31. package/nodes/locales/fr/matterbridge-config.html +34 -0
  32. package/nodes/locales/fr/matterbridge-config.json +20 -0
  33. package/nodes/locales/it/knxUltimateIoTBridge.html +1 -0
  34. package/nodes/locales/it/knxUltimateIoTBridge.json +6 -0
  35. package/nodes/locales/it/knxUltimateMatterBridge.html +13 -24
  36. package/nodes/locales/it/knxUltimateMatterBridge.json +4 -1
  37. package/nodes/locales/it/matterbridge-config.html +34 -0
  38. package/nodes/locales/it/matterbridge-config.json +20 -0
  39. package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +1 -0
  40. package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +6 -0
  41. package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +13 -24
  42. package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +4 -1
  43. package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
  44. package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
  45. package/nodes/matterbridge-config.html +121 -0
  46. package/nodes/matterbridge-config.js +220 -0
  47. package/package.json +2 -1
@@ -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
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "engines": {
4
4
  "node": ">=20.18.1"
5
5
  },
6
- "version": "5.2.1",
6
+ "version": "5.2.3",
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/",
@@ -72,6 +72,7 @@
72
72
  "knxUltimateHueCameraMotion": "/nodes/knxUltimateHueCameraMotion.js",
73
73
  "matter-config": "/nodes/matter-config.js",
74
74
  "knxUltimateMatterDevice": "/nodes/knxUltimateMatterDevice.js",
75
+ "matterbridge-config": "/nodes/matterbridge-config.js",
75
76
  "knxUltimateMatterBridge": "/nodes/knxUltimateMatterBridge.js"
76
77
  }
77
78
  },