Truma D6E (iNet X) integrated into Venus OS large — Node-RED bridge, VRM dashboard, and a custom GX Touch page
Disclaimer:
I am not a developer by trade — I’ve built this step by step with a lot of trial and error, so treat it as one person’s working setup rather than a polished product.
## Architecture
GX Touch 70 (custom QML page)
| D-Bus: com.victronenergy.settings /Settings/Truma/\*
Node-RED “bridge” flow (Venus MQTT mirror on 127.0.0.1:1883)
|
Node-RED command queue → node-red-contrib-truma-inetx (get/set)
|
BlueZ → Truma iNet X panel → D6E
Browser / VRM → Node-RED Dashboard 2 → same command queue
```
Everything the heater sees goes through one place — a single-flight command queue — so the touchscreen, the dashboard, the weekly timers and the failsafe logic can never write to the BLE link at the same time.
## What runs today
**Node-RED, on the Cerbo.** Reads the panel every 2 minutes (topics `RoomClimate`, `AirHeating`, `EnergySrc`, `WaterHeating`, `AirCirculation`), normalises it, and publishes state. Writes are queued one at a time, with optimistic UI (a button shows the value you picked and rolls back after 45 s if the heater never confirms), exponential backoff plus a circuit breaker on read failures, and a “release Bluetooth for 5 minutes” button so I can pair my phone with the Truma app without the Cerbo fighting me for the link.
Some things are two writes on the wire and one button in the UI, which the queue handles as an ordered transaction:
- Hot water: `WaterHeating.Mode` (0 = 40 °C, 1 = 60 °C, 2 = 70 °C), then 2 s later `WaterHeating.Active = 1`.
- Energy source: the panel has `EnergySrc.DieselLevel` and `ElectricLevel`; the five combinations I actually use (diesel / electric 900 / electric 1800 / hybrid 900 / hybrid 1800) are written in an order that never draws more power on the way than at the start or the end — electric down first, diesel next, electric up last.
**Dashboard 2 on VRM**, at the standard `/dashboard` path, so it opens straight from the “Node-RED Dashboard” button in VRM. Pages: Home (room climate, hot water, energy source, ventilator), Schedule (weekly timers), History (temperature graph, daily min/max/average, run hours) and Diagnostics (link health, manual read, reset, queue flush, release Bluetooth, log, raw device data).
**D-Bus.** There is no stock Truma service in Venus OS, so the settings are custom, created at flow start with `AddSetting` on `com.victronenergy.settings`:
```
/Settings/Truma/RoomMode i 0 off / 3 heat / 5 vent
/Settings/Truma/TargetTemperature f whole °C (the protocol uses tenths)
/Settings/Truma/AirMode i 0 fast / 1 comfort
/Settings/Truma/WaterMode i 0 / 1 / 2
/Settings/Truma/WaterActive i 0 / 1
/Settings/Truma/Boost i 0 / 1
/Settings/Truma/EnergyMode i 1..5
/Settings/Truma/FanLevel i 0..10
/Settings/Truma/RoomTemperature f measured
/Settings/Truma/BoilerTemperature f measured
/Settings/Truma/Online i 0 / 1
```
The flow talks to these through the Venus MQTT mirror (`W//settings/0/…` to write, `N/…` to hear about changes), which means the touchscreen and the dashboard stay in step without either of them knowing about the other.
## The part that needed the most care
Because the bridge both writes those settings and listens to them, every write comes straight back as a change notification. Without a guard, each mirror write becomes a new heater command and you get an endless BLE loop. The bridge therefore keeps its own copy of what the heater last reported plus what is currently in flight, and only treats a value as a user action when it differs from both. It also collapses the bursts the touchscreen produces — tapping “Eco 40” writes `WaterMode` and `WaterActive` back to back, but only one command reaches the heater — and has a rate limit as a last resort.
Worth knowing if you build something similar: **Node-RED flow context is per tab.** I lost time to that. State cached on one tab is simply not visible on another, and in this case it would have silently defeated the loop guard.
## BLE reliability
The realistic failure is that someone opens the Truma app and the panel accepts that phone instead. The reads then fail, backoff doubles up to 15 minutes, and by the time the phone walks away the Cerbo sits there doing nothing while the panel is perfectly reachable.
The library’s own message is `Timed out after 30000ms waiting for BlueZ to discover Truma iNetX`, so 30 s per attempt. Rather than fork the node, I added a flow-level watchdog: while reads are failing, at most once every 5 minutes, run `bluetoothctl connect `; if that succeeds, clear the backoff and read immediately. That turns a 15-minute outage into under a minute. It works because the node uses the BlueZ D-Bus transport, so it reuses the connection rather than fighting it — with a raw-HCI transport this trick would be wrong.
Read times, for reference: mostly 1.5–16 s, occasionally over a minute when the panel is slow to be discovered.
## GX Touch page — where I actually am
This is the bit I care most about and the bit that is least finished. The goal is a real top-level page in the swipe view, next to Brief / Overview / Notifications / Settings — not an entry buried under Settings.
What I’ve learned so far:
- The page list is hardcoded in gui-v2’s `components/SwipePageModel.qml`. There’s no setting that adds a page.
- On the device, gui-v2’s QML is on the rootfs and is read at runtime, so it can be edited and picked up with `svc -t /service/gui-v2` (no `systemd` on Venus OS Large). The Remote Console/WASM build is a separate compiled binary and does **not** get this — success on the Touch is not success in the browser, and vice versa.
- A custom page can’t be used as a bare type name there, because it isn’t part of the compiled `Victron.VenusOS` module. It has to be loaded by absolute path with `Loader.setSource(url, { … })`, passing the properties `SwipeViewPage` declares as required.
- I keep my own QML in `/data/` so it survives firmware updates; only the one-line edit to `SwipePageModel.qml` has to be reapplied after an update.
The page itself binds to the settings paths above with `VeQuickItem`, with sliders committing on release rather than on every tick, since the GX Touch CPU is modest. It is written and reviewed but **not yet confirmed running on my Cerbo** — I’m validating it on a Raspberry Pi running Venus OS Large first. I’ll report back once it’s on the real screen.
The two measured temperatures need service names that Node-RED’s `victron-virtual` node can’t produce (it always names its service `…temperature.virtual_`), so those come from a small Python service on `velib_python` that mirrors two of the settings paths into `com.victronenergy.temperature.trumaroom` and `.trumaboiler`. Also written, also not yet proven on the device.
## Still open
- Getting the new QML page confirmed on the GX Touch, and the layout tidy on both phone and desktop for the vrm nodered dashboard.
- A timer page on the GX Touch (the weekly rules live only in Node-RED today).
- **Fault codes.** The five topics I read don’t carry them. The iNet X app clearly shows codes, so the data must be somewhere in the tree — if anyone knows which topic/parameter carries the D6E error state over the iNet X BLE protocol, I’d be glad to hear it. I’ll dump the full tree next time the heater reports something.
- The interlock behaviour (room climate / boost / ventilator being mutually exclusive) is enforced by construction in my UI, but I have not yet validated it against the physical heater. That’s a deliberate hard stop for me: I’ll do it standing next to the van, stepping through every mode, before I trust it to run the heater unattended. **Nobody should treat a DIY integration as a replacement for the heater’s own controls until they have done that themselves.**
## Credits and caveats
The BLE work is not mine: it rests on `node-red-contrib-truma-inetx`, which does the actual protocol. I’ve made no upstream changes. Everything above is a hobby integration on my own van — it is not endorsed by Victron or Truma, it writes to a diesel heater, and if you copy it you own the consequences. Take a backup before you touch a stock gui-v2 file, and keep a rollback path.
Happy to share the flow export and the QML if there’s interest, and very happy to be told where I’ve done something the hard way.