Lightweight, battery-friendly Prometheus exporter for SensorPush 2nd-generation BLE sensors (HT.w, HTP.xw, TC.x). Optimized for long-running operation on a Raspberry Pi. Unofficial; not affiliated with SensorPush.
SensorPush sensors broadcast temperature, humidity and pressure in their BLE advertisement packets. Reading those is completely passive — the sensor advertises (~every 1.3 s by default) whether or not anyone is listening, so harvesting the data costs the sensor nothing. This is the same approach the SensorPush app and Home Assistant use.
Opening a GATT connection, by contrast, forces the sensor's radio into a high-power state. This exporter therefore:
- Runs one continuous scanner and decodes advertisements in real time (temperature / humidity / pressure / RSSI) — no per-poll connections.
- Connects only occasionally (default every 6 h) to read battery voltage and
the device id, which are the only values not broadcast. Set
BATTERY_READ_INTERVAL=0to never connect at all.
The result is roughly ~0–4 connections per day per sensor instead of ~1,440, near-real-time data (~1.3 s vs. 60 s), and far fewer failures (advertisements can't "fail to connect").
Upgrading? v3 reduced every reading metric to a single
device_addresslabel. See Changes in v3.
Published image (linux/arm64 only):
ghcr.io/rmarshall31/sensorpush-exporter:latest
On a Raspberry Pi:
docker compose pull
docker compose up -dOr:
docker pull ghcr.io/rmarshall31/sensorpush-exporter:latestThe exporter auto-discovers every SensorPush sensor in range. To pick up local edits instead of the published image:
docker compose up -d --buildKeep --build when you have changed exporter.py; Compose otherwise reuses
the existing image. A plain docker build on an amd64 machine will not run on
the Pi.
Linux and macOS both work directly, which is handy for development:
pip install -r requirements.txt && ./exporter.pyOn macOS, BLE_ADAPTER has no effect and SCANNING_MODE=passive is unsupported
by the OS; use the active default. Grant the terminal Bluetooth permission when
prompted.
Every per-device metric carries exactly one label, device_address (the sensor's
MAC). Human-readable metadata lives in sensorpush_device_info, which you join
on device_address. Keeping it out of the reading series means a name or id that
arrives late never forces a series to be deleted and recreated.
| Metric | Source | Notes |
|---|---|---|
sensorpush_device_info |
both | metadata: device_name, model, device_id |
sensorpush_temperature_celsius |
advertisement | near real-time |
sensorpush_humidity_percent |
advertisement | near real-time |
sensorpush_pressure_pascals |
advertisement | HTP.xw only |
sensorpush_rssi_dbm |
advertisement | signal strength |
sensorpush_last_update_timestamp |
advertisement | use to alert on stale data |
sensorpush_advertisements_received_total |
advertisement | counter; reliability signal |
sensorpush_battery_voltage_millivolts |
connection | refreshed every BATTERY_READ_INTERVAL |
sensorpush_battery_temperature_celsius |
connection | diagnostic; best-effort |
sensorpush_battery_last_read_timestamp |
connection | last successful battery read |
sensorpush_battery_read_errors_total |
connection | counter; failed battery connections |
sensorpush_exporter_info |
— | version / config |
model is one of HT1, HT.w, HTP.xw, TC.x. device_id is the id the
SensorPush app shows; it is empty until the first successful battery read, and
stays empty permanently if BATTERY_READ_INTERVAL=0.
Attaching names to readings — the join you want in most dashboards:
sensorpush_temperature_celsius
* on(device_address) group_left(device_name, model)
sensorpush_device_info
Detecting a dead/out-of-range sensor:
time() - sensorpush_last_update_timestamp > 300
This only fires while the series still exists. STALE_AFTER deletes a sensor's
metrics once it has been unseen that long, and the expression then has nothing
to evaluate, so the alert silently resolves rather than staying firing. Keep
STALE_AFTER well above your alert threshold plus its pending period, or set it
to 0 to never prune — at the cost of keeping metrics for any sensor that
leaves for good, including someone else's that drifted into range.
Detecting a dying battery (SensorPush documents the sensors as functional down to roughly 2400 mV):
sensorpush_battery_voltage_millivolts < 2500
Set via environment variables (see docker-compose.yml):
| Variable | Default | Description |
|---|---|---|
SCANNING_MODE |
active |
active (works everywhere) or passive (lowest power, Linux/BlueZ only) |
BATTERY_READ_INTERVAL |
21600 |
Seconds between battery reads. 0 = never connect (fully passive) |
STALE_AFTER |
900 |
Drop a device's metrics if unseen for this many seconds. 0 disables |
BATTERY_CONNECT_TIMEOUT |
30 |
Connection timeout for battery reads |
BATTERY_READ_RETRIES |
2 |
Connection attempts per battery read |
BLE_ADAPTER |
(auto) | Force an adapter, e.g. hci0 |
DEVICE_NAME_PREFIX |
SensorPush |
Advertised-name prefix used to identify sensors |
EXPORTER_PORT |
9108 |
Metrics port |
LOG_LEVEL |
INFO |
Logging level |
MAINTENANCE_INTERVAL |
30 |
Main-loop tick (battery-due check + stale prune) |
Both modes never connect to read the sensor data, so both are dramatically better for battery life than the old approach. The difference:
active(default): the adapter sends scan requests. Works on stock Raspberry Pi OS with no extra setup. Recommended.passive: the adapter only listens — marginally lighter and quieter on the air — but on Linux it requires BlueZ ≥ 5.55 with the--experimentalflag on the host and Linux kernel ≥ 5.10. Enable it by addingExperimental = trueunder[General]in/etc/bluetooth/main.confand restartingbluetooth. If a sensor isn't detected in passive mode, switch back toactive.
- Leave
SCANNING_MODE=active(or usepassiveif your host supports it). - Increase
BATTERY_READ_INTERVAL(e.g.86400for daily) or set it to0if you don't need the battery metric — connections are the only thing that drains the sensor. - On the sensor side, the SensorPush app lets you raise the advertising interval (default 1285 ms, max ~20.5 s); longer intervals trade data freshness for battery.
- Do not lower Tx power to save battery. SensorPush recommends the maximum (+5 dBm), noting battery life still exceeds two years, because weaker signal causes exactly the two failure modes this exporter cares about: missed advertisements and failed battery connections.
scrape_configs:
- job_name: 'sensorpush'
static_configs:
- targets: ['localhost:9108']
scrape_interval: 30sNo devices found:
- Verify the sensor is in range (~30 ft) and has a battery installed.
- Confirm it's visible to the host:
bluetoothctl scan on(look forSensorPush ...). - Check the Bluetooth service:
systemctl status bluetooth.
Bluetooth adapter not accessible:
- Ensure
/var/run/dbusis mounted (seedocker-compose.yml). - The container must run as root. Host
bluetoothdD-Bus policy is by uid;privileged: trueis not a substitute. - Check logs:
docker compose logs -f.
Battery voltage never appears:
- Only one sensor is read per
MAINTENANCE_INTERVALtick, because scanning stops while a connection is open. With several sensors, expect the last one's first reading a few minutes after startup, then everyBATTERY_READ_INTERVAL. - Weak-signal sensors may fail to connect — temperature and humidity still stream
from advertisements regardless. Watch
sensorpush_battery_read_errors_total.
Breaking: reading metrics dropped the device_name and device_id labels and
now carry only device_address. Queries that grouped by name need the
group_left join shown under Metrics.
- New
sensorpush_device_infoexposesdevice_name,model, anddevice_idkeyed bydevice_address.modelwas previously decoded and discarded, so sensor models are now visible for the first time. device_idis""rather thanunknownbefore it resolves, and resolving it no longer deletes and recreates every series for that sensor.- A sensor whose first advertisement lacked scan-response data used to keep a synthesized name forever; the real name is now adopted when it arrives.
- Removed
INITIAL_SETTLE. A sensor's first battery read now happens on the first maintenance tick after it is discovered, and only one sensor is read per tick. - Invalid
MAINTENANCE_INTERVAL,BATTERY_READ_RETRIES, orSCANNING_MODEnow exit at startup instead of being silently tolerated.
Based on the SensorPush Bluetooth API. Advertisement decoding adapted (MIT) from Bluetooth-Devices/sensorpush-ble.
MIT. See LICENSE.