ThermometerMonitor is an asynchronous Bluetooth Low Energy (BLE) environmental monitor and telemetry logger. It continuously listens for BLE advertising packets broadcast by smart thermometers and hygrometers (such as Xiaomi Mijia / LYWSD03MMC flashed with custom ATC or PVVX firmware), decodes sensor payloads, deduplicates readings, and stores them in a local SQLite database.
It supports cross-platform operation (Linux, macOS, Windows) out of the box via Bleak, while featuring an automatic Linux raw HCI socket fallback for legacy adapters or specialized container setups.
It includes ready-to-use Docker Compose configurations along with a Grafana dashboard integration for real-time visualization.
- Cross-Platform BLE Scanning:
- Primary driver powered by Bleak for native support across Linux, macOS, and Windows.
- Smart Linux Fallback: On Linux systems where native passive scanning via BlueZ / BT 5.0 is unavailable or unsupported by
the hardware (HCI version < 9), it automatically falls back to a custom low-level HCI raw socket scanner (
aioblescan+HCIPassiveScannerProtocol).
- Multi-Format Sensor Parsing: Supports popular custom firmware advertisement formats:
- PVVX 15-byte (
pvvx_15b) - PVVX 18-byte (
pvvx_18b) - ATC1441 13-byte (
atc1441)
- PVVX 15-byte (
- Metrics Captured:
- Temperature (°C)
- Relative Humidity (%)
- Battery Level (% and mV)
- Signal Strength (RSSI in dBm)
- Frame Counter & Payload Format
- Device MAC Address & Name
- Deduplication: Filters out redundant packets using frame counters to minimize database bloat.
- Async Batch Storage: Queues incoming sensor events and writes them in batches to SQLite with WAL (Write-Ahead Logging) mode and indexed queries.
- Hardware Watchdog: Monitors packet freshness and initiates graceful recovery if the Bluetooth stack or adapter stalls.
- Graceful Shutdown: Intercepts standard termination signals (
SIGINT,SIGTERM), flushing buffered telemetry to disk cleanly. - Grafana Integration: Pre-configured
compose.yamlwith the SQLite datasource plugin (frser-sqlite-datasource) to visualize trends instantly.
+----------------------------------+
| start_scanning(mode) |
+----------------------------------+
|
+----------------------------------+
| Is Linux AND Passive Scanning? |
+----------------------------------+
/ \
[ No / Other OS ] [ Yes ]
/ \
v v
+--------------------+ +--------------------+
| Standard Bleak | | Check BT 5.0 |
| Scanner | | Capabilities |
| (macOS / Win / | +--------------------+
| Linux D-Bus) | / \
+--------------------+ [Supported] [Not Supported]
/ \
v v
+------------------+ +--------------------+
| Bleak + BlueZ | | Raw HCI Socket |
| Passive Patterns | | Fallback Scanner |
+------------------+ | (aioblescan) |
+--------------------+
.
├── compose.yaml # Docker Compose setup (BLE Monitor + Grafana)
├── Dockerfile # Multi-stage container build with uv
├── dot.env.example # Sample environment configuration
├── entrypoint.sh # Container entrypoint script
├── pyproject.toml # Project metadata and dependencies
├── data/
│ └── ble_data.db # SQLite database (generated at runtime)
└── src/ThermometerMonitor/
├── main.py # Application entrypoint & scanner lifecycle
├── settings.py # Configuration and environment variables
├── hci_passive_scanner_protocol.py # Low-level HCI socket protocol handler (Linux fallback)
├── parser.py # BLE payload decoders (PVVX, ATC1441)
├── db_writer.py # Async SQLite batch writer & table schema
├── watchdog.py # Bluetooth stall watchdog worker
├── start_scanning.py # Bluetooth multiplatform start scanner
└── handler_signal.py # Cross-platform signal handlers
Telemetry is recorded into the SQLite database at data/ble_data.db under the table atc_sensor_data:
| Column | Type | Description |
|---|---|---|
id |
INTEGER PRIMARY KEY |
Auto-incrementing record ID |
timestamp |
REAL |
Unix epoch timestamp (seconds) |
mac_address |
TEXT |
Device Bluetooth MAC address (XX:XX:XX:XX:XX:XX) |
device_name |
TEXT |
Local advertised name or generated ID |
rssi |
INTEGER |
Received Signal Strength Indicator (dBm) |
temperature_c |
REAL |
Temperature in Celsius |
humidity_pct |
REAL |
Relative Humidity percentage (0–100%) |
battery_pct |
INTEGER |
Battery level percentage (0–100%) |
battery_mv |
INTEGER |
Battery voltage in millivolts (mV) |
frame_counter |
INTEGER |
Sensor advertisement sequence counter |
payload_format |
TEXT |
Decoded format (pvvx_15b, pvvx_18b, atc1441) |
Configuration is managed via environment variables or a .env file in the project root. Copy the template to get started:
cp dot.env.example .env| Variable | Default | Description |
|---|---|---|
NAME_PREFIXES |
"" (empty / all) |
Comma-separated list of device name prefixes to filter (e.g. ATC_112233,ATC_) |
ADDRESS_PREFIXES |
"" (empty / all) |
Comma-separated list of device addresses prefixes to filter (e.g. A4:C1:38,A1:C1:18) |
DEVICE_PREFIX_DEFAULT |
ATC_ |
Prefix prepended to device names when an explicit name is absent |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
SCANNING_MODE |
auto |
BLE scanning mode (auto, active, passive) |
WATCHDOG_TIMEOUT |
600 |
Inactivity threshold in seconds before watchdog flags a stall |
UUID_ENVIRONMENTAL_SENSING |
0000181a-0000-1000-8000-00805f9b34fb |
BLE Service Data UUID for Environmental Sensing (181A) |
DB_PATH |
data/ble_data.db |
Path to the SQLite database file |
You can run ThermometerMonitor directly on your local machine without Docker or elevated root permissions in most OS environments.
- Clone the repository:
git clone [https://github.com/lexsysko/ThermometerMonitor.git](https://github.com/lexsysko/ThermometerMonitor.git)
cd ThermometerMonitor- Install dependencies:
Using
uv:
uv syncOr standard pip:
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install .- Run the monitor:
thermometermonitor(Note: On Linux, if using the raw HCI fallback mode, ensure your user account has access to raw socket capabilities or run with
sudo setcap cap_net_raw,cap_net_admin+eip $(readlink -f $(which python)))
Running via Docker Compose is ideal for dedicated monitoring servers (e.g., Raspberry Pi, home server) and includes Grafana visualization out of the box.
- Start services:
docker compose up -d- Check monitor logs:
docker compose logs -f ble-monitor- Open Grafana:
- Access Grafana at: http://localhost:3000
- Default login:
admin/admin - Pre-configured datasource path:
/var/lib/grafana/sqlite_data/ble_data.db.
Note: For Docker on Linux utilizing the HCI socket fallback, the container config uses network_mode: host and
cap_add: [NET_RAW, NET_ADMIN].
- SQL for Grafana
SELECT
timestamp AS time,
COALESCE(NULLIF(device_name, ''), mac_address) AS metric,
temperature_c AS value
FROM atc_sensor_data
WHERE timestamp >= ($__from / 1000)
AND timestamp <= ($__to / 1000)
AND COALESCE(NULLIF(device_name, ''), mac_address) IN (${device:singlequote})
ORDER BY timestamp ASC;If running on Linux and the system drops down to the HCI socket fallback:
# 1. Install bluez/rfkill utilities if missing
sudo apt update && sudo apt install -y bluez rfkill
# 2. Unblock the Bluetooth radio
sudo rfkill unblock bluetooth
# 3. Ensure the HCI interface is active
sudo hciconfig hci0 up
-
Xiaomi Mijia Bluetooth Thermometer 2 (LYWSD03MMC)
-
Custom firmwares:
-
Any BLE broadcaster sending standard
0x181Aenvironmental service advertisement payloads.
This project is licensed under the MIT License.