"""Access to power profiles, sensors, fans, battery, lightbar and firmware options. WMI calls go through the predator_wmi kernel module (sysfs attribute predator_call). Wire formats come from Linuwu-Sense and Venator (both reverse-engineered from PredatorSense). """ import glob import os import struct import threading import time GUID_GAMING = "7A4DDFE7-5B5D-40B4-8595-4408E0CC7F56" # WMBH GUID_BATTERY = "79772EC5-04B1-4BFD-843C-61E7F77B6CC9" # WMBE GUID_APGE = "61EF69EA-865C-4BC3-A502-A0DEBA0CB531" # WMAA PLATFORM_PROFILE = "/sys/firmware/acpi/platform_profile" PLATFORM_CHOICES = "/sys/firmware/acpi/platform_profile_choices" PROFILE_LABELS = { "low-power": "Eco", "quiet": "Quiet", "balanced": "Balanced", "balanced-performance": "Performance", "performance": "Turbo", } LIGHTBAR_MODES = { "off": (0x00, "Off"), "static": (0xFF, "Static"), "breathing": (0x01, "Breathing"), "neon": (0x02, "Neon"), "rainbow": (0x03, "Rainbow"), "wave": (0x04, "Wave"), "ripple": (0x05, "Ripple"), "scanner": (0x06, "Scanner"), "strobe": (0x07, "Strobe"), } def _read(path, default=None): try: with open(path) as f: return f.read().strip() except OSError: return default def _read_int(path, default=None): v = _read(path) try: return int(v) except (TypeError, ValueError): return default def _write(path, value): with open(path, "w") as f: f.write(str(value)) # --------------------------------------------------------------- WMI bridge class WMIError(RuntimeError): pass _wmi_lock = threading.Lock() def wmi_available(guid=GUID_GAMING): return bool(glob.glob("/sys/bus/wmi/devices/%s-*/predator_call" % guid)) def wmi_call(guid, method, data=b""): """Call a WMI method. Returns bytes, int or None.""" paths = glob.glob("/sys/bus/wmi/devices/%s-*/predator_call" % guid) if not paths: raise WMIError("kernel module predator_wmi is not loaded") path = paths[0] with _wmi_lock: try: _write(path, "%d %s" % (method, bytes(data).hex())) except OSError as e: raise WMIError("WMI method %d failed: %s" % (method, e)) from e res = _read(path, "err -5").split() if res[0] != "ok": raise WMIError("WMI method %d: %s" % (method, " ".join(res))) if res[1] == "buf": return bytes.fromhex(res[2]) if len(res) > 2 else b"" if res[1] == "int": return int(res[2], 16) return None def _as_u64(res): if isinstance(res, int): return res if isinstance(res, (bytes, bytearray)): return int.from_bytes(bytes(res[:8]).ljust(8, b"\0"), "little") return 0 def gaming_u64(method, value, in_size=8): return _as_u64(wmi_call(GUID_GAMING, method, value.to_bytes(in_size, "little"))) # --------------------------------------------------------------- Profiles def profile_choices(): c = _read(PLATFORM_CHOICES, "") return c.split() if c else [] def get_profile(): return _read(PLATFORM_PROFILE) def set_profile(name): if name not in profile_choices(): raise ValueError("profile not available: %s" % name) _write(PLATFORM_PROFILE, name) # --------------------------------------------------------------- Sensors def _hwmon(name): for h in glob.glob("/sys/class/hwmon/hwmon*"): if _read(h + "/name") == name: return h return None class CpuLoad: def __init__(self): self._last = None def sample(self): line = _read("/proc/stat", "").splitlines()[0].split()[1:] vals = [int(x) for x in line] idle = vals[3] + vals[4] total = sum(vals) pct = None if self._last: dt = total - self._last[0] di = idle - self._last[1] pct = round(100.0 * (dt - di) / dt, 1) if dt > 0 else 0.0 self._last = (total, idle) return pct def cpu_freq_mhz(): freqs = [_read_int(p) for p in glob.glob( "/sys/devices/system/cpu/cpu[0-9]*/cpufreq/scaling_cur_freq")] freqs = [f for f in freqs if f] return round(sum(freqs) / len(freqs) / 1000) if freqs else None def mem_usage(): info = {} for line in _read("/proc/meminfo", "").splitlines(): k, v = line.split(":", 1) info[k] = int(v.split()[0]) total = info.get("MemTotal", 0) avail = info.get("MemAvailable", 0) return {"total_gb": round(total / 1048576, 1), "used_gb": round((total - avail) / 1048576, 1), "percent": round(100.0 * (total - avail) / total, 1) if total else None} def dgpu_active(): """True if the NVIDIA dGPU is awake (checked without waking it up).""" for dev in glob.glob("/sys/bus/pci/devices/*"): if _read(dev + "/vendor") == "0x10de" and (_read(dev + "/class") or "").startswith("0x03"): return _read(dev + "/power_state") == "D0" return None def sensors(): out = {"cpu_temp": None, "gpu_temp": None, "sys_temp": None, "cpu_fan": None, "gpu_fan": None} core = _hwmon("coretemp") if core: pkg = None for lbl in glob.glob(core + "/temp*_label"): if _read(lbl, "").startswith("Package"): pkg = _read_int(lbl.replace("_label", "_input")) out["cpu_temp"] = round(pkg / 1000, 1) if pkg else None acer = _hwmon("acer") if acer: g = _read_int(acer + "/temp2_input") s = _read_int(acer + "/temp3_input") out["gpu_temp"] = round(g / 1000, 1) if g else None out["sys_temp"] = round(s / 1000, 1) if s else None out["cpu_fan"] = _read_int(acer + "/fan1_input") out["gpu_fan"] = _read_int(acer + "/fan2_input") out["dgpu_active"] = dgpu_active() return out # --------------------------------------------------------------- Fans FAN_BEHAVIOR = 14 FAN_SPEED = 16 def _fan_val(percent, index): return ((int(percent) * 25600 // 100) & 0xFF00) + index def set_fans(mode, cpu=0, gpu=0): """mode: 'auto' | 'max' | 'custom' (cpu/gpu in percent, 0 = that fan stays automatic).""" if mode == "auto" or (mode == "custom" and cpu == 0 and gpu == 0): gaming_u64(FAN_BEHAVIOR, 0x410009) elif mode == "max": gaming_u64(FAN_BEHAVIOR, 0x820009) elif mode == "custom": cpu = max(0, min(100, int(cpu))) gpu = max(0, min(100, int(gpu))) if cpu == 0: gaming_u64(FAN_BEHAVIOR, 0x10001) gaming_u64(FAN_BEHAVIOR, 0xC00008) gaming_u64(FAN_SPEED, _fan_val(gpu, 4)) elif gpu == 0: gaming_u64(FAN_BEHAVIOR, 0x400008) gaming_u64(FAN_BEHAVIOR, 0x30001) gaming_u64(FAN_SPEED, _fan_val(cpu, 1)) else: gaming_u64(FAN_BEHAVIOR, 0xC30009) gaming_u64(FAN_SPEED, _fan_val(cpu, 1)) gaming_u64(FAN_SPEED, _fan_val(gpu, 4)) else: raise ValueError("unknown fan mode: %s" % mode) # --------------------------------------------------------------- Lightbar def set_lightbar(mode="static", color=(0, 200, 255), brightness=200, speed=5, direction=1): code = LIGHTBAR_MODES[mode][0] r, g, b = (max(0, min(255, int(c))) for c in color) buf = bytes([code, max(1, min(10, int(speed))), max(0, min(255, int(brightness))), 0, int(direction) & 0xFF, r, g, b, 0x03, 0x02]).ljust(16, b"\0") res = wmi_call(GUID_GAMING, 20, buf) if isinstance(res, (bytes, bytearray)) and res and res[0] != 0: raise WMIError("lightbar mode rejected by firmware (0x%02x)" % res[0]) # --------------------------------------------------------------- Battery def battery(): bat = next(iter(glob.glob("/sys/class/power_supply/BAT*")), None) ac = next((p for p in glob.glob("/sys/class/power_supply/*") if _read(p + "/type") == "Mains"), None) out = {"present": bool(bat), "ac_online": _read_int(ac + "/online", 0) == 1 if ac else True} if not bat: return out cur = _read_int(bat + "/current_now") volt = _read_int(bat + "/voltage_now") pwr = _read_int(bat + "/power_now") if pwr is None and cur is not None and volt is not None: pwr = cur * volt // 1_000_000 full = _read_int(bat + "/charge_full") or _read_int(bat + "/energy_full") design = _read_int(bat + "/charge_full_design") or _read_int(bat + "/energy_full_design") out.update({ "capacity": _read_int(bat + "/capacity"), "status": _read(bat + "/status"), "power_w": round(pwr / 1_000_000, 1) if pwr is not None else None, "cycles": _read_int(bat + "/cycle_count"), "health": round(100.0 * full / design, 1) if full and design else None, }) return out def _battery_status(): res = wmi_call(GUID_BATTERY, 20, bytes([1, 1, 0, 0])) if not isinstance(res, (bytes, bytearray)) or len(res) < 5: raise WMIError("unexpected reply from the battery interface") return res def get_battery_limit(): return _battery_status()[3] == 1 def get_battery_calibration(): return _battery_status()[4] == 1 def _battery_set(function, value): res = wmi_call(GUID_BATTERY, 21, bytes([1, function, 1 if value else 0, 0, 0, 0, 0, 0])) if isinstance(res, (bytes, bytearray)) and res and res[0] != 0: raise WMIError("battery setting rejected by firmware (0x%02x)" % res[0]) def set_battery_limit(enabled): _battery_set(1, enabled) def set_battery_calibration(enabled): _battery_set(2, enabled) # --------------------------------------------------------------- Firmware options def get_boot_sound(): v = gaming_u64(23, 0x02, in_size=4) if v & 0xFF: raise WMIError("Status 0x%02x" % (v & 0xFF)) return ((v >> 8) & 0xFF) == 2 def set_boot_sound(enabled): v = gaming_u64(22, ((2 if enabled else 1) << 8) | 0x02) if v & 0xFF: raise WMIError("boot sound setting rejected (0x%02x)" % (v & 0xFF)) def get_lcd_overdrive(): v = gaming_u64(9, 0x10, in_size=4) if v & 0xFF: raise WMIError("Status 0x%02x" % (v & 0xFF)) return bool((v >> 8) & 0xFF) def set_lcd_overdrive(enabled): v = gaming_u64(8, 0x10 | ((1 << 48) if enabled else 0)) if v & 0xFF: raise WMIError("LCD overdrive setting rejected (0x%02x)" % (v & 0xFF)) _USB_GET = {663296: 0, 659200: 10, 1314560: 20, 1969920: 30} _USB_SET = {0: 663300, 10: 659204, 20: 1314564, 30: 1969924} def get_usb_charging(): v = _as_u64(wmi_call(GUID_APGE, 2, (4).to_bytes(8, "little"))) return _USB_GET.get(v) def set_usb_charging(level): wmi_call(GUID_APGE, 1, _USB_SET[int(level)].to_bytes(8, "little")) def get_kbd_timeout(): v = _as_u64(wmi_call(GUID_APGE, 2, (0x88401).to_bytes(8, "little"))) return {0x1E0000080000: True, 0x80000: False}.get(v) def set_kbd_timeout(enabled): wmi_call(GUID_APGE, 1, (0x1E0000088402 if enabled else 0x88402).to_bytes(8, "little")) FIRMWARE_OPTIONS = { # name: (getter, setter, label) "boot_sound": (get_boot_sound, set_boot_sound, "Boot animation & sound"), "lcd_overdrive": (get_lcd_overdrive, set_lcd_overdrive, "LCD overdrive (faster pixel response)"), "kbd_timeout": (get_kbd_timeout, set_kbd_timeout, "Turn keyboard backlight off after 30 s idle"), "usb_charging": (get_usb_charging, set_usb_charging, "USB charging while powered off"), }