node-red-contrib-knx-ultimate 5.2.1 → 5.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -10
- package/nodes/commonFunctions.js +9 -9
- package/nodes/knxUltimateIoTBridge.html +59 -2
- package/nodes/knxUltimateIoTBridge.js +4 -0
- package/nodes/knxUltimateMatterBridge.html +212 -299
- package/nodes/knxUltimateMatterBridge.js +166 -309
- package/nodes/knxUltimateMatterDevice.html +46 -0
- package/nodes/lib/mqtt-bridge.js +43 -1
- package/nodes/locales/de/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/de/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/de/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/de/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/de/matterbridge-config.html +34 -0
- package/nodes/locales/de/matterbridge-config.json +20 -0
- package/nodes/locales/en/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/en/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/en/knxUltimateMatterBridge.html +14 -25
- package/nodes/locales/en/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/en/matterbridge-config.html +34 -0
- package/nodes/locales/en/matterbridge-config.json +20 -0
- package/nodes/locales/es/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/es/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/es/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/es/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/es/matterbridge-config.html +34 -0
- package/nodes/locales/es/matterbridge-config.json +20 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/fr/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/fr/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/fr/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/fr/matterbridge-config.html +34 -0
- package/nodes/locales/fr/matterbridge-config.json +20 -0
- package/nodes/locales/it/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/it/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/it/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/it/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/it/matterbridge-config.html +34 -0
- package/nodes/locales/it/matterbridge-config.json +20 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +1 -0
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +6 -0
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.html +13 -24
- package/nodes/locales/zh-CN/knxUltimateMatterBridge.json +4 -1
- package/nodes/locales/zh-CN/matterbridge-config.html +34 -0
- package/nodes/locales/zh-CN/matterbridge-config.json +20 -0
- package/nodes/matterbridge-config.html +121 -0
- package/nodes/matterbridge-config.js +220 -0
- package/package.json +2 -1
|
@@ -1,24 +1,88 @@
|
|
|
1
|
-
const path = require('path')
|
|
2
1
|
const dptlib = require('knxultimate').dptlib
|
|
3
2
|
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// The devices are reconciled live instead; the server is closed only when the node
|
|
8
|
-
// is deleted/disabled or Node-RED shuts down.
|
|
9
|
-
const runningBridges = new Map()
|
|
10
|
-
|
|
11
|
-
// Exposes KNX group addresses as Matter devices (bridge/aggregator):
|
|
12
|
-
// external Matter controllers (Alexa, Google Home, Apple Home...) commission this
|
|
13
|
-
// bridge once and control/see every configured KNX virtual device.
|
|
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.
|
|
14
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
|
+
|
|
15
69
|
function knxUltimateMatterBridge (config) {
|
|
16
70
|
RED.nodes.createNode(this, config)
|
|
17
71
|
const node = this
|
|
18
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
|
|
19
84
|
|
|
20
|
-
node.topic =
|
|
21
|
-
node.name = config.name === undefined || config.name === '' ? 'Matter Bridge (BETA)' : config.name
|
|
85
|
+
node.topic = ''
|
|
22
86
|
node.dpt = ''
|
|
23
87
|
node.notifyreadrequest = false
|
|
24
88
|
node.notifyreadrequestalsorespondtobus = 'false'
|
|
@@ -36,87 +100,7 @@ module.exports = function (RED) {
|
|
|
36
100
|
node.formatnegativevalue = 'leave'
|
|
37
101
|
node.formatdecimalsvalue = 2
|
|
38
102
|
|
|
39
|
-
node.bridgePort = Number(config.bridgePort) || 5540
|
|
40
|
-
node.bridgeDeviceName = config.bridgeDeviceName || 'KNX-Ultimate Bridge'
|
|
41
|
-
node.readStatusAtStartup = config.readStatusAtStartup === undefined || config.readStatusAtStartup === 'yes'
|
|
42
|
-
node.enableNodePINS = config.enableNodePINS === 'yes'
|
|
43
|
-
node.inputs = node.enableNodePINS ? 1 : 0
|
|
44
|
-
node.outputs = node.enableNodePINS ? 1 : 0
|
|
45
|
-
node.devices = []
|
|
46
|
-
try {
|
|
47
|
-
node.devices = Array.isArray(config.devices) ? config.devices : JSON.parse(config.devices || '[]')
|
|
48
|
-
} catch (error) {
|
|
49
|
-
node.devices = []
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
node.matterBridge = null
|
|
53
|
-
node.matterInstanceId = `knxultimate-bridge-${node.id.replace(/[^a-zA-Z0-9]/g, '')}`
|
|
54
|
-
node.matterStoragePath = path.join(RED.settings.userDir || '.', 'knxultimatestorage', 'matter')
|
|
55
|
-
let startupTimer = null
|
|
56
103
|
let initialReadTimer = null
|
|
57
|
-
let closing = false
|
|
58
|
-
|
|
59
|
-
// The functions of each device type, with their GA/DPT config fields.
|
|
60
|
-
// direction 'command' = Matter -> KNX (write to the bus), 'status' = KNX -> Matter.
|
|
61
|
-
const DEVICE_FUNCTIONS = {
|
|
62
|
-
onofflight: [
|
|
63
|
-
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
64
|
-
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' }
|
|
65
|
-
],
|
|
66
|
-
onoffplug: [
|
|
67
|
-
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
68
|
-
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' }
|
|
69
|
-
],
|
|
70
|
-
dimmablelight: [
|
|
71
|
-
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
72
|
-
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' },
|
|
73
|
-
{ fn: 'level', direction: 'command', ga: 'gaLevel', dpt: 'dptLevel', fallbackDpt: '5.001' },
|
|
74
|
-
{ fn: 'level', direction: 'status', ga: 'gaLevelStatus', dpt: 'dptLevelStatus', fallbackDpt: '5.001' }
|
|
75
|
-
],
|
|
76
|
-
rgblight: [
|
|
77
|
-
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
78
|
-
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' },
|
|
79
|
-
{ fn: 'level', direction: 'command', ga: 'gaLevel', dpt: 'dptLevel', fallbackDpt: '5.001' },
|
|
80
|
-
{ fn: 'level', direction: 'status', ga: 'gaLevelStatus', dpt: 'dptLevelStatus', fallbackDpt: '5.001' },
|
|
81
|
-
{ fn: 'rgb', direction: 'command', ga: 'gaRGB', dpt: 'dptRGB', fallbackDpt: '232.600' },
|
|
82
|
-
{ fn: 'rgb', direction: 'status', ga: 'gaRGBStatus', dpt: 'dptRGBStatus', fallbackDpt: '232.600' }
|
|
83
|
-
],
|
|
84
|
-
colortemperaturelight: [
|
|
85
|
-
{ fn: 'onoff', direction: 'command', ga: 'gaOnOff', dpt: 'dptOnOff', fallbackDpt: '1.001' },
|
|
86
|
-
{ fn: 'onoff', direction: 'status', ga: 'gaOnOffStatus', dpt: 'dptOnOffStatus', fallbackDpt: '1.001' },
|
|
87
|
-
{ fn: 'level', direction: 'command', ga: 'gaLevel', dpt: 'dptLevel', fallbackDpt: '5.001' },
|
|
88
|
-
{ fn: 'level', direction: 'status', ga: 'gaLevelStatus', dpt: 'dptLevelStatus', fallbackDpt: '5.001' },
|
|
89
|
-
{ fn: 'colortemp', direction: 'command', ga: 'gaCT', dpt: 'dptCT', fallbackDpt: '7.600' },
|
|
90
|
-
{ fn: 'colortemp', direction: 'status', ga: 'gaCTStatus', dpt: 'dptCTStatus', fallbackDpt: '7.600' }
|
|
91
|
-
],
|
|
92
|
-
temperaturesensor: [{ fn: 'temperature', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.001' }],
|
|
93
|
-
humiditysensor: [{ fn: 'humidity', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.007' }],
|
|
94
|
-
lightsensor: [{ fn: 'illuminance', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.004' }],
|
|
95
|
-
occupancysensor: [{ fn: 'occupancy', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '1.011' }],
|
|
96
|
-
contactsensor: [{ fn: 'contact', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '1.002' }],
|
|
97
|
-
windowcovering: [
|
|
98
|
-
{ fn: 'updown', direction: 'command', ga: 'gaUpDown', dpt: 'dptUpDown', fallbackDpt: '1.008' },
|
|
99
|
-
{ fn: 'stop', direction: 'command', ga: 'gaStop', dpt: 'dptStop', fallbackDpt: '1.017' },
|
|
100
|
-
{ fn: 'position', direction: 'command', ga: 'gaPosition', dpt: 'dptPosition', fallbackDpt: '5.001' },
|
|
101
|
-
{ fn: 'position', direction: 'status', ga: 'gaPositionStatus', dpt: 'dptPositionStatus', fallbackDpt: '5.001' }
|
|
102
|
-
],
|
|
103
|
-
thermostat: [
|
|
104
|
-
{ fn: 'currenttemp', direction: 'status', ga: 'gaCurrentTemp', dpt: 'dptCurrentTemp', fallbackDpt: '9.001' },
|
|
105
|
-
{ fn: 'setpoint', direction: 'command', ga: 'gaSetpoint', dpt: 'dptSetpoint', fallbackDpt: '9.001' },
|
|
106
|
-
{ fn: 'setpoint', direction: 'status', ga: 'gaSetpointStatus', dpt: 'dptSetpointStatus', fallbackDpt: '9.001' }
|
|
107
|
-
],
|
|
108
|
-
smokecoalarm: [
|
|
109
|
-
{ fn: 'smoke', direction: 'status', ga: 'gaSmoke', dpt: 'dptSmoke', fallbackDpt: '1.005' },
|
|
110
|
-
{ fn: 'co', direction: 'status', ga: 'gaCO', dpt: 'dptCO', fallbackDpt: '1.005' }
|
|
111
|
-
],
|
|
112
|
-
waterleakdetector: [{ fn: 'leak', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '1.005' }],
|
|
113
|
-
airqualitysensor: [{ fn: 'co2', direction: 'status', ga: 'gaStatus', dpt: 'dptStatus', fallbackDpt: '9.008' }],
|
|
114
|
-
fan: [
|
|
115
|
-
{ fn: 'fanspeed', direction: 'command', ga: 'gaFanSpeed', dpt: 'dptFanSpeed', fallbackDpt: '5.001' },
|
|
116
|
-
{ fn: 'fanspeed', direction: 'status', ga: 'gaFanSpeedStatus', dpt: 'dptFanSpeedStatus', fallbackDpt: '5.001' }
|
|
117
|
-
],
|
|
118
|
-
robotvacuum: [] // Flow-only device: talks to the flow through the node PINs
|
|
119
|
-
}
|
|
120
104
|
|
|
121
105
|
const formatTs = (date) => {
|
|
122
106
|
const d = date instanceof Date ? date : new Date(date)
|
|
@@ -157,205 +141,94 @@ module.exports = function (RED) {
|
|
|
157
141
|
}
|
|
158
142
|
}
|
|
159
143
|
|
|
160
|
-
// Precompute the routing tables:
|
|
161
|
-
// statusRoutes: GA -> [{
|
|
162
|
-
// commandRoutes:
|
|
144
|
+
// Precompute the routing tables for this single device:
|
|
145
|
+
// statusRoutes: GA -> [{ fn, dpt }] (KNX -> Matter)
|
|
146
|
+
// commandRoutes: fn -> { ga, dpt } (Matter -> KNX)
|
|
163
147
|
const statusRoutes = new Map()
|
|
164
148
|
const commandRoutes = new Map()
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (ga === '') return
|
|
177
|
-
const dpt = devConfigValue(dev, fnDef.dpt) || fnDef.fallbackDpt
|
|
178
|
-
if (fnDef.direction === 'status') {
|
|
179
|
-
if (!statusRoutes.has(ga)) statusRoutes.set(ga, [])
|
|
180
|
-
statusRoutes.get(ga).push({ deviceId: dev.id, fn: fnDef.fn, dpt })
|
|
181
|
-
} else {
|
|
182
|
-
commandRoutes.set(`${dev.id}#${fnDef.fn}`, { ga, dpt })
|
|
183
|
-
}
|
|
184
|
-
})
|
|
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
|
+
}
|
|
185
160
|
})
|
|
186
161
|
|
|
187
|
-
|
|
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 = () => {
|
|
188
172
|
try {
|
|
189
|
-
if (node.
|
|
190
|
-
node.setNodeStatus({ fill: '
|
|
173
|
+
if (node.serverMatterBridge === undefined) {
|
|
174
|
+
node.setNodeStatus({ fill: 'red', shape: 'ring', text: 'No Matter bridge selected' })
|
|
191
175
|
return
|
|
192
176
|
}
|
|
193
|
-
const info = node.
|
|
177
|
+
const info = node.serverMatterBridge.getPairingInfo()
|
|
194
178
|
if (!info.running) {
|
|
195
|
-
node.setNodeStatus({ fill: '
|
|
179
|
+
node.setNodeStatus({ fill: 'grey', shape: 'ring', text: 'Matter bridge starting...' })
|
|
196
180
|
} else if (info.commissioned) {
|
|
197
|
-
node.setNodeStatus({ fill: 'green', shape: 'dot', text: `
|
|
181
|
+
node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Ready (bridge paired with ${info.fabrics.length} controller(s))` })
|
|
198
182
|
} else {
|
|
199
|
-
node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: `
|
|
183
|
+
node.setNodeStatus({ fill: 'yellow', shape: 'ring', text: `Bridge awaiting pairing (code ${info.manualPairingCode || ''})` })
|
|
200
184
|
}
|
|
201
185
|
} catch (error) { /* empty */ }
|
|
202
186
|
}
|
|
203
187
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
const doInitialRead = () => {
|
|
207
|
-
if (!node.readStatusAtStartup) return
|
|
208
|
-
if (!node.serverKNX) return // Flow-only bridge (no KNX gateway): nothing to read
|
|
188
|
+
// Matter -> KNX: a controller (Alexa...) sent a command to this device.
|
|
189
|
+
node.handleMatterCommand = (command) => {
|
|
209
190
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} catch (error) {
|
|
218
|
-
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Initial read error ${error.message}` })
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const bindEngineEvents = (engine) => {
|
|
223
|
-
// Matter -> KNX: a controller (Alexa...) sent a command to a bridged device
|
|
224
|
-
engine.on('command', (command) => {
|
|
225
|
-
try {
|
|
226
|
-
const dev = node.devices.find((d) => d.id === command.deviceId)
|
|
227
|
-
// Forward every controller command to the flow, when the pins are enabled
|
|
228
|
-
if (node.enableNodePINS) {
|
|
229
|
-
try {
|
|
230
|
-
node.send({
|
|
231
|
-
topic: dev !== undefined ? dev.name : command.deviceId,
|
|
232
|
-
payload: command.value,
|
|
233
|
-
device: dev !== undefined ? { id: dev.id, type: dev.type, name: dev.name } : { id: command.deviceId },
|
|
234
|
-
matter: command
|
|
235
|
-
})
|
|
236
|
-
} catch (error) { /* empty */ }
|
|
237
|
-
}
|
|
238
|
-
const route = commandRoutes.get(`${command.deviceId}#${command.fn}`)
|
|
239
|
-
if (route === undefined) {
|
|
240
|
-
if (node.enableNodePINS) return // Flow-only device: the flow already got the command
|
|
241
|
-
// Make the misconfiguration visible instead of silently dropping the command
|
|
242
|
-
node.setNodeStatus({
|
|
243
|
-
fill: 'yellow',
|
|
244
|
-
shape: 'ring',
|
|
245
|
-
text: `No command GA configured for "${dev !== undefined ? dev.name : command.deviceId}" (${command.fn}): open the node, check the GA fields and re-deploy`,
|
|
246
|
-
payload: ''
|
|
247
|
-
})
|
|
248
|
-
RED.log.warn(`knxUltimateMatterBridge: Matter command dropped, no GA route for ${command.deviceId}#${command.fn}`)
|
|
249
|
-
return
|
|
250
|
-
}
|
|
251
|
-
if (!node.serverKNX) {
|
|
252
|
-
// Flow-only bridge: with the PINs enabled the flow already received the command
|
|
253
|
-
if (node.enableNodePINS) return
|
|
254
|
-
node.setNodeStatus({
|
|
255
|
-
fill: 'yellow',
|
|
256
|
-
shape: 'ring',
|
|
257
|
-
text: `No KNX gateway configured: enable the node PINs to handle "${dev !== undefined ? dev.name : command.deviceId}" from the flow`,
|
|
258
|
-
payload: ''
|
|
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
|
|
259
198
|
})
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
safeSendToKNX({
|
|
263
|
-
grpaddr: route.ga,
|
|
264
|
-
payload: command.value,
|
|
265
|
-
dpt: route.dpt,
|
|
266
|
-
outputtype: 'write'
|
|
267
|
-
}, 'write')
|
|
268
|
-
node.setNodeStatus({
|
|
269
|
-
fill: 'green',
|
|
270
|
-
shape: 'dot',
|
|
271
|
-
text: `Matter->KNX ${dev !== undefined ? dev.name : command.deviceId} ${command.fn}`,
|
|
272
|
-
payload: command.value
|
|
273
|
-
})
|
|
274
|
-
} catch (error) {
|
|
275
|
-
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Matter->KNX error ${error.message}` })
|
|
199
|
+
} catch (error) { /* empty */ }
|
|
276
200
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
engine.on('fabricsChanged', () => updateBridgeStatus())
|
|
282
|
-
engine.on('online', () => {
|
|
283
|
-
updateBridgeStatus()
|
|
284
|
-
if (initialReadTimer !== null) clearTimeout(initialReadTimer)
|
|
285
|
-
initialReadTimer = setTimeout(() => doInitialRead(), 5000)
|
|
286
|
-
})
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const buildEngineDefs = () => node.devices
|
|
290
|
-
.filter((dev) => dev.id && dev.type && DEVICE_FUNCTIONS[dev.type] !== undefined)
|
|
291
|
-
.map((dev) => ({ id: dev.id, type: dev.type, name: dev.name, invertPosition: dev.invertPosition === true }))
|
|
292
|
-
|
|
293
|
-
const startBridge = async () => {
|
|
294
|
-
try {
|
|
295
|
-
const { classMatterBridge } = await import('./utils/matterBridgeEngine.mjs')
|
|
296
|
-
const engineDefs = buildEngineDefs()
|
|
297
|
-
const existing = runningBridges.get(node.id)
|
|
298
|
-
|
|
299
|
-
if (existing !== undefined && existing.bridgeStatus === 'running' &&
|
|
300
|
-
Number(existing.options.port) === node.bridgePort &&
|
|
301
|
-
existing.options.deviceName === node.bridgeDeviceName) {
|
|
302
|
-
// Re-deploy: reuse the running Matter server and reconcile the devices live,
|
|
303
|
-
// so the paired controllers (Alexa & Co.) pick up the changes immediately.
|
|
304
|
-
existing.removeAllListeners()
|
|
305
|
-
node.matterBridge = existing
|
|
306
|
-
bindEngineEvents(existing)
|
|
307
|
-
await existing.reconcileDevices(engineDefs)
|
|
308
|
-
updateBridgeStatus()
|
|
309
|
-
if (initialReadTimer !== null) clearTimeout(initialReadTimer)
|
|
310
|
-
initialReadTimer = setTimeout(() => doInitialRead(), 5000)
|
|
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` })
|
|
311
205
|
return
|
|
312
206
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
existing.removeAllListeners()
|
|
318
|
-
await existing.close()
|
|
319
|
-
} catch (error) { /* empty */ }
|
|
320
|
-
runningBridges.delete(node.id)
|
|
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
|
|
321
211
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
info: (m) => RED.log.info(m),
|
|
325
|
-
warn: (m) => RED.log.warn(m),
|
|
326
|
-
error: (m) => RED.log.error(m),
|
|
327
|
-
debug: (m) => RED.log.debug(m)
|
|
328
|
-
}, { port: node.bridgePort, deviceName: node.bridgeDeviceName })
|
|
329
|
-
runningBridges.set(node.id, engine)
|
|
330
|
-
node.matterBridge = engine
|
|
331
|
-
bindEngineEvents(engine)
|
|
332
|
-
await engine.start(engineDefs)
|
|
333
|
-
updateBridgeStatus()
|
|
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 })
|
|
334
214
|
} catch (error) {
|
|
335
|
-
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Matter
|
|
336
|
-
RED.log.error(`knxUltimateMatterBridge: start error: ${error.message}`)
|
|
215
|
+
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Matter->KNX error ${error.message}` })
|
|
337
216
|
}
|
|
338
217
|
}
|
|
339
218
|
|
|
340
|
-
// KNX -> Matter: telegrams from the bus update the Matter attributes
|
|
219
|
+
// KNX -> Matter: telegrams from the bus update the Matter attributes of this device.
|
|
341
220
|
node.handleSend = (msg) => {
|
|
342
221
|
try {
|
|
343
222
|
if (!msg || !msg.knx) return
|
|
344
223
|
if (msg.knx.event === 'GroupValue_Read') return
|
|
345
224
|
const routes = statusRoutes.get(msg.knx.destination)
|
|
346
|
-
if (routes === undefined || node.
|
|
225
|
+
if (routes === undefined || node.serverMatterBridge === undefined) return
|
|
347
226
|
routes.forEach((route) => {
|
|
348
227
|
try {
|
|
349
228
|
const value = dptlib.fromBuffer(msg.knx.rawValue, dptlib.resolve(route.dpt))
|
|
350
229
|
if (value === undefined || value === null) return
|
|
351
|
-
node.
|
|
352
|
-
|
|
353
|
-
node.setNodeStatus({
|
|
354
|
-
fill: 'blue',
|
|
355
|
-
shape: 'dot',
|
|
356
|
-
text: `KNX->Matter ${dev !== undefined ? dev.name : route.deviceId} ${route.fn}`,
|
|
357
|
-
payload: value
|
|
358
|
-
})
|
|
230
|
+
node.serverMatterBridge.setDeviceState(node.matterDeviceId, route.fn, value)
|
|
231
|
+
node.setNodeStatus({ fill: 'blue', shape: 'dot', text: `KNX->Matter ${route.fn}`, payload: value })
|
|
359
232
|
} catch (error) {
|
|
360
233
|
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `KNX->Matter error ${error.message}` })
|
|
361
234
|
}
|
|
@@ -365,18 +238,22 @@ module.exports = function (RED) {
|
|
|
365
238
|
}
|
|
366
239
|
}
|
|
367
240
|
|
|
368
|
-
//
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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 */ }
|
|
378
254
|
}
|
|
379
255
|
|
|
256
|
+
// Register with the KNX gateway (to receive bus telegrams) and the Matter bridge.
|
|
380
257
|
if (node.serverKNX) {
|
|
381
258
|
try {
|
|
382
259
|
node.serverKNX.removeClient(node)
|
|
@@ -385,9 +262,20 @@ module.exports = function (RED) {
|
|
|
385
262
|
RED.log.error(`knxUltimateMatterBridge: register KNX client error ${error.message}`)
|
|
386
263
|
}
|
|
387
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)
|
|
388
276
|
|
|
389
|
-
// Flow input pin: updates the Matter state of
|
|
390
|
-
// msg.payload = {
|
|
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: ... }
|
|
391
279
|
node.on('input', (msg, send, done) => {
|
|
392
280
|
if (!node.enableNodePINS) {
|
|
393
281
|
if (done) done()
|
|
@@ -395,16 +283,11 @@ module.exports = function (RED) {
|
|
|
395
283
|
}
|
|
396
284
|
try {
|
|
397
285
|
const payload = msg.payload || {}
|
|
398
|
-
const deviceKey = (payload.device !== undefined ? payload.device : msg.topic || '').toString().trim()
|
|
399
286
|
const fn = (payload.function || payload.fn || '').toString().trim()
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (dev === undefined) throw new Error(`Unknown bridge device "${deviceKey}"`)
|
|
405
|
-
if (node.matterBridge === null) throw new Error('Matter bridge not started yet')
|
|
406
|
-
node.matterBridge.setDeviceState(dev.id, fn, payload.value)
|
|
407
|
-
node.setNodeStatus({ fill: 'green', shape: 'dot', text: `Flow->Matter ${dev.name} ${fn}`, payload: payload.value })
|
|
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 })
|
|
408
291
|
if (done) done()
|
|
409
292
|
} catch (error) {
|
|
410
293
|
node.setNodeStatus({ fill: 'red', shape: 'dot', text: `Flow error ${error.message}`, payload: '' })
|
|
@@ -412,43 +295,17 @@ module.exports = function (RED) {
|
|
|
412
295
|
}
|
|
413
296
|
})
|
|
414
297
|
|
|
415
|
-
node.
|
|
416
|
-
startupTimer = setTimeout(() => {
|
|
417
|
-
(async () => {
|
|
418
|
-
if (closing) return
|
|
419
|
-
try {
|
|
420
|
-
await startBridge()
|
|
421
|
-
} catch (error) {
|
|
422
|
-
RED.log.error(`knxUltimateMatterBridge: startup error: ${error.message}`)
|
|
423
|
-
}
|
|
424
|
-
})()
|
|
425
|
-
}, 5000)
|
|
426
|
-
|
|
427
|
-
// On re-deploy (removed === false) the Matter server stays alive: the new node
|
|
428
|
-
// instance will reuse it and reconcile the devices, so paired controllers never
|
|
429
|
-
// lose the session. The server is closed only when the node is deleted/disabled.
|
|
430
|
-
node.on('close', (removed, done) => {
|
|
431
|
-
closing = true
|
|
298
|
+
node.on('close', (done) => {
|
|
432
299
|
try {
|
|
433
|
-
if (startupTimer !== null) clearTimeout(startupTimer)
|
|
434
300
|
if (initialReadTimer !== null) clearTimeout(initialReadTimer)
|
|
435
301
|
} catch (error) { /* empty */ }
|
|
436
302
|
try {
|
|
437
303
|
if (node.serverKNX) node.serverKNX.removeClient(node)
|
|
438
304
|
} catch (error) { /* empty */ }
|
|
439
305
|
try {
|
|
440
|
-
if (node.
|
|
306
|
+
if (node.serverMatterBridge) node.serverMatterBridge.unregisterDevice(node)
|
|
441
307
|
} catch (error) { /* empty */ }
|
|
442
|
-
(
|
|
443
|
-
try {
|
|
444
|
-
if (removed === true && node.matterBridge !== null) {
|
|
445
|
-
await node.matterBridge.close()
|
|
446
|
-
runningBridges.delete(node.id)
|
|
447
|
-
}
|
|
448
|
-
} catch (error) { /* empty */ }
|
|
449
|
-
node.matterBridge = null
|
|
450
|
-
done()
|
|
451
|
-
})()
|
|
308
|
+
if (done) done()
|
|
452
309
|
})
|
|
453
310
|
}
|
|
454
311
|
|
|
@@ -3,6 +3,32 @@
|
|
|
3
3
|
|
|
4
4
|
<script type="text/javascript">
|
|
5
5
|
(function () {
|
|
6
|
+
let manualListResize = false;
|
|
7
|
+
let programmaticListHeight = -1;
|
|
8
|
+
|
|
9
|
+
// Adds the native browser resize grip (bottom-right corner) to the mappings list,
|
|
10
|
+
// so the user can drag it to any height, exactly like a textarea.
|
|
11
|
+
function enableManualListResize(container) {
|
|
12
|
+
try {
|
|
13
|
+
manualListResize = false;
|
|
14
|
+
programmaticListHeight = -1;
|
|
15
|
+
const $ui = container.closest('.red-ui-editableList-container');
|
|
16
|
+
if (!$ui.length) return;
|
|
17
|
+
$ui.css('resize', 'vertical');
|
|
18
|
+
if (window.ResizeObserver) {
|
|
19
|
+
const observer = new ResizeObserver(() => {
|
|
20
|
+
try {
|
|
21
|
+
const h = $ui.outerHeight();
|
|
22
|
+
if (programmaticListHeight > 0 && Math.abs(h - programmaticListHeight) > 8) {
|
|
23
|
+
manualListResize = true;
|
|
24
|
+
}
|
|
25
|
+
} catch (error) { /* empty */ }
|
|
26
|
+
});
|
|
27
|
+
observer.observe($ui[0]);
|
|
28
|
+
}
|
|
29
|
+
} catch (error) { /* empty */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
6
32
|
RED.nodes.registerType('knxUltimateMatterDevice', {
|
|
7
33
|
category: 'KNX Ultimate Matter',
|
|
8
34
|
color: '#D7C7E9',
|
|
@@ -443,6 +469,8 @@
|
|
|
443
469
|
container.editableList('addItem', { mapping: m });
|
|
444
470
|
});
|
|
445
471
|
|
|
472
|
+
enableManualListResize(container);
|
|
473
|
+
|
|
446
474
|
// Load the structure of the already selected device
|
|
447
475
|
if (node.matterNodeId) {
|
|
448
476
|
loadStructure(node.matterNodeId);
|
|
@@ -487,6 +515,24 @@
|
|
|
487
515
|
},
|
|
488
516
|
oneditcancel() {
|
|
489
517
|
try { RED.sidebar.show('info'); } catch (error) { /* empty */ }
|
|
518
|
+
},
|
|
519
|
+
oneditresize(size) {
|
|
520
|
+
// Grow the mappings list to fill the edit panel. Once the user grabs the native
|
|
521
|
+
// resize grip of the list, the manual height wins and the auto-fit backs off.
|
|
522
|
+
try {
|
|
523
|
+
if (manualListResize) return;
|
|
524
|
+
const rows = $('#dialog-form>div:not(.node-input-mapping-container-row)');
|
|
525
|
+
let height = size.height;
|
|
526
|
+
for (let i = 0; i < rows.length; i++) {
|
|
527
|
+
height -= $(rows[i]).outerHeight(true);
|
|
528
|
+
}
|
|
529
|
+
const editorRow = $('#dialog-form>div.node-input-mapping-container-row');
|
|
530
|
+
height -= (parseInt(editorRow.css('marginTop')) + parseInt(editorRow.css('marginBottom')));
|
|
531
|
+
height += 16;
|
|
532
|
+
const target = Math.max(220, height);
|
|
533
|
+
programmaticListHeight = target;
|
|
534
|
+
$('#node-input-mapping-container').editableList('height', target);
|
|
535
|
+
} catch (error) { /* empty */ }
|
|
490
536
|
}
|
|
491
537
|
});
|
|
492
538
|
}());
|