node-red-contrib-knx-ultimate 5.2.4 → 6.0.0
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 +21 -8
- package/README.md +10 -3
- package/img/readmemain.png +0 -0
- package/nodes/commonFunctions.js +40 -3
- package/nodes/knxUltimate-config.js +379 -335
- package/nodes/knxUltimate.html +8 -9
- package/nodes/knxUltimate.js +5 -5
- package/nodes/knxUltimateAI.js +1 -1
- package/nodes/knxUltimateHueLight.html +3 -3
- package/nodes/knxUltimateHueLight.js +1 -1
- package/nodes/knxUltimateMatterBridge.html +321 -247
- package/nodes/knxUltimateMatterBridge.js +48 -1
- package/nodes/knxUltimateMatterControllerDevice.html +2119 -0
- package/nodes/knxUltimateMatterControllerDevice.js +1701 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/de/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/de/matter-config.json +5 -1
- package/nodes/locales/de/matterbridge-config.json +1 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/en/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/en/matter-config.json +6 -1
- package/nodes/locales/en/matterbridge-config.json +1 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/es/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/es/matter-config.json +5 -1
- package/nodes/locales/es/matterbridge-config.json +1 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/fr/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +89 -0
- package/nodes/locales/fr/matter-config.json +5 -1
- package/nodes/locales/fr/matterbridge-config.json +1 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/it/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +97 -0
- package/nodes/locales/it/matter-config.json +6 -1
- package/nodes/locales/it/matterbridge-config.json +1 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +6 -2
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +14 -1
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +27 -0
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +89 -0
- package/nodes/locales/zh-CN/matter-config.json +5 -1
- package/nodes/locales/zh-CN/matterbridge-config.json +1 -0
- package/nodes/matter-config.html +58 -6
- package/nodes/matter-config.js +7 -2
- package/nodes/matterbridge-config.html +1 -0
- package/nodes/utils/lightEngines/canonicalLight.js +75 -0
- package/nodes/utils/lightEngines/hueLightEngine.js +51 -0
- package/nodes/utils/lightEngines/index.js +25 -0
- package/nodes/utils/lightEngines/knxLightCommand.js +79 -0
- package/nodes/utils/lightEngines/lightEngine.js +46 -0
- package/nodes/utils/lightEngines/matterHueShim.js +59 -0
- package/nodes/utils/lightEngines/matterLightEngine.js +117 -0
- package/nodes/utils/matterEngine.mjs +185 -14
- package/nodes/utils/matterKnxConverter.js +1 -1
- package/package.json +21 -3
- package/resources/KNXSendSnippets.js +53 -53
- package/resources/htmlUtils.js +6 -4
- package/nodes/knxUltimateMatterDevice.html +0 -632
- package/nodes/knxUltimateMatterDevice.js +0 -308
- package/nodes/locales/de/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/de/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/en/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/en/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/es/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/es/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/fr/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/fr/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/it/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/it/knxUltimateMatterDevice.json +0 -109
- package/nodes/locales/zh-CN/knxUltimateMatterDevice.html +0 -56
- package/nodes/locales/zh-CN/knxUltimateMatterDevice.json +0 -109
|
@@ -1,308 +0,0 @@
|
|
|
1
|
-
const dptlib = require('knxultimate').dptlib
|
|
2
|
-
const matterKnxConverter = require('./utils/matterKnxConverter')
|
|
3
|
-
|
|
4
|
-
module.exports = function (RED) {
|
|
5
|
-
function knxUltimateMatterDevice (config) {
|
|
6
|
-
RED.nodes.createNode(this, config)
|
|
7
|
-
const node = this
|
|
8
|
-
node.serverKNX = RED.nodes.getNode(config.server) || undefined
|
|
9
|
-
node.serverMatter = RED.nodes.getNode(config.serverMatter) || undefined
|
|
10
|
-
|
|
11
|
-
node.topic = node.name
|
|
12
|
-
node.name = config.name === undefined || config.name === '' ? 'Matter Device (BETA)' : config.name
|
|
13
|
-
node.dpt = ''
|
|
14
|
-
node.notifyreadrequest = false
|
|
15
|
-
node.notifyreadrequestalsorespondtobus = 'false'
|
|
16
|
-
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
|
|
17
|
-
node.notifyresponse = false
|
|
18
|
-
node.notifywrite = true
|
|
19
|
-
node.initialread = true
|
|
20
|
-
node.listenallga = true // Don't remove
|
|
21
|
-
node.outputtype = 'write'
|
|
22
|
-
node.outputRBE = 'false'
|
|
23
|
-
node.inputRBE = 'false'
|
|
24
|
-
node.currentPayload = ''
|
|
25
|
-
node.passthrough = 'no'
|
|
26
|
-
node.formatmultiplyvalue = 1
|
|
27
|
-
node.formatnegativevalue = 'leave'
|
|
28
|
-
node.formatdecimalsvalue = 2
|
|
29
|
-
|
|
30
|
-
node.matterNodeId = config.matterNodeId || ''
|
|
31
|
-
node.mappings = []
|
|
32
|
-
try {
|
|
33
|
-
node.mappings = Array.isArray(config.mappings) ? config.mappings : JSON.parse(config.mappings || '[]')
|
|
34
|
-
} catch (error) {
|
|
35
|
-
node.mappings = []
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
node.initializingAtStart = config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes'
|
|
39
|
-
node.enableNodePINS = config.enableNodePINS === 'yes'
|
|
40
|
-
node.inputs = node.enableNodePINS ? 1 : 0
|
|
41
|
-
node.outputs = node.enableNodePINS ? 1 : 0
|
|
42
|
-
|
|
43
|
-
let lastInitialReadTs = 0
|
|
44
|
-
|
|
45
|
-
const pushStatus = (status) => {
|
|
46
|
-
if (!status) return
|
|
47
|
-
const provider = node.serverKNX
|
|
48
|
-
if (provider && typeof provider.applyStatusUpdate === 'function') {
|
|
49
|
-
provider.applyStatusUpdate(node, status)
|
|
50
|
-
} else {
|
|
51
|
-
node.status(status)
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const formatTs = (date) => {
|
|
56
|
-
const d = date instanceof Date ? date : new Date(date)
|
|
57
|
-
const provider = node.serverKNX
|
|
58
|
-
if (provider && typeof provider.formatStatusTimestamp === 'function') return provider.formatStatusTimestamp(d)
|
|
59
|
-
return `${d.getDate()}, ${d.toLocaleTimeString()}`
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
node.setNodeStatus = ({ fill, shape, text, payload }) => {
|
|
63
|
-
try {
|
|
64
|
-
if (payload === undefined) payload = ''
|
|
65
|
-
const dDate = new Date()
|
|
66
|
-
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
|
|
67
|
-
node.sKNXNodeStatusText = `|KNX: ${text} ${payload} (${formatTs(dDate)})`
|
|
68
|
-
pushStatus({ fill, shape, text: (node.sMatterNodeStatusText || '') + ' ' + (node.sKNXNodeStatusText || '') })
|
|
69
|
-
} catch (error) { /* empty */ }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
node.setNodeStatusMatter = ({ fill, shape, text, payload }) => {
|
|
73
|
-
try {
|
|
74
|
-
if (payload === undefined) payload = ''
|
|
75
|
-
const dDate = new Date()
|
|
76
|
-
payload = typeof payload === 'object' ? JSON.stringify(payload) : payload.toString()
|
|
77
|
-
node.sMatterNodeStatusText = `|Matter: ${text} ${payload} (${formatTs(dDate)})`
|
|
78
|
-
pushStatus({ fill, shape, text: node.sMatterNodeStatusText + ' ' + (node.sKNXNodeStatusText || '') })
|
|
79
|
-
} catch (error) { /* empty */ }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const safeSendToKNX = (telegram, context = 'write') => {
|
|
83
|
-
try {
|
|
84
|
-
if (!node.serverKNX || typeof node.serverKNX.sendKNXTelegramToKNXEngine !== 'function') {
|
|
85
|
-
pushStatus({ fill: 'red', shape: 'dot', text: `KNX server missing (${context}) (${formatTs(new Date())})` })
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
node.serverKNX.sendKNXTelegramToKNXEngine({ ...telegram, nodecallerid: node.id })
|
|
89
|
-
} catch (error) {
|
|
90
|
-
pushStatus({ fill: 'red', shape: 'dot', text: `KNX send error ${error.message}` })
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const sendMappingToKNX = (mapping, payload, outputtype = 'write') => {
|
|
95
|
-
if (payload === undefined || mapping.ga === undefined || mapping.ga === '') return
|
|
96
|
-
safeSendToKNX({
|
|
97
|
-
grpaddr: mapping.ga,
|
|
98
|
-
payload,
|
|
99
|
-
dpt: mapping.dpt,
|
|
100
|
-
outputtype
|
|
101
|
-
}, outputtype)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const getMatterManager = () => {
|
|
105
|
-
if (!node.serverMatter || node.serverMatter.matterManager === null || node.serverMatter.matterManager === undefined) return undefined
|
|
106
|
-
return node.serverMatter.matterManager
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Send the cached Matter value of a status mapping to KNX
|
|
110
|
-
const sendCachedStatusToKNX = (mapping, outputtype = 'write') => {
|
|
111
|
-
try {
|
|
112
|
-
const manager = getMatterManager()
|
|
113
|
-
if (manager === undefined) return false
|
|
114
|
-
const rawValue = manager.getCachedAttribute(node.matterNodeId, mapping.endpointId, mapping.clusterId, mapping.target)
|
|
115
|
-
if (rawValue === undefined) return false
|
|
116
|
-
const payload = matterKnxConverter.matterToKnx(mapping.clusterId, mapping.target, rawValue)
|
|
117
|
-
if (payload === undefined) return false
|
|
118
|
-
sendMappingToKNX(mapping, payload, outputtype)
|
|
119
|
-
return true
|
|
120
|
-
} catch (error) {
|
|
121
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'dot', text: `Cached status error ${error.message}`, payload: '' })
|
|
122
|
-
return false
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// KNX -> Matter
|
|
127
|
-
node.handleSend = (msg) => {
|
|
128
|
-
if (!msg || !msg.knx) return
|
|
129
|
-
if (node.matterNodeId === '') {
|
|
130
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'ring', text: 'Missing Matter device selection', payload: '' })
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
try {
|
|
134
|
-
if (!Array.isArray(node.mappings)) return
|
|
135
|
-
if (msg.knx.event === 'GroupValue_Read') {
|
|
136
|
-
// Respond to read requests with the cached Matter values
|
|
137
|
-
node.mappings.filter((m) => m.direction === 'status' && m.ga === msg.knx.destination).forEach((mapping) => {
|
|
138
|
-
if (sendCachedStatusToKNX(mapping, 'response')) {
|
|
139
|
-
node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Response', payload: mapping.ga })
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
return
|
|
143
|
-
}
|
|
144
|
-
const manager = getMatterManager()
|
|
145
|
-
node.mappings.filter((m) => m.direction === 'command' && m.ga === msg.knx.destination).forEach((mapping) => {
|
|
146
|
-
const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(mapping.dpt))
|
|
147
|
-
const matterAction = matterKnxConverter.knxToMatter(mapping, value)
|
|
148
|
-
if (matterAction === null) return
|
|
149
|
-
if (manager === undefined || node.serverMatter.linkStatus !== 'connected') {
|
|
150
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'ring', text: 'Matter controller unavailable', payload: '' })
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
manager.writeMatterQueueAdd({
|
|
154
|
-
nodeId: node.matterNodeId,
|
|
155
|
-
endpointId: mapping.endpointId,
|
|
156
|
-
clusterId: mapping.clusterId,
|
|
157
|
-
kind: matterAction.kind,
|
|
158
|
-
name: matterAction.name,
|
|
159
|
-
args: matterAction.args
|
|
160
|
-
})
|
|
161
|
-
node.setNodeStatusMatter({
|
|
162
|
-
fill: 'green',
|
|
163
|
-
shape: 'dot',
|
|
164
|
-
text: `KNX->Matter ${mapping.clusterName || mapping.clusterId}.${matterAction.name}`,
|
|
165
|
-
payload: value
|
|
166
|
-
})
|
|
167
|
-
})
|
|
168
|
-
} catch (error) {
|
|
169
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'dot', text: `KNX->Matter error ${error.message}`, payload: '' })
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Matter -> KNX (attribute change reported by the subscription)
|
|
174
|
-
node.handleSendMatter = (_event) => {
|
|
175
|
-
try {
|
|
176
|
-
if (_event === undefined) return
|
|
177
|
-
if (_event.nodeId !== node.matterNodeId) return
|
|
178
|
-
const matchingMappings = node.mappings.filter((m) => m.direction === 'status' &&
|
|
179
|
-
Number(m.endpointId) === Number(_event.endpointId) &&
|
|
180
|
-
Number(m.clusterId) === Number(_event.clusterId) &&
|
|
181
|
-
m.targetKind === 'attribute' &&
|
|
182
|
-
m.target === _event.attributeName)
|
|
183
|
-
matchingMappings.forEach((mapping) => {
|
|
184
|
-
const payload = matterKnxConverter.matterToKnx(_event.clusterId, _event.attributeName, _event.value)
|
|
185
|
-
if (payload === undefined) return
|
|
186
|
-
sendMappingToKNX(mapping, payload, 'write')
|
|
187
|
-
node.setNodeStatusMatter({
|
|
188
|
-
fill: 'blue',
|
|
189
|
-
shape: 'dot',
|
|
190
|
-
text: `Matter->KNX ${_event.attributeName}`,
|
|
191
|
-
payload
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
if (node.enableNodePINS) {
|
|
195
|
-
node.send({
|
|
196
|
-
topic: `${_event.clusterId}.${_event.attributeName}`,
|
|
197
|
-
payload: _event.value,
|
|
198
|
-
matter: _event
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
} catch (error) {
|
|
202
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'dot', text: `Matter->KNX error ${error.message}`, payload: '' })
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Cluster events (e.g. switch presses). Forwarded to the flow only.
|
|
207
|
-
node.handleMatterClusterEvent = (_event) => {
|
|
208
|
-
try {
|
|
209
|
-
if (_event === undefined || _event.nodeId !== node.matterNodeId) return
|
|
210
|
-
if (node.enableNodePINS) {
|
|
211
|
-
node.send({
|
|
212
|
-
topic: `${_event.clusterId}.${_event.eventName}`,
|
|
213
|
-
payload: _event.events,
|
|
214
|
-
matter: _event
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
} catch (error) { /* empty */ }
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Called by matter-config when the paired node completed its initial sync (or reconnected)
|
|
221
|
-
node.handleMatterNodeInitialized = () => {
|
|
222
|
-
try {
|
|
223
|
-
if (!node.initializingAtStart) return
|
|
224
|
-
if (node.matterNodeId === '') return
|
|
225
|
-
const now = Date.now()
|
|
226
|
-
if ((now - lastInitialReadTs) < 5000) return // Avoid duplicate bursts on reconnections
|
|
227
|
-
let sentCount = 0
|
|
228
|
-
node.mappings.filter((m) => m.direction === 'status' && m.targetKind === 'attribute').forEach((mapping) => {
|
|
229
|
-
if (sendCachedStatusToKNX(mapping, 'write')) sentCount += 1
|
|
230
|
-
})
|
|
231
|
-
if (sentCount > 0) {
|
|
232
|
-
lastInitialReadTs = now
|
|
233
|
-
node.setNodeStatusMatter({ fill: 'green', shape: 'dot', text: `Initial read: ${sentCount} value(s) sent`, payload: '' })
|
|
234
|
-
}
|
|
235
|
-
} catch (error) {
|
|
236
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'dot', text: `Initial read error ${error.message}`, payload: '' })
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (node.serverKNX) {
|
|
241
|
-
try {
|
|
242
|
-
node.serverKNX.removeClient(node)
|
|
243
|
-
node.serverKNX.addClient(node)
|
|
244
|
-
} catch (error) {
|
|
245
|
-
RED.log.error(`knxUltimateMatterDevice: register KNX client error ${error.message}`)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
if (node.serverMatter) {
|
|
249
|
-
try {
|
|
250
|
-
node.serverMatter.removeClient(node)
|
|
251
|
-
node.serverMatter.addClient(node)
|
|
252
|
-
} catch (error) {
|
|
253
|
-
RED.log.error(`knxUltimateMatterDevice: register client error ${error.message}`)
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Flow input pin: send raw Matter commands/attribute writes.
|
|
258
|
-
// msg.payload = { endpointId, clusterId, command: 'on', args: {...} } or { endpointId, clusterId, attribute: 'onOff', value: true }
|
|
259
|
-
node.on('input', (msg, send, done) => {
|
|
260
|
-
if (!node.enableNodePINS) {
|
|
261
|
-
if (done) done()
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
const manager = getMatterManager()
|
|
266
|
-
if (manager === undefined) throw new Error('Matter controller unavailable')
|
|
267
|
-
const payload = msg.payload || {}
|
|
268
|
-
if (payload.command !== undefined) {
|
|
269
|
-
manager.writeMatterQueueAdd({
|
|
270
|
-
nodeId: node.matterNodeId,
|
|
271
|
-
endpointId: payload.endpointId,
|
|
272
|
-
clusterId: payload.clusterId,
|
|
273
|
-
kind: 'command',
|
|
274
|
-
name: payload.command,
|
|
275
|
-
args: payload.args
|
|
276
|
-
})
|
|
277
|
-
} else if (payload.attribute !== undefined) {
|
|
278
|
-
manager.writeMatterQueueAdd({
|
|
279
|
-
nodeId: node.matterNodeId,
|
|
280
|
-
endpointId: payload.endpointId,
|
|
281
|
-
clusterId: payload.clusterId,
|
|
282
|
-
kind: 'attributeWrite',
|
|
283
|
-
name: payload.attribute,
|
|
284
|
-
args: payload.value
|
|
285
|
-
})
|
|
286
|
-
} else {
|
|
287
|
-
throw new Error('msg.payload must contain "command" or "attribute"')
|
|
288
|
-
}
|
|
289
|
-
node.setNodeStatusMatter({ fill: 'green', shape: 'dot', text: 'Flow->Matter', payload })
|
|
290
|
-
if (done) done()
|
|
291
|
-
} catch (error) {
|
|
292
|
-
node.setNodeStatusMatter({ fill: 'red', shape: 'dot', text: `Flow error ${error.message}`, payload: '' })
|
|
293
|
-
if (done) done(error)
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
node.on('close', () => {
|
|
298
|
-
try {
|
|
299
|
-
if (node.serverKNX) node.serverKNX.removeClient(node)
|
|
300
|
-
} catch (error) { /* empty */ }
|
|
301
|
-
try {
|
|
302
|
-
if (node.serverMatter) node.serverMatter.removeClient(node)
|
|
303
|
-
} catch (error) { /* empty */ }
|
|
304
|
-
})
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
RED.nodes.registerType('knxUltimateMatterDevice', knxUltimateMatterDevice)
|
|
308
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<script type="text/markdown" data-help-name="knxUltimateMatterDevice">
|
|
2
|
-
# Matter-Gerät (BETA)
|
|
3
|
-
|
|
4
|
-
> Dieser Node ist in **BETA**: Er funktioniert, aber Details können sich zwischen Releases noch ändern.
|
|
5
|
-
|
|
6
|
-
## Übersicht
|
|
7
|
-
|
|
8
|
-
Der Matter Device Node verbindet ein **Matter-Gerät** mit **KNX-Gruppenadressen**. Der Node arbeitet über den Konfigurations-Node *Matter Controller*, der die Geräte in seine eigene Matter-Fabric kommissioniert.
|
|
9
|
-
|
|
10
|
-
- **Steuere** jedes gekoppelte Matter-Gerät vom KNX-Bus aus (Ein/Aus, Dimmen, Rollläden, Thermostate, Schlösser, ...).
|
|
11
|
-
- **Verfolge** jedes Attribut des Geräts auf KNX-Gruppenadressen (Statusmeldungen, Sensoren, Leistungsmessung, Batterie...).
|
|
12
|
-
- Vollständig generisch: Die Zuordnungsliste wird aus den **realen Endpoints/Clustern** des Geräts aufgebaut.
|
|
13
|
-
|
|
14
|
-
## Konfiguration
|
|
15
|
-
|
|
16
|
-
|Feld|Beschreibung|
|
|
17
|
-
|--|--|
|
|
18
|
-
| KNX GW | KNX-Gateway für die Telegramme |
|
|
19
|
-
| Matter Ctrl | Der Matter-Controller-Konfigurations-Node (dort werden die Geräte gekoppelt) |
|
|
20
|
-
| Gerät | Wähle das gekoppelte Matter-Gerät aus der Autovervollständigung |
|
|
21
|
-
| Zuordnungen | Eine Zeile pro Zuordnung Gruppenadresse ↔ Matter-Cluster |
|
|
22
|
-
| Status beim Start lesen | Wenn aktiviert, sendet der Node die aktuellen zwischengespeicherten Werte beim Deploy/Verbinden |
|
|
23
|
-
| Node Input/Output PINs | Aktiviert die Node-RED-Pins für den rohen Matter-Zugriff |
|
|
24
|
-
|
|
25
|
-
## Zuordnungen
|
|
26
|
-
|
|
27
|
-
Die Ziel-Liste zeigt die unterstützten Funktionen mit verständlichen Namen wie *"Ein/Aus-Schalter"* oder *"Momentanleistung (W)"*, gefiltert nach dem, was das Gerät wirklich bereitstellt, mit dem aktuellen Wert in eckigen Klammern.
|
|
28
|
-
|
|
29
|
-
Jede Zuordnungszeile hat eine **Richtung**:
|
|
30
|
-
|
|
31
|
-
- **KNX → Matter (Befehl)**: Ein Telegramm auf der Gruppenadresse ruft einen Matter-Cluster-Befehl auf oder schreibt ein Attribut. Beispiel: GA `1/1/1` DPT 1.001 → `OnOff.on/off` (der boolesche Wert wählt automatisch Ein oder Aus).
|
|
32
|
-
- **Matter → KNX (Status)**: Wenn sich das abonnierte Matter-Attribut ändert, wird sein Wert konvertiert und auf die Gruppenadresse geschrieben. `GroupValue_Read`-Anfragen werden mit dem zwischengespeicherten Wert beantwortet.
|
|
33
|
-
|
|
34
|
-
Die gängigen Cluster werden automatisch in KNX-freundliche Werte konvertiert:
|
|
35
|
-
|
|
36
|
-
|Cluster|Konvertierung|
|
|
37
|
-
|--|--|
|
|
38
|
-
| OnOff | boolesch (DPT 1.001) |
|
|
39
|
-
| LevelControl | 0-254 ↔ Prozent (DPT 5.001) |
|
|
40
|
-
| WindowCovering | percent100ths ↔ Prozent (DPT 5.001), Auf/Ab (DPT 1.008) |
|
|
41
|
-
| ColorControl | Mired ↔ Kelvin (DPT 7.600) |
|
|
42
|
-
| Thermostat | Zenti-°C ↔ °C (DPT 9.001) |
|
|
43
|
-
| Temperatur/Feuchte | Zenti-Einheiten ↔ Einheiten (DPT 9.001/9.007) |
|
|
44
|
-
| Beleuchtungsstärke | Log-Skala ↔ Lux (DPT 9.004) |
|
|
45
|
-
| PowerSource | Halb-Prozent ↔ Batterie-Prozent (DPT 5.001) |
|
|
46
|
-
| Elektrische Leistung/Energie | mW ↔ W (DPT 14.056), mWh ↔ kWh (DPT 13.013) |
|
|
47
|
-
|
|
48
|
-
Andere Cluster/Attribute werden unverändert durchgereicht; der gewählte DPT übernimmt die finale KNX-Kodierung.
|
|
49
|
-
|
|
50
|
-
## Node-PINs
|
|
51
|
-
|
|
52
|
-
Wenn du die Node-PINs aktivierst:
|
|
53
|
-
|
|
54
|
-
- **Input**: sende rohe Befehle: `msg.payload = { endpointId: 1, clusterId: 6, command: "toggle" }` oder Attribut-Schreibzugriffe: `msg.payload = { endpointId: 1, clusterId: 8, attribute: "onLevel", value: 128 }`
|
|
55
|
-
- **Output**: empfängt jede Attributänderung (`msg.matter` enthält das komplette Ereignis) und jedes Cluster-Ereignis (Tastendrücke usw.).
|
|
56
|
-
</script>
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"knxUltimateMatterDevice": {
|
|
3
|
-
"title": "Matter-Gerät (BETA)",
|
|
4
|
-
"paletteLabel": "Matter Device (BETA)",
|
|
5
|
-
"matter_controller": "Matter Ctrl",
|
|
6
|
-
"matter_device": "Gerät",
|
|
7
|
-
"device_placeholder": "Klicken, um ein gekoppeltes Matter-Gerät auszuwählen",
|
|
8
|
-
"mapping_help": "Füge pro Zuordnung eine Zeile hinzu. \"KNX → Matter\" ruft einen Cluster-Befehl auf (oder schreibt ein Attribut), wenn ein Telegramm auf der Gruppenadresse eintrifft. \"Matter → KNX\" schreibt den Attributwert bei jeder Änderung auf die Gruppenadresse (Leseanfragen werden mit dem zwischengespeicherten Wert beantwortet).",
|
|
9
|
-
"select_target": "-- Matter-Ziel auswählen --",
|
|
10
|
-
"stored_target": "Gespeichert",
|
|
11
|
-
"read_status_startup": "Status beim Start lesen",
|
|
12
|
-
"opt_yes_emit": "Ja, und KNX-Telegramme senden.",
|
|
13
|
-
"opt_no": "Nein",
|
|
14
|
-
"node_pins": "Node Input/Output PINs",
|
|
15
|
-
"node_pins_hide": "Ausblenden",
|
|
16
|
-
"node_pins_show": "Input/Output-PINs anzeigen",
|
|
17
|
-
"direction": {
|
|
18
|
-
"command": "KNX → Matter (Befehl)",
|
|
19
|
-
"status": "Matter → KNX (Status)"
|
|
20
|
-
},
|
|
21
|
-
"fields": {
|
|
22
|
-
"direction": "Richtung",
|
|
23
|
-
"target": "Matter Endpoint / Cluster / Ziel",
|
|
24
|
-
"ga": "Gruppenadresse",
|
|
25
|
-
"dpt": "DPT",
|
|
26
|
-
"ganame": "Name"
|
|
27
|
-
},
|
|
28
|
-
"no_simple_targets": "Keine unterstützten Funktionen auf diesem Gerät gefunden (oder das Gerät ist noch nicht verbunden).",
|
|
29
|
-
"endpoint": "Endpoint",
|
|
30
|
-
"friendly": {
|
|
31
|
-
"onoff_switch": "Ein/Aus-Schalter (ein-/ausschalten)",
|
|
32
|
-
"onoff_toggle": "Umschalten (Toggle)",
|
|
33
|
-
"onoff_state": "Ein/Aus-Status",
|
|
34
|
-
"level_set": "Helligkeit / Stufe setzen (%)",
|
|
35
|
-
"level_state": "Aktuelle Helligkeit / Stufe (%)",
|
|
36
|
-
"colortemp_set": "Farbtemperatur setzen (Kelvin)",
|
|
37
|
-
"colortemp_state": "Aktuelle Farbtemperatur (Kelvin)",
|
|
38
|
-
"cover_updown": "Rollladen Auf/Ab",
|
|
39
|
-
"cover_stop": "Rollladen Stopp",
|
|
40
|
-
"cover_position_set": "Rollladen auf Position fahren (%)",
|
|
41
|
-
"cover_position_state": "Aktuelle Rollladenposition (%)",
|
|
42
|
-
"cover_tilt_set": "Lamellen neigen (%)",
|
|
43
|
-
"cover_tilt_state": "Aktuelle Lamellenneigung (%)",
|
|
44
|
-
"thermo_localtemp": "Raumtemperatur (°C)",
|
|
45
|
-
"thermo_heatsp_set": "Heiz-Sollwert setzen (°C)",
|
|
46
|
-
"thermo_coolsp_set": "Kühl-Sollwert setzen (°C)",
|
|
47
|
-
"thermo_heatsp_state": "Heiz-Sollwert (°C)",
|
|
48
|
-
"thermo_coolsp_state": "Kühl-Sollwert (°C)",
|
|
49
|
-
"fan_speed_set": "Lüftergeschwindigkeit setzen (%)",
|
|
50
|
-
"fan_speed_state": "Aktuelle Lüftergeschwindigkeit (%)",
|
|
51
|
-
"lock_cmd": "Tür ver-/entriegeln",
|
|
52
|
-
"lock_state": "Schlossstatus (verriegelt)",
|
|
53
|
-
"temp_measured": "Gemessene Temperatur (°C)",
|
|
54
|
-
"humidity_measured": "Relative Luftfeuchte (%)",
|
|
55
|
-
"illuminance_measured": "Beleuchtungsstärke (Lux)",
|
|
56
|
-
"occupancy_state": "Anwesenheit erkannt",
|
|
57
|
-
"contact_state": "Kontaktstatus (geschlossen/offen)",
|
|
58
|
-
"battery_percent": "Batteriestand (%)",
|
|
59
|
-
"power_active": "Momentanleistung (W)",
|
|
60
|
-
"power_voltage": "Spannung (V)",
|
|
61
|
-
"power_current": "Strom (A)",
|
|
62
|
-
"energy_imported": "Energieverbrauch (kWh)",
|
|
63
|
-
"identify_cmd": "Gerät identifizieren (blinken)",
|
|
64
|
-
"switch_position": "Tastenposition"
|
|
65
|
-
},
|
|
66
|
-
"devtype": {
|
|
67
|
-
"onofflight": "Ein/Aus-Licht",
|
|
68
|
-
"dimmablelight": "Dimmbares Licht",
|
|
69
|
-
"colortemperaturelight": "Licht mit Farbtemperatur",
|
|
70
|
-
"extendedcolorlight": "Farblicht",
|
|
71
|
-
"onoffpluginunit": "Steckdose (Ein/Aus)",
|
|
72
|
-
"dimmablepluginunit": "Steckdose (dimmbar)",
|
|
73
|
-
"smartplug": "Smarte Steckdose",
|
|
74
|
-
"contactsensor": "Kontaktsensor",
|
|
75
|
-
"lightsensor": "Lichtsensor",
|
|
76
|
-
"occupancysensor": "Präsenzsensor",
|
|
77
|
-
"temperaturesensor": "Temperatursensor",
|
|
78
|
-
"humiditysensor": "Feuchtesensor",
|
|
79
|
-
"pressuresensor": "Drucksensor",
|
|
80
|
-
"flowsensor": "Durchflusssensor",
|
|
81
|
-
"doorlock": "Türschloss",
|
|
82
|
-
"windowcovering": "Rollladen/Jalousie",
|
|
83
|
-
"thermostat": "Thermostat",
|
|
84
|
-
"fan": "Ventilator",
|
|
85
|
-
"genericswitch": "Taster/Schalter",
|
|
86
|
-
"airqualitysensor": "Luftqualitätssensor",
|
|
87
|
-
"smokecoalarm": "Rauch-/CO-Melder",
|
|
88
|
-
"waterleakdetector": "Wasserleckmelder",
|
|
89
|
-
"roomairconditioner": "Klimagerät",
|
|
90
|
-
"airpurifier": "Luftreiniger",
|
|
91
|
-
"speaker": "Lautsprecher",
|
|
92
|
-
"pumpcontroller": "Pumpe",
|
|
93
|
-
"onofflightswitch": "Lichtschalter",
|
|
94
|
-
"dimmerswitch": "Dimmerschalter",
|
|
95
|
-
"colordimmerswitch": "Farbdimmerschalter",
|
|
96
|
-
"powersource": "Stromversorgung",
|
|
97
|
-
"electricalsensor": "Elektrischer Sensor",
|
|
98
|
-
"bridgednode": "Bridged-Gerät",
|
|
99
|
-
"aggregator": "Bridge/Aggregator",
|
|
100
|
-
"rootnode": "Root-Node"
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
"common": {
|
|
104
|
-
"ga": "GA",
|
|
105
|
-
"dpt": "DPT",
|
|
106
|
-
"name": "Name",
|
|
107
|
-
"knx_gw": "KNX GW"
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
<script type="text/markdown" data-help-name="knxUltimateMatterDevice">
|
|
2
|
-
# Matter Device (BETA)
|
|
3
|
-
|
|
4
|
-
> This node is in **BETA**: it works, but details may still change between releases.
|
|
5
|
-
|
|
6
|
-
## Overview
|
|
7
|
-
|
|
8
|
-
The Matter Device node bridges a **Matter device** to **KNX group addresses**. The node acts through the *Matter Controller* configuration node, which commissions (pairs) the devices into its own Matter fabric.
|
|
9
|
-
|
|
10
|
-
- **Control** any commissioned Matter device from the KNX bus (on/off, dimming, covers, thermostats, locks, ...).
|
|
11
|
-
- **Track** every attribute of the device on KNX group addresses (status feedback, sensors, power metering, battery...).
|
|
12
|
-
- Fully generic: the mapping list is built from the **real endpoints/clusters** exposed by the device.
|
|
13
|
-
|
|
14
|
-
## Configuration
|
|
15
|
-
|
|
16
|
-
|Field|Description|
|
|
17
|
-
|--|--|
|
|
18
|
-
| KNX GW | KNX gateway used for telegrams |
|
|
19
|
-
| Matter Ctrl | The Matter Controller configuration node (where you pair the devices) |
|
|
20
|
-
| Device | Pick the commissioned Matter device from the autocomplete list |
|
|
21
|
-
| Mappings | One row per group address ↔ Matter cluster mapping |
|
|
22
|
-
| Read status at startup | When enabled, the node emits the current cached values on deploy/connection |
|
|
23
|
-
| Node Input/Output PINs | Enable Node-RED input/output pins for raw Matter access |
|
|
24
|
-
|
|
25
|
-
## Mappings
|
|
26
|
-
|
|
27
|
-
The target list shows the supported functions with friendly names such as *"On/Off switch"* or *"Instantaneous power (W)"*, filtered on what the device really exposes and with the current value shown in brackets.
|
|
28
|
-
|
|
29
|
-
Each mapping row has a **direction**:
|
|
30
|
-
|
|
31
|
-
- **KNX → Matter (command)**: a telegram received on the group address invokes a Matter cluster command or writes an attribute. Example: GA `1/1/1` DPT 1.001 → `OnOff.on/off` (the boolean payload selects on or off automatically).
|
|
32
|
-
- **Matter → KNX (status)**: whenever the subscribed Matter attribute changes, its value is converted and written to the group address. `GroupValue_Read` requests are answered with the cached value.
|
|
33
|
-
|
|
34
|
-
Well-known clusters are converted automatically to KNX-friendly values:
|
|
35
|
-
|
|
36
|
-
|Cluster|Conversion|
|
|
37
|
-
|--|--|
|
|
38
|
-
| OnOff | boolean (DPT 1.001) |
|
|
39
|
-
| LevelControl | 0-254 ↔ percent (DPT 5.001) |
|
|
40
|
-
| WindowCovering | percent100ths ↔ percent (DPT 5.001), up/down (DPT 1.008) |
|
|
41
|
-
| ColorControl | mireds ↔ Kelvin (DPT 7.600) |
|
|
42
|
-
| Thermostat | centi-°C ↔ °C (DPT 9.001) |
|
|
43
|
-
| Temperature/Humidity | centi-units ↔ units (DPT 9.001/9.007) |
|
|
44
|
-
| Illuminance | log scale ↔ Lux (DPT 9.004) |
|
|
45
|
-
| PowerSource | half-percent ↔ percent battery (DPT 5.001) |
|
|
46
|
-
| Electrical Power/Energy | mW ↔ W (DPT 14.056), mWh ↔ kWh (DPT 13.013) |
|
|
47
|
-
|
|
48
|
-
Other clusters/attributes are passed through raw; the DPT you pick performs the final KNX encoding.
|
|
49
|
-
|
|
50
|
-
## Node PINs
|
|
51
|
-
|
|
52
|
-
If you enable the node PINs:
|
|
53
|
-
|
|
54
|
-
- **Input**: send raw commands: `msg.payload = { endpointId: 1, clusterId: 6, command: "toggle" }` or attribute writes: `msg.payload = { endpointId: 1, clusterId: 8, attribute: "onLevel", value: 128 }`
|
|
55
|
-
- **Output**: receives every attribute change (`msg.matter` contains the full event) and every cluster event (button presses etc.).
|
|
56
|
-
</script>
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"knxUltimateMatterDevice": {
|
|
3
|
-
"title": "Matter Device (BETA)",
|
|
4
|
-
"paletteLabel": "Matter Device (BETA)",
|
|
5
|
-
"matter_controller": "Matter Ctrl",
|
|
6
|
-
"matter_device": "Device",
|
|
7
|
-
"device_placeholder": "Click to pick a paired Matter device",
|
|
8
|
-
"mapping_help": "Add one row per mapping. \"KNX → Matter\" invokes a cluster command (or writes an attribute) when a telegram arrives on the group address. \"Matter → KNX\" writes the attribute value to the group address whenever it changes (read requests are answered with the cached value).",
|
|
9
|
-
"select_target": "-- select the Matter target --",
|
|
10
|
-
"stored_target": "Stored",
|
|
11
|
-
"read_status_startup": "Read status at startup",
|
|
12
|
-
"opt_yes_emit": "Yes, and emit KNX telegrams.",
|
|
13
|
-
"opt_no": "No",
|
|
14
|
-
"node_pins": "Node Input/Output PINs",
|
|
15
|
-
"node_pins_hide": "Hide",
|
|
16
|
-
"node_pins_show": "Show node input/output PINs",
|
|
17
|
-
"direction": {
|
|
18
|
-
"command": "KNX → Matter (command)",
|
|
19
|
-
"status": "Matter → KNX (status)"
|
|
20
|
-
},
|
|
21
|
-
"fields": {
|
|
22
|
-
"direction": "Direction",
|
|
23
|
-
"target": "Matter endpoint / cluster / target",
|
|
24
|
-
"ga": "Group address",
|
|
25
|
-
"dpt": "DPT",
|
|
26
|
-
"ganame": "Name"
|
|
27
|
-
},
|
|
28
|
-
"no_simple_targets": "No supported functions found on this device (or the device is not connected yet).",
|
|
29
|
-
"endpoint": "Endpoint",
|
|
30
|
-
"friendly": {
|
|
31
|
-
"onoff_switch": "On/Off switch (turn on/off)",
|
|
32
|
-
"onoff_toggle": "Toggle (invert state)",
|
|
33
|
-
"onoff_state": "On/Off state",
|
|
34
|
-
"level_set": "Set brightness / level (%)",
|
|
35
|
-
"level_state": "Current brightness / level (%)",
|
|
36
|
-
"colortemp_set": "Set color temperature (Kelvin)",
|
|
37
|
-
"colortemp_state": "Current color temperature (Kelvin)",
|
|
38
|
-
"cover_updown": "Cover Up/Down",
|
|
39
|
-
"cover_stop": "Cover Stop",
|
|
40
|
-
"cover_position_set": "Move cover to position (%)",
|
|
41
|
-
"cover_position_state": "Current cover position (%)",
|
|
42
|
-
"cover_tilt_set": "Move slats to tilt (%)",
|
|
43
|
-
"cover_tilt_state": "Current slat tilt (%)",
|
|
44
|
-
"thermo_localtemp": "Room temperature (°C)",
|
|
45
|
-
"thermo_heatsp_set": "Set heating setpoint (°C)",
|
|
46
|
-
"thermo_coolsp_set": "Set cooling setpoint (°C)",
|
|
47
|
-
"thermo_heatsp_state": "Heating setpoint (°C)",
|
|
48
|
-
"thermo_coolsp_state": "Cooling setpoint (°C)",
|
|
49
|
-
"fan_speed_set": "Set fan speed (%)",
|
|
50
|
-
"fan_speed_state": "Current fan speed (%)",
|
|
51
|
-
"lock_cmd": "Lock/Unlock door",
|
|
52
|
-
"lock_state": "Lock state (locked)",
|
|
53
|
-
"temp_measured": "Measured temperature (°C)",
|
|
54
|
-
"humidity_measured": "Relative humidity (%)",
|
|
55
|
-
"illuminance_measured": "Illuminance (Lux)",
|
|
56
|
-
"occupancy_state": "Presence detected",
|
|
57
|
-
"contact_state": "Contact state (closed/open)",
|
|
58
|
-
"battery_percent": "Battery level (%)",
|
|
59
|
-
"power_active": "Instantaneous power (W)",
|
|
60
|
-
"power_voltage": "Voltage (V)",
|
|
61
|
-
"power_current": "Current (A)",
|
|
62
|
-
"energy_imported": "Energy consumed (kWh)",
|
|
63
|
-
"identify_cmd": "Identify device (blink)",
|
|
64
|
-
"switch_position": "Button position"
|
|
65
|
-
},
|
|
66
|
-
"devtype": {
|
|
67
|
-
"onofflight": "On/Off light",
|
|
68
|
-
"dimmablelight": "Dimmable light",
|
|
69
|
-
"colortemperaturelight": "Color temperature light",
|
|
70
|
-
"extendedcolorlight": "Color light",
|
|
71
|
-
"onoffpluginunit": "Plug (on/off)",
|
|
72
|
-
"dimmablepluginunit": "Plug (dimmable)",
|
|
73
|
-
"smartplug": "Smart plug",
|
|
74
|
-
"contactsensor": "Contact sensor",
|
|
75
|
-
"lightsensor": "Light sensor",
|
|
76
|
-
"occupancysensor": "Occupancy sensor",
|
|
77
|
-
"temperaturesensor": "Temperature sensor",
|
|
78
|
-
"humiditysensor": "Humidity sensor",
|
|
79
|
-
"pressuresensor": "Pressure sensor",
|
|
80
|
-
"flowsensor": "Flow sensor",
|
|
81
|
-
"doorlock": "Door lock",
|
|
82
|
-
"windowcovering": "Window covering",
|
|
83
|
-
"thermostat": "Thermostat",
|
|
84
|
-
"fan": "Fan",
|
|
85
|
-
"genericswitch": "Button/Switch",
|
|
86
|
-
"airqualitysensor": "Air quality sensor",
|
|
87
|
-
"smokecoalarm": "Smoke/CO alarm",
|
|
88
|
-
"waterleakdetector": "Water leak detector",
|
|
89
|
-
"roomairconditioner": "Air conditioner",
|
|
90
|
-
"airpurifier": "Air purifier",
|
|
91
|
-
"speaker": "Speaker",
|
|
92
|
-
"pumpcontroller": "Pump",
|
|
93
|
-
"onofflightswitch": "Light switch",
|
|
94
|
-
"dimmerswitch": "Dimmer switch",
|
|
95
|
-
"colordimmerswitch": "Color dimmer switch",
|
|
96
|
-
"powersource": "Power source",
|
|
97
|
-
"electricalsensor": "Electrical sensor",
|
|
98
|
-
"bridgednode": "Bridged device",
|
|
99
|
-
"aggregator": "Bridge/Aggregator",
|
|
100
|
-
"rootnode": "Root node"
|
|
101
|
-
}
|
|
102
|
-
},
|
|
103
|
-
"common": {
|
|
104
|
-
"ga": "GA",
|
|
105
|
-
"dpt": "DPT",
|
|
106
|
-
"name": "Name",
|
|
107
|
-
"knx_gw": "KNX GW"
|
|
108
|
-
}
|
|
109
|
-
}
|