node-red-contrib-knx-ultimate 4.3.24 → 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 +13 -0
- package/nodes/commonFunctions.js +115 -0
- package/nodes/knxUltimate-config.html +61 -7
- package/nodes/knxUltimate-config.js +11 -0
- 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/knxUltimate-config.json +5 -1
- package/nodes/locales/de/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/de/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/en/knxUltimate-config.json +13 -9
- package/nodes/locales/en/knxUltimateIoTBridge.html +23 -2
- package/nodes/locales/en/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/es/knxUltimate-config.json +5 -1
- package/nodes/locales/es/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/es/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/fr/knxUltimate-config.json +5 -1
- package/nodes/locales/fr/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/fr/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/it/knxUltimate-config.json +19 -15
- package/nodes/locales/it/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/it/knxUltimateIoTBridge.json +44 -2
- package/nodes/locales/zh-CN/knxUltimate-config.json +5 -1
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.html +22 -1
- package/nodes/locales/zh-CN/knxUltimateIoTBridge.json +44 -2
- package/package.json +4 -3
|
@@ -22,7 +22,7 @@ module.exports = function (RED) {
|
|
|
22
22
|
return
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
node.name = config.name || 'KNX IoT
|
|
25
|
+
node.name = config.name || 'KNX MQTT - IoT'
|
|
26
26
|
node.outputtopic = config.outputtopic || ''
|
|
27
27
|
|
|
28
28
|
node.listenallga = true
|
|
@@ -40,6 +40,20 @@ module.exports = function (RED) {
|
|
|
40
40
|
|
|
41
41
|
node.mappings = Array.isArray(config.mappings) ? config.mappings : []
|
|
42
42
|
|
|
43
|
+
// Operation mode: 'iot' (classic IoT mappings, default) or 'homeassistant' (native
|
|
44
|
+
// MQTT bridge with Home Assistant discovery for every group address + cover/climate).
|
|
45
|
+
node.nodeMode = config.nodeMode === 'homeassistant' ? 'homeassistant' : 'iot'
|
|
46
|
+
node.mqttUrl = typeof config.mqttUrl === 'string' ? config.mqttUrl.trim() : ''
|
|
47
|
+
node.mqttBaseTopic = typeof config.mqttBaseTopic === 'string' && config.mqttBaseTopic.trim() !== '' ? config.mqttBaseTopic.trim() : 'knx-ultimate'
|
|
48
|
+
node.mqttDiscovery = config.mqttDiscovery !== false && config.mqttDiscovery !== 'false'
|
|
49
|
+
node.mqttDiscoveryPrefix = typeof config.mqttDiscoveryPrefix === 'string' && config.mqttDiscoveryPrefix.trim() !== '' ? config.mqttDiscoveryPrefix.trim() : 'homeassistant'
|
|
50
|
+
node.mqttCustomEntities = Array.isArray(config.mqttCustomEntities) ? config.mqttCustomEntities : []
|
|
51
|
+
// Group addresses to expose as simple entities. Once the user curates the list
|
|
52
|
+
// (mqttExposeConfigured), only the listed GAs are exposed; otherwise all imported GAs are.
|
|
53
|
+
node.mqttExposeConfigured = config.mqttExposeConfigured === true
|
|
54
|
+
node.mqttExposedGAs = Array.isArray(config.mqttExposedGAs) ? config.mqttExposedGAs : []
|
|
55
|
+
node.mqttBridge = null
|
|
56
|
+
|
|
43
57
|
const safeNumber = (value, fallback = 0) => {
|
|
44
58
|
if (value === null || value === undefined || value === '') return fallback
|
|
45
59
|
const parsed = Number(value)
|
|
@@ -143,6 +157,85 @@ module.exports = function (RED) {
|
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
|
|
160
|
+
// HOME ASSISTANT (MQTT) BRIDGE -----------------------------------------------------------
|
|
161
|
+
node.startMqttBridge = () => {
|
|
162
|
+
if (node.mqttBridge !== null) return // already running
|
|
163
|
+
if (!node.mqttUrl) {
|
|
164
|
+
pushStatus({ fill: 'red', shape: 'dot', text: 'MQTT broker URL missing' })
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
// Lazy-require so the node still loads if the optional mqtt dependency is missing.
|
|
169
|
+
const { createMqttBridge } = require('./lib/mqtt-bridge.js')
|
|
170
|
+
node.mqttBridge = createMqttBridge({
|
|
171
|
+
node,
|
|
172
|
+
url: node.mqttUrl,
|
|
173
|
+
baseTopic: node.mqttBaseTopic,
|
|
174
|
+
discovery: node.mqttDiscovery,
|
|
175
|
+
discoveryPrefix: node.mqttDiscoveryPrefix,
|
|
176
|
+
username: node.credentials ? node.credentials.mqttUsername : undefined,
|
|
177
|
+
password: node.credentials ? node.credentials.mqttPassword : undefined,
|
|
178
|
+
groupAddresses: (node.serverKNX && Array.isArray(node.serverKNX.csv)) ? node.serverKNX.csv : [],
|
|
179
|
+
customEntities: node.mqttCustomEntities,
|
|
180
|
+
// null => expose all imported GAs (until the user curates the list).
|
|
181
|
+
exposedGAs: node.mqttExposeConfigured ? node.mqttExposedGAs : null,
|
|
182
|
+
onCommand: ({ ga, dpt, value }) => {
|
|
183
|
+
// A Home Assistant command arrived: write it to the KNX bus.
|
|
184
|
+
try {
|
|
185
|
+
node.serverKNX.sendKNXTelegramToKNXEngine({
|
|
186
|
+
grpaddr: ga,
|
|
187
|
+
payload: value,
|
|
188
|
+
dpt,
|
|
189
|
+
outputtype: 'write',
|
|
190
|
+
nodecallerid: node.id
|
|
191
|
+
})
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (node.sysLogger) node.sysLogger.error('HA bridge write failed (' + ga + '): ' + error.message)
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
onStatus: (status) => {
|
|
197
|
+
if (!status) return
|
|
198
|
+
if (status.state === 'connected') {
|
|
199
|
+
pushStatus({ fill: 'green', shape: 'dot', text: 'HA connected (' + (status.detail || '0') + ' entities)' })
|
|
200
|
+
} else if (status.state === 'error') {
|
|
201
|
+
pushStatus({ fill: 'red', shape: 'dot', text: 'MQTT ' + (status.detail || 'error') })
|
|
202
|
+
} else if (status.state === 'reconnect' || status.state === 'offline') {
|
|
203
|
+
pushStatus({ fill: 'yellow', shape: 'ring', text: 'MQTT ' + status.state })
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
node.mqttBridge.connect()
|
|
208
|
+
pushStatus({ fill: 'grey', shape: 'ring', text: 'HA mode: connecting (' + node.mqttBridge.entityCount + ' entities)' })
|
|
209
|
+
} catch (error) {
|
|
210
|
+
node.mqttBridge = null
|
|
211
|
+
if (node.sysLogger) node.sysLogger.error('startMqttBridge failed: ' + error.message)
|
|
212
|
+
pushStatus({ fill: 'red', shape: 'dot', text: 'MQTT bridge: ' + error.message })
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
node.stopMqttBridge = (done) => {
|
|
217
|
+
const bridge = node.mqttBridge
|
|
218
|
+
node.mqttBridge = null
|
|
219
|
+
let called = false
|
|
220
|
+
const cb = () => {
|
|
221
|
+
if (called) return
|
|
222
|
+
called = true
|
|
223
|
+
if (typeof done === 'function') {
|
|
224
|
+
try { done() } catch (error) { /* ignore */ }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (!bridge) {
|
|
228
|
+
cb()
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
bridge.close(cb)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
if (node.sysLogger) node.sysLogger.error('stopMqttBridge error: ' + (error && error.message))
|
|
235
|
+
cb()
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
146
239
|
const isBooleanDpt = (dpt) => typeof dpt === 'string' && dpt.startsWith('1.')
|
|
147
240
|
|
|
148
241
|
const toBoolean = (value) => {
|
|
@@ -339,6 +432,15 @@ module.exports = function (RED) {
|
|
|
339
432
|
const destination = msg.knx && msg.knx.destination ? msg.knx.destination : sanitizeString(msg.topic)
|
|
340
433
|
if (!destination) return
|
|
341
434
|
|
|
435
|
+
// Home Assistant mode: mirror the decoded value to MQTT and stop (no IoT mappings).
|
|
436
|
+
if (node.nodeMode === 'homeassistant') {
|
|
437
|
+
const event = msg.knx ? msg.knx.event : undefined
|
|
438
|
+
if (node.mqttBridge && event !== 'GroupValue_Read') {
|
|
439
|
+
node.mqttBridge.publishState(destination, msg.payload)
|
|
440
|
+
}
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
342
444
|
const meta = {
|
|
343
445
|
event: msg.knx ? msg.knx.event : undefined,
|
|
344
446
|
source: msg.knx ? msg.knx.source : undefined,
|
|
@@ -382,6 +484,11 @@ module.exports = function (RED) {
|
|
|
382
484
|
node.handleSend = handleKnxTelegram
|
|
383
485
|
|
|
384
486
|
node.on('input', (msg, send, done) => {
|
|
487
|
+
// In Home Assistant mode, commands flow in over MQTT, not via the flow input.
|
|
488
|
+
if (node.nodeMode === 'homeassistant') {
|
|
489
|
+
if (done) done()
|
|
490
|
+
return
|
|
491
|
+
}
|
|
385
492
|
if (!node.acceptFlowInput) {
|
|
386
493
|
if (done) done()
|
|
387
494
|
return
|
|
@@ -446,14 +553,30 @@ module.exports = function (RED) {
|
|
|
446
553
|
})
|
|
447
554
|
|
|
448
555
|
node.on('close', (done) => {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
556
|
+
// Always call done() exactly once, even if something throws, so a deploy / Node-RED exit
|
|
557
|
+
// is never blocked by the bridge teardown.
|
|
558
|
+
let finished = false
|
|
559
|
+
const finish = () => {
|
|
560
|
+
if (finished) return
|
|
561
|
+
finished = true
|
|
562
|
+
if (typeof done === 'function') {
|
|
563
|
+
try { done() } catch (error) { /* ignore */ }
|
|
454
564
|
}
|
|
455
565
|
}
|
|
456
|
-
|
|
566
|
+
try {
|
|
567
|
+
if (node.serverKNX && typeof node.serverKNX.removeClient === 'function') {
|
|
568
|
+
try {
|
|
569
|
+
node.serverKNX.removeClient(node)
|
|
570
|
+
} catch (error) {
|
|
571
|
+
/* empty */
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
// Stop the MQTT bridge (best-effort, hard-capped so redeploy never blocks on the broker).
|
|
575
|
+
node.stopMqttBridge(finish)
|
|
576
|
+
} catch (error) {
|
|
577
|
+
if (node.sysLogger) node.sysLogger.error('close handler error: ' + (error && error.message))
|
|
578
|
+
finish()
|
|
579
|
+
}
|
|
457
580
|
})
|
|
458
581
|
|
|
459
582
|
const registerClient = () => {
|
|
@@ -492,9 +615,18 @@ module.exports = function (RED) {
|
|
|
492
615
|
}
|
|
493
616
|
|
|
494
617
|
registerClient()
|
|
495
|
-
|
|
496
|
-
|
|
618
|
+
if (node.nodeMode === 'homeassistant') {
|
|
619
|
+
node.startMqttBridge()
|
|
620
|
+
} else {
|
|
621
|
+
updateIdleStatus()
|
|
622
|
+
issueInitialReads()
|
|
623
|
+
}
|
|
497
624
|
}
|
|
498
625
|
|
|
499
|
-
RED.nodes.registerType('knxUltimateIoTBridge', knxUltimateIoTBridge
|
|
626
|
+
RED.nodes.registerType('knxUltimateIoTBridge', knxUltimateIoTBridge, {
|
|
627
|
+
credentials: {
|
|
628
|
+
mqttUsername: { type: 'text' },
|
|
629
|
+
mqttPassword: { type: 'password' }
|
|
630
|
+
}
|
|
631
|
+
})
|
|
500
632
|
}
|
|
@@ -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
|
+
}
|