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.
- package/CHANGELOG.md +20 -10
- package/nodes/commonFunctions.js +9 -9
- package/nodes/knxUltimateIoTBridge.html +59 -2
- package/nodes/knxUltimateIoTBridge.js +4 -0
- package/nodes/knxUltimateMatterBridge.html +212 -299
- package/nodes/knxUltimateMatterBridge.js +166 -309
- package/nodes/knxUltimateMatterDevice.html +46 -0
- package/nodes/lib/mqtt-bridge.js +43 -1
- package/nodes/locales/de/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/de/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/de/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/de/matterbridge-config.html +34 -0
- package/nodes/locales/de/matterbridge-config.json +20 -0
- package/nodes/locales/en/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/en/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +14 -25
- package/nodes/locales/en/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/en/matterbridge-config.html +34 -0
- package/nodes/locales/en/matterbridge-config.json +20 -0
- package/nodes/locales/es/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/es/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/es/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/es/matterbridge-config.html +34 -0
- package/nodes/locales/es/matterbridge-config.json +20 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/fr/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/fr/matterbridge-config.html +34 -0
- package/nodes/locales/fr/matterbridge-config.json +20 -0
- package/nodes/locales/it/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/it/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/it/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/it/matterbridge-config.html +34 -0
- package/nodes/locales/it/matterbridge-config.json +20 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
- package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
- package/nodes/matterbridge-config.html +121 -0
- package/nodes/matterbridge-config.js +220 -0
- 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> <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> <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> <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.
|
|
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
|
},
|