TermometerMonitor 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 the sensor payloads, deduplicates readings, and stores them in a local SQLite database.
It comes with ready-to-use Docker Compose configurations including a Grafana dashboard integration for real-time visualization.
- 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
SIGINT(Ctrl+C) andSIGTERMsignals, flushing buffered telemetry to disk cleanly. - Grafana Integration: Pre-configured
compose.yamlwith the SQLite datasource plugin (frser-sqlite-datasource) to visualize trends instantly.
.
├── 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/
├── main.py # Application entrypoint & scanner lifecycle
├── settings.py # Configuration and environment variables
├── parser.py # BLE payload decoders (PVVX, ATC1441)
├── db_writer.py # Async SQLite batch writer & table schema
├── watchdog.py # Bluetooth stall watchdog worker
└── 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 |
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 addresess prefixes to filter (e.g. A1:C1:18:11:22:33,A1..) |
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) |
- Bluetooth Adapter: BLE-compatible Bluetooth 4.0+ hardware.
- Operating System:
- Linux with BlueZ and DBus (recommended for production / 24x7 monitoring).
- Windows or macOS (for development and local testing).
- Python: Python 3.13 or higher (or uv).
-
Clone the repository:
git clone https://github.com/lexsysko/TermometerMonitor.git cd TermometerMonitor -
Install dependencies: Using
uv:uv sync
Or standard
pip/venv:python -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate pip install .
-
Configure environment:
cp dot.env.example .env # Edit .env to set prefixes or logging level -
Run the monitor:
python src/main.py
Running via Docker Compose is recommended on Linux hosts to ensure persistent background monitoring and integrated Grafana dashboarding.
Note (Linux / BlueZ): Host network mode (
network_mode: host) and access to/var/run/dbus/system_bus_socketalong withNET_ADMINandNET_RAWcapabilities are configured incompose.yamlto enable direct Bluetooth hardware access.
-
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(or configuredGF_SECURITY_ADMIN_PASSWORD) - The SQLite plugin
frser-sqlite-datasourceis automatically installed. - Set up SQLite datasource pointing to
/var/lib/grafana/sqlite_data/ble_data.db.
This project is tested and compatible with:
- Xiaomi Mijia Bluetooth Thermometer 2 (LYWSD03MMC)
- Custom firmwares:
- Any BLE broadcaster sending standard
0x181Aenvironmental service advertisement payloads.
# 1. Install bluez on the host if missing
sudo apt update && sudo apt install -y bluez rfkill
# 2. Check if bluetooth is blocked by rfkill
sudo rfkill unblock bluetooth
# 3. Enable and start the Bluetooth service
sudo systemctl enable --now bluetooth
# 4. Verify BlueZ is active and registered on D-Bus
sudo systemctl status bluetoothBlueZ passive scanning with advertisement pattern filtering requires both Kernel >= 5.10 and the BlueZ experimental interface flag enabled on the host machine.
Without --experimental turned on in the host's bluetooth.service, BlueZ refuses to expose the AdvertisementMonitor1 D-Bus interface that Bleak relies on for passive pattern filtering.
- Edit the host's systemd service for Bluetooth:
sudo systemctl edit bluetooth.service- Add the experimental flag: Paste the following configuration into the file override and save:
[Service]
ExecStart =
ExecStart = /usr/libexec/bluetooth/bluetoothd --experimental(Note: On older Ubuntu versions, the binary path might be /usr/lib/bluetooth/bluetoothd).
- Reload systemd and restart the service:
sudo systemctl daemon-reload
sudo systemctl restart bluetooth
- Verify --experimental is active:
systemctl status bluetoothLook for bluetoothd --experimental in the active process line.
This project is licensed under the MIT License.