Add Waybar widget for fan controller

Includes a Python script, Waybar config snippet, and README with install
instructions, usage, and optional CSS styling.
This commit is contained in:
2026-09-11 10:33:41 +02:00
parent 72e9c1392a
commit eb095dcb47
3 changed files with 176 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# Waybar widget for the fan controller
A `custom/fan` Waybar module that talks to `fan_controller` over its Unix
socket (`/tmp/fan_controller.sock`) — the same protocol as `FanController.qml`.
## Install
1. Copy the script:
```sh
mkdir -p ~/.config/waybar/scripts
cp waybar/fan.py ~/.config/waybar/scripts/
chmod +x ~/.config/waybar/scripts/fan.py
```
2. Add the `custom/fan` entry from [`fan.json`](fan.json) to
`~/.config/waybar/config.json` (merge it into your existing module list,
e.g. under `modules-right`).
3. Restart Waybar.
## Usage
- **Status** — shows `temp / mode`, e.g. `48.3° auto 55°` or `51.0° 75%`.
Hover for a tooltip with temp, duty, RPM, mode and safe-mode state.
- **Left click** — cycles `auto → 25% → 50% → 75% → 100% → auto`.
- **Right click** — returns to auto mode.
- CLI equivalents:
```sh
fan.py cycle # same as left click
fan.py auto # same as right click
fan.py duty 75 # set manual duty (0-100)
fan.py setpoint 55 # set auto setpoint (35-85)
```
## Optional styling
The script sets CSS classes `fan`, plus `offline` when the controller is
unreachable and `safe` in safe mode. Example for `style.css`:
```css
#waybar .fan.offline { background: #505050; }
#waybar .fan.safe { background: #e05050; }
```
## Notes
- `FAN_CONTROLLER_SOCK` env var overrides the socket path if needed.
- Each poll is a short-lived process (5 s interval), matching how the
Quickshell widget talks to the daemon — no persistent client required.
+9
View File
@@ -0,0 +1,9 @@
{
"custom/fan": {
"return-type": "json",
"exec": "~/.config/waybar/scripts/fan.py",
"interval": 5,
"on-click": "python3 ~/.config/waybar/scripts/fan.py cycle",
"on-right-click": "python3 ~/.config/waybar/scripts/fan.py auto"
}
}
Executable
+116
View File
@@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Waybar widget for the cooling-pad fan controller.
Talks to fan_controller over its Unix socket (/tmp/fan_controller.sock).
Usage:
fan.py print status as Waybar JSON (return-type: "json")
fan.py cycle auto -> 25% -> 50% -> 75% -> 100% -> auto (on-click)
fan.py auto switch back to auto mode (on-right-click)
fan.py duty 75 set manual duty, 0-100
fan.py setpoint 55 set auto-mode setpoint, 35-85
"""
import json
import os
import socket
import sys
SOCK = os.environ.get("FAN_CONTROLLER_SOCK", "/tmp/fan_controller.sock")
STEPS = [25, 50, 75, 100]
def cmd(command: str) -> str:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s:
s.settimeout(2)
s.connect(SOCK)
s.sendall(command.encode() + b"\n")
chunks = []
while True:
data = s.recv(1024)
if not data:
break
chunks.append(data)
return b"".join(chunks).decode().strip()
def status():
return json.loads(cmd("STATUS"))
def emit(text, tooltip=None, cls=None, alt=None):
out = {"text": text}
if tooltip:
out["tooltip"] = tooltip
if cls:
out["class"] = " ".join(cls)
if alt:
out["alt"] = alt
print(json.dumps(out))
def main():
arg = sys.argv[1] if len(sys.argv) > 1 else None
try:
if arg is None:
st = status()
mode = st.get("mode", "?")
if mode == "auto":
label = f"auto {st['setpoint']:.0f}°"
else:
label = f"{st['duty']:.0f}%"
text = f"{st['temp']:.1f}° {label}"
tooltip = (
f"Temp: {st['temp']:.1f}°C\n"
f"Fan: {st['duty']:.0f}% / {st['rpm']} rpm\n"
f"Mode: {mode}"
)
if mode == "auto":
tooltip += f" (setpoint {st['setpoint']:.0f}°C)"
if st.get("safe"):
tooltip += "\nSafe mode: ON"
cls = ["fan"]
if st.get("safe"):
cls.append("safe")
emit(text, tooltip, cls, alt=f"{st['rpm']} rpm")
elif arg == "cycle":
st = status()
if st["mode"] == "manual":
d = float(st["duty"])
nxt = next((x for x in STEPS if x > d + 0.5), None)
if nxt is None:
cmd("SET_MODE auto")
else:
cmd(f"SET_DUTY {nxt}")
else:
cmd("SET_MODE manual")
cmd(f"SET_DUTY {STEPS[0]}")
elif arg == "auto":
cmd("SET_MODE auto")
elif arg == "duty":
d = float(sys.argv[2])
if not 0 <= d <= 100:
sys.exit("duty must be 0-100")
cmd("SET_MODE manual")
cmd(f"SET_DUTY {d:.0f}")
elif arg == "setpoint":
sp = float(sys.argv[2])
if not 35 <= sp <= 85:
sys.exit("setpoint must be 35-85")
cmd(f"SET_SETPOINT {sp:.1f}")
else:
sys.exit(__doc__)
except (OSError, ValueError) as e:
if arg is None:
emit("fan: offline", "fan_controller not reachable", ["fan", "offline"])
else:
sys.exit(f"error: {e}")
if __name__ == "__main__":
main()