- To build ESP32 based IoT devices for Home Assistant (HA), that run as long as possible on a battery.
- To get as close as possible to the responsiveness of battery powered Z-Wave binary sensors such as my AEON Labs, MultiSensor 6, model ZW100, Firmware: 1.10, which can run for months off batteries and update a motion sensor in HA UI in about 1s.
Having the ESP32 in deep sleep mode most of the time and paying attention to the power draw of any components still powered in this mode seems like a good place to start, but responsiveness from deep sleep is an issue.
Guide to Reduce the ESP32 Power Consumption by 95% shows that of the boards tested, DFRobot's FireBeetle has the lowest deep sleep power draw. It also has a connector and charger (from USB power) for a 3.7V Lipoly battery.
We are using Home Assistant (HA) as the IoT hub/controller/user interface and generate firmware with:
- ESPHome for IoT firmware using Wifi; and
- Arduino IDE for IoT firmware using Bluetooth Low Energy (BLE) (without WiFi).
This uses an AS312 (AM312) Mini PIR module to wake up the ESP32 from deep sleep when motion is detected and to send motion detection events. The PIR sensor has to always be powered, but draws very little power. Red and yellow LEDs are available to indicate state. For now I'm using a cheap ESP-WROOM-32 board without particularly good power characteristics.
esphome/esppir1.yaml
is the ESPHome input file for our first firmware, which uses the HA native API.
PIR:
- exposed as binary_sensor
pir
LEDs:
- Red: connected to switch
awake
; turned on after waking up; turned off before deep sleep - Yellow: connected to switch
stay_awake
; turned on to inhibit deep sleep e.g. to facilitate OTA updates.
Touch input:
- Touching the exposed end of the yellow wire on GPIO32 toggles the switch
stay_awake
.
- PIR sensor on duration (1 sec) is shorter than the time it takes the ESP32 to startup and the HA api to connect to it. WiFi connection takes 1-2 secs, but it is ~6 secs before the HA API is connected and a change in state is reflected in the HA UI.
- State changes before the API is connected are not reflected in the UI after the API connects.
- Even at the lowest priority,
on_boot
runs before the HA API is connected. - The template binary_sensor
motion
can be programatically controlled, but the gpio binary_sensorpir
cannot. - Awake from deep sleep is the same as RST or first boot; (non-RTC) memory is initialised and
setup()
is run, thenloop()
(these are Aduino IDE entry points implemented under-the-hood by ESPHome). I thought that power cycle would clear RTC memory, but it appears to be non-volatile (at least on my current test board) and survives power cycle, RST, OTA update, as well as deep sleep. This means it cannot be used to distinguish between deep sleep wake up and other types of start up. I had wanted to togglemotion
(see below) only on deep sleep wake up.
- Use template binary_sensor
motion
as the high level motion sensor. - Under normal operation (
state == 3
in the yaml)pir
state changes are copied tomotion
. - When
pir
wakes the ESP32on_boot
is run. It waits for the HA api to connect (or timeout because we don't want to flatten the battery waiting too long) then runsstartup_motion_toggle
. startup_motion_toggle
turnsmotion
on then off (to reflect thatpir
has gone on and off before the HA api was connected) then sets normal operation (state = 3
in the yaml).sleep_timer
is restarted whenpir
changes state. When it expires we enter deep sleep (unlessstay_awake
is on).
Waiting for the HA API to connect is too slow for many applications e.g. to turn on a light for safety or start video recording for security. Using a static IP could shave a little off the WiFi connection time, but not enough to change this.
The long connection delay wouldn't matter for some applications e.g. an analogue soil moisture sensor that sends one or more values each time it wakes up then sleeps a configured period (e.g. 30 min). The sensor need not be powered in deep sleep.
Same as Experiment 1
.
esphome/esppir2.yaml
is the ESPHome input file for our second firmware, this time using a static IP address and MQTT instead of the HA native API. PIR, LED and touch input usage is the same as in Experiment 1
. As the MQTT connection is under the control of the IoT device (rather that waiting for HA to poll it) it should be quicker to establish. The logic is simpler without any need for the template binary_sensor motion
and the state
variable.
- It still takes ~4s between a motion event and it being reflected in the HA UI.
- No MQTT message is generated for the PIR on event that awakens the ESP32. A corresponding PIR off MQTT message is generated.
- Turning the
awake
switch off and then immediately entering deep sleep, the change of switch state is not reflected in the HA UI (the corresponding MQTT message is not sent).
- In
on_boot
we manually send the missing PIR on MQTT message. The automatic PIR off MQTT message is transmitted immediately afterwards, so the on duration is very short. - A 300ms delay between turning the
awake
switch off and entering deep sleep appears to be sufficient for the change of switch state to be reflected in the HA UI. 100ms is not sufficient.
This is marginally faster (and simpler) than Experiment 1
, but still too slow for many applications.
Same as Experiment 1
.
arduinoIde/BLE_server.ino
is the Arduino IDE source file for our third firmware, this time implementing a Bluetooth Low Energy (BLE) server (with no WiFi).
- The esphome BLE client can't use Bluetooth security (pair with a PIN) and requires the server MAC address to be configured, which is rather limiting.
- A proxy will be required to implement the BLE client and send the data to HA (using either the HA native API or MQTT, so it requires WiFi). The proxy would need to be always awake (no deep sleep) and so not battery powered. Using an ESP32 as the proxy, each could handle up to three BLE servers (a limitation of the ESP32 Bluetooth stack).
- What happens with multiple simultaneous clients?
This BLE server:
- waits for client connection, enters deep sleep if it doesn't happen by
TIME_CONNECT
- notifies motion updates, enters deep sleep after
TIME_NOTIFY
since the last update.
After a deep sleep it restarts from scratch and the client must reconnect.
PIR, LED and touch input usage is the same as in Experiment 1
.
The nRF Connect
app (on an Android or Apple mobile device) is suitable as a test client.
With an unpaired client, the sequence of events is:
setup()
completesloop()
called repeatedly- when the client connects:
MyServerCallbacks::onConnect
is called - when the client says yes, pair with the device:
MySecurity::onPassKeyNotify
prints a random PIN - when the client enters this PIN:
MySecurity::onAuthenticationComplete
is called - the client can then poll or elect to recieve notifications
With a previously paired client, the sequence is the same except without step 4. This sequence, from server wakeup through to the client receiving notifications, can complete in about 1s.
This matches our goal of achieving close to the responsiveness of Z-Wave devices.
A bare ESP-WROOM-32 board.
arduinoIde/BLE_client.ino
is the Arduino IDE source file, this time implementing a Bluetooth Low Energy (BLE) client. At the moment it has no WiFi, but this will be needed later to communicate with HA.
At the moment BLE security is not implemented.
- Add BLE security to the client.
- Change the server to only allow connection with security.
- Add WiFi and HA API or MQTT to the client.
- BLE mesh networking sounds interesting; Arduino IDE can't do it; Exspressif IDE can but it sounds complicated to implement.