Ramblings of an aging IT geek
← Ramblings of an aging IT geek
hardware

poking at a cheap ble thermometer until it talked

How I sniffed the Bluetooth Low Energy protocol of a five-pound thermometer to pull readings without its app.

A soldering iron and electronics on a bench

I bought a five-pound BLE thermometer-hygrometer off the usual marketplace, the little white square ones with the e-ink screen. It works fine with its own app. The app is the problem: it wants an account, it phones home, and it has no way to get the reading out into anything I own. I just wanted the temperature in Home Assistant. So I decided to find out what it was actually saying over the air.

The first job is seeing the traffic. On Android, the developer options have a "Bluetooth HCI snoop log" toggle, which dumps every packet to a file you can open in Wireshark. Turn it on, open the vendor app, let it connect and read for a minute, then pull the log off the device. No special hardware needed for a first look, which is a pleasant surprise the first time you discover it.

In Wireshark, filtered to btatt, the structure is the usual BLE story. The device exposes a handful of GATT services, each with characteristics that are just handles you read from or subscribe to. Most of the noise is the standard stuff: device name, battery service, the generic attribute profile. The interesting one was a custom service with a vendor UUID and a characteristic that the app subscribed to for notifications.

A small circuit board

The payload was five bytes. Watching it change as I breathed on the sensor told me most of what I needed:

T2 09 1F 64 0A

The first two bytes were temperature as a little-endian signed 16-bit integer in hundredths of a degree, so 0x09T2 read back as roughly 23.5 °C, which matched the screen. The third byte was humidity as a plain percentage. The fourth was battery percent, and the last was a counter that incremented each notification. No encryption, no obfuscation, no checksum worth the name. For a fiver, you get exactly the engineering you pay for.

From there it is just bleak in Python to confirm it from the laptop:

import asyncio
from bleak import BleakClient

ADDR = "A4:C1:38:xx:xx:xx"
CHAR = "0000fff2-0000-1000-8000-00805f9b34fb"

def handle(_, data: bytearray):
    temp = int.from_bytes(data[0:2], "little", signed=True) / 100
    hum = data[2]
    print(f"{temp:.1f} C  {hum}% RH  batt {data[3]}%")

async def main():
    async with BleakClient(ADDR) as c:
        await c.start_notify(CHAR, handle)
        await asyncio.sleep(30)

asyncio.run(main())

That printed clean readings within seconds, and from there wiring it into Home Assistant via a passive BLE integration was the easy part. The whole exercise took an evening, most of which was squinting at hex and second-guessing the byte order.

The lesson I keep relearning with these gadgets: there is rarely anything clever to defeat. The "protocol" is a few bytes a junior firmware engineer threw together against a deadline. The skill is not in cracking it, it is in being patient enough to watch the numbers move and trust what they tell you. And the reward is a sensor that answers to me instead of to a server in a datacentre I will never see.