I have a SensorPush outdoor temperature/pressure/humidity sensor that I’m pretty happy with. It’s BLE, it’s reliable, its range is good, it seems pretty accurate. However it doesn’t report its battery level in its adverts, but the manufacturer app can read this level through reading GATT characteristics. I previously had a go at adding this to my HA two years ago. It worked for a while, but the distance between my desktop at the front of the house, and the sensor in the back garden became too far when I tried to protect the sensor from the sun with more aluminium foil.
Using a desktop for this always felt a little over-engineered, so moving back to the UK has given me an excuse to try it again with ESPhome.
To reiterate this then, I don’t want to hold a connection open to the sensor 24/7 to drain the battery I’m trying to read. It wouldn’t surprise me if the standard allows for doing this at minimal extra battery cost, but if I’m wrong I get to buy an expensive fat coin cell. I definitely know the Android app will routinely stop the sensor from broadcasting while it’s connected, so I’m not giving it any help.
Code
# Define the actual device, give it an ID
ble_client:
- id: sensorpush
mac_address: xx:xx:xx:xx:xx:xx
auto_connect: false
on_connect:
then:
- component.update: sensorpush_battery
# Define the battery level characteristic
# but leave it disabled
sensor:
- platform: ble_client
type: characteristic
ble_client_id: sensorpush
name: "Sensorpush Battery Voltage"
service_uuid: 'ef090000-11d6-42ba-93b8-9dd7ec090ab0'
characteristic_uuid: 'ef090007-11d6-42ba-93b8-9dd7ec090aa9'
icon: 'mdi:battery'
unit_of_measurement: 'mV'
device_class: voltage
update_interval: never
entity_category: diagnostic
id: sensorpush_battery
# battery millivolts is the first two bytes as an unsigned int
lambda: |-
return *((uint16_t*)(&x[0]));
filters:
# Disconnecting will set this to nan, remove that value
- filter_out:
- nan
on_value:
- logger.log: "Triggering disconnect"
- ble_client.disconnect: sensorpush
- esp32_ble_tracker.start_scan:
- script.stop: start_sensorpush_connect
script:
- id: start_sensorpush_connect
then:
- esp32_ble_tracker.stop_scan:
- delay: 5s
- logger.log: "Triggering connect"
- ble_client.connect: sensorpush
- delay: 1min
# This is a backup if the connection hung for some reason. Give up.
- logger.log: "Timeout"
- ble_client.disconnect: sensorpush
- esp32_ble_tracker.start_scan:
time:
- platform: homeassistant
on_time:
# Every 6 hours, trigger this
- seconds: 0
minutes: 0
hours: "/6"
then:
- script.execute: start_sensorpush_connect
# No config, enable the proxy and tracker
bluetooth_proxy:
esp32_ble_tracker:
logger:
api:
Explanation
There are four parts to this asynchronous automation:
- The ID for the physical sensor in
ble_client
- The method of reading a
ble_client
-based sensor - A script for firing a read of the battery
- A timer to read the battery regularly
To explain what’s going on here, it might make more sense to read from the bottom.
A timer runs every 6h1, and triggers our first script.
The script:
- Disables the BLE scanning that is the primary purpose of this device
- Waits 5s otherwise the connection completely fails in weird ways. Pretty sure this is due to the stop of BLE scanning is only scheduled
- Connects to the SensorPush
- Does some clean-up we’ll come back to
The ble_client
configuration for the SensorPush has an on_connect
trigger that then pokes the actual BLE GATT sensor.
That’s it.
The sensor
knows what characteristic
to read once connected, and how to decode the raw value in its lambda
.
It has a filter to ignore NaNs so its on_value
2 will only trigger on real data.
This trigger disconnects the SensorPush (and tries to set the sensor to unavailable/NaN), hence the filter.
It finally restarts the BLE tracker we stopped at the beginning.
The final part of the script is a backup, and restarts the BLE tracker if something in the previous logic broke. It is ignored if it does run twice, worst case.
I also need the Bluetooth proxy and the BLE tracker. You most likely do too, although in theory it’s not needed for this specific application. In which case, a lot of this gets simpler.
End
Thanks (again, again) to sseib
for helping me work out which bits of this script blocks and which bits don’t.
Someone industrious could maybe find the race that requires the first delay to fix.
There is still one problem, which is this is adding a sensor to my ESPhome device in HomeAssistant with the battery level and not the actual SensorPush one. I don’t know how to fix this at the moment. Some parts of the UI can assign sensors to devices, but it’s not obvious on how to do this without going very deep into the code. Or faking it with MQTT.
Technically, the simplest solution might be for ESPHome to send the result via MQTT 🤦
Or every 5m, or maybe even off a button if you’re having issues. I started this with the
interval
trigger which is fine but restarts of the device will affect the update time. ↩︎on_raw_value
is available if you want to trigger on unfiltered stuff. I keep niggling as to whether this is somehow required here. ↩︎