Add an electrical waterboiler next to a gas geyser, heat the water with excess electricity from the PV panels system with battery, completely automated and configurable, report on Home Assistant.

Project J

Status of the project

This project was executed in begin may 2026.

Electric boiler and gas-geyser

A geyser (or instantaneous water heater) heats water on demand as it passes through, providing unlimited hot water but no storage. A boiler stores a fixed amount of water in a tank, keeping it hot for instant use.

In our country, almost all geysers are burning natural gas to heat the water, and the boilers are using electricity to warm the water.

Context and rationale

My brother has solar panels with a battery, and a watergeyser on natural gas, mostly for showering and in the kitchen. Natural gas is still 3 to 5 times cheaper than (bought in) electricity in our area. It would be nice if the solar energy could be used to heat the water.

In the winter, there is not enough sun to cover the complete house consumption, so it is better to remain using gas in that situation. However, in the summer, the solar panels generate enough energy to also heat the shower water. Even so, especially in the seasons in between, it all depends on the weather.

My brother also wants to be less dependent on energy supplied from outside.

The domotics system of the house is Home Assistant.

Components

This project aims to add an additional electric waterboiler, and run the electric waterboiler only on excess electricity from the solar panels. The presence of a battery eases this: as long as the battery is full enough, we can heat the water in the electric boiler - and once the battery goes below a certain percentage, we stop heating water.

If there is no hot water left in the electric waterboiler, and hot water is requested, then the gas-boiler should take over. This is possible by an electric three-way valve fitted in the hot-water pipe. We use a brass motorized ball valve with 3-Wire electric control on DC12-24V with manual switch. The manual switch allows overruling the system when the electricity is down.

To know which source of hot water to use, we need to measure if the water in the electric boiler is warm enough. To this end, we need to mount a DS18B20 temperature sensor inside it.

One electric component shall control all these items:

  • the switching of the heater element in the electric boiler,
  • the switching of the three-way valve,
  • the measuring of the temperature water inside in the electric boiler.

Last but no least, one component shall contain the smart software to check the measurements and settings, contain the algorithms, and steer the waterboiler and three-way valve. This component shall also allow the enduser to overrule the algorithm:

  • switch between the waterboiler and gas-boiler and automatic,
  • set the temperature thresholds for the temperature sensor.

The last two components can be combined in one node by using an ESP32-C3 with Tasmota and using the Berry language for the automation software. This way, the whole system is fitted in one box, independend of other modules. It can easily be switched off, and operated manually.

Hardware components

Three-way ball valve

See Three-way-valve. We use the 12..24V DC version here.

Relay module for three-way-valve

See RelayModule2Channel

The module should have 12V relays, and support control signals of 3.3V.

Relay module for waterboiler

Tongou relay

This is a commercial 63A DIN-rail relay of the brand Tongou, supplied with Tasmota software. Model: TO-Q-SY2-JWM.
Previous brand name: Tongou
Current brand name: Chayo

See Chayo.

12V DC power supply

Harvested from an old charger.

DC-DC converter 12VDC to 5VDC

The GND of the 12V input side is connected to the GND of the 5V side. This converter is build around a LM2596, a buck-converter.

Temperature sensor DS18B20

Includes a small PCB with a 4K7 pull-up resistor and a capacitor to stabalise the supply voltage.

See DS18B20.

Cabinet

ABS material and self-extinguishing.

Measurements: 230x150x85.

CPU module ESP32-C3 SuperMini with OLED

ESP32-C3 SuperMini with OLED
ESP32-C3 SuperMini with OLED

This module is used to control the valve.

The standard ESP32-C3 SuperMini expansion board does not work for the module with OLED, since it has different pin-outs.

This board is similar to the one described here, but deviates from other 0.42-inch screens: The starting point of the screen is 128,64 (13, 14).

Display properties

Function Pin
OLED SDA 5
OLED SCL 6

LED indicators for the three-way valve

It would be nice if you can see on the box what it is doing: 2 LEDs are to be mounted on the box to indicate the three-way valve position: Electric boiler or gas-boiler.

Valvecontrol hardware: Assembly of the parts

Module Pin Function Pin Module
ESP32-C3-SuperMini-OLED +5 power supply +5V Power supply
ESP32-C3-SuperMini-OLED GND power supply GND Power supply
ESP32-C3-SuperMini-OLED 10 1-wire DAT DS18B20
ESP32-C3-SuperMini-OLED 10 pull-up DAT Resistor 4K7 to +3.3V
ESP32-C3-SuperMini-OLED 3 relais-electric IN1 Double relay module
ESP32-C3-SuperMini-OLED 4 relais-gas IN2 Double relay module
ESP32-C3-SuperMini-OLED 0 LED Electric GND Power supply
ESP32-C3-SuperMini-OLED 1 LED Gas GND Power supply
ESP32-C3-SuperMini-OLED +3v3 power supply Vcc DS18B20
Double relay module DC+ power supply +12V Power supply
Double relay module DC- power supply GND Power supply
DS18B20 GND power supply GND Power supply

LEDs: Connect one side of the LED to GND and the other to GPIO 0 and GPIO 1. The LEDs we use here have a build-in resistor, and are meant for 3V3.

DS18B20 Temperature Sensor: Use GPIO 10. Important: A 4.7kΩ pull-up resistor must be connected between the data pin (DQ) and the 3.3V pin for the sensor to function.

Power: Do not connect both the USB-C cable and an external 5V source simultaneously to the 5V pin, as this can damage the board.

The control box

The inside of the box

The outside of the box

The plumbing

The plumbing

Valvecontrol hardware: How to control the three-way valve

Switch the relay 1 and relay 2

We use the commands Power1 and Power2 to control the relays that turn the three-way-valve. Typing these commands in the Tasmota console, gives the following log:

21:25:10.030 CMD: power1 1
21:25:10.037 MQT: stat/tasmota_BA0CD4/RESULT = {"POWER1":"ON"}
21:25:10.042 MQT: stat/tasmota_BA0CD4/POWER1 = ON
21:25:31.146 CMD: power2 1
21:25:31.154 MQT: stat/tasmota_BA0CD4/RESULT = {"POWER2":"ON"}
21:25:31.155 MQT: stat/tasmota_BA0CD4/POWER2 = ON

21:35:11.461 CMD: power1 0
21:35:11.469 MQT: stat/tasmota_BA0CD4/RESULT = {"POWER1":"OFF"}
21:35:11.477 MQT: stat/tasmota_BA0CD4/POWER1 = OFF
21:35:16.781 CMD: power2 0
21:35:16.789 MQT: stat/tasmota_BA0CD4/RESULT = {"POWER2":"OFF"}
21:35:16.791 MQT: stat/tasmota_BA0CD4/POWER2 = OFF

Valvecontrol software: the algorithm 97-99 is not good

The proposed algoritm to switch at 97% and 99% is not good

The proposed algorithm

It is more important to fill the battery, than to warm the water in the boiler. So, we only want to switch the boiler on when the battery is full. Hence, the waterboiler shall switch on depending on the state of charge (SOC) of the batttery.

If the SOC > 99% then the boiler goes on, if the SOC < 97% then the boiler goes off.

Problem 1

The problem with this approach is: If the sun disappears when the boiler is on, the battery SOC will go down rapidly, en will go below 97%. The the boiler goes off, and the battery goes charging again. It will reach 99% quickly (especially if the sun comes back), and the boiler will go on again. This kind of oscillating, we want to prevent.

Problem 2

The battery management system has different charging methods: bulk, absorption and trickle.

Bulk: The charger operates at maximum power and pumps as much current as possible into the battery to charge it as quickly as possible. The battery voltage rises during this phase. Absorption: Once the battery has reached the absorption voltage (e.g., 14.4V), the charger switches to the absorption phase. The voltage remains constant and the charging current slowly decreases until the battery is fully charged. Trickle or Float: After the absorption phase, the charger switches to the float voltage (e.g., 13.8V). This is a lower voltage to keep the battery fully charged and compensate for self-discharge without damaging the battery.

At 97% the system charges in trickle mode, which is only a few Ampere, maybe 300Watt. Let’s presume that when the sun shines, the panels provide 5000W. At a SOC of 97%, 300W goes into the battery, the boiler is still off, and 4700W goes lost.

Once 99% is reached the boiler goes on. The system still charges in trickle mode. From the 5000W delivered by the panels, 300W goes into the battery, 1800W into the boiler, and 2900W goes lost.

Problem 3

With a typical 150 liter boiler of 1800W, it takes almost 7 hours to bring it from 10°C to 80°C. And the time that we have sunshine in the winter is not that long.

Problem 4

The current situation in my brother’s house, is that the system also sells excess energy to the net at profitable times. If the battery is charged more than 55%, the algorithm will sell the charge to the mains net when the price is highest (typically early in de evening). It stops selling at 55% to 50%.

Also, if the battery is close to empty, it buys electricity from the net - but it will never fill above 55% this way. So, in the winter, the battery SOC almost never reaches 90%.

Conclusion

Hence, switching at these high percentages (97% and 99%) is not good. Maybe we switch on the boiler at 65% and off by 55%?

Or we could change the rule and switch on the boiler when the house is delivering 1800W to the net, independent of the battery SOC. Or a rule that depends on the month. With software we can make it as smart as we need.

Valvecontrol software: programming the ESP32-C3

Flashing the ESP32

The bin files are here.
First, we will need this binary: tasmota32c3-lvgl.factory.bin.

The factory image includes the boot-loader, partition-table, boot-app and the Tasmota binary.

Later, to update to new versions, we use OTA, which only requires the Tasmota binary tasmota32c3-lvgl.bin.

esptool.py -b 460800 write_flash 0 ~/Downloads/tasmota32c3-lvgl.factory.bin

Result:

(penv) michiel@Delphinus:~/development/solar-waterboiler/Solar-Waterboiler-With-Battery> esptool.py -b 460800 write_flash 0 ~/Downloads/tasmota32c3-lvgl.factory.bin
esptool.py v4.8.1
Found 33 serial ports
Serial port /dev/ttyS9
/dev/ttyS9 failed to connect: Could not open /dev/ttyS9, the port is busy or does not exist.
(Could not configure port: (5, 'Input/output error'))

Serial port /dev/ttyS8
/dev/ttyS8 failed to connect: Could not open /dev/ttyS8, the port is busy or does not exist.
(Could not configure port: (5, 'Input/output error'))

(... many more ...)

Serial port /dev/ttyS0
/dev/ttyS0 failed to connect: Could not open /dev/ttyS0, the port is busy or does not exist.
(Could not configure port: (5, 'Input/output error'))

Serial port /dev/ttyACM0
Connecting...
Detecting chip type... ESP32-C3
Chip is ESP32-C3 (QFN32) (revision v0.4)
Features: WiFi, BLE, Embedded Flash 4MB (XMC)
Crystal is 40MHz
MAC: ac:a7:04:ba:0c:d4
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x00373fff...
Compressed 3617360 bytes to 2172667...
Wrote 3617360 bytes (2172667 compressed) at 0x00000000 in 11.0 seconds (effective 2635.5 kbit/s)...
Hash of data verified.

Leaving...
Hard resetting via RTS pin...
(penv) michiel@Delphinus:~/development/solar-waterboiler/Solar-Waterboiler-With-Battery> 

Monitoring the ESP32-C3 serial port

If you have PlatformIO installed, the monitor function autoselects the port and baud-rate:

(penv) michiel@Delphinus:~/development/solar-waterboiler/Solar-Waterboiler-With-Battery> pio device monitor
--- Terminal on /dev/ttyACM0 | 115200 8-N-1
--- Available filters and text transformations: debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
00:00:09.005 WIF: Connect failed as AP cannot be reached
00:00:13.190 WIF: Connecting to AP1 J&E-2.4G in mode HT20 as tasmota-BA0CD4-3284...
00:00:16.471 WIF: Connect failed as AP cannot be reached
00:00:17.494 WIF: Connecting to AP1 J&E-2.4G in mode HT20 as tasmota-BA0CD4-3284...
00:00:20.776 WIF: Connect failed as AP cannot be reached
00:00:22.798 WIF: Connecting to AP1 J&E-2.4G in mode HT20 as tasmota-BA0CD4-3284...

If you have miniterm installed, you have to choose the serial port:

(penv) michiel@Delphinus:~/development/solar-waterboiler/Solar-Waterboiler-With-Battery> pyserial-miniterm

--- Available ports:
---  1: /dev/ttyACM0         'USB JTAG/serial debug unit'
---  2: /dev/ttyS0           'n/a'
---  3: /dev/ttyS1           'n/a'
---  4: /dev/ttyS2           'n/a'
---  5: /dev/ttyS3           'n/a'
(... many more ...)
--- 32: /dev/ttyS30          'n/a'
--- 33: /dev/ttyS31          'n/a'
--- Enter port index or full name: 1
--- Miniterm on /dev/ttyACM0  9600,8,N,1 ---
--- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
00:00:17.494 WIF: Connecting to AP1 J&E-2.4G in mode HT20 as tasmota-BA0CD4-3284...
00:00:20.776 WIF: Connect failed as AP cannot be reached

Setting the correct Wifi properties

You will have to enter a backlog command in miniterm as follows (and concluding by pressing the enter key):

backlog ssid1 Fritz; password1 xxxMySecretPasswordxxx

Problem is, that you do not see anything when typing it. So, it is easier to just copy it from elsewhere and paste it in. Just paste it in the terminal window. The device will answer with a line “CMD: “ and your typed text.

(penv) michiel@Delphinus:~/development/solar-waterboiler/Solar-Waterboiler-With-Battery> pio device monitor -b 115200
--- Terminal on /dev/ttyACM0 | 115200 8-N-1
--- Available filters and text transformations: debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at https://bit.ly/pio-monitor-filters
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H
00:06:33.410 WIF: Connect failed as AP cannot be reached
00:06:34.434 WIF: Connecting to AP1 Joost&Ellen-2.4G in mode HT20 as tasmota-BA0CD4-3284...
00:06:39.740 WIF: Connecting to AP1 Joost&Ellen-2.4G in mode HT20 as tasmota-BA0CD4-3284...
00:06:43.022 WIF: Connect failed as AP cannot be reached
00:06:44.046 WIF: Connecting to AP1 Joost&Ellen-2.4G in mode HT20 as tasmota-BA0CD4-3284...
00:06:46.928 CMD: backlog ssid1 Fritz; password1 xxxMySecretPasswordxxx
00:06:46.977 RSL: RESULT = {"SSId1":"Fritz"}
00:06:47.177 RSL: RESULT = {"Password1":"xxxMySecretPasswordxxx"}
00:06:47.328 WIF: Connect failed as AP cannot be reached
00:06:49.001 WIF: Connecting to AP1 Fritz in mode HT20 as tasmota-BA0CD4-3284...
00:06:51.183 APP: Restarting
ESP-ROM:esp32c3-api1-20210207
Build:Feb  7 2021
rst:0xc (RTC_SW_CPU_RST),boot:0xc (SPI_FAST_FLASH_BOOT)
Saved PC:0x40381d16
SPIWP:0xee
mode:DIO, clock div:1
load:0x3fcd5820,len:0x98
load:0x403cc710,len:0x824
load:0x403ce710,len:0x2204
entry 0x403cc710

00:00:00.001 CMD: Using USB CDC
00:00:00.001 HDW: ESP32-C3 v0.4 
00:00:00.017 UFS: FlashFS mounted with 304 kB free
00:00:00.024 CFG: Loaded from File, Count 23
00:00:00.026 SER: Set to 8N1 115200 bit/s
00:00:00.026 SER: HWCDC supports 115200 bit/s only
00:00:00.030 QPC: Count 1
00:00:00.072 BRY: Berry initialized, RAM used 5667 bytes
00:00:00.084 Project tasmota - Tasmota Version 15.3.0.1(faf9067-lvgl-haspmota)-3.3.7(2026-02-23T16:57:17)
00:00:01.001 WIF: Connecting to AP1 Fritz in mode HT20 as tasmota-BA0CD4-3284...
00:00:03.362 WIF: Connected
00:00:03.616 HTP: Web server active on tasmota-BA0CD4-3284 with IP address 10.0.3.75

So, now we can access the Tasmota UI in a web browser at http://10.0.3.75.

Or, with a second module:

00:08:45.001 WIF: Connecting to AP1 Fritz in mode HT20 as tasmota-2FD018-4120...
00:08:55.291 WIF: Connected
00:08:55.558 HTP: Web server active on tasmota-2FD018-4120 with IP address 10.0.3.120
00:08:56.530 RSL: INFO1 = {"Info1":{"Module":"ESP32C3","Version":"15.3.0.1(1eb6c0f-lvgl-haspmota)","FallbackTopic":"cmnd/DVES_2FD018_fb/","GroupTopic":"cmnd/tasmotas/"}}
00:08:56.534 RSL: INFO2 = {"Info2":{"WebServerMode":"Admin","Hostname":"tasmota-2FD018-4120","IPAddress":"10.0.3.120","IP6Global":"","IP6Local":"fe80::1a8b:eff:fe2f:d018%st156"}}
00:08:56.539 RSL: INFO3 = {"Info3":{"RestartReason":"Software reset CPU","BootCount":21}}
00:09:00.993 RSL: STATE = {"Time":"1970-01-01T00:09:00","Uptime":"0T00:08:16","UptimeSec":496,"Heap":182,"SleepMode":"Dynamic","Sleep":50,"LoadAvg":19,"MqttCount":0,"Berry":{"HeapUsed":5,"Objects":59},"Wifi":{"AP":1,"SSId":"Fritz","BSSId":"98:9B:CB:2A:6E:DF","Channel":6,"Mode":"HT20","RSSI":92,"Signal":-54,"LinkCount":1,"Downtime":"0T00:08:10"},"Hostname":"tasmota-2FD018-4120","IPAddress":"10.0.3.120"}

Setting a secondary SSID

You can also set a secondary wifi address as follows:

backlog ssid2 Fritz; password1 xxxMySecretPasswordxxx

Valvecontrol Tasmota configuration

Timezone

Look up the commands here: https://tasmota.github.io/docs/Timezone-Table/
I live in Europe/Brussels zone, so for me this is:

Backlog0 Timezone 99; TimeStd 0,0,10,1,3,60; TimeDst 0,0,3,1,2,120

The result is:

20:24:43.208 CMD: Backlog0 Timezone 99; TimeStd 0,0,10,1,3,60; TimeDst 0,0,3,1,2,120
20:24:43.211 RSL: RESULT = {"Timezone":99}
20:24:43.214 RSL: RESULT = {"TimeStd":{"Hemisphere":0,"Week":0,"Month":10,"Day":1,"Hour":3,"Offset":60}}
20:24:43.218 RSL: RESULT = {"TimeDst":{"Hemisphere":0,"Week":0,"Month":3,"Day":1,"Hour":2,"Offset":120}}

MQTT

Set the MQTT properties in the Tasmota UI in the browser, and you will see this in the terminal:

20:28:10.531 MQT: Attempting connection...
20:28:10.552 MQT: Connected

Device name SolarWBWB

In the console, enter the commands:
FriendlyName1 SolarWBWB Devicename SolarWBWB
Hostname SolarWBWB

SolarWBWB stands for Solar Water Boiler With Battery.

You can now access the device at this URL in your local network, instead of using the IP-address: http://solarwbwb/.

Template

The initial (empty) template is: {"NAME":"ESP32C3","GPIO":[1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1],"FLAG":0,"BASE":1}.

In the Tasmota UI in the browser, go to Configuration -> Module.

Configure the GPIOs:

Pin Component number UI label Comment
GPIO10 1312 DS18x20 DS18B20 or DS18S20
GPIO5 640 I2C SDA1 Software I2C
GPIO6 608 I2C SCL1 Software I2C
GPIO3 224 Relay1 Relay Valve Electric
GPIO4 225 Relay2 Relay Valve Gas
GPIO0 226 Relay3 LED for Electric
GPIO1 227 Relay4 LED for GAS
GPIO2 5730 OptionA3 Specific device options to be served in code

Or do them all at once in the console (except the last one, do that in the UI): Backlog0 GPIO10 1312; GPIO5 640; GPIO6 608; GPIO3 224; GPIO4 225; GPIO0 226; GPIO1 227;

Configuration-Module

Setting the teleperiod

The console in the Tasmota web-UI shows measurements from sensors as soon as they arrive. Independently, at a certain interval called the “teleperiod”, a Tasmota device reports its status and measured values to other devices or platforms (often via MQTT).

By default, this interval is 300 seconds (5 minutes). You can adjust this interval, but it cannot be less than 10 seconds. For me, 300 seconds is a bit long, so we change to 30s:

20:01:38.067 CMD: teleperiod 30 20:01:38.071 MQT: stat/tasmota_BA0CD4/RESULT = {“TelePeriod”:30}

Setting the web-button names

We use the WebButton command to set the names for the valve relays and the LEDs:

WebButton1 Valve-Elec
WebButton2 Valve-Gas
WebButton3 LED-Elec 
WebButton4 LED-Gas 

The 5th button switches the display on and off, so:

WebButton5 Display

The result:

The result

The limited on-time of the valve relays

The valve relays only need to be on for 25s or so, because in this time, the motor inside has reached the end-position.

For this, we use the PulseTime command. For 25 seconds, we need to encode this value as: 100 + 25 = 125.

PulseTime1 125
PulseTime2 125

The valve relays shall never be on both at the same time

To prevent that both relays are on at the same time, we use the interlock command. This ensures that turning one on immediately turns the other off.

InterLock 1,2
InterLock 1

The first command defines the group of relays that belong together. The second command switches the interlocking on.

12:06:23.233 CMD: interlock 1,2
12:06:23.238 MQT: stat/tasmota_2FD018/RESULT = {"Interlock":"OFF","Groups":"1,2"}

12:07:07.775 CMD: interlock 1
12:07:07.783 MQT: stat/tasmota_2FD018/RESULT = {"Interlock":"ON","Groups":"1,2"}

Valvecontrol Tasmota display configuration

Configure the Display

The command to scan the I2C bus finds one device - the OLED display:

21:47:37.101 CMD: I2CScan0
21:47:37.120 MQT: stat/tasmota_BA0CD4/RESULT = {"I2CScan":"Device(s) found on bus1 at 0x3c"}

Use the universal display driver by entering this command: DisplayModel 17

We also need a descriptor file SSD1306_70x42_display.ini. Create this file as display.ini in the Tasmota file system.

Show something on the display

If we go to the menu “Tools” and then “Console”, we can enter a command to show a text on the OLED display.
First set the mode (mode 0 is default, but anyhow):

19:48:06.225 CMD: displaymode
19:48:06.230 MQT: stat/tasmota_BA0CD4/RESULT = {"DisplayMode":0}

Then try to display some text:

19:54:17.434 CMD: DisplayText Hello world
19:54:17.489 MQT: stat/tasmota_BA0CD4/RESULT = {"DisplayText":"Hello world"}

Show the measured temperature on the display

The display should update whenever a new value of the temperature is measured by the DS18B20 sensor. We will implement this in the programming language Berry, by uploading a file named autoexec.be to the Tasmota filesystem.

We can use this function to show the measured temperature value with some Berry code. Add the following to the autoexec.be:

var oldvalue = 5000
def updatetemp(value)
    if (value != oldvalue)
        tasmota.cmd("DisplayText T=" .. value .. " C")
        oldvalue = value
    end
end
tasmota.add_rule("DS18B20#Temperature", updatetemp)

Valvecontrol software: temperature to valve

Temperature to valve architecture

Here we describe the software algorithm to steer the valve based on the internal waterboiler temperature.

We measure the internal temperature of the waterboiler with a DS18B20 sensor. This sensor is wired via a connector to the SolarWBWB2 module.

This module is also connected to the double-relay module which controls the valve.

The software is running on the Tasmota SolarWBWB2 module, stored in its filesystem as file autoexec.be, written in the language Berry.

The algorithm: the thresholds

I read somewhere that a professional, who sells a similar system like we are building here, switches to the electrical waterboiler at 65°C, and switches back to the gasboiler at 45°C. This seems a good choice, since water for the shower should ideally be quite hot, more like 80°C. But from 65°C it is surely usable, but below 45°C, it will cool so much in the tubes before it reaches the shower, that the comfort is gone. So, these are the values we will use.

var upper_threshold = 65
var lower_threshold = 45
var t # the actual measured temperature

The algorithm: when to run

Since heating the waterboiler takes a long time, we only need to check the temperature every half hour or so. If we limit ourselves to every half hour, then we also do not have to keep track of the current position of the valve.

There is an exception for this: If you are using hot water, and draining the hot-waterboiler, the temperature goes down fast. In this case, you may want the system to switch to the gas-boiler on the go.

So, we store the actual three-way valve position, and check for this case.

The following code implements this algorithm. The temperature is put in the variable ‘t’ elsewhere in the code. See autoexec.be.

# Switch between the waterboilers based on the temperature.
# This code runs every 30 minutes.
def t2v()
    if (t > upper_threshold)
        # Switch to the electric waterboiler
        tasmota.cmd('power1 1')
        print( 'Switching to the electric waterboiler')
    elif (t < lower_threshold)
        # Switch to the gas waterboiler
        tasmota.cmd('power2 1')
        print( 'Switching to the gas waterboiler')
    end
end
tasmota.add_cron("0 */30 * * * *", t2v, "every_30m")

The first 0 in the cron job specifier can not be replaced with a *, since that would cause the cron job to trigger every second during the 30th minute.

This is the result in the console, for the example where at 20:00 sharp, the above code switches to the gas boiler:

19:59:42.763 MQT: tele/tasmota_2FD018/SENSOR = {"Time":"2026-03-08T19:59:42","DS18B20":{"Id":"000000259CDA","Temperature":21.4},"TempUnit":"C"}
20:00:00.058 MQT: stat/tasmota_2FD018/RESULT = {"POWER2":"ON"}
20:00:00.059 MQT: stat/tasmota_2FD018/POWER2 = ON
20:00:00.061 Switching to the gas waterboiler
20:00:05.008 MQT: stat/tasmota_2FD018/RESULT = {"DisplayText":"21.3 C"}
20:00:07.093 MQT: stat/tasmota_2FD018/RESULT = {"DisplayText":"21.4 C"}
20:00:12.742 MQT: tele/tasmota_2FD018/STATE = {"Time":"2026-03-08T20:00:12","Uptime":"0T00:28:39","UptimeSec":1719,"Heap":145,"SleepMode":"Dynamic","Sleep":10,"LoadAvg":99,"MqttCount":1,"Berry":{"HeapUsed":7,"Objects":95},"POWER1":"OFF","POWER2":"ON","POWER3":"ON","Wifi":{"AP":1,"SSId":"Fritz","BSSId":"98:9B:CB:2A:6E:DF","Channel":6,"Mode":"HT20","RSSI":100,"Signal":-39,"LinkCount":1,"Downtime":"0T00:00:03"},"Hostname":"SolarWBWB","IPAddress":"10.0.3.120"}
20:00:12.769 MQT: tele/tasmota_2FD018/SENSOR = {"Time":"2026-03-08T20:00:12","DS18B20":{"Id":"000000259CDA","Temperature":21.4},"TempUnit":"C"}
20:00:25.141 MQT: stat/tasmota_2FD018/RESULT = {"POWER2":"OFF"}
20:00:25.143 MQT: stat/tasmota_2FD018/POWER2 = OFF
20:00:42.742 MQT: tele/tasmota_2FD018/STATE = {"Time":"2026-03-08T20:00:42","Uptime":"0T00:29:09","UptimeSec":1749,"Heap":148,"SleepMode":"Dynamic","Sleep":10,"LoadAvg":99,"MqttCount":1,"Berry":{"HeapUsed":7,"Objects":95},"POWER1":"OFF","POWER2":"OFF","POWER3":"ON","Wifi":{"AP":1,"SSId":"Fritz","BSSId":"98:9B:CB:2A:6E:DF","Channel":6,"Mode":"HT20","RSSI":100,"Signal":-43,"LinkCount":1,"Downtime":"0T00:00:03"},"Hostname":"SolarWBWB","IPAddress":"10.0.3.120"}

Hence, at 20:00, relay2 switches on to operate the valve. And 25s after 20:00, the relay2 switches off again, thanks to the way it is configured with PulseTime2 125.

Storing the three-way valve position

In Tasmota, a relay is an output, and a Switch is an input. Our three-way valve position is something that we would like to publish (=output) to others, e.g. show on a dashboard with HA.

In Tasmota, we can create a virtual relay in the Berry console. This is the code for the one-time actual creation:

Welcome to the Berry Scripting console. Check the documentation.
tasmota.global.devices_present
5
tasmota.global.devices_present = 6
tasmota.global.devices_present
6

Now, we can switch relay6 in Berry:

tasmota.cmd('power6 1') # the virtual relay4, ON = Electric
tasmota.cmd('power6 0') # the virtual relay4, OFF = Gas

In Berry, we can also switch with (counting the relays from 0):

tasmota.set_power(5, true) # the virtual relay6, ON = Electric
tasmota.set_power(5, false) # the virtual relay6, OFF = Gas

Main page of SolarWBWB

Refactoring the code

After I wrote a working version of the code, I asked “lovable.dev” to refactor this code to:

  1. Wrap everything in a class.
  2. Make 2 functions for the two cases in “t2v()”, and use them appropriately.
  3. Make these 2 functions available on the console as Tasmota commands, that I can activate also via MQTT.

And then I asked lovable to have the “Emergency fast switch-back to gas heater” use the new switch_to_gas() function.

The result can be found in the complete autoexec.be.

Manual operation / Auto operation

So, now the valves can be operated via MQTT with the topic: cmnd/tasmota_BA39AC/ValveGas and cmnd/tasmota_BA39AC/ValveElec.

As an additional feature, we can switch the module from automatic mode to manual mode.

I used lovable.dev to extend the code with these functionalities. See the documentation it wrote, and the code in autoexec.be.

Configuration of both valvecontrol thresholds

An other improvement was the addition of Tasmota commands to set both values for the thresholds, and persist them. This was implemented mostly by AI.

The thresholds can be set from the Tasmota console, over MQTT and from the Home Assistant interface.

Heatercontrol hardware

The system has a separate Tasmota module, the Tongou SY2, of which the relay is connected to the electric waterboiler heater element. This module can also be programmed with Berry.

How to control the relay

Since there is only one relay, it can be controlled from the Tasmota console by the Power ON and Power OFF commands. From Berry code, that becomes: tasmota.cmd("Power ON").

Heatercontrol software

Programming the TongouSY2

The module is bought with Tasmota installed, completely configured. It allows switching the relay, and measures the full set of power consumption properties. In the filesystem of the module, we create a file named autoexec.be with the Berry code.

The algorithm

The module subscribes to MQTT messages that tell us the State Of Charge (SOC) of the house battery in percent.

If the SOC is above an upper threshold, then the relay is activated, so the waterboiler starts heating the water.
If the SOC then drops below a lower threshold, the relay is opened, and the heating stops.
The SOC range between both thresholds invoke no action - the relay stays in the state it is.

This way we create a hysteresis, which prevents too many rapid switching actions.

The resulting Berry code

All code is contained in the file autoexec.be.

The software is described in the AI-written documentation.

Configuration of both heatercontrol thresholds

The Heater_Lower_Threshold and the Heater_Upper_Threshold are persisted, can be altered by Tasmota commands, and can also be shown and edited in Home Assistant.

Home Assistant interface

In HomeAssistant, we show the status of the system, and allow the user to switch between auto, manual elec, manual gas. Also, the 4 thresholds are shown and can be edited.

HomeAssistant dashboard

Comments: