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.
- package/CHANGELOG.md +24 -0
- package/nodes/commonFunctions.js +93 -0
- package/nodes/icons/node-matter-icon.svg +12 -0
- package/nodes/knxUltimateMatterBridge.html +342 -0
- package/nodes/knxUltimateMatterBridge.js +313 -0
- package/nodes/knxUltimateMatterDevice.html +632 -0
- package/nodes/knxUltimateMatterDevice.js +308 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +53 -0
- package/nodes/locales/de/knxUltimateMatterBridge.json +83 -0
- package/nodes/locales/de/knxUltimateMatterDevice.html +56 -0
- package/nodes/locales/de/knxUltimateMatterDevice.json +109 -0
- package/nodes/locales/de/matter-config.html +25 -0
- package/nodes/locales/de/matter-config.json +26 -0
- package/nodes/locales/de/matterbridge-config.html +34 -0
- package/nodes/locales/de/matterbridge-config.json +20 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +53 -0
- package/nodes/locales/en/knxUltimateMatterBridge.json +83 -0
- package/nodes/locales/en/knxUltimateMatterDevice.html +56 -0
- package/nodes/locales/en/knxUltimateMatterDevice.json +109 -0
- package/nodes/locales/en/matter-config.html +25 -0
- package/nodes/locales/en/matter-config.json +26 -0
- package/nodes/locales/en/matterbridge-config.html +34 -0
- package/nodes/locales/en/matterbridge-config.json +20 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +53 -0
- package/nodes/locales/es/knxUltimateMatterBridge.json +83 -0
- package/nodes/locales/es/knxUltimateMatterDevice.html +56 -0
- package/nodes/locales/es/knxUltimateMatterDevice.json +109 -0
- package/nodes/locales/es/matter-config.html +25 -0
- package/nodes/locales/es/matter-config.json +26 -0
- package/nodes/locales/es/matterbridge-config.html +34 -0
- package/nodes/locales/es/matterbridge-config.json +20 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +53 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.json +83 -0
- package/nodes/locales/fr/knxUltimateMatterDevice.html +56 -0
- package/nodes/locales/fr/knxUltimateMatterDevice.json +109 -0
- package/nodes/locales/fr/matter-config.html +25 -0
- package/nodes/locales/fr/matter-config.json +26 -0
- package/nodes/locales/fr/matterbridge-config.html +34 -0
- package/nodes/locales/fr/matterbridge-config.json +20 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +53 -0
- package/nodes/locales/it/knxUltimateMatterBridge.json +83 -0
- package/nodes/locales/it/knxUltimateMatterDevice.html +56 -0
- package/nodes/locales/it/knxUltimateMatterDevice.json +109 -0
- package/nodes/locales/it/matter-config.html +25 -0
- package/nodes/locales/it/matter-config.json +26 -0
- package/nodes/locales/it/matterbridge-config.html +34 -0
- package/nodes/locales/it/matterbridge-config.json +20 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +53 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +83 -0
- package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +56 -0
- package/nodes/locales/zh-CN/knxUltimateMatterDevice.json +109 -0
- package/nodes/locales/zh-CN/matter-config.html +25 -0
- package/nodes/locales/zh-CN/matter-config.json +26 -0
- package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
- package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
- package/nodes/matter-config.html +226 -0
- package/nodes/matter-config.js +242 -0
- package/nodes/matterbridge-config.html +121 -0
- package/nodes/matterbridge-config.js +220 -0
- package/nodes/utils/matterBridgeDeviceFactory.mjs +511 -0
- package/nodes/utils/matterBridgeEngine.mjs +290 -0
- package/nodes/utils/matterEngine.mjs +475 -0
- package/nodes/utils/matterKnxConverter.js +204 -0
- package/package.json +8 -2
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const dptlib = require('knxultimate').dptlib
|
|
2
|
+
|
|
3
|
+
// One KNX device exposed as a Matter device. It points to a matterbridge-config node
|
|
4
|
+
// (the actual Matter server/bridge, paired once by Alexa/Google/Apple Home) and registers
|
|
5
|
+
// itself as one bridged device. This is the opposite direction of the Matter Device node.
|
|
6
|
+
module.exports = function (RED) {
|
|
7
|
+
// The functions of each device type, with their GA/DPT config fields.
|
|
8
|
+
// direction 'command' = Matter -> KNX (write to the bus), 'status' = KNX -> Matter.
|
|
9
|
+
const DEVICE_FUNCTIONS = {
|
|
10
|
+
onofflight: [
|
|
11
|
+
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
12
|
+
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' }
|
|
13
|
+
],
|
|
14
|
+
onoffplug: [
|
|
15
|
+
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
16
|
+
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' }
|
|
17
|
+
],
|
|
18
|
+
dimmablelight: [
|
|
19
|
+
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
20
|
+
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' },
|
|
21
|
+
{ fn: 'level', direction: 'command', ga: 'gaLevel', dpt: 'dptLevel', fallbackDpt: '5.001' },
|
|
22
|
+
{ fn: 'level', direction: 'status', ga: 'gaLevelStatus', dpt: 'dptLevelStatus', fallbackDpt: '5.001' }
|
|
23
|
+
],
|
|
24
|
+
rgblight: [
|
|
25
|
+
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
26
|
+
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' },
|
|
27
|
+
{ fn: 'level', direction: 'command', ga: 'gaLevel', dpt: 'dptLevel', fallbackDpt: '5.001' },
|
|
28
|
+
{ fn: 'level', direction: 'status', ga: 'gaLevelStatus', dpt: 'dptLevelStatus', fallbackDpt: '5.001' },
|
|
29
|
+
{ fn: 'rgb', direction: 'command', ga: 'gaRGB', dpt: 'dptRGB', fallbackDpt: '232.600' },
|
|
30
|
+
{ fn: 'rgb', direction: 'status', ga: 'gaRGBStatus', dpt: 'dptRGBStatus', fallbackDpt: '232.600' }
|
|
31
|
+
],
|
|
32
|
+
colortemperaturelight: [
|
|
33
|
+
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
34
|
+
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' },
|
|
35
|
+
{ fn: 'level', direction: 'command', ga: 'gaLevel', dpt: 'dptLevel', fallbackDpt: '5.001' },
|
|
36
|
+
{ fn: 'level', direction: 'status', ga: 'gaLevelStatus', dpt: 'dptLevelStatus', fallbackDpt: '5.001' },
|
|
37
|
+
{ fn: 'colortemp', direction: 'command', ga: 'gaCT', dpt: 'dptCT', fallbackDpt: '7.600' },
|
|
38
|
+
{ fn: 'colortemp', direction: 'status', ga: 'gaCTStatus', dpt: 'dptCTStatus', fallbackDpt: '7.600' }
|
|
39
|
+
],
|
|
40
|
+
temperaturesensor: [{ fn: 'temperature', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.001' }],
|
|
41
|
+
humiditysensor: [{ fn: 'humidity', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.007' }],
|
|
42
|
+
lightsensor: [{ fn: 'illuminance', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.004' }],
|
|
43
|
+
occupancysensor: [{ fn: 'occupancy', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '1.011' }],
|
|
44
|
+
contactsensor: [{ fn: 'contact', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '1.002' }],
|
|
45
|
+
windowcovering: [
|
|
46
|
+
{ fn: 'updown', direction: 'command', ga: 'gaUpDown', dpt: 'dptUpDown', fallbackDpt: '1.008' },
|
|
47
|
+
{ fn: 'stop', direction: 'command', ga: 'gaStop', dpt: 'dptStop', fallbackDpt: '1.017' },
|
|
48
|
+
{ fn: 'position', direction: 'command', ga: 'gaPosition', dpt: 'dptPosition', fallbackDpt: '5.001' },
|
|
49
|
+
{ fn: 'position', direction: 'status', ga: 'gaPositionStatus', dpt: 'dptPositionStatus', fallbackDpt: '5.001' }
|
|
50
|
+
],
|
|
51
|
+
thermostat: [
|
|
52
|
+
{ fn: 'currenttemp', direction: 'status', ga: 'gaCurrentTemp', dpt: 'dptCurrentTemp', fallbackDpt: '9.001' },
|
|
53
|
+
{ fn: 'setpoint', direction: 'command', ga: 'gaSetpoint', dpt: 'dptSetpoint', fallbackDpt: '9.001' },
|
|
54
|
+
{ fn: 'setpoint', direction: 'status', ga: 'gaSetpointStatus', dpt: 'dptSetpointStatus', fallbackDpt: '9.001' }
|
|
55
|
+
],
|
|
56
|
+
smokecoalarm: [
|
|
57
|
+
{ fn: 'smoke', direction: 'status', ga: 'gaSmoke', dpt: 'dptSmoke', fallbackDpt: '1.005' },
|
|
58
|
+
{ fn: 'co', direction: 'status', ga: 'gaCO', dpt: 'dptCO', fallbackDpt: '1.005' }
|
|
59
|
+
],
|
|
60
|
+
waterleakdetector: [{ fn: 'leak', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '1.005' }],
|
|
61
|
+
airqualitysensor: [{ fn: 'co2', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.008' }],
|
|
62
|
+
fan: [
|
|
63
|
+
{ fn: 'fanspeed', direction: 'command', ga: 'gaFanSpeed', dpt: 'dptFanSpeed', fallbackDpt: '5.001' },
|
|
64
|
+
{ fn: 'fanspeed', direction: 'status', ga: 'gaFanSpeedStatus', dpt: 'dptFanSpeedStatus', fallbackDpt: '5.001' }
|
|
65
|
+
],
|
|
66
|
+
robotvacuum: [] // Flow-only device: talks to the flow through the node PINs
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function knxUltimateMatterBridge (config) {
|
|
70
|
+
RED.nodes.createNode(this, config)
|
|
71
|
+
const node = this
|
|
72
|
+
node.serverKNX = RED.nodes.getNode(config.server) || undefined
|
|
73
|
+
node.serverMatterBridge = RED.nodes.getNode(config.serverMatterBridge) || undefined
|
|
74
|
+
|
|
75
|
+
node.deviceType = config.deviceType || 'onofflight'
|
|
76
|
+
node.name = config.name === undefined || config.name === '' ? 'Matter device' : config.name
|
|
77
|
+
// The Matter device id is the (stable) Node-RED node id, so the endpoint survives re-deploys.
|
|
78
|
+
node.matterDeviceId = node.id
|
|
79
|
+
node.invertPosition = config.invertPosition === true || config.invertPosition === 'true'
|
|
80
|
+
node.readStatusAtStartup = config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes'
|
|
81
|
+
node.enableNodePINS = config.enableNodePINS === 'yes'
|
|
82
|
+
node.inputs = node.enableNodePINS ? 1 : 0
|
|
83
|
+
node.outputs = node.enableNodePINS ? 1 : 0
|
|
84
|
+
|
|
85
|
+
node.topic = ''
|
|
86
|
+
node.dpt = ''
|
|
87
|
+
node.notifyreadrequest = false
|
|
88
|
+
node.notifyreadrequestalsorespondtobus = 'false'
|
|
89
|
+
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
|
|
90
|
+
node.notifyresponse = false
|
|
91
|
+
node.notifywrite = true
|
|
92
|
+
node.initialread = false
|
|
93
|
+
node.listenallga = true // Don't remove
|
|
94
|
+
node.outputtype = 'write'
|
|
95
|
+
node.outputRBE = 'false'
|
|
96
|
+
node.inputRBE = 'false'
|
|
97
|
+
node.currentPayload = ''
|
|
98
|
+
node.passthrough = 'no'
|
|
99
|
+
node.formatmultiplyvalue = 1
|
|
100
|
+
node.formatnegativevalue = 'leave'
|
|
101
|
+
node.formatdecimalsvalue = 2
|
|
102
|
+
|
|
103
|
+
let initialReadTimer = null
|
|
104
|
+
|
|
105
|
+
const formatTs = (date) => {
|
|
106
|
+
const d = date instanceof Date ? date : new Date(date)
|
|
107
|
+
const provider = node.serverKNX
|
|
108
|
+
if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
|
|
109
|
+
return `${d.getDate()}, ${d.toLocaleTimeString()}`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pushStatus = (status) => {
|
|
113
|
+
if (!status) return
|
|
114
|
+
try {
|
|
115
|
+
const provider = node.serverKNX
|
|
116
|
+
if (provider && typeof provider.applyStatusUpdate === 'function') {
|
|
117
|
+
provider.applyStatusUpdate(node, status)
|
|
118
|
+
} else {
|
|
119
|
+
node.status(status)
|
|
120
|
+
}
|
|
121
|
+
} catch (error) { /* empty */ }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
node.setNodeStatus = ({ fill, shape, text, payload }) => {
|
|
125
|
+
try {
|
|
126
|
+
if (payload === undefined) payload = ''
|
|
127
|
+
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
|
|
128
|
+
pushStatus({ fill, shape, text: `${text} ${payload} (${formatTs(new Date())})` })
|
|
129
|
+
} catch (error) { /* empty */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const safeSendToKNX = (telegram, context = 'write') => {
|
|
133
|
+
try {
|
|
134
|
+
if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
|
|
135
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context})` })
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
|
|
139
|
+
} catch (error) {
|
|
140
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Precompute the routing tables for this single device:
|
|
145
|
+
// statusRoutes: GA -> [{ fn, dpt }] (KNX -> Matter)
|
|
146
|
+
// commandRoutes: fn -> { ga, dpt } (Matter -> KNX)
|
|
147
|
+
const statusRoutes = new Map()
|
|
148
|
+
const commandRoutes = new Map()
|
|
149
|
+
const devConfigValue = (key) => (config[key] || '').toString().trim()
|
|
150
|
+
;(DEVICE_FUNCTIONS[node.deviceType] || []).forEach((fnDef) => {
|
|
151
|
+
const ga = devConfigValue(fnDef.ga)
|
|
152
|
+
if (ga === '') return
|
|
153
|
+
const dpt = devConfigValue(fnDef.dpt) || fnDef.fallbackDpt
|
|
154
|
+
if (fnDef.direction === 'status') {
|
|
155
|
+
if (!statusRoutes.has(ga)) statusRoutes.set(ga, [])
|
|
156
|
+
statusRoutes.get(ga).push({ fn: fnDef.fn, dpt })
|
|
157
|
+
} else {
|
|
158
|
+
commandRoutes.set(fnDef.fn, { ga, dpt })
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// The device definition consumed by the bridge engine (via the config node).
|
|
163
|
+
node.getMatterDef = () => ({
|
|
164
|
+
id: node.matterDeviceId,
|
|
165
|
+
type: node.deviceType,
|
|
166
|
+
name: node.name,
|
|
167
|
+
invertPosition: node.invertPosition === true
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Reflects the bridge (config node) status on this device node.
|
|
171
|
+
node.handleBridgeStatus = () => {
|
|
172
|
+
try {
|
|
173
|
+
if (node.serverMatterBridge === undefined) {
|
|
174
|
+
node.setNodeStatus({ fill: 'red', shape: 'ring', text: 'No Matter bridge selected' })
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
const info = node.serverMatterBridge.getPairingInfo()
|
|
178
|
+
if (!info.running) {
|
|
179
|
+
node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Matter bridge starting...' })
|
|
180
|
+
} else if (info.commissioned) {
|
|
181
|
+
node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Ready (bridge paired with ${info.fabrics.length} controller(s))` })
|
|
182
|
+
} else {
|
|
183
|
+
node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: `Bridge awaiting pairing (code ${info.manualPairingCode || ''})` })
|
|
184
|
+
}
|
|
185
|
+
} catch (error) { /* empty */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Matter -> KNX: a controller (Alexa...) sent a command to this device.
|
|
189
|
+
node.handleMatterCommand = (command) => {
|
|
190
|
+
try {
|
|
191
|
+
if (node.enableNodePINS) {
|
|
192
|
+
try {
|
|
193
|
+
node.send({
|
|
194
|
+
topic: node.name,
|
|
195
|
+
payload: command.value,
|
|
196
|
+
device: { id: node.matterDeviceId, type: node.deviceType, name: node.name },
|
|
197
|
+
matter: command
|
|
198
|
+
})
|
|
199
|
+
} catch (error) { /* empty */ }
|
|
200
|
+
}
|
|
201
|
+
const route = commandRoutes.get(command.fn)
|
|
202
|
+
if (route === undefined) {
|
|
203
|
+
if (node.enableNodePINS) return // Flow-only device: the flow already got the command
|
|
204
|
+
node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: `No command GA for ${command.fn}: check the node and re-deploy` })
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
if (!node.serverKNX) {
|
|
208
|
+
if (node.enableNodePINS) return
|
|
209
|
+
node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: `No KNX gateway: enable the node PINs to handle ${command.fn}` })
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
safeSendToKNX({ grpaddr: route.ga, payload: command.value, dpt: route.dpt, outputtype: 'write' }, 'write')
|
|
213
|
+
node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Matter->KNX ${command.fn}`, payload: command.value })
|
|
214
|
+
} catch (error) {
|
|
215
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Matter->KNX error ${error.message}` })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// KNX -> Matter: telegrams from the bus update the Matter attributes of this device.
|
|
220
|
+
node.handleSend = (msg) => {
|
|
221
|
+
try {
|
|
222
|
+
if (!msg || !msg.knx) return
|
|
223
|
+
if (msg.knx.event === 'GroupValue_Read') return
|
|
224
|
+
const routes = statusRoutes.get(msg.knx.destination)
|
|
225
|
+
if (routes === undefined || node.serverMatterBridge === undefined) return
|
|
226
|
+
routes.forEach((route) => {
|
|
227
|
+
try {
|
|
228
|
+
const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(route.dpt))
|
|
229
|
+
if (value === undefined || value === null) return
|
|
230
|
+
node.serverMatterBridge.setDeviceState(node.matterDeviceId, route.fn, value)
|
|
231
|
+
node.setNodeStatus({ fill: 'blue', shape: 'dot', text: `KNX->Matter ${route.fn}`, payload: value })
|
|
232
|
+
} catch (error) {
|
|
233
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX->Matter error ${error.message}` })
|
|
234
|
+
}
|
|
235
|
+
})
|
|
236
|
+
} catch (error) {
|
|
237
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX->Matter error ${error.message}` })
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Ask the KNX bus for the current values of the status GAs, so the Matter
|
|
242
|
+
// attributes are populated before a controller reads them.
|
|
243
|
+
const doInitialRead = () => {
|
|
244
|
+
if (!node.readStatusAtStartup || !node.serverKNX) return
|
|
245
|
+
try {
|
|
246
|
+
let delay = 0
|
|
247
|
+
statusRoutes.forEach((routes, ga) => {
|
|
248
|
+
delay += 200
|
|
249
|
+
setTimeout(() => {
|
|
250
|
+
safeSendToKNX({ grpaddr: ga, payload: '', dpt: '', outputtype: 'read' }, 'read')
|
|
251
|
+
}, delay)
|
|
252
|
+
})
|
|
253
|
+
} catch (error) { /* empty */ }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Register with the KNX gateway (to receive bus telegrams) and the Matter bridge.
|
|
257
|
+
if (node.serverKNX) {
|
|
258
|
+
try {
|
|
259
|
+
node.serverKNX.removeClient(node)
|
|
260
|
+
node.serverKNX.addClient(node)
|
|
261
|
+
} catch (error) {
|
|
262
|
+
RED.log.error(`knxUltimateMatterBridge: register KNX client error ${error.message}`)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (node.serverMatterBridge) {
|
|
266
|
+
try {
|
|
267
|
+
node.serverMatterBridge.registerDevice(node)
|
|
268
|
+
} catch (error) {
|
|
269
|
+
RED.log.error(`knxUltimateMatterBridge: register bridge device error ${error.message}`)
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
node.setNodeStatus({ fill: 'red', shape: 'ring', text: 'No Matter bridge selected' })
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
initialReadTimer = setTimeout(() => doInitialRead(), 12000)
|
|
276
|
+
|
|
277
|
+
// Flow input pin: updates the Matter state of this device without going through the KNX bus.
|
|
278
|
+
// msg.payload = { function: 'onoff'|'level'|'position'|'temperature'|..., value: ... }
|
|
279
|
+
node.on('input', (msg, send, done) => {
|
|
280
|
+
if (!node.enableNodePINS) {
|
|
281
|
+
if (done) done()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const payload = msg.payload || {}
|
|
286
|
+
const fn = (payload.function || payload.fn || '').toString().trim()
|
|
287
|
+
if (fn === '' || payload.value === undefined) throw new Error('msg.payload must be { function, value }')
|
|
288
|
+
if (node.serverMatterBridge === undefined) throw new Error('No Matter bridge selected')
|
|
289
|
+
node.serverMatterBridge.setDeviceState(node.matterDeviceId, fn, payload.value)
|
|
290
|
+
node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Flow->Matter ${fn}`, payload: payload.value })
|
|
291
|
+
if (done) done()
|
|
292
|
+
} catch (error) {
|
|
293
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Flow error ${error.message}`, payload: '' })
|
|
294
|
+
if (done) done(error)
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
node.on('close', (done) => {
|
|
299
|
+
try {
|
|
300
|
+
if (initialReadTimer !== null) clearTimeout(initialReadTimer)
|
|
301
|
+
} catch (error) { /* empty */ }
|
|
302
|
+
try {
|
|
303
|
+
if (node.serverKNX) node.serverKNX.removeClient(node)
|
|
304
|
+
} catch (error) { /* empty */ }
|
|
305
|
+
try {
|
|
306
|
+
if (node.serverMatterBridge) node.serverMatterBridge.unregisterDevice(node)
|
|
307
|
+
} catch (error) { /* empty */ }
|
|
308
|
+
if (done) done()
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
RED.nodes.registerType('knxUltimateMatterBridge', knxUltimateMatterBridge)
|
|
313
|
+
}
|