Add power settings for the lighting, bump to 1.4.0

New Power page:
- toggle for the firmware's 30 s keyboard backlight timeout
- idle timeout for keyboard and lightbar, separately for AC and battery;
  predatord watches all input devices and restores the lighting on the
  next input
- battery rules: dimmed keyboard brightness, keyboard off, lightbar off

The rules only change what is sent to the hardware; the saved lighting
settings stay untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 17:59:42 +02:00
co-authored by Claude Opus 5.5
parent 5b7534560f
commit 571cfcca2a
10 changed files with 232 additions and 10 deletions
+63 -2
View File
@@ -26,6 +26,7 @@ from . import CONFIG_DIR, CONFIG_PATH, SOCKET_PATH, STATE_PATH, __version__
from . import hardware as hw
from . import updater
from .hotkey import HotkeyListener, launch_in_session
from .idle import IdleWatcher
from .keyboard import EFFECTS, NUM_CELLS, Keyboard
DEFAULT_CONFIG = {
@@ -51,6 +52,13 @@ DEFAULT_CONFIG = {
"keyboard_presets": {},
"updates": {"auto_check": True, "interval_hours": 6},
"predator_key": "open", # open | cycle_profile | none
"power": {
"idle_off_ac": 0, # seconds without input until lighting turns off, 0 = never
"idle_off_battery": 120,
"battery_brightness": 100, # keyboard brightness on battery, % of the normal value
"battery_lights_off": False,
"battery_lightbar_off": False,
},
}
@@ -100,6 +108,9 @@ class Daemon:
self.update_info = None
self.update_error = None
self._update_checking = False
self.idle = IdleWatcher(self._activity)
self.lights_idle = False
self.on_ac = True
# ---------------------------------------------------------- Configuration
def _load_config(self):
@@ -132,8 +143,35 @@ class Daemon:
self.errors[key] = str(e)
return None
# ---------------------------------------------------------- Power rules
def keyboard_effective(self):
k = dict(self.config["keyboard"])
p = self.config["power"]
if self.lights_idle or (not self.on_ac and p.get("battery_lights_off")):
k["effect"] = "off"
elif not self.on_ac and p.get("battery_brightness", 100) < 100:
k["brightness"] = int(k["brightness"] * p["battery_brightness"] / 100)
return k
def idle_timeout(self):
p = self.config["power"]
return int(p.get("idle_off_ac" if self.on_ac else "idle_off_battery") or 0)
def update_idle(self):
t = self.idle_timeout()
idle = t > 0 and self.idle.idle_seconds() >= t
if idle != self.lights_idle:
self.lights_idle = idle
log("Lighting %s" % ("off (idle)" if idle else "restored"))
self.apply_keyboard()
def _activity(self):
if self.lights_idle:
with self.lock:
self.update_idle()
def apply_keyboard(self, cfg=None):
k = cfg or self.config["keyboard"]
k = cfg or self.keyboard_effective()
frame = bytes.fromhex(k.get("frame") or "") if k["effect"] == "perkey" else None
self._try("keyboard", self.kbd.apply, k["effect"], hex_to_rgb(k["color"]),
k["brightness"], frame, int(k.get("effect_id") or 0))
@@ -142,8 +180,11 @@ class Daemon:
def lightbar_effective(self):
lb = dict(self.config["lightbar"])
if self.lights_idle or (not self.on_ac and self.config["power"].get("battery_lightbar_off")):
lb["mode"] = "off"
return lb
if lb.get("sync_keyboard"):
k = self.config["keyboard"]
k = self.keyboard_effective()
lb["brightness"] = k["brightness"]
if k["effect"] == "off":
lb["mode"] = "off"
@@ -179,6 +220,9 @@ class Daemon:
def restore(self):
log("Restoring settings")
bat = hw.battery()
self.on_ac = bat.get("ac_online", True)
self.idle.last_input = time.monotonic() # e.g. after resume: lights on
self.lights_idle = False
if self.config["profile"].get("auto_switch"):
self.apply_profile_for_power(bat.get("ac_online", True))
self.apply_fans()
@@ -237,6 +281,7 @@ class Daemon:
"lightbar_effective": self.lightbar_effective(),
"fan_curve_applied": self.curve_applied,
"battery_limit": self.battery_limit,
"lights_idle": self.lights_idle,
"update": self.update_info,
"update_error": self.update_error,
"update_status": updater.status(),
@@ -308,7 +353,11 @@ class Daemon:
log("Power source changed:", "AC" if ac else "battery")
if self.config["profile"].get("auto_switch"):
self.apply_profile_for_power(ac)
if ac != self.on_ac:
self.on_ac = ac
self.apply_keyboard() # battery lighting rules
self.last_ac = ac
self.update_idle()
self.fan_curve_tick()
self._ticks += 1
if self._ticks % 20 == 1:
@@ -342,6 +391,17 @@ class Daemon:
merge(self.config["updates"], {k: a[k] for k in ("auto_check", "interval_hours") if k in a})
self.save_config()
return {}
if cmd == "set_power":
p = self.config["power"]
for key in ("idle_off_ac", "idle_off_battery", "battery_brightness",
"battery_lights_off", "battery_lightbar_off"):
if key in a:
p[key] = a[key]
p["battery_brightness"] = max(0, min(100, int(p["battery_brightness"])))
self.save_config()
self.update_idle()
self.apply_keyboard()
return {}
if cmd == "set_predator_key":
if a["action"] not in ("open", "cycle_profile", "none"):
raise ValueError("unknown action")
@@ -532,6 +592,7 @@ def main():
signal.signal(signal.SIGUSR1, lambda *_: threading.Thread(target=lambda: (d.lock.acquire(), d.restore(), d.lock.release())).start())
threading.Thread(target=d.loop, daemon=True).start()
HotkeyListener(d.predator_key, log).start()
d.idle.start()
server.serve_forever()