OpenTherm, Modbus, and Boiler Protocols: When to Read Data, and When to Control It
OpenTherm, Modbus, and Boiler Protocols: When to Read Data, and When to Control It
In Lesson 6 you built a virtual on/off thermostat with generic_thermostat. Now you're entering the world of boiler and heat pump protocols: OpenTherm Gateway, Modbus, EMS-ESP. Here, HA doesn't replace the controller: first you read, then you cautiously decide about writing.
This lesson is for houses with their own heat source: a gas boiler, a heat pump, a manufacturer's bus. With district heating (Lesson 3), you don't connect OpenTherm or Modbus: there, room measurement and TRV heads make sense instead.
Plan on about 115 to 125 minutes. From here on, I'll shorten Home Assistant to HA.
A reminder from Lesson 6
Generic Thermostat controls an on/off switch: that's a different route than OpenTherm or Modbus.
Don't mix them on the same device. Set the boiler_integration_level helper from Lesson 3 to Read-only until you have stable data for several days. The comfort_automation_enabled helper stays off for writing to the boiler.
A real-life problem: addresses from a forum, and the gateway "doesn't work"
You copied Modbus YAML from a forum for a "Nibe heat pump": registers 100, 101, 102. After restarting HA, the sensors show unknown or absurd values: -3276°F, 6553%. It turns out the author had a different model, a different endian mapping, and a different slave ID. Instead of diagnostics, you have noise and the temptation to "just add one more write register."
Or worse: you bought an OpenTherm Gateway, wired it to the boiler, and you see a climate entity: but changing the target temperature in HA does nothing for several minutes. Frustration. Meanwhile, the data from the gateway and the boiler doesn't behave like an instant Wi-Fi switch. The change can show up after a moment, and not every boiler supports the full feature set despite having an OT connector.
This lesson teaches a different order: first confirm that the read makes sense before you touch the write. The addresses in the examples are deliberately marked as examples: don't guess them for your own system.
What's actually the risk
Writing to a register with no documentation: changing a heat pump's parameter with no visible error in HA.
Two controllers: a wall controller plus HA plus generic_thermostat on the same loop.
Two gateway clients: HA and another diagnostic tool try to talk to the same gateway at once.
Advanced OT switches turned on: the boiler running continuously due to a misconfiguration.
An automation with no delays: a fast loop of target-temperature changes on top of a polled protocol.
The module's rule: read, then write
The boiler_integration_level helper from Lesson 3 is your safety brake. In this lesson, you mostly work at the Read-only level. Only once the sensors are stable, the values make physical sense (e.g. a flow temperature of about 86 to 113°F, not -3000), and you have a Plan B: do you consider Suggestions and alerts or Control: deliberately.
Read-only: sensors, charts, a dashboard. HA changes nothing on the boiler or heat pump.
Suggestions and alerts: a notification like "flow temperature high," "integration unavailable." A person decides.
Control: deliberately: changing the target temperature through a climate entity or writing a register. Only with documentation and a Plan B.
Also set the heating_integration_path helper to OpenTherm, Modbus, or another protocol route: matching the map from Lesson 3. It's a decision record, not an automatic switch for the boiler's operating mode.
OpenTherm Gateway in Home Assistant
The opentherm_gw integration connects to an OpenTherm Gateway device (USB, serial, or network). The main control element is one climate entity on the "OpenTherm Thermostat" device. Sensors and binary_sensor entities also appear: many are disabled by default, and you enable them manually when you need diagnostics.
Configuration through the UI: Settings → Devices & Services → Add Integration → OpenTherm Gateway. You provide the device path (e.g. /dev/ttyUSB0) or a network address (e.g. socket://192.168.0.250:25238) and the gateway's identifier. After importing, check the integration's options for the read and set-temperature precision: match it to the physical thermostat, if that matters to you.
One client per gateway
Don't connect both HA and a second diagnostic tool to the same gateway at once. HA should be the gateway's only active client: otherwise results can be unpredictable.
In practice, data from the OpenTherm Gateway in HA doesn't behave like an instant switch. The gateway, the thermostat, and the boiler exchange messages, and HA updates its entities with some delay. A target-temperature change in HA might not be visible until a moment later. That doesn't necessarily mean an error, but during tests, time the delay with a stopwatch and don't build a fast automation loop.
The gateway device can also expose advanced switch entities (a heating override, domestic hot water, etc.): disabled by default. The documentation warns: used incorrectly, they can force the boiler to run continuously and raise energy use. Leave them off at the start. Controlling the target temperature through the climate entity is enough while you're learning.
What to watch after connecting OT: with no writing
Before you change anything on the boiler, enable the diagnostic sensors you want (if the integration offers them) and note down:
Target and current temperature on the climate entity: does it match the wall thermostat?
hvac_action: does HA see heating when you can hear the boiler running?
Flow/return temperature sensors (if available): do the values make physical sense?
Flame/load binary_sensor entities: do they change in step with the boiler's operation?
If you don't have a gateway yet: skip to the Modbus section, or read the integration's documentation and note what your boiler actually supports. Don't buy hardware blind.
Modbus: a read-only example
The Modbus integration in HA is generic: it doesn't know your heat pump. You have to map every register yourself, based on the manufacturer's documentation or a verified configuration for your specific model. The snippet below is an educational template: the addresses, register type, and scale are examples.
Don't guess addresses
Registers 100, 101, 102 below don't come from your heat pump. Copy the YAML structure, but take the addresses, slave, data_type, and scale from the manufacturer's manual or a community wiki for your exact model.
# Modbus: READ-ONLY, EXAMPLE addresses
# Check the manufacturer's register map before using this on your own system!
modbus:
- name: pump_example
type: tcp
host: 192.168.1.50
port: 502
sensors:
- name: "Pump flow temp EXAMPLE"
unique_id: modbus_pump_flow_temp_example
slave: 1
address: 100
input_type: holding
data_type: int16
scale: 0.1
unit_of_measurement: "°C"
precision: 1
- name: "Pump return temp EXAMPLE"
unique_id: modbus_pump_return_temp_example
slave: 1
address: 101
input_type: holding
data_type: int16
scale: 0.1
unit_of_measurement: "°C"
precision: 1
- name: "Pump operating state EXAMPLE"
unique_id: modbus_pump_state_example
slave: 1
address: 102
input_type: holding
data_type: uint16
Always check the field names in a Modbus configuration against the current HA documentation. Examples from the internet may come from an older version of the integration or a different device type.
After adding the configuration, restart HA or reload the Modbus integration (depending on your version and how you added it). In Developer Tools → States, check whether the entities have sensible values. If you see unavailable: it's a problem with the network, port, slave ID, or a wrong address, not "a bad automation."
We deliberately skip write registers (target temperature, operating mode) in this YAML. Only once the read has been stable for several days, and you understand the consequences of a change: do you go back to the documentation's "write holding register" section, or a dedicated manufacturer integration.
eBUS, Nibe, and other manufacturer buses
Not everything is a universal OpenTherm or a generic Modbus. Vaillant uses the eBUS bus, Nibe and others use their own HA integrations or community gateways. A gateway built for one brand won't work with another company's boiler. Bosch, Buderus, Junkers, and related boilers are a separate route: the EMS bus and EMS-ESP: covered in the next section.
| Protocol / route | Typical device | Getting started in HA |
|---|---|---|
| OpenTherm Gateway | A gas boiler with OT | The opentherm_gw integration, a climate entity |
| Modbus TCP/RTU | A heat pump, heat-recovery ventilation | Read-only sensors in YAML |
| eBUS | Vaillant and related systems | An eBUS adapter plus a community integration, reading first |
| EMS / EMS+ / EMS-ESP | Bosch, Buderus, Junkers, Nefit, Worcester | EMS-ESP through MQTT Discovery, reading first |
| A Nibe / manufacturer integration | Nibe heat pumps, other brands | An official or community HA integration: reading first |
With community integrations, read the GitHub issues and threads for your exact model. A device's firmware changes which registers are available. Plan B: manual operation on the boiler's or pump's panel when HA is down.
Bosch, Buderus, Junkers, and EMS-ESP
Not every boiler communicates through OpenTherm or Modbus. Many devices in the Bosch, Buderus, Junkers, Nefit, Worcester, and related families use the manufacturer's own EMS, EMS+, or similar bus variants (Heatronic, Logamatic). In that case, a popular route in the Home Assistant world is EMS-ESP: an add-on interface based on an ESP32 that talks to the boiler's bus and publishes data to HA through MQTT Discovery. On the HA side, you typically get MQTT entities, not an official Bosch/Buderus integration directly.
EMS-ESP can expose a lot of entities: temperatures, operating states, domestic hot water parameters, heating circuits, errors, and sometimes the ability to change selected settings. Don't treat this like a regular smart home integration. This is communication with a heating device. The safest start is reading data, a diagnostic dashboard, and a note: what you see, what you don't touch, which entities are purely informational. A full EMS-ESP installation guide doesn't fit in this lesson: here you're learning where this route fits in the integration map.
A safety rule for EMS-ESP
Use EMS-ESP for reading first. Don't send write commands to the boiler's bus if you don't understand the specific parameter, don't have the device's documentation, and don't know how to revert to the manufacturer's settings. A bad change to the boiler's settings can have real consequences for heating, domestic hot water, and the device's operation.
If you have a Bosch, Buderus, Junkers, Nefit, or Worcester boiler:
Don't assume OpenTherm: check whether the device uses EMS / EMS+ / Heatronic / Logamatic.
Check EMS-ESP's compatibility with your boiler model and firmware version.
Start with MQTT Discovery and reading: your MQTT broker needs to be stable.
Don't change boiler settings without documentation and a Plan B on the manufacturer's panel.
Don't confuse this with generic_thermostat
From Lesson 6: generic_thermostat pairs a room sensor with an on/off switch. OpenTherm and Modbus talk to the controller or heat pump in its own language: modulation, flow temperature, operating modes. These aren't interchangeable routes.
| Aspect | generic_thermostat (L6) | OpenTherm / Modbus / EMS-ESP (L7) |
|---|---|---|
| Controls | An on/off switch | The controller / the device's bus |
| Documentation | Hysteresis, min_cycle_duration | Register map / protocol compatibility / MQTT |
| A typical mistake | A clicking relay | Bad Modbus addresses, two controllers |
If you already have a manufacturer's controller on the wall: don't add a second control loop through a relay "because HA." Either you integrate through the protocol, or you leave the controller alone and only read data.
Comfort automation versus the boiler protocol
The automation from Lesson 5 changed the target temperature on a TRV head based on comfort_mode. With OpenTherm, you could eventually do something similar: through climate.set_temperature on the entity from the gateway, not by directly switching the boiler.
In this lesson, don't turn on comfort_automation_enabled for boiler control yet. First, observe the delays. OpenTherm is active communication on a bus: how HA updates depends on the gateway and the integration, not just "HA polling." A fast loop of "change the target temperature every 5 minutes" is a simple road to chaos.
Once you move to the control level, the pattern stays the same as with a TRV: one climate entity as the single point for changing the target temperature, a condition on comfort_automation_enabled, and sensible values from the temperature helpers. Don't control the switch and the boiler's climate entity in parallel.
Alert: the integration is unavailable
Even at the read-only level, it's worth knowing when HA has lost contact with the boiler or heat pump. Set up separate alerts for OpenTherm and Modbus: they use different entities. Before any write, note down: the factory value, the range, the unit, the documentation source, and how to undo the change.
alias: "Heating: alert, OpenTherm unavailable"
mode: single
triggers:
- trigger: state
entity_id: climate.opentherm_thermostat
to:
- unavailable
- unknown
for: "00:10:00"
actions:
- action: persistent_notification.create
data:
title: "Heating: no OpenTherm data"
message: >
climate.opentherm_thermostat hasn't reported in 10 minutes.
Check the OTGW gateway, the network, and the power.
The boiler may be running on the controller's last settings.
alias: "Heating: alert, Modbus sensor unavailable"
mode: single
triggers:
- trigger: state
entity_id: sensor.boiler_flow_temperature
to:
- unavailable
- unknown
for: "00:10:00"
actions:
- action: persistent_notification.create
data:
title: "Heating: no Modbus reading"
message: >
The Modbus sensor isn't reporting. Check the unit ID, RTU/TCP,
the hub, and the register documentation: not climate.opentherm.
A Modbus reminder: RTU ≠ TCP; slave/unit ID; holding vs. input; a number in the documentation (e.g. 40001) can need a different offset in HA; byte order; writing one register isn't always safe. On the dashboard, show only the entities you actually have: not all three routes at once.
This alert doesn't turn off the boiler: it just informs you. During a communication failure, the device often keeps running on the controller's last program. That's an argument for a Plan B and for a cautious approach to writing.
A protocol integration note
# Boiler/heat pump protocol integration: diagnostics
Date: ____
Device (model, year): ____
Protocol: OpenTherm / Modbus / eBUS / EMS / EMS-ESP / other: ____
HA / MQTT integration: ____
MQTT Discovery / broker: yes / no / not applicable
EMS-ESP compatibility checked for the model: yes / no / not applicable
Level (helper): Read-only / Suggestions / Control: ____
Climate entity (if any): ____
Key read sensors: ____
Average update delay in HA: ____ min
Plan B (manual controller): ____
One client per OT gateway: yes / not applicable
Source of the Modbus register map: manufacturer documentation / a verified configuration for the exact model: ____
Alert on unavailable: yes / no
Dashboard: the boiler / heat pump (diagnostics)
The dashboard below is a template. Remove entities marked example, or swap them for your real entities once you have a successful read: including MQTT entities from EMS-ESP, if you have them.
type: vertical-stack
cards:
- type: entities
title: Heat source: readout
entities:
- entity: input_select.boiler_integration_level
name: Integration level
- entity: input_select.heating_integration_path
name: Integration path
- entity: climate.opentherm_thermostat
name: OT thermostat (if you have one)
- entity: sensor.pump_flow_temperature_example
name: Flow temp. (Modbus example)
- type: history-graph
title: Source temperatures: 24 h
entities:
- sensor.pump_flow_temperature_example
- sensor.pump_return_temperature_example
hours_to_show: 24
Remove any entities from the card that you don't have. The dashboard should only show real data: empty slots are misleading. Add a card with the comfort_automation_enabled helper as a reminder that it stays off at this stage.
How to test this lesson
1. Set boiler_integration_level to Read-only. Update heating_integration_path.
2. Connect the integration (OT through the UI, Modbus read-only from documentation, or EMS-ESP through MQTT): no writing.
3. In Developer Tools, check the entity states. Do the values make physical sense?
4. Time the delay: a change on the wall controller versus a visible change in HA (or the reverse with OT).
5. Fill in the protocol integration note. Record your Plan B.
6. If you don't have a boiler or heat pump to integrate: review the documentation for one integration from the table and write down what the first step would be for your house.
Common mistakes
Addresses from someone else's YAML: absurd values, or unknown.
Writing before reading: changing a parameter with no verification of the scale and unit.
Two gateway clients: HA and another diagnostic tool on one gateway.
generic_thermostat on the boiler: a plain on/off instead of the protocol: Lesson 6 versus 7.
What not to do
Don't copy Modbus registers from a forum without verifying them for your model and firmware.
Don't turn on OpenTherm Gateway's advanced switches "just to check," without understanding the consequences.
Don't turn on comfort_automation_enabled for boiler control until you know your integration's delays.
Don't apply this lesson with district heating: there, you're not integrating a boiler in the building's shared basement.
Practical assignment
☐ Set the boiler_integration_level and heating_integration_path helpers to match your system map.
☐ Configure a readout (OpenTherm Gateway, Modbus read-only from documentation, or EMS-ESP through MQTT): no writing.
☐ Fill in the protocol integration note and time the typical data delay.
☐ Create the heat source diagnostic dashboard (only existing entities).
☐ Add an alert for a key entity going unavailable (or note why you're skipping it for now).
☐ Record your Plan B and your decision: are you staying on read-only, or will you move to control someday?
Key takeaways
Read first: sensors and a dashboard, then any eventual writing.
OpenTherm in HA has delays: that's normal; one client per gateway.
Modbus needs a register map: example addresses aren't universal.
Bosch/Buderus often means EMS-ESP: MQTT Discovery, reading first, not guessing commands.
This isn't generic_thermostat: a different route, different risk, different documentation.
What's next
In Lesson 8 we'll move beyond room temperature alone: humidity, the bathroom, a fan, and your first cautious rules for high humidity: with no fight against the boiler, but with an eye on both comfort and the installation's safety.
If you understand the difference between reading and writing, and between on/off and a boiler protocol: you have the foundation for the rest of the module. Watch the data for a few days before you change anything.