mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 22:03:06 +02:00
200 lines
5.8 KiB
Python
Executable File
200 lines
5.8 KiB
Python
Executable File
#!/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()
|