Pale blue cloud  



Monitoring OpenTherm communication with Arduino


When I learned that the new heating appliance I had installed in my home communicates with the room thermostat using an actual protocol (as opposed to 'switch on'/'switch off' commands) I became very interested in finding out if it would be possible to listen in on those communications and do interesting stuff with it.

The protocol in question is called OpenTherm (TM). From the OpenTherm website: "OpenTherm is the name of a non-manufacturer-dependent system of communication between modulating HVAC heating appliances and room thermostats. The system consists of a  communication protocol and an interface specification. OpenTherm is futuristic system, which combines simple installation techniques with high functionality and future expansion possibilities.".

It seemed that nobody in the Arduino community had tried to read it yet, so that made it an interesting challenge.

Contrary to what the name suggests, the OpenTherm ('OT') protocol is not open in the sense that its specifications are available for implementation by anybody. Manufacturers of heating, ventilating, and air conditioning (HVAC) equipment may request a copy of the protocol from the OpenTherm Association, 'for evaluation purposes'. The rest of the world will have to make do with information that others gathered and posted on the internet. I found enough information to get me going.

Project setup

This project contains the following parts:

  • an electronic circuit that sits transparently between the heater and the room thermostat, converting the electrical signals into levels suitable for processing by the Arduino
  • the Arduino, which processes the electrical signals into data that can be displayed

Note that this allows me only to listen to the communication. I'm not manipulating any commands, or injecting commands of my own.

Disclaimer: I did this project entirely to satisfy my own curiosity, using data available for free on the internet and electronics and software I built myself. You may use the information in this article for your own purposes, and at your own risk. The software published in this article is released under the GNU Public License.
I will not accept any responsibility for whatever damages might occur from using or applying the information in this article, or from following directions therein.

Short protocol description

The OT protocol is a means to transfer data between a master device (for example, the room thermostat) and a slave device (the heater device). Data exchange is done in a request/reply fashion, where the master initiates the communication and expects a reply from the slave within a certain period of time. If the data is incomplete or corrupt it will be discarded and the conversation ends. If the communication is ended prematurely, or when the slave does not reply within the time specified in the protocol, the master will resend the request to the slave.

What I saw in practise (communication between a Honeywell Chronotherm thermostat and an Intergas heater) is that a considerable percentage of requests is not answered by the slave. When that happens, the master will just repeat the request a couple of times in the hope of a reply, before giving up and requesting some other data type.

The protocol specifies that at least one message per second should be posted by the master.

The data that is exchanged consists of 32 bits (so, 4 bytes of data), to which are added a start- and a stop bit, and it is Manchester encoded. A frame consists of a number of blocks of data:

  • 1 start bit (always 1)
  • 1 parity bit (set in such a way that there's an even number of 1s in the entire message)
  • 3 bits for the message type
  • 4 spare bits (which are all 0)
  • 8 bits data ID
  • 16 bits data value
  • 1 stop bit

OpenTherm frame description

The message type specifies the kind of message: there are master-to-slave and slave-to-master messages. The master can either request some kind of data value (a 'read data' request, for example: boiler water temperature), or it can order the slave to set a certain parameter to a  new value ('write data' request, for example: to deliver hot water or to fire up the heater burner).

The slave acknowledges the master's request, either by replying with the requested data ID's value, or by ackowledging (repeating) the 'write data' request.

The data ID consists of one unsigned byte of information, so a total 255 data IDs is possible (2^8 - 1). The protocol defined 127 data IDs, the rest are available to vendors, upon request.

For more details, refer to the OpenTherm Protocol Specification.

Electrical implementation

The connection between the heater and the thermostat consists of two wires and is set up in such a way that it does not matter how they are connected (i.e. the polarity is protected). The heater provides power to the thermostat over these wires.
To communicate to the slave, the master device changes the voltage on the wire. To communication back to the master, the slave changes the current through the wire. The voltage/current levels are as follows:

  logic 0 logic 1
voltage ≤ 7V 15-18V
current 5-9mA 17-23mA

The voltage levels are too high to be used by Arduino directly, and hooking up your own electrical circuitry to (expensive!) hardware is not advisable without taking due precautions to prevent damage. Luckily, an old issue of Elektuur magazine contained an electrical circuit that takes care of both of these issues: voltage/current levels are transformed to Arduino-compatible levels and optocouplers take care of electrical separation of the heater and Arduino circuits. And even though the article is quite old, the PCB for it can still be ordered, which I did, to make life easier.

I adapted the circuit a bit for my needs: changed one resistor value (discussed below) and I removed a couple of diodes that had become unnecessary. I also added two MOSFETs to invert the final signal, giving it the same polarity as the signal from the heater/thermostat. Here is the Eagle schematic (the Eagle file can be downloaded from the Downloads section, below), click to enlarge.

Eagle schematic for OpTh-listener

The heater is connected to X1 and the thermostat to X2. The polarity is not important: D1 - D4 take care of that. IC1, an LP2950, creates a stable reference voltage of 5V. Do not use an 78LS05 because it won't work!

Master to Slave communication: voltage monitoring

IC2B compares the voltage levels at R7/R8 and R1/R2 (which is fixed at 2.5V). When the voltage at pin 6 of IC2 rises above the (fixed) voltage at pin 5 (signal goes HIGH), the output of IC2B goes low, switching on the LED inside the optocoupler OK2. The darlingtons in OK2 will conduct and the output pin 6 goes LOW. The gate of MOSFET Q1 goes LOW too, the MOSFET stops conducting and the voltage level at its drain goes HIGH.

The reverse happens when the voltage at pin 6 of IC2 drops below the level of pin 5.

The current, modulated by the slave, results in a potential (voltage) difference over R4. A high current through R4 results in a large voltage drop, and the output of IC2A will go LOW. Etcetera.

Putting it together

Both MOSFET outputs are combined at the inputs of IC3A, which is one gate of the quad OR gate 7432N. There are two reasons for this:

  • I want to use one Arduino input for monitoring both signals.
  • When the voltage is modulated, this affects the current too. This can be seen as small spikes that run parallel with the slave-to-master communication. By combining the results of both signals in the OR gate, these spikes are made to disappear in the regular signal.

When an oscilloscope is connected pin 3 of the 7432N, you can see the communication going over the line, as a square wave, see the photograph of my scope's screen, below.

When I first looked at it, the waveform was not symmetrical: the time that the signal was high was quite a bit lower than the time it was low. This is probably caused by tolerances in the signal and tolerances in the circuit. I wrote a small sketch, OT_dutycycle (see Dowloads section), to measure the dutycycle of the signal, and it was about 20%, IIRC. By changing the value of R8 from the original value of 33k, to 15k, I obtained a duty cycle of ~49%, which is close enough to perfect.

Signal at JP2

OT listener

Above: The OT listener application: top left the PCB for reading the OT communication with connections for heater (top) and thermostat. Bottom left Arduino Duemilanove with protoshield stacked on top, powered by an external 7.5V adapter. The protoshield has a mini breadboard for the quad OR gate and the two MOSFETs with their resistors. To the right the 4x16 LCD.

Decoding the Manchester signal

I found a really good functional description of the Manchester code and of a method for decoding Manchester signals of arbitrary length. It is written by Zoran Ristic from mikroElektronika, for  a PIC18F452, using mE development systems and compilers. I based my library on his description and code.

Because it took me some time to find anything useful to help me on my way and because his description is quite good, I asked Zoran for, and received, permission to quote from his work. The original set of files (description and mikroBasic/mikroPascal code) can be found here.

Note 1: The logic levels in this article are the reverse of the logic levels used in the OpenTherm protocol, where a transition from high to low is logic one, and a transition from low to high is logic zero. I took that into account when I wrote my own code.

Note 2: For Arduino I chose to use 16 bit Timer1 because with the selected prescaler of 64 it provides both a good resolution (of 4 μs) and a long sampling time (i.e. the time it takes for the counter to roll over). In contrast, if 8 bit Timer2 were used with a similar resolution, it would rollover at 1020 μs. For nominal measurements this is OK, since the nominal OpenTherm signal period is 1000 μs. However, the protocol allows for a deviation of +15%, resulting in a theoretical maximum period of 1150 μs. If an HVAC installation were to exceed our limit of 1020 μs, the Arduino application will not be able to measure the signal.

' * Project name:
' ManReceiver6
' * Copyright:
' (c) mikroElektronika, 2005 - 2006 (ZR)
' * Revision History:
' 20060913:
' - initial release.

Manchester code is a form of line code where each bit is represented with two voltage levels. As a result
of convention, logic one and logic zero are represented in the following way:

- "Logic one"
| <--- transition from low to high

- "Logic zero"
| <--- transition from high to low

For example, the sequence of bits 110-10011010 is represented as:

<T1><T1>< T2 > < T2 > < T2 ><T1><T1> < T2 ><T1><T1>< T2 > < T2 > < T2 > <T1>
___ ______ ______ ___ ___ ______ ______
| | | | | | | | | | | | | |
| | | | | | | | | | | | | |
__ | |___| |______| |___| |______| |___| |______| |___

| | | | | | | | | | | |
| | | | | | | | | | | |
| | | | | | | | | | | |
1 1 0 1 0 0 1 1 0 1 0

Note: It is just a convention of polarity. There is also a possiblity that voltage levels are inverted, i.e.
logic one is transition from high to low and logic zero is vice versa. This, however, does not influence
general conclusion in this example.
It is important to identify two characteristic periods of duration, T1 and T2. The relation between the
two should be: T2 = 2 T1.
Manchester code carries information about bit rate and this is very useful to make transmitter and receiver
synchronized. However, two levels per each bit require double the bandwith for transmition. These two
arguments are crucial for deciding whether to use the code or not.

The following example shows how to:
- Measure T1 and T2
- Decode Manchester bits
- Identify change in bitrate
- Store incoming bits into useful data format

The example consists of two parts. Part one measures T1 and T2, while part two is decoding the incoming bits.

1. Measuring T1 and T2.

Incoming signal is expected on RB0 [interrupt] pin. PIC is configured so that each rising edge of the signal
generates interrupt. When the first rising edge is detected, TIMER1 is reset to zero and then started. When
the second rising edge is detected, TIMER1 is stopped and the value of TIMER1 is stored. The process is
repeated 10 times, though this can be changed. Each time a new value of TIMER1 is compared to the previous value.
If the new value is less then the previous value, the new value is taken for T2. This algorythm
is not very safe in case of noisy signals, but it is simple enough for demonstration. User should consider
calculating average value for T2.

2. Decoding incoming bits.

Once T1 and T2 are known it is now easy to start sampling the incoming signal. Interrupt on RB0 is turned off,
since it is not needed anymore. Timer1 is now configured to make interrupts each T2 seconds (a single bit lasts
exactly T2 seconds). In order to make Timer1 generate interrupt every T2 seconds, it is necessary to make it
count from 0xFFFF-T2 (timer makes interrupt when it rolls over 0xFFFF). Technically speaking, we should subtract
T2 from 0x10000, but since TIMER1 is 16bits in length then we take the value 0xFFFF. First of all, we wait for
the rising edge of the signal. The assumption is that this is a start bit. When the edge is detected we start
TIMER1 and let it run. Each time TIMER1 rolls over the value 0xFFFF it will generate interrupt (T2 seconds have
elapsed). At this moment we shall sample our signal. Do not forget that when Timer1 rolls over, it will start
counting from 0x0000. Therefore, we have to preload it again with the value of 0xFFFF-T2 in order to get correct
sampling intervals. Beware that we started sampling immediately after the rising edge is detected. This can cause
us trouble, because the moment of sampling can fall exactly in transition zone of the pulse, thus increasing
chances to get errornous results.

In order to solve the transition zone problem, we shall start sampling some time after the rising edge is detected.
The best moment for this is T1/2 seconds after the first rising edge, i.e. in the middle of T1 interval.
This is how correct sampling should look like:

| | | | | | | | | | | |
| | | | | | | | | | | |
| | | | | | | | | | | |
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |
| ___ ______ ______ ___ ___ ______ ______ |
| | | | | | | | | | | | | | | |
| | | | | | | | | | | | | | |
___| |___| |______| |___| |______| |___| |______| |___

| | | | | | | | | | |
|< T2 >|< T2 > |< T2 > |< T2 >|< T2 >|< T2 >|< T2 > |< T2 > |< T2 >|< T2 >|
| | | | | | | | | | |
sample sample sample sample sample sample sample sample sample sample

Note that sample moments fall exactly on the right polarity of the pulse. Therefore, if the level of the signal
is logic high at the moment of sampling, then we can be sure it is a logic one that we sampled. All the same,
if the level of incoming signal is low at the moment of sampling, then this means we are sampling a logic zero.

OpTh library

I wrote a library for Arduino, OpTh, to access the contents of the OpenTherm communication, once the signal is converted to a level that Arduino can work with (for example, by using the schematic discussed above). The library gives access to each functional component of the OpenTherm frame: parity, message type, data ID, data value, etc. You may download it from the Downloads section, below.

For an example of how to use the library, refer to the OpTh listener application section and the corresponding code in the examples folder that is packaged with the library. The table below gives a brief description of the functions that are available in the library. For a more detailed view take a look at the code, which has a lot of comments.

Note that OpTh uses Timer1 and INT0. This may affect the implementation of (the rest of) your project, depending on what resources you need for that.





OpTh() Creates a new variable of type OpTh. OpTh OT = OpTh()
measureOtPeriod() Measures the period of the OpenTherm signal. OT.measureOtPeriod()
getPeriod() Returns the value measured by measureOtPeriod. long p = OT.getPeriod()
waitFrame() Wait for the next frame. OT.waitFrame()


Read the bits that make up one frame.
Returns 0 for failure of 1 for success and sets an error message.
byte success = OT.readFrame
errmsg() if readFrame() returns 0 then errmsg() can contain the reason for the error. char *msg = errmsg();


Return the 'raw' frame as an unsigned long. unsigned long frame = OT.getFrame();
setFrame() Set the frame manually. Can be convenient if you receive the frame (wirelessly)
from another Arduino and you want to use OpTh functions to access its content.
unsigned long f = 0x28005555;
getParity() Returns the parity bit (0 or 1). byte par = OT.getParity();
getMsgType() Returns the message type. byte msg = OT.getMsgType();
getDataId() Returns the data ID. byte data_id = OT.getDataId();
getDataValue() Returns the data value. unsigned int data_value = OT.getDataValue();
isMaster() Returns 1 if the frame is sent by the Master device and 0 if it's sent by the
Slave device.
if (OT.isMaster() ) {

OpTh listener application

The OpTh listener application for Arduino, included in the examples directory of the OpTh library, measures the period of the OpenTherm signal and, using that, continuously monitors the communication between the heater and the thermostat. The output of a selected number of messages (status, temperatures) is formatted and sent to a 4x16 LCD screen.

The following sequence of events is displayed on the LCD photographs below:

  • start-up of Arduino
  • measurement of the period of the OpenTherm signal
  • display data when it becomes available
  • central heating (CH) active
  • domestic hot water (DHW) demand
Initialization screen The period of the OpenTherm frame has been measured Waiting for data
Room set/actual temperature, boiler water temperature,
no central heating demand
Room temperature increased to 20°C - increased
CH setpoint
CH setpoint still rising
Central heating is active Central heating is active - flame is on Central heating is active - flame is on, new room setpoint is displayed
Domestic hot water demand - flame on Domestic hot water demand - flame on, boiler
temperature increasing
Domestic hot water demand - flame on, max. boiler temperature reached


For 24 hours I recorded the traffic between thermostat and heater, and I found out that only a small subset of the 127 allocated data IDs is used. Also, some data IDs are exchanged more frequent than others, and some are always 0.

A total of 86391 requests was sent by the room thermostat in those 24 hours, and 49244 replies were sent by the heater. Some requests had to be repeated a couple of times before the slave answered, and some were not answered at all. In all, 34689 valid request/reply pairs were sent, an average of about 24 per minute. The table below breaks those 34698 valid requests down by data ID.

data ID description count % of total
0 Master and slave status flags 5133 15
1 Control setpoint: CH water temperature setpoint 10661 30
9 Remote override room setpoint (1) 1096 3
14 Maximum relative modulation level setting (%) (2) 1000 3
15 Maximum boiler capacity (kW) / minimum boiler modulation level(%) (1) 3 0
16 Room setpoint (°C) 366 1
17 Relative modulation level (%) (1) 2550 7
18 Water pressure in CH circuit  (bar) (3) 2 0
20 Day of week and time of day 157 0
24 Room temperature (°C) 513 1
25 Boiler flow water temperature (°C) 9955 29
26 DHW temperature (°C) (1) 45 0
27 Outside temperature (°C) (3) 15 0
28 Return water temperature (°C) (1) 83 0
56 DHW setpoint (°C) (1) 1057 3
57 Max CH water setpoint (°C) (1) 1057 3
100 Remote override function (1) 1005 3

(1) Value always 0
Value either 0 or 100%
Values out of bounds

Modulation visualized

Many 'traditional' central heating systems operate in an ON/OFF mode: when there's a heating demand, the heater is switched on. When the heating demand is over the heater is switched off. But with a combination of certain Honeywell room thermostats and suitable heaters, both of which must support the OpenTherm protocol, a modulating heating system can be made.

The graph below shows how this works for the system installed in my home. It displays the value of the CH setpoint (Y-axis) sent by the Master, as a function of time (X-axis). The measurements are taken on a weekday morning, and run from 05:25 to 08:00. It can clearly be seen that the thermostat modulates the output of the heater by sending it higher or lower temperature set points.

Click to enlarge.

CH command settings illustrating heater modulation



© Palebluedot