Originally derived from bendikwa/esphome-igrill by Bendik Wang Andreassen.
A standalone BLE polling service for Weber iGrill thermometer devices. It continuously scans for and connects to iGrill devices over Bluetooth Low Energy, exposes real-time temperature data via an HTTP + WebSocket API, and serves a single-page web dashboard for live monitoring.
- iGrill mini
- iGrill mini V2
- iGrill V2
- iGrill V202
- iGrill V3
- iDevices Kitchen Thermometer
- Weber Pulse 1000
- Weber Pulse 2000
The image is built automatically by CI and published to the GitHub Container Registry.
cp env.example .env # edit values as needed
docker compose up -d
curl http://localhost:39120/healthTo update to the latest image:
docker compose pull && docker compose up -dBy default the container exposes the port directly. To place the service behind a Traefik reverse proxy, create a docker-compose.override.yml (gitignored) alongside the base compose file:
services:
igrill:
ports: !reset []
labels:
- "traefik.enable=true"
- "traefik.http.routers.igrill.rule=Host(`igrill.${HOSTNAME}`)"
- "traefik.http.routers.igrill.entrypoints=web-secure"
- "traefik.http.routers.igrill.tls.certresolver=myresolver"
- "traefik.http.services.igrill.loadbalancer.server.port=39120"
networks:
- proxy
networks:
proxy:
external: trueEnsure the external network exists: docker network create proxy
pip install -r requirements-dev.txt
python -m service.mainThe server binds to 0.0.0.0:39120 by default. All configuration is via environment variables (see below).
Copy env.example to .env and edit values as needed. All variables are optional and have sensible defaults.
| Variable | Default | Description |
|---|---|---|
IGRILL_PORT |
39120 |
HTTP server port. |
IGRILL_POLL_INTERVAL |
15 |
BLE polling interval in seconds (clamped to 5-60). |
IGRILL_TIMEOUT |
30 |
GATT characteristic read timeout in seconds. |
IGRILL_CONNECT_TIMEOUT |
10 |
BLE connection timeout in seconds (separate from read timeout). |
IGRILL_MAX_BACKOFF |
60 |
Maximum exponential backoff delay in seconds between reconnection attempts. |
IGRILL_SCAN_INTERVAL |
60 |
Time between BLE discovery scans in seconds. |
IGRILL_SCAN_TIMEOUT |
5 |
Duration of each BLE discovery scan in seconds. |
IGRILL_RECONNECT_GRACE |
60 |
Seconds within which a reconnecting device reuses its existing session membership. |
IGRILL_DB_PATH |
/data/igrill.db |
SQLite database path for persisted sessions and readings. |
IGRILL_MAC_PREFIX |
70:91:8F |
MAC address prefix used to filter devices during scans. |
IGRILL_BIND_ADDRESS |
0.0.0.0 |
HTTP server bind address. |
IGRILL_LOG_LEVEL |
INFO |
Global log level (DEBUG, INFO, WARNING, ERROR). |
IGRILL_LOG_LEVEL_BLE |
(global) | Override log level for the BLE subsystem (igrill.ble). |
IGRILL_LOG_LEVEL_WS |
(global) | Override log level for the WebSocket subsystem (igrill.ws). |
IGRILL_LOG_LEVEL_SESSION |
(global) | Override log level for the session/history subsystem (igrill.session). |
IGRILL_LOG_LEVEL_ALERT |
(global) | Override log level for the alert subsystem (igrill.alert). |
IGRILL_LOG_LEVEL_HTTP |
(global) | Override log level for the HTTP subsystem (igrill.http). |
IGRILL_SESSION_TOKEN |
(empty) | If set, requires Authorization: Bearer <token> on WebSocket session-control messages. |
IGRILL_CORS_ORIGIN |
(empty) | If set, adds CORS Access-Control-Allow-Origin headers (e.g. * for development). A warning is logged if set to *. |
| Method | Path | Description |
|---|---|---|
GET |
/ |
Web dashboard — tab-based single-page UI (Live, History, Settings) with real-time WebSocket updates, session controls, BLE state indicators, live temperature charts (uPlot), past session browsing with full-timeline charts and summary statistics, and runtime log level management. |
GET |
/health |
Health check with uptime, device counts, active session ID, poll interval, and scan interval. |
GET |
/api/sessions |
Paginated session list (?limit=20&offset=0). |
GET |
/api/sessions/{id} |
Session detail with devices, targets, and readings. Returns 404 if the session does not exist. |
PUT |
/api/config/log-levels |
Runtime log level update (requires authorisation). |
Connect to /ws for real-time streaming. All messages use the v2 envelope format:
{"v": 2, "type": "<msg_type>", "ts": "...", "requestId": "...", "payload": {}}Client request types:
| Type | Description |
|---|---|
status_request |
Returns device state, session info, sample rate, active targets, and session devices. |
sessions_request |
Lists recent sessions (payload.limit defaults to 20, max 100). |
history_request |
Streams history chunks (sinceTs, untilTs, limit (max 10,000), sessionId, chunkSize). |
session_start_request |
Starts a new user-initiated session. Accepts optional targets array and deviceAddresses (array) or deviceAddress (string). If no devices are specified, all currently connected devices are included. Requires authorisation. |
session_end_request |
Ends the current session. Requires authorisation. |
session_add_device_request |
Adds a device to the active session mid-cook. Requires deviceAddress in payload. Requires authorisation. |
target_update_request |
Updates targets for the current session. Accepts optional deviceAddress to scope targets to a specific device. Requires authorisation. |
Server response types:
| Type | Description |
|---|---|
status |
Response to status_request with device state, session info, sample rate, active targets, and session devices. |
sessions_list |
Response to sessions_request with recent session summaries. |
history_chunk / history_end |
Streamed response to history_request. |
session_start_ack |
Acknowledgement for session_start_request. |
session_end_ack |
Acknowledgement for session_end_request. |
target_update_ack |
Acknowledgement for target_update_request. |
session_add_device_ack |
Acknowledgement for session_add_device_request. |
Server broadcast types:
| Type | Description |
|---|---|
reading |
Pushed on each poll cycle with latest probe data (always broadcast, regardless of session state). |
session_start / session_end |
Broadcast when sessions change. |
device_joined |
Broadcast when a device is added to an active session. |
target_approaching |
Probe temperature crossed the pre-alert threshold. |
target_reached |
Probe temperature hit the target. |
target_exceeded |
Probe temperature went above the target. |
target_reminder |
Periodic nudge while temperature remains above target. |
device_state_change |
Broadcast when a device's connection state changes (e.g. connecting, polling, disconnected, backoff). |
Note:
curldoes not support WebSockets. Use a client such aswebsocat,wscat, or an iOSURLSessionWebSocketTask.
Each device worker manages a six-state connection lifecycle: discovered -> connecting -> authenticating -> polling -> disconnected -> backoff -> connecting (retry). On disconnect or error, the worker uses exponential backoff (starting at 2 seconds, capped at IGRILL_MAX_BACKOFF) before attempting reconnection. A successful connection resets the backoff counter. Authentication is retried up to three times before failing. Probe readings are zeroed on disconnect to avoid displaying stale data.
Sessions are user-initiated only — no session is auto-created on startup or when a device connects. The device worker always polls BLE and broadcasts live readings to WebSocket clients, but only records to the database and evaluates alert targets when a session is active and the device is part of it.
A single session can include multiple iGrill devices. Devices can be added to an active session at any time via session_add_device_request. When a device disconnects during a session, it is marked as having left; on reconnect within the grace period, it is automatically rejoined.
Session data is stored in a normalised SQLite schema: sessions, session-device membership, per-probe readings, and per-device targets are all separate tables linked by foreign keys with UUID session identifiers. Schema changes are applied automatically via a sequential migration runner on startup.
When a session ends, the raw readings are downsampled to reduce storage. Both probe readings and device readings (battery, propane, heating) are cleaned up together so that historical queries remain consistent. This preserves the overall shape of the temperature curve while significantly reducing database size for long cooks.
The device manager monitors worker health on each scan cycle. If a worker task crashes due to an unhandled exception, it is automatically respawned and the device store is updated with an error status.
service/
__init__.py
config.py # Centralised configuration from environment variables
logging_setup.py # Structured logging with per-subsystem level control
main.py # App factory and entry point
alerts/
evaluator.py # Checks probes against targets, emits alert events
api/
envelope.py # WebSocket v2 message envelope construction
routes.py # HTTP route handlers and route registration
websocket.py # WebSocketHub, WebSocketClient, and v2 protocol handler
ble/
protocol.py # BLE protocol constants, model definitions, and detection
connection_state.py # ConnectionStateMachine with exponential backoff
device_worker.py # Connects, authenticates, and polls a single iGrill device
device_manager.py # Scans for iGrill devices and spawns/monitors workers
db/
schema.py # Normalised database schema definitions and init_db()
migrations.py # Sequential schema migration runner
history/
downsampler.py # Post-session reading downsampling
store.py # SQLite-backed sessions, readings, and targets
models/
device.py # DeviceStore — async-safe in-memory device state
reading.py # Temperature probe parsing and reading payload builder
session.py # TargetConfig dataclass for probe target temperatures
web/
dashboard.py # Dashboard route handler and static file serving
static/
index.html # Tab-based monitoring dashboard with live view, session history with full-timeline charts and summary statistics, settings with device info and runtime log level controls (vanilla HTML/CSS/JS, uPlot)
tests/
conftest.py # Shared pytest fixtures
test_alerts.py # AlertEvaluator tests
test_config.py # Configuration module tests
test_config_new.py # Extended configuration tests
test_connection_state.py # ConnectionStateMachine tests
test_downsampler.py # Post-session downsampling tests
test_history_store.py # HistoryStore tests
test_logging.py # Structured logging tests
test_models.py # Data models tests
test_protocol.py # BLE protocol module tests
test_integration.py # Full-server integration tests
test_routes.py # HTTP route handler tests
test_schema.py # Database schema tests
pip install -r requirements-dev.txt
python -m pytest tests/ -vdocker compose build
docker compose up -dA GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request to main:
- Test — installs dependencies and runs
pyteston Ubuntu. - Docker — on merge to
main, builds and pushes the Docker image toghcr.io/jaydenk/igrill-remote-server:latest(and a SHA-tagged variant).
To pull the pre-built image instead of building locally:
docker pull ghcr.io/jaydenk/igrill-remote-server:latest- The host must run BlueZ. Mount
/run/dbusinto the container and setDBUS_SYSTEM_BUS_ADDRESS=unix:path=/run/dbus/system_bus_socket. - The container must be able to access the host Bluetooth adapter (runs as root in Docker by default).
- BLE devices accept only one connection at a time — disconnect the mobile app before connecting the server.