Home Automation, Part 1: ESP32 and Ultrasonic Sensor Setup

First article in a series of articles around my personal home automation project. Using an ultrasonic sensor and ESP32 module to detect water tank capacity

ESP32-WROOM-32 devkit held in hand, jumper wires attached

Thanks to AI making any kind of programming very accessible, I decided to try my hand at automating certain things in my home. I’ll publish a series of articles which share my learnings as I make progress with the project.

The Use Case

We have a couple of overhead water tanks which supply water throughout the house. The challenge we have at the moment is our lack of awareness of the water level in these tanks. We will only get to know that a tank is empty when water stops running through the taps — the event that requires turning on a motor to refill the tanks. And after this, we usually detect that the tank is full only when the tank starts overflowing and we see water running waste into the drain. There are simple solutions to these problems available in the form of buzzers or what not but I wanted to pick this use case to begin the home automation journey. I want to eventually get to a point where the water motor that fills up the tank should switch on and off autonomously depending on the water level.

Approach

Instead of picking up and solving the entire problem at once, I picked a smaller MVP to be addressed first. This should help me do both a feasibility check that the system would work and also give me confidence that I can do this. So, two primary questions need answers.

  • How to detect the level of water in the tank?
  • Once I detect this, how do I publish that data?

I decided to worry about all the other problems later. So this article would only talk about these two. The rest of the setup will be covered in follow-up articles in this series.

How to detect the level of water?

A02YYUW waterproof ultrasonic sensor held in hand Water is solid to sound. To sound waves it is yet another medium from which they end up bouncing off. Lot of sensors are built around this fundamental that sound waves can bounce back when they meet water. So the answer to our first question is an Ultrasonic sensor.

There are various ultrasonic sensors available with varying degrees of cost and their corresponding quality.

The following are a few models I found

Amongst the three, A02YYUW module turned out to be a good fit as I was planning to install it on the overhead tank and it is prone to rain, dust and humidity from the water within the tank itself. I purchased one from a local electronics market in Hyderabad for Rs. 1850/-.

How to publish the data from the sensor?

ESP32-WROOM-32 devkit held in hand, jumper wires attached I needed a small microcontroller board which can do two things, connect to the sensor to read the water level and connect to WiFi to transmit the sensor data read. This can be achieved either using an Arduino board or an ESP32. An ESP32 is a better choice here as it has better performance and is widely used with the IoT ecosystem.

So, along with the Ultrasonic Sensor, I purchased an ESP32-WROOM-32 Devkit for Rs.240/- from the same electronics store. I also purchased a few jumper cables of all combinations (male-male, male-female and female-male) to connect easily.

The Voltage problem

Another reason, that I did not call out earlier, for going with A02YYUW sensor is its operating voltage. While researching, I found out that GPIO pins of ESP32 expect to receive 3.3V, whereas the other ultrasonic sensor modules transmit 5V. This would be harmful to the ESP32. If we use a sensor that transmits 5V, we would have to build a voltage step-down converter.

The Implementation

So the final setup is going to be an ultrasonic sensor connected to ESP32, which is in turn connected to WIFI. For this iteration, I limited the setup to just connect to WIFI rather than transmitting the reading over WiFi. So I will tether to ESP32 module and see if I am able to read the data in development mode.

Here’s how the physical pieces sit relative to each other for this iteration:

%% float-right
flowchart TB
    subgraph Tank["Overhead Water Tank"]
        Sensor["A02YYUW\nUltrasonic Sensor"]
    end

    subgraph Board["ESP32-WROOM-32 Devkit"]
        UART["UART RX · GPIO16\n9600 baud"]
        Firmware["ESPHome firmware"]
    end

    subgraph Laptop["Development Mac"]
        CLI["esphome CLI"]
    end

    subgraph LAN["Home WiFi Network"]
        Router["Router"]
    end

    Sensor -->|"4-byte frame · 3.3V"| UART
    UART --> Firmware
    Laptop -->|"USB tether\npower + flash + logs"| Board
    Firmware -.->|"WiFi (connectivity only,\nno publish yet)"| Router

The Hardware Setup

As I am not going to do the full installation of the sensor with this iteration itself, the only hardware setup needed is

  • Properly connect the A02YYUW sensor to ESP32.
  • Connect my Mac and ESP32 with a MicroUSB Cable. This will work as both power supply and a data tether for flashing the ESP32 module.

Connections between Ultrasonic Sensor and ESP32

I have an ESP32 module which has DXX labelled pins. The A02YYUW has a 4-pin JST connector. This is how I connected the ESP32 and the A02YYUW sensor

ESP32 Pin A02YYUW Pin What for?
VIN 1 Voltage Input to the sensor
GND 2 Ground/Voltage output
RX2 4 Signal Receiver

I didn’t connect anything to the Transmit pin (TX2) as we are only reading data from the sensor. I then found an old MicroUSB cable and connected the ESP32 to my laptop.

The Software Setup

This is the interesting bit. The next task is to flash the ESP32 with the functionality we want: Connect to WiFi and Connect to A02YYUW. This can either be done using bare bones C/C++ or using multiple frameworks that are available out there. While researching for the right framework, I came across ESPHome (https://github.com/esphome/esphome). It is a framework that supports flashing the ESP32 using a configuration file. We don’t have to write the daunting C/C++ code. ESPHome also supports integration with Home Assistant which we will talk about in a future article.

I installed ESPHome locally and had AI prepare this configuration file. I’ve trimmed the comments down here — the interesting bits are explained below the code instead.

esphome:
  name: overhead-tank-monitor
  friendly_name: "Overhead Tank Monitor"

esp32:
  board: esp32dev  # ESP32-WROOM-32 / ESP-32D
  framework:
    type: arduino

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Overhead-Tank-Monitor Fallback"
    password: !secret ap_fallback_password

captive_portal:
logger:
api: {}

ota:
  - platform: esphome

uart:
  - id: uart_tank1
    rx_pin: GPIO16
    baud_rate: 9600

interval:
  - interval: 50ms
    then:
      - lambda: |-
          static uint8_t buf[4];
          static uint8_t len = 0;
          static uint32_t last_publish = 0;
          const uint32_t PUBLISH_PERIOD_MS = 3000;  // a tank's level doesn't change fast; matches level-% update_interval
          while (id(uart_tank1).available()) {
            uint8_t data;
            id(uart_tank1).read_byte(&data);
            if (len == 0 && data != 0xFF)
              continue;
            buf[len++] = data;
            if (len == 4) {
              uint8_t checksum = buf[0] + buf[1] + buf[2];
              uint32_t now = millis();
              if (buf[3] == checksum && now - last_publish >= PUBLISH_PERIOD_MS) {
                last_publish = now;
                uint16_t distance = (uint16_t(buf[1]) << 8) | buf[2];
                ESP_LOGI("tank1", "Distance: %u mm", distance);
                id(tank1_raw_distance).publish_state(distance);
              }
              len = 0;
            }
          }

sensor:
  - platform: template
    name: "Tank 1 Raw Distance"
    id: tank1_raw_distance
    unit_of_measurement: "mm"
    accuracy_decimals: 0

  - platform: template
    name: "Tank 1 Water Level"
    id: tank1_level_pct
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: 30s
    lambda: |-
      const float SENSOR_TO_EMPTY_MM = 2000.0;
      const float SENSOR_TO_FULL_MM  = 200.0;
      if (isnan(id(tank1_raw_distance).state)) {
        ESP_LOGI("tank1_pct", "raw distance not yet available (NaN)");
        return {};
      }
      float raw = id(tank1_raw_distance).state;
      float pct = (SENSOR_TO_EMPTY_MM - raw) / (SENSOR_TO_EMPTY_MM - SENSOR_TO_FULL_MM) * 100.0;
      if (pct < 0) pct = 0;
      if (pct > 100) pct = 100;
      ESP_LOGI("tank1_pct", "raw=%.0f mm -> level=%.1f %%", raw, pct);
      return pct;

A few things worth calling out about that config that didn’t fit in a comment:

  • The UART parser in the interval: block is hand-rolled, not ESPHome’s built-in jsn_sr04t component. I tried the built-in component first and it produced nothing — no valid readings, no blind-zone warnings, zero output — across repeated tests, including direct-to-Mac power and 30+ seconds of continuous hand-sweep motion. The exact same UART byte stream, read manually with a parser matching the sensor’s 0xFF-header/high-byte/low-byte/checksum frame format, read reliably the whole time. So that hand-rolled parser is now the permanent driver for this sensor.
  • Only Tank 1 is wired up. Tank 2 gets its own uart: entry once the second A02YYUW is mounted.
  • MQTT is left out entirely for now — Mosquitto isn’t running yet, so there’s nothing to publish to. Both of these, along with the Home Assistant integration, are for a future article.
  • SENSOR_TO_EMPTY_MM and SENSOR_TO_FULL_MM are still placeholders. They need calibrating against the real tank once it’s mounted.

Flashing the ESP32

The command to run to flash the ESP32 module is this

esphome logs esphome/tank-monitor.yaml --device /dev/cu.usbserial-0001

You will have to identify the device identifier of the ESP32 connected through USB and provide that as the --device argument. The following command on macOS provides the files of devices connected through USB.

ls /dev/cu.usbserial*

This will only have to be done one time. After we flash ESPHome for the first time, we can then flash it OTA using WiFi. If you notice, the configuration YAML contains the following entry to support that

ota:
  - platform: esphome

So, after the first flash, the following command reflashes the ESP32 over WiFi with any updates to the configuration YAML.

esphome run esphome/overhead-tank-monitor.yaml --device overhead-tank-monitor.local

The Test

You should be able to see the logs by running

esphome logs esphome/overhead-tank-monitor.yaml --device overhead-tank-monitor.local

If you are not able to connect using the hostname, first try connecting to it through the USB tether.

A successful WiFi connection would produce the following logs

[16:20:42.852][C][wifi:1553]: WiFi:
[16:20:42.852][C][wifi:1553]:   Local MAC: 8C:94:DF:72:47:34
[16:20:42.852][C][wifi:1553]:   Connected: YES
[16:20:42.852][C][wifi:1259]:   IP Address: 192.168.68.114
[16:20:42.852][C][wifi:1270]:   SSID: 'Gyandus'
[16:20:42.852][C][wifi:1270]:   BSSID: 6C:5A:B0:C9:E5:1A
[16:20:42.852][C][wifi:1270]:   Hostname: 'overhead-tank-monitor'
[16:20:42.852][C][wifi:1270]:   Signal strength: -58 dB ▂▄▆█
[16:20:42.852][C][wifi:1270]:   Channel: 4
[16:20:42.852][C][wifi:1270]:   Subnet: 255.255.255.0
[16:20:42.852][C][wifi:1270]:   Gateway: 192.168.68.1
[16:20:42.852][C][wifi:1270]:   DNS1: 49.205.171.194
[16:20:42.852][C][wifi:1270]:   DNS2: 49.207.34.210

And the Ultrasonic sensor should produce the following log entries if everything is successful.

[16:22:42.685][I][tank1_pct:111]: raw=2643 mm -> level=0.0 %
[16:22:42.787][S][sensor]: 'Tank 1 Water Level' >> 0 %
[16:22:43.684][I][tank1:076]: Distance: 2643 mm
[16:22:43.793][S][sensor]: 'Tank 1 Raw Distance' >> 2643 mm
[16:22:45.690][I][tank1_pct:111]: raw=2643 mm -> level=0.0 %
[16:22:45.800][S][sensor]: 'Tank 1 Water Level' >> 0 %
[16:22:46.733][I][tank1:076]: Distance: 379 mm
[16:22:46.836][S][sensor]: 'Tank 1 Raw Distance' >> 379 mm
[16:22:48.688][I][tank1_pct:111]: raw=379 mm -> level=90.1 %
[16:22:48.800][S][sensor]: 'Tank 1 Water Level' >> 90 %
[16:22:49.762][I][tank1:076]: Distance: 377 mm
[16:22:49.837][S][sensor]: 'Tank 1 Raw Distance' >> 377 mm
[16:22:51.692][I][tank1_pct:111]: raw=377 mm -> level=90.2 %
[16:22:51.797][S][sensor]: 'Tank 1 Water Level' >> 90 %
[16:22:52.806][I][tank1:076]: Distance: 269 mm
[16:22:52.898][S][sensor]: 'Tank 1 Raw Distance' >> 269 mm
[16:22:54.689][I][tank1_pct:111]: raw=269 mm -> level=96.2 %
[16:22:54.794][S][sensor]: 'Tank 1 Water Level' >> 96 %
[16:22:55.834][I][tank1:076]: Distance: 262 mm
[16:22:55.948][S][sensor]: 'Tank 1 Raw Distance' >> 262 mm

The distance is tested using my own hand as an obstacle. That should be it for now — we have evidence that the setup works. Time to move to the next step.