node-red-contrib-knx-ultimate 6.0.11 → 6.0.12
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 +5 -1
- package/nodes/knxUltimateHueLight.html +1 -5
- package/nodes/knxUltimateMatterControllerDevice.html +318 -41
- package/nodes/locales/de/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/en/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/es/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/fr/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/it/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +0 -2
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +10 -2
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +17 -1
- package/nodes/matter-config.js +5 -4
- package/nodes/utils/matterControllerProfiles/index.js +3 -1
- package/nodes/utils/matterControllerProfiles/universal.js +462 -0
- package/nodes/utils/matterEngine.mjs +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const UNIVERSAL_NODE_ID = '__UNIVERSAL__'
|
|
4
|
+
const POWER_SOURCE_FEATURE_BATTERY = 0x02
|
|
5
|
+
|
|
6
|
+
const setupUniversalProfile = (RED, node, config) => {
|
|
7
|
+
node.name = config.name || node.matterDeviceName || 'Matter Universal Controller'
|
|
8
|
+
node.topic = node.name
|
|
9
|
+
node.matterProfile = 'universal'
|
|
10
|
+
node.notifyreadrequest = true
|
|
11
|
+
node.notifyresponse = false
|
|
12
|
+
node.notifywrite = true
|
|
13
|
+
node.listenallga = true
|
|
14
|
+
node.outputtype = 'write'
|
|
15
|
+
node.outputRBE = 'false'
|
|
16
|
+
node.inputRBE = 'false'
|
|
17
|
+
node.passthrough = 'no'
|
|
18
|
+
const universalService = config.universalService || 'batteryMonitor'
|
|
19
|
+
const lowBatteryGa = String(config.universalLowBatteryGA || '').trim()
|
|
20
|
+
const lowBatteryTextGa = String(config.universalLowBatteryTextGA || '').trim()
|
|
21
|
+
node.knxUltimateAcceptedGAs = [lowBatteryGa, lowBatteryTextGa].filter((ga) => ga !== '')
|
|
22
|
+
const enabledUnlessFalse = (value) => value !== false && value !== 'false'
|
|
23
|
+
const batteryOnlyLow = enabledUnlessFalse(config.universalBatteryOnlyLow)
|
|
24
|
+
const emitInitialState = enabledUnlessFalse(config.universalEmitInitialState)
|
|
25
|
+
const includeRawEvent = enabledUnlessFalse(config.universalIncludeRawEvent)
|
|
26
|
+
const configuredBatteryThreshold = Number(config.universalBatteryThreshold ?? 20)
|
|
27
|
+
const batteryThreshold = Number.isFinite(configuredBatteryThreshold)
|
|
28
|
+
? Math.min(100, Math.max(0, configuredBatteryThreshold))
|
|
29
|
+
: 20
|
|
30
|
+
const batteryStates = new Map()
|
|
31
|
+
const lastEmittedBatteryStates = new Map()
|
|
32
|
+
const deviceNames = new Map()
|
|
33
|
+
const endpointNames = new Map()
|
|
34
|
+
let lastKnxAlarm
|
|
35
|
+
let lastLowKeys = ''
|
|
36
|
+
let lowBatteryCycleTimer
|
|
37
|
+
let lowBatteryCycleIndex = 0
|
|
38
|
+
let currentLowBatteryText = ''
|
|
39
|
+
|
|
40
|
+
const status = (fill, shape, text) => node.status({ fill, shape, text })
|
|
41
|
+
const manager = () => node.serverMatter?.matterManager
|
|
42
|
+
const valueFrom = (msg, key) => msg[key] !== undefined ? msg[key] : msg.matter?.[key]
|
|
43
|
+
const batteryKey = (nodeId, endpointId) => `${String(nodeId)}/${Number(endpointId)}`
|
|
44
|
+
const chargeLevelName = (value) => ['ok', 'warning', 'critical'][Number(value)] || value
|
|
45
|
+
const replaceabilityName = (value) => ['unspecified', 'notReplaceable', 'userReplaceable', 'factoryReplaceable'][Number(value)] || value
|
|
46
|
+
const isBatteryPowerSource = (state) => {
|
|
47
|
+
const featureMap = Number(state.attributes.featureMap)
|
|
48
|
+
return Number.isFinite(featureMap) && (featureMap & POWER_SOURCE_FEATURE_BATTERY) !== 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const normalizedBattery = (state) => {
|
|
52
|
+
const attributes = state.attributes
|
|
53
|
+
const rawPercentRemaining = attributes.batPercentRemaining
|
|
54
|
+
const percent = Number.isFinite(Number(rawPercentRemaining)) ? Number(rawPercentRemaining) / 2 : undefined
|
|
55
|
+
const chargeLevel = attributes.batChargeLevel
|
|
56
|
+
const replacementNeeded = attributes.batReplacementNeeded === true
|
|
57
|
+
const lowBattery = (percent !== undefined && percent <= batteryThreshold) ||
|
|
58
|
+
[1, 2, 'warning', 'critical'].includes(chargeLevel) ||
|
|
59
|
+
replacementNeeded
|
|
60
|
+
return {
|
|
61
|
+
percent,
|
|
62
|
+
rawPercentRemaining,
|
|
63
|
+
chargeLevel: chargeLevel === undefined ? undefined : chargeLevelName(chargeLevel),
|
|
64
|
+
replacementNeeded: attributes.batReplacementNeeded,
|
|
65
|
+
replaceability: attributes.batReplaceability === undefined ? undefined : replaceabilityName(attributes.batReplaceability),
|
|
66
|
+
voltageMv: attributes.batVoltage,
|
|
67
|
+
status: attributes.status,
|
|
68
|
+
lowBattery,
|
|
69
|
+
threshold: batteryThreshold
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const batteryList = () => [...batteryStates.values()]
|
|
74
|
+
.filter(isBatteryPowerSource)
|
|
75
|
+
.map((state) => ({
|
|
76
|
+
nodeId: state.nodeId,
|
|
77
|
+
deviceName: state.deviceName,
|
|
78
|
+
endpointId: state.endpointId,
|
|
79
|
+
battery: normalizedBattery(state),
|
|
80
|
+
lastSeen: state.lastSeen
|
|
81
|
+
}))
|
|
82
|
+
|
|
83
|
+
const sendKnx = (ga, payload, dpt, outputtype = 'write') => {
|
|
84
|
+
if (ga === '' || !node.serverKNX?.sendKNXTelegramToKNXEngine) return
|
|
85
|
+
node.serverKNX.sendKNXTelegramToKNXEngine({ grpaddr: ga, payload, dpt, outputtype, nodecallerid: node.id })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const lowBatteries = () => batteryList().filter((entry) => entry.battery.lowBattery)
|
|
89
|
+
const batteryDisplayName = (entry) => String(entry.deviceName || `Matter ${entry.nodeId}/${entry.endpointId}`).slice(0, 14)
|
|
90
|
+
const stopBatteryCycle = () => {
|
|
91
|
+
if (lowBatteryCycleTimer) clearInterval(lowBatteryCycleTimer)
|
|
92
|
+
lowBatteryCycleTimer = undefined
|
|
93
|
+
}
|
|
94
|
+
const sendNextLowBatteryName = () => {
|
|
95
|
+
const entries = lowBatteries()
|
|
96
|
+
if (entries.length === 0) return
|
|
97
|
+
const entry = entries[lowBatteryCycleIndex % entries.length]
|
|
98
|
+
lowBatteryCycleIndex = (lowBatteryCycleIndex + 1) % entries.length
|
|
99
|
+
currentLowBatteryText = batteryDisplayName(entry)
|
|
100
|
+
sendKnx(lowBatteryTextGa, currentLowBatteryText, '16.001')
|
|
101
|
+
}
|
|
102
|
+
const syncKnxBatteryOutputs = () => {
|
|
103
|
+
const entries = lowBatteries()
|
|
104
|
+
const alarm = entries.length > 0
|
|
105
|
+
if (lastKnxAlarm !== alarm) {
|
|
106
|
+
sendKnx(lowBatteryGa, alarm, '1.005')
|
|
107
|
+
lastKnxAlarm = alarm
|
|
108
|
+
}
|
|
109
|
+
const lowKeys = entries.map((entry) => batteryKey(entry.nodeId, entry.endpointId)).sort().join(',')
|
|
110
|
+
if (lowKeys === lastLowKeys) return
|
|
111
|
+
lastLowKeys = lowKeys
|
|
112
|
+
stopBatteryCycle()
|
|
113
|
+
lowBatteryCycleIndex = 0
|
|
114
|
+
if (!alarm) {
|
|
115
|
+
currentLowBatteryText = ''
|
|
116
|
+
sendKnx(lowBatteryTextGa, '', '16.001')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
sendNextLowBatteryName()
|
|
120
|
+
lowBatteryCycleTimer = setInterval(sendNextLowBatteryName, 2000)
|
|
121
|
+
lowBatteryCycleTimer.unref?.()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const refreshBatteryStatus = () => {
|
|
125
|
+
const batteries = batteryList()
|
|
126
|
+
const lowCount = batteries.filter((entry) => entry.battery.lowBattery).length
|
|
127
|
+
status(lowCount > 0 ? 'yellow' : 'green', 'dot',
|
|
128
|
+
lowCount > 0 ? `Batteries: ${batteries.length} (${lowCount} low)` : `Batteries: ${batteries.length}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const emitBatteryState = (state, source, rawEvent) => {
|
|
132
|
+
const key = batteryKey(state.nodeId, state.endpointId)
|
|
133
|
+
if (!isBatteryPowerSource(state)) {
|
|
134
|
+
lastEmittedBatteryStates.delete(key)
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
const battery = normalizedBattery(state)
|
|
138
|
+
if (batteryOnlyLow && !battery.lowBattery) {
|
|
139
|
+
lastEmittedBatteryStates.delete(key)
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
const signature = JSON.stringify(battery)
|
|
143
|
+
if (lastEmittedBatteryStates.get(key) === signature) return
|
|
144
|
+
lastEmittedBatteryStates.set(key, signature)
|
|
145
|
+
const timestamp = new Date().toISOString()
|
|
146
|
+
node.send({
|
|
147
|
+
topic: `matter/battery/${state.nodeId}/${state.endpointId}`,
|
|
148
|
+
payload: {
|
|
149
|
+
nodeId: state.nodeId,
|
|
150
|
+
deviceName: state.deviceName,
|
|
151
|
+
endpointId: state.endpointId,
|
|
152
|
+
battery,
|
|
153
|
+
timestamp
|
|
154
|
+
},
|
|
155
|
+
matter: {
|
|
156
|
+
source,
|
|
157
|
+
nodeId: state.nodeId,
|
|
158
|
+
endpointId: state.endpointId,
|
|
159
|
+
clusterId: 47,
|
|
160
|
+
clusterName: rawEvent?.clusterName || 'PowerSource',
|
|
161
|
+
attributeId: rawEvent?.attributeId,
|
|
162
|
+
attributeName: rawEvent?.attributeName,
|
|
163
|
+
value: rawEvent?.value,
|
|
164
|
+
raw: includeRawEvent ? rawEvent : undefined
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
syncKnxBatteryOutputs()
|
|
168
|
+
refreshBatteryStatus()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const updateBatteryState = (event, source = 'batteryReport', emit = true) => {
|
|
172
|
+
if (Number(event?.clusterId) !== 47 || event.attributeName === undefined) return
|
|
173
|
+
const key = batteryKey(event.nodeId, event.endpointId)
|
|
174
|
+
const state = batteryStates.get(key) || {
|
|
175
|
+
nodeId: String(event.nodeId),
|
|
176
|
+
deviceName: endpointNames.get(key) || deviceNames.get(String(event.nodeId)) || '',
|
|
177
|
+
endpointId: Number(event.endpointId),
|
|
178
|
+
attributes: {}
|
|
179
|
+
}
|
|
180
|
+
state.deviceName = endpointNames.get(key) || deviceNames.get(state.nodeId) || state.deviceName
|
|
181
|
+
state.attributes[event.attributeName] = event.value
|
|
182
|
+
state.lastSeen = new Date().toISOString()
|
|
183
|
+
if (event.attributeName === 'featureMap' && !isBatteryPowerSource(state)) {
|
|
184
|
+
batteryStates.delete(key)
|
|
185
|
+
lastEmittedBatteryStates.delete(key)
|
|
186
|
+
syncKnxBatteryOutputs()
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
batteryStates.set(key, state)
|
|
190
|
+
if (emit) {
|
|
191
|
+
emitBatteryState(state, source, event)
|
|
192
|
+
syncKnxBatteryOutputs()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scanBatterySnapshot = (nodeId) => {
|
|
197
|
+
if (!node.serverMatter || !emitInitialState) return
|
|
198
|
+
const details = node.serverMatter.getCommissionedNodesDetails?.() || []
|
|
199
|
+
details.forEach((detail) => deviceNames.set(String(detail.nodeId), detail.name || ''))
|
|
200
|
+
const selected = nodeId === undefined
|
|
201
|
+
? details
|
|
202
|
+
: details.filter((detail) => String(detail.nodeId) === String(nodeId))
|
|
203
|
+
selected.forEach((detail) => {
|
|
204
|
+
try {
|
|
205
|
+
const structure = node.serverMatter.getNodeStructure(String(detail.nodeId))
|
|
206
|
+
;(structure?.endpoints || []).forEach((endpoint) => {
|
|
207
|
+
endpointNames.set(batteryKey(detail.nodeId, endpoint.endpointId), endpoint.name || detail.name || '')
|
|
208
|
+
;(endpoint.clusters || []).filter((cluster) => Number(cluster.id) === 47).forEach((cluster) => {
|
|
209
|
+
const events = (cluster.attributes || [])
|
|
210
|
+
.filter((attribute) => attribute.value !== undefined)
|
|
211
|
+
.map((attribute) => ({
|
|
212
|
+
nodeId: String(detail.nodeId),
|
|
213
|
+
endpointId: Number(endpoint.endpointId),
|
|
214
|
+
clusterId: 47,
|
|
215
|
+
clusterName: cluster.name,
|
|
216
|
+
attributeId: attribute.id,
|
|
217
|
+
attributeName: attribute.name,
|
|
218
|
+
value: attribute.value
|
|
219
|
+
}))
|
|
220
|
+
events.forEach((event) => updateBatteryState(event, 'batterySnapshot', false))
|
|
221
|
+
const state = batteryStates.get(batteryKey(detail.nodeId, endpoint.endpointId))
|
|
222
|
+
if (state) emitBatteryState(state, 'batterySnapshot', events.at(-1))
|
|
223
|
+
})
|
|
224
|
+
})
|
|
225
|
+
} catch (error) {
|
|
226
|
+
RED.log.debug(`knxUltimateMatterControllerDevice Universal battery snapshot: ${error.message}`)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
syncKnxBatteryOutputs()
|
|
230
|
+
refreshBatteryStatus()
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) => {
|
|
234
|
+
if (/disconnected|waiting|error/i.test(text)) status(fill, shape, text)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
node.handleSend = (msg) => {
|
|
238
|
+
if (msg?.knx?.event !== 'GroupValue_Read') return
|
|
239
|
+
const destination = String(msg.knx.destination || '')
|
|
240
|
+
if (destination === lowBatteryGa) sendKnx(lowBatteryGa, lowBatteries().length > 0, '1.005', 'response')
|
|
241
|
+
if (destination === lowBatteryTextGa) sendKnx(lowBatteryTextGa, currentLowBatteryText, '16.001', 'response')
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
node.setNodeStatusMatter = (value) => {
|
|
245
|
+
if (!value?.text) return
|
|
246
|
+
if (/^(matter disconnected|waiting for matter controller)$/i.test(value.text)) {
|
|
247
|
+
status(value.fill || 'red', value.shape || 'ring', value.text)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
if (universalService === 'batteryMonitor') {
|
|
251
|
+
refreshBatteryStatus()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
status('green', 'dot', 'Universal Matter mode')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
node.handleSendMatter = (event) => {
|
|
258
|
+
try {
|
|
259
|
+
if (!event || event.nodeId === undefined || event.endpointId === undefined || event.clusterId === undefined) return
|
|
260
|
+
const attributeName = event.attributeName ?? event.attributeId ?? 'attribute'
|
|
261
|
+
if (universalService === 'batteryMonitor') {
|
|
262
|
+
updateBatteryState(event)
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
const matter = {
|
|
266
|
+
...event,
|
|
267
|
+
source: 'attributeReport',
|
|
268
|
+
nodeId: String(event.nodeId),
|
|
269
|
+
endpointId: Number(event.endpointId),
|
|
270
|
+
clusterId: Number(event.clusterId),
|
|
271
|
+
attributeName,
|
|
272
|
+
value: event.value,
|
|
273
|
+
timestamp: Date.now(),
|
|
274
|
+
raw: event
|
|
275
|
+
}
|
|
276
|
+
node.send({
|
|
277
|
+
topic: `matter/${matter.nodeId}/${matter.endpointId}/${matter.clusterId}/${attributeName}`,
|
|
278
|
+
payload: event.value,
|
|
279
|
+
matter
|
|
280
|
+
})
|
|
281
|
+
status('blue', 'dot', `Attribute: ${attributeName}`)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
RED.log.error(`knxUltimateMatterControllerDevice Universal attribute: ${error.message}`)
|
|
284
|
+
status('red', 'ring', error.message)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
node.handleMatterClusterEvent = (event) => {
|
|
289
|
+
try {
|
|
290
|
+
if (universalService === 'batteryMonitor') return
|
|
291
|
+
if (!event || event.nodeId === undefined || event.endpointId === undefined || event.clusterId === undefined) return
|
|
292
|
+
const eventName = event.eventName ?? event.eventId ?? 'event'
|
|
293
|
+
const matter = {
|
|
294
|
+
...event,
|
|
295
|
+
source: 'clusterEvent',
|
|
296
|
+
nodeId: String(event.nodeId),
|
|
297
|
+
endpointId: Number(event.endpointId),
|
|
298
|
+
clusterId: Number(event.clusterId),
|
|
299
|
+
eventName,
|
|
300
|
+
timestamp: Date.now(),
|
|
301
|
+
raw: event
|
|
302
|
+
}
|
|
303
|
+
node.send({
|
|
304
|
+
topic: `matter/${matter.nodeId}/${matter.endpointId}/${matter.clusterId}/event/${eventName}`,
|
|
305
|
+
payload: event.events !== undefined ? event.events : event.value,
|
|
306
|
+
matter
|
|
307
|
+
})
|
|
308
|
+
status('blue', 'ring', `Event: ${eventName}`)
|
|
309
|
+
} catch (error) {
|
|
310
|
+
RED.log.error(`knxUltimateMatterControllerDevice Universal cluster event: ${error.message}`)
|
|
311
|
+
status('red', 'ring', error.message)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
node.handleMatterNodeInitialized = (event) => {
|
|
316
|
+
try {
|
|
317
|
+
const nodeId = event && typeof event === 'object' ? event.nodeId : event
|
|
318
|
+
if (universalService === 'batteryMonitor') {
|
|
319
|
+
scanBatterySnapshot(nodeId)
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
if (nodeId === undefined || nodeId === null || String(nodeId) === '') return
|
|
323
|
+
node.send({
|
|
324
|
+
topic: `matter/${String(nodeId)}/initialized`,
|
|
325
|
+
payload: true,
|
|
326
|
+
matter: {
|
|
327
|
+
source: 'nodeInitialized',
|
|
328
|
+
nodeId: String(nodeId),
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
raw: event
|
|
331
|
+
}
|
|
332
|
+
})
|
|
333
|
+
status('green', 'dot', `Universal: node ${String(nodeId)}`)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
RED.log.error(`knxUltimateMatterControllerDevice Universal initialization: ${error.message}`)
|
|
336
|
+
status('red', 'ring', error.message)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (node.serverMatter) {
|
|
341
|
+
node.serverMatter.removeClient(node)
|
|
342
|
+
node.serverMatter.addClient(node)
|
|
343
|
+
if (universalService === 'batteryMonitor') {
|
|
344
|
+
Promise.resolve().then(() => scanBatterySnapshot()).catch((error) => {
|
|
345
|
+
RED.log.error(`knxUltimateMatterControllerDevice Universal battery startup: ${error.message}`)
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
status('yellow', 'ring', 'Matter controller not ready')
|
|
350
|
+
}
|
|
351
|
+
if (node.serverKNX) {
|
|
352
|
+
node.serverKNX.removeClient(node)
|
|
353
|
+
node.serverKNX.addClient(node)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
node.on('input', (msg, send, done) => {
|
|
357
|
+
const output = typeof send === 'function' ? send : node.send.bind(node)
|
|
358
|
+
Promise.resolve().then(async () => {
|
|
359
|
+
const action = msg.action ?? msg.payload?.action ?? msg.matter?.action
|
|
360
|
+
if (action === 'getAllBatteries') {
|
|
361
|
+
output({
|
|
362
|
+
...msg,
|
|
363
|
+
payload: batteryList(),
|
|
364
|
+
matter: {
|
|
365
|
+
...(msg.matter || {}),
|
|
366
|
+
source: 'getAllBatteries',
|
|
367
|
+
timestamp: Date.now()
|
|
368
|
+
}
|
|
369
|
+
})
|
|
370
|
+
refreshBatteryStatus()
|
|
371
|
+
if (done) done()
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
const nodeIdValue = valueFrom(msg, 'nodeId')
|
|
375
|
+
const endpointIdValue = valueFrom(msg, 'endpointId')
|
|
376
|
+
const clusterIdValue = valueFrom(msg, 'clusterId')
|
|
377
|
+
const command = valueFrom(msg, 'command')
|
|
378
|
+
const attribute = valueFrom(msg, 'attribute')
|
|
379
|
+
if (nodeIdValue === undefined || nodeIdValue === null || String(nodeIdValue) === '' ||
|
|
380
|
+
endpointIdValue === undefined || endpointIdValue === null || String(endpointIdValue).trim() === '' ||
|
|
381
|
+
clusterIdValue === undefined || clusterIdValue === null || String(clusterIdValue).trim() === '') {
|
|
382
|
+
throw new Error('Matter input requires nodeId, endpointId and clusterId')
|
|
383
|
+
}
|
|
384
|
+
if ((command === undefined || command === null || command === '') &&
|
|
385
|
+
(attribute === undefined || attribute === null || attribute === '')) {
|
|
386
|
+
throw new Error('Matter input requires command or attribute')
|
|
387
|
+
}
|
|
388
|
+
const currentManager = manager()
|
|
389
|
+
if (!currentManager) throw new Error('Matter controller not ready')
|
|
390
|
+
const nodeId = String(nodeIdValue)
|
|
391
|
+
const endpointId = Number(endpointIdValue)
|
|
392
|
+
const clusterId = Number(clusterIdValue)
|
|
393
|
+
if (!Number.isFinite(endpointId) || !Number.isFinite(clusterId)) {
|
|
394
|
+
throw new Error('Matter input requires nodeId, endpointId and clusterId')
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (command !== undefined && command !== null && command !== '') {
|
|
398
|
+
await currentManager.writeMatterQueueAdd({
|
|
399
|
+
nodeId,
|
|
400
|
+
endpointId,
|
|
401
|
+
clusterId,
|
|
402
|
+
kind: 'command',
|
|
403
|
+
name: command,
|
|
404
|
+
args: valueFrom(msg, 'args') ?? {}
|
|
405
|
+
})
|
|
406
|
+
status('green', 'dot', `Matter command: ${command}`)
|
|
407
|
+
if (done) done()
|
|
408
|
+
return
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const value = valueFrom(msg, 'value')
|
|
412
|
+
if (value !== undefined) {
|
|
413
|
+
await currentManager.writeMatterQueueAdd({
|
|
414
|
+
nodeId,
|
|
415
|
+
endpointId,
|
|
416
|
+
clusterId,
|
|
417
|
+
kind: 'attributeWrite',
|
|
418
|
+
name: attribute,
|
|
419
|
+
args: value
|
|
420
|
+
})
|
|
421
|
+
status('green', 'dot', `Matter write: ${attribute}`)
|
|
422
|
+
if (done) done()
|
|
423
|
+
return
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const readValue = await currentManager.readAttribute(
|
|
427
|
+
nodeId,
|
|
428
|
+
endpointId,
|
|
429
|
+
clusterId,
|
|
430
|
+
attribute,
|
|
431
|
+
valueFrom(msg, 'requestFromRemote') === true
|
|
432
|
+
)
|
|
433
|
+
output({
|
|
434
|
+
...msg,
|
|
435
|
+
payload: readValue,
|
|
436
|
+
matter: {
|
|
437
|
+
...(msg.matter || {}),
|
|
438
|
+
source: 'inputRead',
|
|
439
|
+
nodeId,
|
|
440
|
+
endpointId,
|
|
441
|
+
clusterId,
|
|
442
|
+
attribute
|
|
443
|
+
}
|
|
444
|
+
})
|
|
445
|
+
status('blue', 'dot', `Matter read: ${attribute}`)
|
|
446
|
+
if (done) done()
|
|
447
|
+
}).catch((error) => {
|
|
448
|
+
status('red', 'ring', error.message)
|
|
449
|
+
if (typeof done === 'function') done(error)
|
|
450
|
+
else node.error(error, msg)
|
|
451
|
+
})
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
node.on('close', (done) => {
|
|
455
|
+
stopBatteryCycle()
|
|
456
|
+
try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
|
|
457
|
+
try { if (node.serverMatter) node.serverMatter.removeClient(node) } catch (error) { /* empty */ }
|
|
458
|
+
done()
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
module.exports = { UNIVERSAL_NODE_ID, setupUniversalProfile }
|
|
@@ -505,7 +505,8 @@ class classMatter extends EventEmitter {
|
|
|
505
505
|
if (typeof cc.isAttributeSupportedByName === 'function' && !cc.isAttributeSupportedByName(attrName)) return
|
|
506
506
|
// Skip the global meta attributes (FeatureMap, AttributeList, ClusterRevision...):
|
|
507
507
|
// they are useless for KNX mappings and only confuse the user.
|
|
508
|
-
|
|
508
|
+
const exposeFeatureMap = [47, 768].includes(Number(cc.id)) && attrName === 'featureMap'
|
|
509
|
+
if (Number(cc.attributes[attrName].id) >= 65528 && !exposeFeatureMap) return
|
|
509
510
|
let value
|
|
510
511
|
try {
|
|
511
512
|
value = cc.attributes[attrName].getLocal()
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"engines": {
|
|
4
4
|
"node": ">=20.18.1"
|
|
5
5
|
},
|
|
6
|
-
"version": "6.0.
|
|
6
|
+
"version": "6.0.12",
|
|
7
7
|
"description": "KNX Ultimate is the most advanced KNX integration for Node-RED, providing secure KNX/IP communication, routing, ETS project import, Philips Hue, Matter Controller and Matter Bridge (control matter device via KNX and expose KNX GA via Matter), MQTT, diagnostics with AI, virtual devices, and powerful automation nodes. Build professional, reliable, and scalable smart home and building automation projects with minimal effort.",
|
|
8
8
|
"files": [
|
|
9
9
|
"nodes/",
|