node-red-contrib-knx-ultimate 6.0.6 → 6.0.8
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 +6 -1
- package/nodes/knxUltimateMatterControllerDevice.html +183 -46
- package/nodes/knxUltimateMatterControllerDevice.js +11 -6
- package/nodes/locales/de/knxUltimateMatterControllerDevice.html +3 -1
- package/nodes/locales/de/knxUltimateMatterControllerDevice.json +25 -15
- package/nodes/locales/en/knxUltimateMatterControllerDevice.html +3 -1
- package/nodes/locales/en/knxUltimateMatterControllerDevice.json +26 -16
- package/nodes/locales/es/knxUltimateMatterControllerDevice.html +3 -1
- package/nodes/locales/es/knxUltimateMatterControllerDevice.json +19 -9
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.html +3 -1
- package/nodes/locales/fr/knxUltimateMatterControllerDevice.json +16 -6
- package/nodes/locales/it/knxUltimateMatterControllerDevice.html +3 -1
- package/nodes/locales/it/knxUltimateMatterControllerDevice.json +25 -15
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.html +3 -1
- package/nodes/locales/zh-CN/knxUltimateMatterControllerDevice.json +21 -11
- package/nodes/utils/matterControllerProfiles/doorLock.js +214 -0
- package/nodes/utils/matterControllerProfiles/index.js +21 -0
- package/nodes/utils/matterControllerProfiles/mappedEndpoint.js +150 -0
- package/package.json +6 -2
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const dptlib = require('knxultimate').dptlib
|
|
4
|
+
|
|
5
|
+
const DOOR_LOCK_CLUSTER_ID = 0x0101
|
|
6
|
+
const LOCK_STATE = {
|
|
7
|
+
NOT_FULLY_LOCKED: 0,
|
|
8
|
+
LOCKED: 1,
|
|
9
|
+
UNLOCKED: 2,
|
|
10
|
+
UNLATCHED: 3
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const decodeKnxBoolean = (msg, dpt) => {
|
|
14
|
+
if (msg && msg.knx && Buffer.isBuffer(msg.knx.rawValue)) {
|
|
15
|
+
return !!dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(dpt || '1.001'))
|
|
16
|
+
}
|
|
17
|
+
return !!(msg && msg.payload)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lockStateName = (value) => {
|
|
21
|
+
switch (Number(value)) {
|
|
22
|
+
case LOCK_STATE.LOCKED: return 'locked'
|
|
23
|
+
case LOCK_STATE.UNLOCKED: return 'unlocked'
|
|
24
|
+
case LOCK_STATE.NOT_FULLY_LOCKED: return 'notFullyLocked'
|
|
25
|
+
case LOCK_STATE.UNLATCHED: return 'unlatched'
|
|
26
|
+
default: return 'unknown'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lockStateToBoolean = (value) => {
|
|
31
|
+
if (Number(value) === LOCK_STATE.LOCKED) return true
|
|
32
|
+
if (Number(value) === LOCK_STATE.UNLOCKED) return false
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const setupDoorLockProfile = (RED, node, config) => {
|
|
37
|
+
node.name = config.name || node.matterDeviceName || 'Control Matter door lock from KNX'
|
|
38
|
+
node.topic = node.name
|
|
39
|
+
node.notifyreadrequest = true
|
|
40
|
+
node.notifyreadrequestalsorespondtobus = 'false'
|
|
41
|
+
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
|
|
42
|
+
node.notifyresponse = false
|
|
43
|
+
node.notifywrite = true
|
|
44
|
+
node.initialread = true
|
|
45
|
+
node.listenallga = true
|
|
46
|
+
node.outputtype = 'write'
|
|
47
|
+
node.outputRBE = 'false'
|
|
48
|
+
node.inputRBE = 'false'
|
|
49
|
+
node.passthrough = 'no'
|
|
50
|
+
node.currentLockState = undefined
|
|
51
|
+
node.knxUltimateAcceptedGAs = [config.GALightSwitch, config.GALightState]
|
|
52
|
+
.map((ga) => String(ga || '').trim())
|
|
53
|
+
.filter((ga) => ga !== '')
|
|
54
|
+
|
|
55
|
+
const setStatus = (fill, shape, text) => node.status({ fill, shape, text })
|
|
56
|
+
// knxUltimate-config invokes this synchronously from addClient(). Profiles return
|
|
57
|
+
// early from the main light constructor, so they must expose the callback before
|
|
58
|
+
// registering with the shared KNX configuration node.
|
|
59
|
+
node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) => setStatus(fill, shape, text)
|
|
60
|
+
|
|
61
|
+
const commandArgs = () => {
|
|
62
|
+
const pin = String(node.credentials?.doorLockPin || '')
|
|
63
|
+
return pin === '' ? {} : { pinCode: Buffer.from(pin, 'utf8') }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sendFlow = (source, state, rawState) => {
|
|
67
|
+
if (config.enableNodePINS !== 'yes') return
|
|
68
|
+
node.send({
|
|
69
|
+
topic: node.topic,
|
|
70
|
+
payload: state,
|
|
71
|
+
matter: {
|
|
72
|
+
source,
|
|
73
|
+
nodeId: node.matterNodeId,
|
|
74
|
+
endpointId: node.matterEndpointId,
|
|
75
|
+
clusterId: DOOR_LOCK_CLUSTER_ID,
|
|
76
|
+
lockState: rawState,
|
|
77
|
+
lockStateName: rawState === undefined ? (state ? 'locked' : 'unlocked') : lockStateName(rawState)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const writeKnxState = (state, outputtype = 'write') => {
|
|
83
|
+
const ga = String(config.GALightState || '').trim()
|
|
84
|
+
if (ga === '' || !node.serverKNX) return
|
|
85
|
+
node.serverKNX.sendKNXTelegramToKNXEngine({
|
|
86
|
+
grpaddr: ga,
|
|
87
|
+
payload: state,
|
|
88
|
+
dpt: config.dptLightState || '1.001',
|
|
89
|
+
outputtype,
|
|
90
|
+
nodecallerid: node.id
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const publishMatterState = (rawState, source = 'matter') => {
|
|
95
|
+
node.currentLockState = Number(rawState)
|
|
96
|
+
const state = lockStateToBoolean(rawState)
|
|
97
|
+
const name = lockStateName(rawState)
|
|
98
|
+
if (state !== undefined) writeKnxState(state)
|
|
99
|
+
sendFlow(source, state, rawState)
|
|
100
|
+
setStatus(state === undefined ? 'yellow' : 'blue', state === undefined ? 'ring' : 'dot', `Matter: ${name}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const queueCommand = (locked, source = 'knx') => {
|
|
104
|
+
const manager = node.serverMatter?.matterManager
|
|
105
|
+
if (!manager) throw new Error('Matter controller not ready')
|
|
106
|
+
const capabilities = (() => {
|
|
107
|
+
try { return JSON.parse(config.matterDeviceCapabilities || '{}') } catch (error) { return {} }
|
|
108
|
+
})()
|
|
109
|
+
if (locked && capabilities.lockDoor === false) throw new Error('The Matter endpoint does not expose lockDoor')
|
|
110
|
+
if (!locked && capabilities.unlockDoor === false) throw new Error('The Matter endpoint does not expose unlockDoor')
|
|
111
|
+
const queued = manager.writeMatterQueueAdd({
|
|
112
|
+
nodeId: node.matterNodeId,
|
|
113
|
+
endpointId: node.matterEndpointId,
|
|
114
|
+
clusterId: DOOR_LOCK_CLUSTER_ID,
|
|
115
|
+
kind: 'command',
|
|
116
|
+
name: locked ? 'lockDoor' : 'unlockDoor',
|
|
117
|
+
args: commandArgs()
|
|
118
|
+
})
|
|
119
|
+
if (queued && typeof queued.catch === 'function') {
|
|
120
|
+
queued.catch((error) => {
|
|
121
|
+
RED.log.error(`knxUltimateMatterControllerDevice DoorLock command: ${error.message}`)
|
|
122
|
+
setStatus('red', 'ring', error.message)
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
sendFlow(source, locked)
|
|
126
|
+
setStatus('green', 'dot', `KNX→Matter: ${locked ? 'lock' : 'unlock'}`)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
node.handleSend = (msg) => {
|
|
130
|
+
try {
|
|
131
|
+
if (!msg?.knx || !node.knxUltimateAcceptedGAs.includes(String(msg.knx.destination || '').trim())) return
|
|
132
|
+
if (msg.knx.event === 'GroupValue_Read') {
|
|
133
|
+
if (String(msg.knx.destination) === String(config.GALightState)) {
|
|
134
|
+
const state = lockStateToBoolean(node.currentLockState)
|
|
135
|
+
if (state !== undefined) writeKnxState(state, 'response')
|
|
136
|
+
}
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
if (String(msg.knx.destination) !== String(config.GALightSwitch)) return
|
|
140
|
+
queueCommand(decodeKnxBoolean(msg, config.dptLightSwitch), 'knx')
|
|
141
|
+
} catch (error) {
|
|
142
|
+
RED.log.error(`knxUltimateMatterControllerDevice DoorLock KNX: ${error.message}`)
|
|
143
|
+
setStatus('red', 'ring', error.message)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
node.handleSendMatter = (event) => {
|
|
148
|
+
try {
|
|
149
|
+
if (String(event?.nodeId) !== String(node.matterNodeId)) return
|
|
150
|
+
if (Number(event?.endpointId) !== Number(node.matterEndpointId)) return
|
|
151
|
+
if (Number(event?.clusterId) !== DOOR_LOCK_CLUSTER_ID || event.attributeName !== 'lockState') return
|
|
152
|
+
publishMatterState(event.value)
|
|
153
|
+
} catch (error) {
|
|
154
|
+
RED.log.error(`knxUltimateMatterControllerDevice DoorLock Matter: ${error.message}`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
node.handleMatterClusterEvent = () => {}
|
|
158
|
+
node.handleMatterNodeInitialized = () => {
|
|
159
|
+
try {
|
|
160
|
+
const value = node.serverMatter?.matterManager?.getCachedAttribute(
|
|
161
|
+
node.matterNodeId,
|
|
162
|
+
node.matterEndpointId,
|
|
163
|
+
DOOR_LOCK_CLUSTER_ID,
|
|
164
|
+
'lockState'
|
|
165
|
+
)
|
|
166
|
+
if (value !== undefined && value !== null) publishMatterState(value, 'initial')
|
|
167
|
+
} catch (error) {
|
|
168
|
+
RED.log.error(`knxUltimateMatterControllerDevice DoorLock initial state: ${error.message}`)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
node.setNodeStatusMatter = (status) => {
|
|
172
|
+
if (status && status.text) setStatus(status.fill || 'grey', status.shape || 'ring', status.text)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (node.serverKNX) {
|
|
176
|
+
node.serverKNX.removeClient(node)
|
|
177
|
+
node.serverKNX.addClient(node)
|
|
178
|
+
} else {
|
|
179
|
+
setStatus('yellow', 'ring', 'No KNX gateway selected')
|
|
180
|
+
}
|
|
181
|
+
if (node.serverMatter) {
|
|
182
|
+
node.serverMatter.removeClient(node)
|
|
183
|
+
node.serverMatter.addClient(node)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
node.on('input', (msg, send, done) => {
|
|
187
|
+
try {
|
|
188
|
+
const value = typeof msg.payload === 'object' && msg.payload !== null && msg.payload.locked !== undefined
|
|
189
|
+
? msg.payload.locked
|
|
190
|
+
: msg.payload
|
|
191
|
+
if (typeof value !== 'boolean') throw new Error('Door Lock input requires msg.payload boolean or { locked: boolean }')
|
|
192
|
+
queueCommand(value, 'flow')
|
|
193
|
+
if (done) done()
|
|
194
|
+
} catch (error) {
|
|
195
|
+
setStatus('red', 'ring', error.message)
|
|
196
|
+
if (done) done(error)
|
|
197
|
+
else node.error(error, msg)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
node.on('close', (done) => {
|
|
202
|
+
try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
|
|
203
|
+
try { if (node.serverMatter) node.serverMatter.removeClient(node) } catch (error) { /* empty */ }
|
|
204
|
+
done()
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = {
|
|
209
|
+
DOOR_LOCK_CLUSTER_ID,
|
|
210
|
+
LOCK_STATE,
|
|
211
|
+
lockStateName,
|
|
212
|
+
lockStateToBoolean,
|
|
213
|
+
setupDoorLockProfile
|
|
214
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { setupDoorLockProfile } = require('./doorLock')
|
|
4
|
+
const { setupMappedEndpointProfile } = require('./mappedEndpoint')
|
|
5
|
+
|
|
6
|
+
// Controller-side Matter device profiles live behind this registry. Keep profile
|
|
7
|
+
// selection capability-driven: the editor records the profile only after inspecting
|
|
8
|
+
// the endpoint's actual device types, clusters, attributes and supported commands.
|
|
9
|
+
const PROFILE_SETUPS = Object.freeze({
|
|
10
|
+
doorLock: setupDoorLockProfile,
|
|
11
|
+
mapped: setupMappedEndpointProfile
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const setupMatterControllerProfile = (profile, RED, node, config) => {
|
|
15
|
+
const setup = PROFILE_SETUPS[profile]
|
|
16
|
+
if (typeof setup !== 'function') return false
|
|
17
|
+
setup(RED, node, config)
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { PROFILE_SETUPS, setupMatterControllerProfile }
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const dptlib = require('knxultimate').dptlib
|
|
4
|
+
const { knxToMatter, matterToKnx } = require('../matterKnxConverter')
|
|
5
|
+
|
|
6
|
+
const isValidGroupAddress = (value) => {
|
|
7
|
+
const parts = String(value || '').trim().split('/')
|
|
8
|
+
if (parts.length !== 3 || parts.some((part) => !/^\d+$/.test(part))) return false
|
|
9
|
+
const [main, middle, sub] = parts.map(Number)
|
|
10
|
+
return main >= 0 && main <= 31 && middle >= 0 && middle <= 7 && sub >= 0 && sub <= 255
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const parseMappings = (value) => {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = Array.isArray(value) ? value : JSON.parse(value || '[]')
|
|
16
|
+
return parsed.filter((mapping) => {
|
|
17
|
+
if (!mapping || !isValidGroupAddress(mapping.ga) || !mapping.target || !mapping.dpt) return false
|
|
18
|
+
try { dptlib.resolve(mapping.dpt); return true } catch (error) { return false }
|
|
19
|
+
})
|
|
20
|
+
} catch (error) {
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const setupMappedEndpointProfile = (RED, node, config) => {
|
|
26
|
+
node.name = config.name || node.matterDeviceName || 'Control Matter from KNX'
|
|
27
|
+
node.topic = node.name
|
|
28
|
+
node.mappings = parseMappings(config.matterMappings)
|
|
29
|
+
node.knxUltimateAcceptedGAs = [...new Set(node.mappings.map((mapping) => String(mapping.ga).trim()))]
|
|
30
|
+
node.notifyreadrequest = true
|
|
31
|
+
node.notifyreadrequestalsorespondtobus = 'false'
|
|
32
|
+
node.notifyreadrequestalsorespondtobusdefaultvalueifnotinitialized = ''
|
|
33
|
+
node.notifyresponse = false
|
|
34
|
+
node.notifywrite = true
|
|
35
|
+
node.initialread = true
|
|
36
|
+
node.listenallga = true
|
|
37
|
+
node.outputtype = 'write'
|
|
38
|
+
node.outputRBE = 'false'
|
|
39
|
+
node.inputRBE = 'false'
|
|
40
|
+
node.passthrough = 'no'
|
|
41
|
+
const enablePins = config.enableNodePINS === 'yes'
|
|
42
|
+
let lastInitialReadTs = 0
|
|
43
|
+
|
|
44
|
+
const status = (fill, shape, text) => node.status({ fill, shape, text })
|
|
45
|
+
// knxUltimate-config calls setNodeStatus synchronously while adding the client.
|
|
46
|
+
node.setNodeStatus = ({ fill = 'grey', shape = 'ring', text = '' } = {}) => status(fill, shape, text)
|
|
47
|
+
const manager = () => node.serverMatter?.matterManager
|
|
48
|
+
const sendKnx = (mapping, payload, outputtype = 'write') => {
|
|
49
|
+
if (payload === undefined || !node.serverKNX) return false
|
|
50
|
+
node.serverKNX.sendKNXTelegramToKNXEngine({
|
|
51
|
+
grpaddr: mapping.ga,
|
|
52
|
+
payload,
|
|
53
|
+
dpt: mapping.dpt,
|
|
54
|
+
outputtype,
|
|
55
|
+
nodecallerid: node.id
|
|
56
|
+
})
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
const sendCached = (mapping, outputtype) => {
|
|
60
|
+
const currentManager = manager()
|
|
61
|
+
if (!currentManager) return false
|
|
62
|
+
const value = currentManager.getCachedAttribute(node.matterNodeId, mapping.endpointId, mapping.clusterId, mapping.target)
|
|
63
|
+
return sendKnx(mapping, matterToKnx(mapping.clusterId, mapping.target, value), outputtype)
|
|
64
|
+
}
|
|
65
|
+
const enqueue = (mapping, value) => {
|
|
66
|
+
const currentManager = manager()
|
|
67
|
+
if (!currentManager) throw new Error('Matter controller not ready')
|
|
68
|
+
const action = knxToMatter(mapping, value)
|
|
69
|
+
if (!action) return
|
|
70
|
+
const queued = currentManager.writeMatterQueueAdd({
|
|
71
|
+
nodeId: node.matterNodeId,
|
|
72
|
+
endpointId: mapping.endpointId,
|
|
73
|
+
clusterId: mapping.clusterId,
|
|
74
|
+
kind: action.kind,
|
|
75
|
+
name: action.name,
|
|
76
|
+
args: action.args
|
|
77
|
+
})
|
|
78
|
+
if (queued && typeof queued.catch === 'function') queued.catch((error) => status('red', 'ring', error.message))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
node.handleSend = (msg) => {
|
|
82
|
+
try {
|
|
83
|
+
if (!msg?.knx || !node.knxUltimateAcceptedGAs.includes(String(msg.knx.destination || '').trim())) return
|
|
84
|
+
const matches = node.mappings.filter((mapping) => mapping.ga === msg.knx.destination)
|
|
85
|
+
if (msg.knx.event === 'GroupValue_Read') {
|
|
86
|
+
matches.filter((mapping) => mapping.direction === 'status').forEach((mapping) => sendCached(mapping, 'response'))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
matches.filter((mapping) => mapping.direction === 'command').forEach((mapping) => {
|
|
90
|
+
const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(mapping.dpt))
|
|
91
|
+
enqueue(mapping, value)
|
|
92
|
+
})
|
|
93
|
+
status('green', 'dot', 'KNX→Matter')
|
|
94
|
+
} catch (error) {
|
|
95
|
+
RED.log.error(`knxUltimateMatterControllerDevice mapped KNX: ${error.message}`)
|
|
96
|
+
status('red', 'ring', error.message)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
node.handleSendMatter = (event) => {
|
|
100
|
+
try {
|
|
101
|
+
if (String(event?.nodeId) !== String(node.matterNodeId) || Number(event?.endpointId) !== Number(node.matterEndpointId)) return
|
|
102
|
+
node.mappings.filter((mapping) => mapping.direction === 'status' && Number(mapping.clusterId) === Number(event.clusterId) && mapping.target === event.attributeName).forEach((mapping) => {
|
|
103
|
+
sendKnx(mapping, matterToKnx(event.clusterId, event.attributeName, event.value))
|
|
104
|
+
})
|
|
105
|
+
if (enablePins) node.send({ topic: `${event.clusterId}.${event.attributeName}`, payload: event.value, matter: event })
|
|
106
|
+
status('blue', 'dot', `Matter→KNX: ${event.attributeName}`)
|
|
107
|
+
} catch (error) {
|
|
108
|
+
RED.log.error(`knxUltimateMatterControllerDevice mapped Matter: ${error.message}`)
|
|
109
|
+
status('red', 'ring', error.message)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
node.handleMatterClusterEvent = (event) => {
|
|
113
|
+
if (enablePins && String(event?.nodeId) === String(node.matterNodeId) && Number(event?.endpointId) === Number(node.matterEndpointId)) {
|
|
114
|
+
node.send({ topic: `${event.clusterId}.${event.eventName}`, payload: event.events, matter: event })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
node.handleMatterNodeInitialized = () => {
|
|
118
|
+
if (config.readStatusAtStartup === 'no' || Date.now() - lastInitialReadTs < 5000) return
|
|
119
|
+
let sent = 0
|
|
120
|
+
node.mappings.filter((mapping) => mapping.direction === 'status').forEach((mapping) => { if (sendCached(mapping, 'write')) sent += 1 })
|
|
121
|
+
if (sent > 0) lastInitialReadTs = Date.now()
|
|
122
|
+
}
|
|
123
|
+
node.setNodeStatusMatter = (value) => { if (value?.text) status(value.fill || 'grey', value.shape || 'ring', value.text) }
|
|
124
|
+
|
|
125
|
+
if (node.serverKNX) { node.serverKNX.removeClient(node); node.serverKNX.addClient(node) } else status('yellow', 'ring', 'No KNX gateway selected')
|
|
126
|
+
if (node.serverMatter) { node.serverMatter.removeClient(node); node.serverMatter.addClient(node) }
|
|
127
|
+
node.on('input', (msg, send, done) => {
|
|
128
|
+
try {
|
|
129
|
+
const payload = msg.payload || {}
|
|
130
|
+
const mapping = {
|
|
131
|
+
endpointId: payload.endpointId ?? node.matterEndpointId,
|
|
132
|
+
clusterId: payload.clusterId,
|
|
133
|
+
targetKind: payload.command !== undefined ? 'command' : 'attribute',
|
|
134
|
+
target: payload.command ?? payload.attribute
|
|
135
|
+
}
|
|
136
|
+
if (!mapping.target || mapping.clusterId === undefined) throw new Error('Matter input requires clusterId and command or attribute')
|
|
137
|
+
enqueue(mapping, payload.args ?? payload.value)
|
|
138
|
+
if (done) done()
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (done) done(error); else node.error(error, msg)
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
node.on('close', (done) => {
|
|
144
|
+
try { if (node.serverKNX) node.serverKNX.removeClient(node) } catch (error) { /* empty */ }
|
|
145
|
+
try { if (node.serverMatter) node.serverMatter.removeClient(node) } catch (error) { /* empty */ }
|
|
146
|
+
done()
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { isValidGroupAddress, parseMappings, setupMappedEndpointProfile }
|
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.8",
|
|
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/",
|
|
@@ -99,7 +99,11 @@
|
|
|
99
99
|
"smarthome",
|
|
100
100
|
"routing"
|
|
101
101
|
],
|
|
102
|
-
"author":
|
|
102
|
+
"author": {
|
|
103
|
+
"name": "Massimo Saccani",
|
|
104
|
+
"email": "maxsupergiovane@icloud.com",
|
|
105
|
+
"url": "https://youtube.com/@maxsupervibe"
|
|
106
|
+
},
|
|
103
107
|
"license": "MIT",
|
|
104
108
|
"scripts": {
|
|
105
109
|
"build": "npm run knx-ai:build && npm run knx-viewer:build",
|