Initial release of Predator Control 1.0.0

PredatorSense replacement for the Acer Predator Helios 16 (PH16-71) on Linux:

- predatord: root service for profiles, fans (incl. temperature curves),
  keyboard RGB, lightbar and battery health; restores settings at boot
  and after resume, switches profiles on AC/battery changes
- Qt desktop app with dashboard, fan curve editor and per-key RGB studio
- Plasma 6 panel widget with quick controls
- predatorctl command line tool
- predator_wmi kernel module (DKMS) as a minimal WMI method bridge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 18:38:14 +02:00
co-authored by Claude Opus 5.5
commit ea7ce49452
34 changed files with 3774 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
"""predatorctl – command line interface for Predator Control.
Examples:
predatorctl status
predatorctl profile performance
predatorctl fans max | auto | custom 60 70 | curve
predatorctl kbd static '#ff0000' --brightness 255
predatorctl kbd off
predatorctl lightbar wave --color '#00ffcc' --speed 7
predatorctl battery-limit on
predatorctl open rgb (opens the main window on a given page)
"""
import argparse
import json
import os
import subprocess
import sys
from .client import Client, DaemonError, read_state
def _bool(v):
return v.lower() in ("1", "on", "true", "yes")
def main(argv=None):
p = argparse.ArgumentParser(prog="predatorctl", description="Predator Control CLI")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("status", help="show status")
sp = sub.add_parser("state", help="raw state as JSON")
sp.add_argument("--cached", action="store_true", help="read state.json instead of asking the daemon")
sub.add_parser("restore", help="re-apply saved settings")
sp = sub.add_parser("profile", help="set power profile")
sp.add_argument("profile")
sp = sub.add_parser("profile-cycle", help="switch to the next profile")
sp = sub.add_parser("fans", help="fan mode")
sp.add_argument("mode", choices=["auto", "max", "custom", "curve"])
sp.add_argument("cpu", nargs="?", type=int)
sp.add_argument("gpu", nargs="?", type=int)
sp = sub.add_parser("kbd", help="keyboard lighting (effect name, 'on' or 'off')")
sp.add_argument("effect")
sp.add_argument("color", nargs="?")
sp.add_argument("--brightness", "-b", type=int)
sp = sub.add_parser("kbd-brightness", help="keyboard brightness 0-255 or +/-step")
sp.add_argument("value")
sp = sub.add_parser("preset", help="load a keyboard preset")
sp.add_argument("name")
sp = sub.add_parser("lightbar", help="Lightbar")
sp.add_argument("mode", nargs="?")
sp.add_argument("--color", "-c")
sp.add_argument("--brightness", "-b", type=int)
sp.add_argument("--speed", "-s", type=int)
sp.add_argument("--sync", choices=["on", "off"])
sp = sub.add_parser("battery-limit", help="80 %% charge limit")
sp.add_argument("value", choices=["on", "off"])
sp = sub.add_parser("open", help="open the main window")
sp.add_argument("page", nargs="?", default="home")
a = p.parse_args(argv)
if a.cmd == "open":
subprocess.Popen(["predator-control", "--page", a.page], start_new_session=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return 0
if a.cmd == "state" and a.cached:
print(json.dumps(read_state()))
return 0
c = Client()
try:
if a.cmd == "status":
st = c.call("state")
s, b = st["sensors"], st["battery"]
lbl = st["profile_labels"].get(st["profile"], st["profile"])
print("Profile: %s" % lbl)
print("CPU: %s °C %s %% fan %s RPM" % (s["cpu_temp"], st["cpu_load"], s["cpu_fan"]))
print("GPU: %s fan %s RPM" % ("%s °C" % s["gpu_temp"] if s["gpu_temp"] else "asleep", s["gpu_fan"]))
print("Fan mode: %s" % st["config"]["fans"]["mode"])
print("Battery: %s %% (%s), AC: %s" % (b.get("capacity"), b.get("status"), "yes" if b["ac_online"] else "no"))
k = st["config"]["keyboard"]
print("Keyboard: %s %s brightness %s" % (k["effect"], k["color"], k["brightness"]))
for k, v in st["errors"].items():
print("Error [%s]: %s" % (k, v))
elif a.cmd == "state":
print(json.dumps(c.call("state")))
elif a.cmd == "restore":
c.call("restore")
elif a.cmd == "profile":
c.call("set_profile", profile=a.profile)
elif a.cmd == "profile-cycle":
st = c.call("state")
ch = st["profile_choices"]
nxt = ch[(ch.index(st["profile"]) + 1) % len(ch)] if st["profile"] in ch else ch[0]
c.call("set_profile", profile=nxt)
print(st["profile_labels"].get(nxt, nxt))
elif a.cmd == "fans":
args = {"mode": a.mode}
if a.cpu is not None:
args["cpu"] = a.cpu
if a.gpu is not None:
args["gpu"] = a.gpu
c.call("set_fans", **args)
elif a.cmd == "kbd":
args = {"effect": a.effect}
if a.color:
args["color"] = a.color if a.color.startswith("#") else "#" + a.color
if a.brightness is not None:
args["brightness"] = a.brightness
c.call("set_keyboard", **args)
elif a.cmd == "kbd-brightness":
cur = c.call("state")["config"]["keyboard"]["brightness"]
v = a.value
val = cur + int(v) if v[0] in "+-" else int(v)
c.call("set_keyboard", brightness=max(0, min(255, val)))
elif a.cmd == "preset":
c.call("load_preset", name=a.name)
elif a.cmd == "lightbar":
args = {"mode": a.mode} if a.mode else {}
if a.color:
args["color"] = a.color if a.color.startswith("#") else "#" + a.color
if a.brightness is not None:
args["brightness"] = a.brightness
if a.speed is not None:
args["speed"] = a.speed
if a.sync:
args["sync_keyboard"] = a.sync == "on"
c.call("set_lightbar", **args)
elif a.cmd == "battery-limit":
c.call("set_battery_limit", enabled=_bool(a.value))
except DaemonError as e:
print("Error: %s" % e, file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main())