Reversing a Solar Water Heater Sensor - B03 (2025)
The B03 is a temperature and level sensor for solar water heaters. It is used by the SR501 solar water heater controller. Although it looks the same as the GHBO1 used on the SR500, it is a different model. Some of the key differences are:
- The B03 use RS232 as the communication protocol.
- It can measure 1/10 of a degree Celsius.
- It has an overflow sensor.
- More differences can be seen on this article
Exploring the sensor module

From the PCB view we can see:
- A Holtek HT66F017 CPU which among other features it has 4 12bit ADCs
- A 7805 voltage regulator
- What it seems to be a resistor ladder for the level sensor
Initial wiring confirm that the red and black wires are power (12V) and the white wire is the data output. As in the SR500 sensor the white wire is the data output. Its voltage matches the supply voltage (i.e. if VCC=5V then it behaves like TTL, but if VCC=12 volts then the data voltage is 12V).
Analyzing the comm protocol
I used an oscilloscope to analyze the communication protocol. The sensor sends 3 small packets and then a long one. The following image shows the long packet.
_resized.jpg)
After some analysis I managed to determine that it was standard inverted serial communication at 4800 bps with 8 data bits, no parity and 1 stop bit.
The next step was to power the sensor with 5V and create a TTL inverter on a breadboard to start collecting data.
This is how the data looks like on a terminal:
02 09 6B 08 26 00 0F 01 01 B3 02 04 6B 08 77 02 04 6B 08 77 02 04 6C 08 78 02 04 6C 08 78
02 09 6C 08 26 00 0F 01 01 B4 02 04 6C 08 78 02 04 6C 08 78 02 04 6E 08 7A 02 04 6E 08 7A
02 09 6E 08 26 00 0F 01 01 B6 02 04 6E 08 7A 02 04 6E 08 7A 02 04 6F 08 7B 02 04 6F 08 7B
02 09 6F 08 26 00 0F 01 01 B7 02 04 6F 08 7B 02 04 6F 08 7B 02 04 70 08 7C 02 04 70 08 7C
Packet types
The sensor emits two types of self-contained 10-byte packets, both starting with 0x02:
| Second byte | Type | Contents |
|---|---|---|
0x04 | Short packet | Temperature only |
0x09 | Long packet | Temperature and water level |
Only the long (02 09) packet is needed for full sensor reading.
Long packet layout (02 09 — 10 bytes)
| Pos | Byte | Meaning |
|---|---|---|
| 0 | 0x02 | Frame marker |
| 1 | 0x09 | Long packet type |
| 2 | T2 | ADC low byte |
| 3 | T1 | ADC high byte |
| 4 | L2 | Level raw low byte |
| 5 | L1 | Level raw high byte |
| 6 | P6 | Internal state byte (see note below) |
| 7 | 0x01 | Unknown |
| 8 | 0x01 | Unknown |
| 9 | CS | Checksum = (pos1 + pos2 + ... + pos8) & 0xFF |
P6 — internal state byte: observed values are
0x00,0x0F,0x10,0x11. It is not a fixed constant and is not correlated with instantaneous capacitive-strip crossings. Current hypothesis: it is a slow internal counter in the SR501 firmware that increments roughly every 12 hours. The checksum must always use the actual transmitted byte — never hard-code an expected value for P6.
Temperature decoding
The 12-bit ADC value is reconstructed as: ADC = (T1 << 8) | T2
Converting ADC to Celsius requires a lookup table with linear interpolation — a logarithmic calibration curve maps the NTC thermistor resistance to temperature (see the sample code).
Water level decoding
The raw 16-bit level value is: raw = (L1 << 8) | L2
The percentage is: level_pct = (100 * raw) / 450
The raw values are not linear and require normalization:
| Raw value | Raw % | Normalized | Meaning |
|---|---|---|---|
| 0x0026 (38) | 8% | 0% | No water touching sensor |
| 0x0092 (146) | 32% | 32% | L1 |
| 0x00E4 (228) | 50% | 50% | L2 |
| 0x013E (318) | 70% | 70% | L3 |
| 0x0149 (329) | 72% | 80% | L4 (snapped up) |
| 0x01C2 (450) | 100% | 100% | Full |
A complete code for decoding the sensor data can be found in the example directory on GitHub. A Python reference decoder is also available in the Exploration_2026/ directory.
ESPHome / Home Assistant implementation
A ready-to-flash ESPHome component for the Wemos D1 Mini (ESP8266) is available in the wemos-esphome implementation/ directory. It integrates directly with Home Assistant and exposes temperature and water level as native sensors.
Note on framing: this implementation uses the older
00 01outer-stream framing to locate the payload bytes by fixed offset inside the 30-byte burst. It does not use02 09packet detection or checksum validation. The approach is slightly weaker — a corrupt or shifted stream could produce a bad reading — but in practice it works reliably for typical home deployments where the sensor output is clean.