A Complete MQTT+ESP32 Sensor Tutorial

Building an MQTT enabled door sensor using ESP32 and PlatformIO

Anuradha Wickramarachchi
5 min readFeb 5, 2021

This article is motivated by my previous article on ESPHome. Previously, we built a door sensor using ESPHome library. However, using an off-the-shelf platform grants us with little to no flexibility when it comes to micro-optimizations. Having said that, I noted the best implementation consumed over 1mA and took nearly 2 seconds to respond (Even after using MQTT, ESPHome API is known to be slower with HomeAssistant).

Photo by chris panas on Unsplash

Key Points to Consider

There are several considerations that we need to think of when it comes to a critical component like a door sensor.

Photo by Zan on Unsplash

Response Time and Reliability

A door sensor is supposed to be fast and reliable. It should also be able to withstand a certain degree of tampering. It someone is capable of cutting some wires to convince a door is closed the sensor is basically useless. At the same time, the sensor is useless if the notifications are delayed by a significant amount. To make the system tamper-proof a little, we can use a normally closed reed switch. Which means, damaging the switch, or cutting the reed switch wire will default the sensor to broadcast door open.

Power Usage

The sensor should last long with the batteries. Replacing or recharging batteries too often is cumbersome. You need the device to work even if you’re away for months. We will see this under the Deep Sleep topic later in this article.

Standardization

The device we make should be discovered and ready to be used by popular platforms without special configurations. In this article, we will use HomeAssistant discovery mode as the standard. This should work similarly for other MQTT based IoT automation platforms.

Circuitry

We will be using the following components;

  1. CR123A battery (3.7V 2300mAh) with a PCB mountable holder
  2. ESP32 Mini D1 module
  3. 100kOhm resistor
  4. Reed switch (Normally Closed/NC)

Our hookup is as follows.

Circuit Diagram by Author

Note that, we have an external pull-down resistor. When the door is closed the GPIO Pin 4 will be high.

MQTT Topics Structure

If MQTT seems a new topic, you can always read the following article!

Let’s see how we structure our MQTT commands for our door sensor. First, we need a discovery message. This will sort of self explain the sensor to the other party, which is HomeAssistant in our case. Following will be our discovery message. You can expand this with more details if you wish!

{
"name": "door_sensor_1304A4",
"device_class": "door",
"state_topic": "door_1304A4/state",
"unique_id": "door_sensor_1304A4"
}

Note that we have a Code at the end of the name as 1304A4. This is to uniquely identify multiple sensors. In simple terms, this is the last 3 pieces of the mac address. I did this because I was bored to assign unique codes myself and edit my program. Now we always get a unique id for each sensor. :-)

Each sensor must have a unique_id in order to identify them separately. Changing it in future can break the functionality!

We this is the MQTT discovery body or discovery JSON. For home assistant, by default the discovery prefix is homeassistant. So I am using the following topic to publish the discovery JSON.

homeassistant/binary_sensor/door_1304A4/config

We can easily generate these topics using the device mac address!

Photo by Jordan Harrison on Unsplash

Operation

When the door is opened, ON will be published to the topic door_1304A4/state. OFF will be published when the door is closed. Whenever the device starts, the current state will be published too. The discovery message will be published too.

Basic Operation and Deep Sleep

Now that we have our device in place, we need to ensure it runs optimally and responsively. For this, we can use Interrupts and Deep Sleep function.

Look for interrupts on GPIO Pin 4 and if the device receives no interrupts after 10 seconds sleep we see another interrupt

Attaching an interrupt is easy;

// in setup function
attachInterrupt(digitalPinToInterrupt(4), door_changed, CHANGE);
// function to be triggered
void door_changed()
{
last_changed = millis();
needs_update = true;
}

Note that we attach interrupt for a state change on Pin 4. Within the Interrupt Service Routine (ISR), we change a global variable value. These variables must be initialized in global scope as follows.

// volatile variables used to ensure atomic read/write
volatile unsigned long last_changed = 0;
volatile boolean needs_update = false;

We can use last_changed to check for 10 second idle time. Variable needs_update can be used to tell the main loop to make mqtt messages, ON or OFF. Finally, the boolean will be assigned false.

Now we need to manage the sleep routine. Our sleep function is as follows;

if (millis() - last_changed > 10000)
{
if (digitalRead(4) == HIGH)
{
esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,
ESP_EXT1_WAKEUP_ALL_LOW);
}
else
{
esp_sleep_enable_ext1_wakeup(BUTTON_PIN_BITMASK,
ESP_EXT1_WAKEUP_ANY_HIGH);
}
start_sleep();
}

If the door is closed, the Pin 4 will be HIGH. So the wakeup shall happen when it goes LOW and vice versa. This is done as above. The variable BUTTON_PIN_BITMASK simply is the hex value of 2^4 (pin number). I am using ext1 mode of wakeup as I noted power consumption is lower in this mode.

The sleep function is as follows; I learnt a lot from this article. The following code is from the original writer. :-)

void start_sleep()
{
WiFi.disconnect(true);
WiFi.mode(WIFI_OFF);
btStop();
adc_power_off();
esp_wifi_stop();
esp_bt_controller_disable();
esp_deep_sleep_start();
}

Now our program is complete. I haven't included all the codes here, but feel free to have a look at the original repository in the following link. I have used the PlatformIO IDE.

Benchmarks

The complete setup consumed about 50mA, which makes it’s lifetime around 13 hours. During deep sleep, the device only consumes 50–150micro amperes. In the worst case, the lifetime would now be around a year!.

The WiFi connection after a deep sleep took 400ms in the worst case. Usually, it is about 350ms. The MQTT message is received under a second or 1s latest.

I am quite happy with the setup. This is my setup and I am quite happy with it. Have a look! :-)

The JST connector connects to the reed switch which is attached on the door and frame.

Image by Author

I hope you enjoyed reading the article! I’ll see you in another interesting writeup.

Cheers!

--

--