A QML app for the Eneco Toon thermostat display that shows live air-quality data from local ESPHome devices: CO₂, PM2.5 (indoor and outdoor) and fan speed.
Everything is polled straight from the ESPHome web server over your LAN — no cloud, no broker, no Home Assistant required.
The tile renders up to three rows, each shown only when the matching device is configured:
CO₂ (ppm): 612
PM2.5 (µg/m³): 4|11 ← indoor|outdoor
🌀 (%): 35
Tapping the tile opens the settings screen. The app also adds a Luchtkwaliteit menu item that opens the same screen.
-
A rooted Toon running the ToonStore client (
tsc). Settings are stored in/mnt/data/tsc/, which is the ToonStore data directory. -
One or more ESPHome devices with the web server component enabled:
web_server: port: 80 version: 2
The app reads
http://<ip>/<sensor path>and expects a JSON body with a numericvaluefield, which is what the ESPHome web server returns for a sensor:{ "id": "sensor-carbon_dioxide", "value": 612, "state": "612 ppm" } -
The Toon and the ESPHome devices must be on the same network, reachable by plain HTTP (no TLS, no authentication — the app sends unauthenticated
GETrequests).
Copy the repository contents into an esphomeair directory under the Toon's apps
directory and restart the UI:
scp -r *.qml qmldir version.txt root@<toon-ip>:/qmf/qml/apps/esphomeair/The exact apps path depends on your firmware and root method — check where your other ToonStore apps live and use the same parent directory. After restarting the Toon UI, add the tile via the tile screen's + button (category general, label Luchtkwaliteit).
Open the settings screen from the tile or the menu. All fields are optional; a row disappears from the tile when its device is left blank.
| Setting | Default | Description |
|---|---|---|
| ESPHome sensor IP | (empty) | Device serving indoor CO₂ and PM2.5 |
| CO2 sensor | sensor/Carbon Dioxide |
Path for the CO₂ reading on that device |
| PM2.5 sensor | sensor/PM2.5 |
Path for the indoor PM2.5 reading |
| Buiten PM2.5 IP | (empty) | Second device serving an outdoor PM2.5 reading |
| Buiten PM2.5 sensor | sensor/PM2.5 |
Path for the outdoor PM2.5 reading |
| ESPHome ventilator IP | (empty) | Device serving the fan speed |
| Ventilator pad | sensor/Fan Speed |
Path for the fan speed reading |
| Verversing (s) | 60 |
Poll interval, clamped to 10–300 seconds |
Press Opslaan to save. Settings are written to
/mnt/data/tsc/esphomeair.userSettings.json and reloaded on startup; saving also
triggers an immediate refresh.
Open http://<device-ip>/ in a browser and look at the entity rows, or check the
id:/name: of the sensor in your ESPHome YAML. The defaults above match the author's
devices — yours will likely differ, which is why every path is editable.
EsphomeairApp.qml runs a Timer at the configured interval and walks a chain of
XMLHttpRequest calls — CO₂ → indoor PM2.5 → outdoor PM2.5 → fan speed — each step
invoking the next from its readyState handler, so at most one request is in flight at
a time. Steps whose device is left blank fall through to the next one. A response that
fails, times out or does not contain a number resets that reading to —, so an
unreachable device is visible rather than frozen on its last value.
| File | Role |
|---|---|
EsphomeairApp.qml |
App root: settings persistence, fetch chain, refresh timer |
EsphomeairTile.qml |
The tile — three label/value rows plus a drawn fan icon |
EsphomeairSettingsScreen.qml |
Settings screen with on-screen keyboard input |
EsphomeairMenu.qml |
Menu entry that opens the settings screen |
EditTextLabel4421.qml |
Editable label control, copied from the TSC buienradar app |
qmldir |
Declares the apps.esphomeair module |
The UI is in Dutch, matching the rest of the Toon interface.
Formatting and linting run through pre-commit:
pip install pre-commit
pre-commit install
pre-commit run --all-filesThat runs prettier on JSON/YAML/Markdown and pyside6-qmlformat / pyside6-qmllint
(from PySide6) on the QML. The same checks run on pull requests via
.github/workflows/pr-lint.yml; qmllint is advisory,
since the Toon's qb.components types are not available outside the device.
There is no way to run the app off-device: it depends on Toon-private QML modules
(qb.components, qb.base, FileIO, BasicUIControls). Test on a real Toon.
A tile is 142 × 60 px in the grid (~2.4 : 1), drawn in a scaled-up coordinate space.
Content must stay compact horizontally — a wide label (unit): value row runs into the
right edge and clips. isNxt selects the larger font/width values for Toon 2 (NXT)
hardware.
- Temperature is not displayed. The app used to fetch it and throw it away; the request was removed rather than squeezing a fourth row into the tile.
- Sensor readings are rounded to whole numbers.
lastUpdatedis tracked on every refresh but no tile row shows it yet.
MIT, with one exception: EditTextLabel4421.qml is copied from the
Toon Software Collective buienradar app
and is not covered by that grant. That project declares no license of its own.