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

Module 19 · Lesson 7

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.

Finished this lesson?