I’m pretty happy with my SensorPush outdoor temperature/humidity/pressure sensor. It’s not cheap but it is calibrated to work in a Canadian winter (most of it, at least). Their BLE adverts are automatically detected by HomeAssistant and decoded.

Sometimes it appears to have a bit of a nap, adverts stop arriving (and I’ve checked with other BLE receivers) until the Android app wakes it up, probably the direct Bluetooth connection is all it wants.

Also, the battery voltage doesn’t appear to be broadcast. SensorPush do actually publish an API but they only show the GATT characteristics that require active scanning. Also my readings don’t agree with their statement that I can read battery temperature. The Android app happily returns 20.9C (which seems high!), raw values seem to float around 0xffff.

So, rather than try to get an ESP32 to lock up its active connection, draining the battery to see how quickly the battery is draining, I’m going for something more intermittent.

If you write some JSON to the MQTT broker that HomeAssistant is monitoring, it’ll discover it and start tracking the appropriate state. For instance, at homeassistant/sensor/underlay/outdoor_battery_voltage/config, publishing the following:

{
  "name": "My Battery Voltage",
  "device_class": "voltage",
  "state_topic": "homeassistant/sensor/underlay/outdoor_battery_voltage/state",
  "unique_id": "unique name",
  "unit_of_measurement": "mV"
}

Means that writing e.g. 3050 to the state topic there will appear in HomeAssistant.

The following Python script will use bleak to grab the BLE sensor and read the Battery Voltage characteristic, and then use aiomqtt to write it into the broker.

This is replicating a bit of another project but that only reads BLE adverts. I forget now but this might have been where the original Home Assistant version comes from.

"""
For Home Assistant, this needs a JSON config publishing (retain on) at homeassistant/sensor/some_name/config first
"""

import argparse
import asyncio
from binascii import hexlify
import logging
import struct

import aiomqtt
from bleak import BleakClient


BATT_HANDLE = "ef090007-11d6-42ba-93b8-9dd7ec090aa9"

async def main():
    parser = argparse.ArgumentParser(description="Sensorpush 98E.wx battery voltage relay")

    # Mandatory args, will fail without
    parser.add_argument("address", help="Sensor address in the form AA:BB:CC:DD:EE:FF")
    parser.add_argument("server", help="MQTT server")
    parser.add_argument("topic")

    parser.add_argument("--port", type=int, default=1883)

    args = parser.parse_args()

    logging.basicConfig(level=logging.INFO)
    logging.info("Starting up")

    async with BleakClient(args.address) as client:
        logging.info("Connected")
        res = await client.read_gatt_char(BATT_HANDLE)

    logging.debug("Read bytes " + hexlify(res).decode())
    voltage, temp_raw = struct.unpack_from("Hh", res)
    logging.info(f"Battery voltage {float(voltage)/1000:.2f}V")

    logging.debug(f"Connecting to {args.server}:{args.port}")
    async with aiomqtt.Client(args.server, port=args.port) as client:
        logging.info("Publishing to " + args.topic)
        await client.publish(
            args.topic,
            payload=voltage,
            qos=2,
        )


asyncio.run(main())

To automate this, I’ve created1 a systemd user service. Not actually tried these before but I don’t need privileges and it’s just my desktop, why bother? If this was on a headless RPi or something, then yeah may as well be a normal service. cmd.sh is a bash one-line that’s invoking all the arguments to my script. Just because I’m using a python environment, I thought it would be easier to read.

[Unit]
Description=Read the Sensorpush battery voltage

[Service]
ExecStart=/path/to/venv/cmd.sh
WorkingDirectory=/path/to/venv
Type=oneshot
RemainAfterExit=true
StandardOutput=journal

I then used a timer to run this just after login. Systemd timers are great, in that you have flexibility to do things other than running /bin/sh as root. There’s also the ability to print the schedules of the timers you write and check they’re correct, although that doesn’t help here.

[Unit]
Description=Run the ble service every boot

[Timer]
OnBootSec=15min
Unit=ble.service

[Install]
WantedBy=default.target

ESPhome version (unsuccessful)

On the HA forums, one person has apparently read the battery voltage from a sensor and reported success. However they also are trying to read temperature every fifteen seconds so they’re a little crazy.

I tried something similar but without much success. As compiling for this particular board is very slow, I got bored and gave up.

# Connects and remains connected by default
ble_client:
  - mac_address: $sensorpush_garden_mac
    id: sensorpush_ble_id1
    on_connect:
      then:
        - lambda: |-
            ESP_LOGI("ble_client_lambda", "Connected to SensorPush device");            
    on_disconnect:
      then:
        - lambda: |-
            ESP_LOGI("ble_client_lambda", "Disconnected from SensorPush device");            

# Should be off by default but HA keeps turning it on?
switch:
  - platform: ble_client
    ble_client_id: sensorpush_ble_id1
    name: "Enable Active Connection"
    id: active_outdoor_ble
    restore_mode: ALWAYS_OFF

button:
  - platform: restart
    name: "$esp_name Restart"
  - platform: template
    name: "Request Battery Voltage"
    on_press:
      then:
        - logger.log: Battery voltage update requested
        - component.update: outdoor_battery_voltage

sensor:
  # Error code 10 when doing this
  - platform: ble_client
    type: characteristic
    id: outdoor_battery_voltage
    ble_client_id: sensorpush_ble_id1
    name: "SensorPush Battery Voltage"
    service_uuid: ef090000-11d6-42ba-93b8-9dd7ec090ab0
    characteristic_uuid: ef090007-11d6-42ba-93b8-9dd7ec090aa9
    icon: 'mdi:battery-bluetooth-variant'
    unit_of_measurement: 'mV'
    update_interval: 12h
    notify: YES
    device_class: "voltage"
    state_class: "measurement"
    lambda: |-
      return (float)((int16_t)(x[1]<< 8) + x[0]);      

This defines a connection to the sensor, which is then established (watch in the logs) and remains, unless disconnected (as I mentioned right at the start). I used a BLE Client switch to try to default the connection to off, but this appeared to result in the connection never establishing at all. Also Home Assistant would enable the switch when it’s set to default off. And most of the logs vanishing, more annoyingly. Finally the actual characteristic read fails, for a reason I’ve not decoded.

I’ll try it again at some point.


  1. systemctl --user --force --full edit ble.service the first time to create the file. ↩︎