Office Air Quality Widget in Waybar via Home Assistant
By Thomas Sileo • • 6 min read
It’s winter, and I’m trying to be more careful about not overheating and maintaining good air quality at home. I’m also a big fan of Home Assistant, and I rely on the SenseCAP Indicator D1 to gather air quality metrics:
- Temperature
- Humidity
- CO2 level
- TVOC (Total Volatile Organic Compounds)
I recently had an epiphany: why not display these sensors directly in Waybar? Bonus points for having some kind of alerting reminding me to ventilate the room when needed!
Building a custom Waybar widget
Building a custom widget/module for Waybar is surprisingly easy, see https://github.com/Alexays/Waybar/wiki/Module:-Custom.
We need a script that outputs a simple JSON object:
{"text":"hello"}
Making a bash script would be one way to do it, but using Python would make our life easier.
I’ve been looking for an opportunity to play with uv’s script features, and this looks like a perfect one.
It relies on a new Python feature PEP 723 Inline script metadata and will allow us to embed external dependencies directly as inline metadata.
$ uv init --script waybar_module.py --python 3.12
Initialized script at `waybar_module.py`
$ uv add --script waybar_module.py 'httpx'
Updated `waybar_module.py`
$ vim waybar_module.py
$ cat waybar_module.py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx",
# ]
# ///
import json
def main() -> None:
print(json.dumps({"text": "hello"}))
if __name__ == "__main__":
main()
$ uv run waybar_module.py
Installed 6 packages in 12ms
{"text": "hello"}
One more nice thing we can do is add a shebang to make the script executable by prepending #!/usr/bin/env -S uv run --script
$ head -n 1 waybar_module.py
#!/usr/bin/env -S uv run --script
$ chmod +x waybar_module.py
$ ./waybar_module.py
{"text": "hello"}
Now we’re ready to fetch sensor data from Home Assistant.
Fetching data from Home Assistant
Home Assistant offers a REST API making it easy to fetch sensor data.
You will need to grab a long-lived access token:
All API calls have to be accompanied by the header Authorization: Bearer TOKEN, where TOKEN is replaced by your unique access token. You obtain a token (“Long-Lived Access Token”) by logging into the frontend using a web browser, and going to your profile http://IP_ADDRESS:8123/profile
$ http get https://your-ha-instance.tld/api/states/sensor.sensecap_indicator_temperature Authorization:"Bearer XYZ"
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 428
Content-Type: application/json
Date: Sun, 30 Nov 2025 15:45:03 GMT
Referrer-Policy: no-referrer
Server: nginx
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
{
"attributes": {
"device_class": "temperature",
"friendly_name": "SenseCAP Indicator Indicator Temperature",
"unit_of_measurement": "°C"
},
"context": {
"id": "01KBAQ0NZ928BQCEAPKJBNSNP3",
"parent_id": null,
"user_id": null
},
"entity_id": "sensor.sensecap_indicator_temperature",
"last_changed": "2025-11-30T15:45:00.905560+00:00",
"last_reported": "2025-11-30T15:45:00.905560+00:00",
"last_updated": "2025-11-30T15:45:00.905560+00:00",
"state": "17.6"
}
This gives us access to the sensor state and unit_of_measurement.
However, since we’re going to display multiple sensors, we can just fetch all the sensors at once and do filtering in our script:
$ http get https://your-ha-instance.tld/api/states Authorization:"Bearer XYZ"
Final script
Here is the final script that I am using.
Using class (CSS class), it will set a warning/critical class name if sensors go above a specified threshold. We will style the text to be yellow/red, acting as a gentle reminder to open the window!
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.13"
# dependencies = [
# "httpx",
# ]
# ///
import json
import httpx
HA_URL = "https://your-ha-instance.tld"
HA_TOKEN = "XYZ"
# Sensors list defined as (<sensor_id>, <warning_threshold>, <critical_threshold>)
SENSORS = [
("sensor.sensecap_indicator_temperature", None, None),
("sensor.sensecap_indicator_humidity", None, None),
("sensor.sensecap_indicator_co2", 800, 1200),
("sensor.sensecap_indicator_tvoc", 250, 450),
]
def main() -> None:
css_class = "normal"
states = httpx.get(
f"{HA_URL}/api/states",
headers={"Authorization": f"Bearer {HA_TOKEN}"},
).json()
text = ""
states_idx = {s["entity_id"]: s for s in states}
for sensor, warning_threshold, critical_threshold in SENSORS:
if sensor not in states_idx:
continue
state = states_idx[sensor]
sensor_value = state["state"]
# Make sure we don't override a warning/critical class
if warning_threshold is not None and critical_threshold is not None:
try:
float_value = float(sensor_value)
if float_value >= critical_threshold:
css_class = "critical"
elif float_value >= warning_threshold and css_class == "normal":
css_class = "warning"
except ValueError:
pass
text += sensor_value
if unit := state["attributes"].get("unit_of_measurement"):
text += f"{unit} "
else:
text += " "
print(json.dumps({"text": text[:-1], "class": css_class}))
if __name__ == "__main__":
main()
On my system this script lives in ~/.config/waybar/scripts/ha.py, and I’ve added the warning/critical styling in the CSS file at ~/.config/waybar/style.css:
#custom-ha.warning {
color: #f9e2af;
}
#custom-ha.critical {
color: #f38ba8;
}
Now, we just need to update the waybar config at ~/.config/waybar/config:
- Enable the custom module by adding
custom/hatomodules-right - Define the module configuration
- Final touch, we can use
on-clickto make the widget a shortcut to open Home Assistant
{
[...]
"modules-right": ["custom/ha", "tray", "temperature", "cpu", "memory", "pulseaudio", "battery", "clock", "custom/power"],
[...]
"custom/ha": {
"exec": "~/.config/waybar/scripts/ha.py",
"return-type": "json",
"interval": 30,
"format": "🏠 {}",
"on-click": "xdg-open https://your-ha-instance.tld"
}
}
