node-red-contrib-knx-ultimate 5.0.0 → 5.0.1
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 +8 -0
- package/nodes/knxUltimate-config.html +1 -3
- package/nodes/knxUltimateIoTBridge.html +305 -15
- package/nodes/knxUltimateIoTBridge.js +142 -10
- package/nodes/lib/knx-home-assistant.js +171 -0
- package/nodes/lib/mqtt-bridge.js +584 -0
- package/nodes/locales/de/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/de/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/en/knxUltimate-config.json +8 -8
- package/nodes/locales/en/knxUltimateIoTBridge.html +23 -2
- package/nodes/locales/en/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/es/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/es/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/fr/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/fr/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/it/knxUltimate-config.json +14 -14
- package/nodes/locales/it/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/it/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +44 -2
- package/package.json +3 -2
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Shared Home Assistant mapping helpers for the native KNX MQTT bridge.
|
|
4
|
+
//
|
|
5
|
+
// A KNX gateway exposes many group addresses (GA), each carrying a Datapoint Type (DPT).
|
|
6
|
+
// Home Assistant, on the other hand, models things as typed entities (switch, sensor,
|
|
7
|
+
// binary_sensor, number, text...). This module is the translation layer between the two:
|
|
8
|
+
//
|
|
9
|
+
// - mapDptToHa(dpt) -> { domain, deviceClass?, unit?, min?, max?, step? }
|
|
10
|
+
// - formatValueForMqtt(value) -> string to publish on a HA state topic
|
|
11
|
+
// - parseCommandFromMqtt(text, dpt) -> JS value to write on the KNX bus (or null)
|
|
12
|
+
//
|
|
13
|
+
// The mapping is intentionally pragmatic: it covers the common DPTs and falls back to a
|
|
14
|
+
// read-only "sensor" for everything else, so an unknown DPT never breaks discovery.
|
|
15
|
+
|
|
16
|
+
// Extract the main/sub parts of a DPT string ("DPT9.001", "9.001", "9" ...).
|
|
17
|
+
function splitDpt (dptRaw) {
|
|
18
|
+
const clean = String(dptRaw == null ? '' : dptRaw).replace(/^dpt/i, '').trim()
|
|
19
|
+
const parts = clean.split('.')
|
|
20
|
+
const main = parseInt(parts[0], 10)
|
|
21
|
+
const sub = parts.length > 1 ? parts[1].trim() : ''
|
|
22
|
+
return { main: Number.isFinite(main) ? main : null, sub }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Unit + HA device_class for the most common 2-byte float DPTs (DPT 9.xxx).
|
|
26
|
+
function unitForFloat (sub) {
|
|
27
|
+
switch (sub) {
|
|
28
|
+
case '001': return { unit: '°C', deviceClass: 'temperature' }
|
|
29
|
+
case '002': return { unit: 'K' }
|
|
30
|
+
case '003': return { unit: 'K/h' }
|
|
31
|
+
case '004': return { unit: 'lx', deviceClass: 'illuminance' }
|
|
32
|
+
case '005': return { unit: 'm/s', deviceClass: 'wind_speed' }
|
|
33
|
+
case '006': return { unit: 'Pa', deviceClass: 'pressure' }
|
|
34
|
+
case '007': return { unit: '%', deviceClass: 'humidity' }
|
|
35
|
+
case '008': return { unit: 'ppm' }
|
|
36
|
+
case '009': return { unit: 'mg/m³' }
|
|
37
|
+
case '010': return { unit: 'm/s' }
|
|
38
|
+
case '011': return { unit: 's' }
|
|
39
|
+
case '020': return { unit: 'mV', deviceClass: 'voltage' }
|
|
40
|
+
case '021': return { unit: 'mA', deviceClass: 'current' }
|
|
41
|
+
case '022': return { unit: 'W/m²', deviceClass: 'irradiance' }
|
|
42
|
+
case '024': return { unit: 'K' }
|
|
43
|
+
case '025': return { unit: '1/h' }
|
|
44
|
+
case '026': return { unit: 'l/h' }
|
|
45
|
+
case '027': return { unit: '°F', deviceClass: 'temperature' }
|
|
46
|
+
case '028': return { unit: 'km/h', deviceClass: 'wind_speed' }
|
|
47
|
+
default: return {}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Map a KNX DPT to a Home Assistant entity descriptor.
|
|
52
|
+
// `domain` is the HA MQTT platform; the extra fields tune the discovery config.
|
|
53
|
+
function mapDptToHa (dptRaw) {
|
|
54
|
+
const { main, sub } = splitDpt(dptRaw)
|
|
55
|
+
|
|
56
|
+
if (main === 1) {
|
|
57
|
+
// 1-bit boolean. A handful of subtypes are clearly read-only states; the rest are
|
|
58
|
+
// treated as controllable switches.
|
|
59
|
+
const readOnly = ['005', '011', '019']
|
|
60
|
+
if (readOnly.includes(sub)) {
|
|
61
|
+
const deviceClass = sub === '005' ? 'problem' : (sub === '019' ? 'opening' : undefined)
|
|
62
|
+
return { domain: 'binary_sensor', deviceClass, writable: false }
|
|
63
|
+
}
|
|
64
|
+
return { domain: 'switch', writable: true }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (main === 5) {
|
|
68
|
+
if (sub === '001') return { domain: 'number', min: 0, max: 100, step: 1, unit: '%', writable: true } // scaling
|
|
69
|
+
if (sub === '003') return { domain: 'number', min: 0, max: 360, step: 1, unit: '°', writable: true } // angle
|
|
70
|
+
return { domain: 'number', min: 0, max: 255, step: 1, writable: true }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (main === 6) return { domain: 'sensor', writable: false } // 1-byte signed
|
|
74
|
+
if (main === 7) return { domain: 'sensor', writable: false } // 2-byte unsigned
|
|
75
|
+
if (main === 8) return { domain: 'sensor', writable: false } // 2-byte signed
|
|
76
|
+
|
|
77
|
+
if (main === 9) {
|
|
78
|
+
const u = unitForFloat(sub)
|
|
79
|
+
return { domain: 'sensor', unit: u.unit, deviceClass: u.deviceClass, writable: false }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (main === 12) return { domain: 'sensor', writable: false } // 4-byte unsigned
|
|
83
|
+
if (main === 13) {
|
|
84
|
+
if (sub === '010') return { domain: 'sensor', unit: 'Wh', deviceClass: 'energy', writable: false }
|
|
85
|
+
if (sub === '013') return { domain: 'sensor', unit: 'kWh', deviceClass: 'energy', writable: false }
|
|
86
|
+
return { domain: 'sensor', writable: false } // 4-byte signed
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (main === 14) {
|
|
90
|
+
// 4-byte float (engineering values). A few well-known subtypes carry a clear unit.
|
|
91
|
+
if (sub === '056') return { domain: 'sensor', unit: 'W', deviceClass: 'power', writable: false }
|
|
92
|
+
if (sub === '027') return { domain: 'sensor', unit: 'V', deviceClass: 'voltage', writable: false }
|
|
93
|
+
if (sub === '019') return { domain: 'sensor', unit: 'A', deviceClass: 'current', writable: false }
|
|
94
|
+
if (sub === '068') return { domain: 'sensor', unit: '°C', deviceClass: 'temperature', writable: false }
|
|
95
|
+
return { domain: 'sensor', writable: false }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (main === 16) return { domain: 'text', writable: true } // character string
|
|
99
|
+
|
|
100
|
+
// Scenes (17/18), HVAC modes (20), colours (232) and anything else: expose as a
|
|
101
|
+
// read-only sensor so the value is visible without risking a wrong write encoding.
|
|
102
|
+
return { domain: 'sensor', writable: false }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Render a KNX-decoded JS value to the string published on a HA state topic.
|
|
106
|
+
function formatValueForMqtt (value) {
|
|
107
|
+
if (value === null || value === undefined) return ''
|
|
108
|
+
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
|
109
|
+
if (typeof value === 'object') {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.stringify(value)
|
|
112
|
+
} catch (_err) {
|
|
113
|
+
return ''
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return String(value)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Parse a HA command payload (string) into the JS value to write on the KNX bus.
|
|
120
|
+
// Returns null when the payload can't be coerced to the DPT (the write is then skipped).
|
|
121
|
+
function parseCommandFromMqtt (text, dptRaw) {
|
|
122
|
+
const { main } = splitDpt(dptRaw)
|
|
123
|
+
const s = (text == null ? '' : String(text)).trim()
|
|
124
|
+
if (s === '') return null
|
|
125
|
+
|
|
126
|
+
if (main === 1) {
|
|
127
|
+
const low = s.toLowerCase()
|
|
128
|
+
if (['true', 'on', '1', 'open', 'up', 'yes'].includes(low)) return true
|
|
129
|
+
if (['false', 'off', '0', 'closed', 'close', 'down', 'no'].includes(low)) return false
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if ([5, 6, 7, 8, 9, 12, 13, 14].includes(main)) {
|
|
134
|
+
const n = Number(s)
|
|
135
|
+
return Number.isFinite(n) ? n : null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (main === 16) return s
|
|
139
|
+
|
|
140
|
+
// Unknown/structured DPT: accept JSON objects, otherwise pass the raw string through.
|
|
141
|
+
if (s.startsWith('{') || s.startsWith('[')) {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(s)
|
|
144
|
+
} catch (_err) {
|
|
145
|
+
return s
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return s
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Default KNX DPTs for the roles of composite (cover/climate) entities. Used to encode
|
|
152
|
+
// writes when the role GA is not present in the ETS CSV (so its DPT can't be resolved).
|
|
153
|
+
const COVER_DEFAULT_DPT = {
|
|
154
|
+
upDown: '1.008', // 0 = Up/Open, 1 = Down/Close
|
|
155
|
+
stop: '1.007', // step/stop
|
|
156
|
+
position: '5.001' // 0..100 %
|
|
157
|
+
}
|
|
158
|
+
const CLIMATE_DEFAULT_DPT = {
|
|
159
|
+
currentTemp: '9.001',
|
|
160
|
+
setpoint: '9.001',
|
|
161
|
+
onOff: '1.001'
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
module.exports = {
|
|
165
|
+
splitDpt,
|
|
166
|
+
mapDptToHa,
|
|
167
|
+
formatValueForMqtt,
|
|
168
|
+
parseCommandFromMqtt,
|
|
169
|
+
COVER_DEFAULT_DPT,
|
|
170
|
+
CLIMATE_DEFAULT_DPT
|
|
171
|
+
}
|