testsAndMisc-archive/python_pkg/brightness_controller/auto_brightness_daemon.py

200 lines
5.8 KiB
Python
Raw Normal View History

2026-02-20 20:43:37 +01:00
#!/usr/bin/env python3
"""Background daemon for automatic brightness adjustment via ambient light sensor.
Reads the IIO ambient light sensor periodically and adjusts screen backlight
using brightnessctl. Designed to run as a systemd user service.
Usage:
python -m python_pkg.brightness_controller.auto_brightness_daemon
# or directly:
./auto_brightness_daemon.py
Control file: ~/.config/brightness-auto/enabled
- Write "1" to enable auto-brightness, "0" to disable.
- The daemon checks this file each cycle.
- The GUI writes to this file to toggle auto mode.
"""
from __future__ import annotations
import logging
from pathlib import Path
import shutil
import signal
import subprocess
import sys
import time
POLL_INTERVAL_S = 2.0
MIN_CHANGE_PERCENT = 2
# Smoothing: move at most this many % per tick to avoid jarring jumps.
MAX_STEP_PER_TICK = 5
_BRIGHTNESSCTL = shutil.which("brightnessctl") or "/usr/bin/brightnessctl"
# Minimum fields in brightnessctl machine-readable info output.
_MIN_BRIGHTNESSCTL_FIELDS = 4
CONFIG_DIR = Path.home() / ".config" / "brightness-auto"
ENABLED_FILE = CONFIG_DIR / "enabled"
# Lux-to-brightness mapping (must match the GUI curve).
LUX_CURVE: list[tuple[float, int]] = [
(0.0, 10),
(5.0, 40),
(50.0, 75),
(200.0, 90),
(500.0, 95),
(1000.0, 100),
(5000.0, 100),
]
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [auto-brightness] %(message)s",
datefmt="%H:%M:%S",
)
log = logging.getLogger(__name__)
def _find_als_device() -> Path | None:
"""Find the first IIO ambient light sensor device path."""
matches = list(Path("/sys/bus/iio/devices").glob("*/in_illuminance_raw"))
if matches:
return matches[0].parent
return None
def _read_lux(als_path: Path) -> float:
"""Read current illuminance in lux from the ALS."""
raw = float((als_path / "in_illuminance_raw").read_text().strip())
try:
scale = float((als_path / "in_illuminance_scale").read_text().strip())
except (FileNotFoundError, ValueError):
scale = 1.0
try:
offset = float((als_path / "in_illuminance_offset").read_text().strip())
except (FileNotFoundError, ValueError):
offset = 0.0
return (raw + offset) * scale
def _lux_to_brightness(lux: float) -> int:
"""Map a lux reading to a screen brightness percentage using the curve."""
if lux <= LUX_CURVE[0][0]:
return LUX_CURVE[0][1]
if lux >= LUX_CURVE[-1][0]:
return LUX_CURVE[-1][1]
for i in range(len(LUX_CURVE) - 1):
lux_lo, bri_lo = LUX_CURVE[i]
lux_hi, bri_hi = LUX_CURVE[i + 1]
if lux_lo <= lux <= lux_hi:
ratio = (lux - lux_lo) / (lux_hi - lux_lo)
return int(bri_lo + ratio * (bri_hi - bri_lo))
return LUX_CURVE[-1][1]
def _get_brightness() -> int:
"""Get current backlight brightness percentage."""
result = subprocess.run(
[_BRIGHTNESSCTL, "-m", "info"],
capture_output=True,
text=True,
check=False,
)
for line in result.stdout.strip().splitlines():
parts = line.split(",")
if len(parts) >= _MIN_BRIGHTNESSCTL_FIELDS and parts[1] == "backlight":
return int(parts[3].rstrip("%"))
return -1
def _set_brightness(percent: int) -> None:
"""Set backlight brightness percentage."""
subprocess.run(
[_BRIGHTNESSCTL, "-q", "set", f"{percent}%"],
check=False,
)
def _is_enabled() -> bool:
"""Check if auto-brightness is enabled via the control file."""
try:
return ENABLED_FILE.read_text().strip() == "1"
except FileNotFoundError:
return False
def _set_enabled(*, enabled: bool) -> None:
"""Write the enabled state to the control file."""
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
ENABLED_FILE.write_text("1" if enabled else "0")
def _clamp(value: int, lo: int, hi: int) -> int:
"""Clamp value between lo and hi."""
return max(lo, min(hi, value))
def main() -> None:
"""Main daemon loop."""
als_path = _find_als_device()
if als_path is None:
log.error("No ambient light sensor found. Exiting.")
sys.exit(1)
log.info("ALS device: %s", als_path)
log.info("Control file: %s", ENABLED_FILE)
# Ensure the control file exists (default: enabled when started as service)
if not ENABLED_FILE.exists():
_set_enabled(enabled=True)
log.info("Created control file with auto-brightness ENABLED.")
# Handle SIGTERM gracefully for systemd
running = True
def _handle_signal(signum: int, _frame: object) -> None:
nonlocal running
log.info("Received signal %d, shutting down.", signum)
running = False
signal.signal(signal.SIGTERM, _handle_signal)
signal.signal(signal.SIGINT, _handle_signal)
log.info("Daemon started. Polling every %.1fs.", POLL_INTERVAL_S)
while running:
try:
if _is_enabled():
lux = _read_lux(als_path)
target = _lux_to_brightness(lux)
current = _get_brightness()
if current >= 0 and abs(target - current) >= MIN_CHANGE_PERCENT:
# Smooth: step gradually toward the target
delta = target - current
step = _clamp(delta, -MAX_STEP_PER_TICK, MAX_STEP_PER_TICK)
new_val = _clamp(current + step, 0, 100)
_set_brightness(new_val)
log.info(
"lux=%.1f target=%d%% current=%d%% -> set %d%%",
lux,
target,
current,
new_val,
)
except Exception:
log.exception("Error in auto-brightness loop")
time.sleep(POLL_INTERVAL_S)
log.info("Daemon stopped.")
if __name__ == "__main__":
main()