Used components
Waveshare ESP32-S3 DIN-rail module with RS485 and CAN
See ESP32-S3-Module-RS485-CAN.

QDY30A Submersible level transmitter with RS485
See SubmersibleLevelTransmitter.

Beware the air-vent of the transmitter cable: it can not be put in an air-tight cabinet, nor can it stand humidity or water - keep it dry!

Power supply Mean-Well HDR-15-24
The Mean-Well HDR-15-24 is a small but reliable and especially safe power supply.

DIN-rail box
I bought a very cheap IP65 waterproof distribution box with DIN rail.

Programming the ESP32-S3 module
The ESP32-S3 module will be connected via its RS485-interface with the waterlevel transmitter.
Plugging the module with its USB-C connection into a PC and probing its processor gives:
michiel@Delphinus:~/development/gitlab.vanderwulp.be/waterlevel> esptool chip-id
esptool v5.1.0
(...)
Connected to ESP32-S3 on /dev/ttyACM1:
Chip type: ESP32-S3 (QFN56) (revision v0.2)
Features: Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 8MB (AP_3v3)
Crystal frequency: 40MHz
USB mode: USB-Serial/JTAG
MAC: 1c:db:d4:7a:39:bc
Stub flasher running.
Warning: ESP32-S3 has no chip ID. Reading MAC address instead.
MAC: 1c:db:d4:7a:39:bc
Hard resetting via RTS pin...
michiel@Delphinus:~/development/gitlab.vanderwulp.be/waterlevel>
And idem for the flash:
michiel@Delphinus:~/development/gitlab.vanderwulp.be/waterlevel> esptool flash-id
esptool v5.1.0
Found 33 serial ports...
(...)
Connected to ESP32-S3 on /dev/ttyACM1:
Chip type: ESP32-S3 (QFN56) (revision v0.2)
Features: Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 8MB (AP_3v3)
Crystal frequency: 40MHz
USB mode: USB-Serial/JTAG
MAC: 1c:db:d4:7a:39:bc
Stub flasher running.
Flash Memory Information:
=========================
Manufacturer: 20
Device: 4018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
Flash voltage set by eFuse: 3.3V
Hard resetting via RTS pin...
michiel@Delphinus:~/development/gitlab.vanderwulp.be/waterlevel>
Get the Tasmota binary image
The tasmota32s3.factory.bin file is to be used initially, since it erases and partitions all memory. Later, while updating (and maintaining configuration) we can use the tasmota32s3.bin file.
So we download the tasmota32s3.factory.bin from https://ota.tasmota.com/tasmota32/release/.
Erasing the flash
As described at Tasmota - Getting started, we erase the flash first.
michiel@Delphinus:~> esptool erase_flash
Warning: Deprecated: Command 'erase_flash' is deprecated. Use 'erase-flash' instead.
esptool v5.1.0
Found 33 serial ports...
(...)
Connected to ESP32-S3 on /dev/ttyACM1:
Chip type: ESP32-S3 (QFN56) (revision v0.2)
Features: Wi-Fi, BT 5 (LE), Dual Core + LP Core, 240MHz, Embedded PSRAM 8MB (AP_3v3)
Crystal frequency: 40MHz
USB mode: USB-Serial/JTAG
MAC: 1c:db:d4:7a:39:bc
Stub flasher running.
Flash memory erased successfully in 3.0 seconds.
Hard resetting via RTS pin...
michiel@Delphinus:~>
Tasmota flashing
Let’s tell esptool to use a baudrate of 460800 to speed things up a little with -b 460800.
michiel@Delphinus:~/Downloads> esptool.py -b 460800 write_flash 0 tasmota32s3.factory.bin
esptool.py v4.8.1
Found 33 serial ports
(...)
Serial port /dev/ttyACM1
Connecting...
Detecting chip type... ESP32-S3
Chip is ESP32-S3 (QFN56) (revision v0.2)
Features: WiFi, BLE, Embedded PSRAM 8MB (AP_3v3)
Crystal is 40MHz
MAC: 1c:db:d4:7a:39:bc
Uploading stub...
Running stub...
Stub running...
Changing baud rate to 460800
Changed.
Configuring flash size...
Flash will be erased from 0x00000000 to 0x002dcfff...
Compressed 2999744 bytes to 1868595...
Wrote 2999744 bytes (1868595 compressed) at 0x00000000 in 15.0 seconds (effective 1601.3 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
michiel@Delphinus:~/Downloads>
Use serial monitor
We use Platformio here as serial terminal emulator, but feel free to use any other tool.
(penv) michiel@Delphinus:~/development/gitlab.vanderwulp.be/waterlevel> pio device monitor
--- Terminal on /dev/ttyACM1 | 9600 8-N-1
--- Available filters and text transformations: debug, default, direct, 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
-tasmota32)-3.3.7(2026-02-19T13:57:18)
00:00:02.010 HTP: Web server active on tasmota-7A39BC-6588 with IP address 192.168.4.1
Setting the Wifi properties via the serial connection
You will have to enter a backlog command in the terminal as follows (and concluding by pressing the enter key):
backlog ssid1 FRITZ!Box 7530 AA; 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.
Do not worry, if your SSID contains spaces or special characters - mine is “FRITZ!Box 7530 AA” - it works as long as the “;” can be used as a delimiter.
Here we just pasted it:
00:08:54.810 CMD: backlog ssid1 Fritz; password1 xxxMySecretPasswordxxx
00:08:54.858 RSL: RESULT = {"SSId1":"Fritz"}
00:08:55.059 RSL: RESULT = {"Password1":"xxxMySecretPasswordxxx"}
00:08:56.938 APP: Restarting
and when restarted, we get to see that it worked and what our new IP address is:
00:00:00.082 Project tasmota - Tasmota Version 15.3.0(release-tasmota32)-3.3.7(2026-02-19T13:57:18)
00:00:01.001 WIF: Connecting to AP1 Fritz in mode HT20 as tasmota-7A39BC-6588...
00:00:02.815 WIF: Connected
21:02:00.050 HTP: Web server active on tasmota-7A39BC-6588 with IP address 10.0.3.77
Use the Tasmota webserver
We can disconnect the serial connection now and go with a browser to the given IP address.
From now on, we can configure Tasmota in the browser with the provided menu structure.
Tasmota also has a console (under the Tools menu), where you can type Tasmota commands. And there is a Berry console, where you can type Berry code (Berry is a programming language).
There is also a menu for file management, which we can e.g. use to store Berry software code.
Tasmota name
Let’s give the module a nice name:
DeviceName WaterLevel
FriendlyName WaterLevel
Hostname WaterLevel
SetOption55 1
The SetOption55 1 command enables the mDNS service, which allows us to use the following in a browser to access the Tasmota UI: http://waterlevel/.
Tasmota template
First, let’s set the template definition under Configuration -> Other and paste the following under “Template” and set the Activate checkmark.
The original text is:
{"NAME":"ESP32S3","GPIO":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1],"FLAG":0,"BASE":1}
We replace this by:
{"NAME":"Waveshare ESP32-S3-RS485-CAN","GPIO":[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9408,9440,1,1,9952,0,0,0,0,0,608,640,1,1,1,1,1,1,1,1,1],"FLAG":0,"BASE":1}
Tasmota configure GPIO pins
Instead of setting the template in one go, like described above, we can also set the GPIO pins individually.
Set GPIO17 to ModBr Tx.
Set GPIO18 to ModBr Rx.
Set GPIO21 to ModBr Tx Ena.
Set GPIO38 to I2C SCL 1.
Set GPIO39 to I2C SDA 1.
We do not use the CAN bus, so no need to configure it.
Save and restart.
Test Modbus configuration commands
Now we can see if the Modbus bridge commands work:
22:44:08.158 CMD: ModbusBaudrate
22:44:08.167 RSL: RESULT = {"ModbusBaudrate":9600}
22:44:23.239 CMD: ModbusSerialConfig
22:44:23.246 RSL: RESULT = {"ModbusSerialConfig":"5N1"}
The default baudrate of 9600 is good, the serial configuration not, it should be ‘8N1’, so we change it:
21:00:21.418 CMD: modbusserialconfig
21:00:21.424 RSL: RESULT = {"ModbusSerialConfig":"5N1"}
21:00:30.853 CMD: modbusserialconfig 8n1
21:00:30.862 RSL: RESULT = {"ModbusSerialConfig":"8N1"}
Timezone
Look up the commands here. 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
Copy/paste the command in the tasmota console. You can check the result with the command “Time”.
The RTC PCF85063A
A scan command on the console finds the RTC at the 0x51 address:
21:37:37.717 CMD: i2cscan
21:37:37.750 RSL: RESULT = {"I2CScan":"Device(s) found on bus1 at 0x51"}
The RTC retains the time when there is no internet connection.
Connect and mount everything
So, I put the level transmitter in the underground rainwater tank, and connected it to the RS485 module in the junction box.
The 24V DC power supply supplies the ESP32-S3 module, and also the level transmitter needs 24VDC.
A mains fuse completes the setup in the box, so that we can switch it on and off easily and have a protection in case something might go wrong.

Test RS485 connection
The specification of the QDY30A, my RS485 level transmitter, is Datasheet-RS485-LiquidLevelSensor.pdf.
The command to read the level:
ModbusSend {"deviceAddress":1, "functionCode":3, "startAddress":4, "type":"int16","count":1 "endian":"msb"}
Gives:
14:26:03.681 CMD: ModbusSend {"deviceAddress":1, "functionCode":3, "startAddress":4, "type":"int16","count":1 "endian":"msb"}
14:26:03.696 RSL: RESULT = {"ModbusSend":"Done"}
14:26:03.757 RSL: RESULT = {"ModbusReceived":{"DeviceAddress":1,"FunctionCode":3,"StartAddress":4,"Length":7,"Count":1,"Values":[1298]}}
I think the ‘1298’ is in mm, let’s verify that:
ModbusSend {"deviceAddress":1, "functionCode":3, "startAddress":2, "type":"int16","count":1 "endian":"msb"}
14:28:45.516 CMD: ModbusSend {"deviceAddress":1, "functionCode":3, "startAddress":2, "type":"int16","count":1 "endian":"msb"}
14:28:45.530 RSL: RESULT = {"ModbusSend":"Done"}
14:28:45.592 RSL: RESULT = {"ModbusReceived":{"DeviceAddress":1,"FunctionCode":3,"StartAddress":2,"Length":7,"Count":1,"Values":[17]}}
The 17 is cm according to the specification, but that is not correct, the waterlevel is 1 meter 29.8 cm. So, the unit is mm.
Create autoexec.be
Use the Tools -> Manage file system menu to create a file with the name “autoexec.be”, and start by putting the following Berry code:
#------------------------------------------------------
This 'autoexec.be' configures this module,
regularly reads the water level,
shows the waterlevel in de web UI,
and adds it to the teleperiod message for sensors.
------------------------------------------------------#
tasmota.cmd('Teleperiod 30') # send sensors to MQTT every 30s
tasmota.cmd('SetOption65 1') # Disable Fast Power Cycle Device Recovery
tasmota.cmd('ModbusBaudrate 9600') # set the Baudrate for the RS485 Modbus
tasmota.cmd('modbusserialconfig 8n1') # set the serial configuration for the RS485 Modbus
import string
var waterlevel = 0 # the level as integer, measured in mm
The global variable waterlevel will contain the actual measured value, and is initialised to 0 at boot.

Send ModBus command regularly
First thing to do is requesting the water level every 10s by sending a RS485 command:
# Trigger reading the waterlevel sensor every 10s:
def read_sensor()
tasmota.cmd('ModbusSend {"deviceAddress":1, "functionCode":3, "startAddress":4, "type":"int16","count":1 "endian":"msb"}')
end
tasmota.add_cron('*/10 * * * * *', read_sensor, "read_sensor")
You can see in the console that this works, and that the sensor replies.
React to the ModBusReceived
Then we use a rule to store the measured value whenever a ModBusReceived arrives:
# Whenever a ModBusReceived event occurs, we extract the current water level:
def modbus_rule(value, trigger)
if value.contains('Values')
waterlevel = value['Values'][0]
end
# print( waterlevel)
end
tasmota.add_rule("ModBusReceived", modbus_rule)
Add a driver
Next we add a driver for two purposes:
- show the measured value in the Tasmota web-page,
- add the value to the SENSOR JSON after every Teleperiod and in the MQTT messages.
class WaterLevelDriver
# Append the waterlevel to the teleperiod message (including MQTT)
def json_append()
var msg = string.format(',"WaterLevel":%i', waterlevel)
tasmota.response_append(msg)
end
# Show the measured waterlevel value in de web page
def web_sensor()
import string
var msg = string.format(
"{s}Water level{m}%i mm{e}",
waterlevel
)
tasmota.web_send_decimal(msg)
end
end
waterleveldriver = WaterLevelDriver()
tasmota.add_driver(waterleveldriver)
The complete autoexec.be
See also the complete autoexec.be.
MQTT configuration
Go to the menu Configuration -> MQTT and fill in the “Host”. If you have a set a password for the MQTT broker, fill in the name and password, too. Save.
The device restarts and now also shows an MQTT indicator at the top-right of the page:

We can test the messages on the Linux commandline as follows:
michiel@Delphinus:~> mosquitto_sub -h mqtt -t "tele/tasmota_7A39BC/#" -v | ts
This gives a stream of messages, e.g.:
apr 12 18:46:00 tele/tasmota_7A39BC/SENSOR {"Time":"2026-04-12T18:45:59","WaterLevel":1297}