From 8775c022bdb3a67f252fc7604dd5e61652f7528f Mon Sep 17 00:00:00 2001 From: snowy-yang <102860110+snowy-yang@users.noreply.github.com> Date: Sat, 12 Sep 2026 19:53:24 +0800 Subject: [PATCH] feat: windows adaptation (WMI thermal zones, battery fallback) --- README.md | 11 +++++++++++ bot.py | 22 ++++++++++++++-------- monitor.py | 35 ++++++++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b22bf96..72e6c9d 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,17 @@ uv run pytest - Intel CPU:读取 RAPL `/sys/class/powercap/intel-rapl:*/energy_uj`,两次采样差分计算瓦数 - AMD CPU / 多数虚拟机 / 云主机:无硬件计数器,`/power` 会提示不支持(属正常) +## 平台兼容性 + +| 功能 | Linux | Windows | +|---|---|---| +| CPU/内存/进程/磁盘/网络 | 完整支持 | 完整支持 | +| 负载 (load average) | 内核提供 | psutil 模拟值 | +| 功耗 (瓦数) | RAPL / 电池功率 | 不可测,笔记本显示电池电量与充放电状态 | +| 温度 | lm-sensors / sysfs | WMI 热区(部分主板支持,可能需管理员权限) | + +机器人本体(OneBot 反向 WS、命令、告警)在两个平台行为一致。 + ## 本地测试(Windows 也可) ## systemd 部署(Linux) diff --git a/bot.py b/bot.py index 01f8c79..2d477f2 100644 --- a/bot.py +++ b/bot.py @@ -40,11 +40,14 @@ def _fmt_uptime(seconds: float) -> str: def _power_str(p: dict) -> str: - if p["total_watts"] is None: - return "不支持(需要电池或Intel RAPL)" - src = {"battery": "电池", "rapl": "RAPL"}.get(p["source"], p["source"]) - extra = f" (电池 {p['battery']}W)" if p["battery"] is not None and p["source"] == "rapl" else "" - return f"{p['total_watts']}W [{src}]{extra}" + if p["total_watts"] is not None: + src = {"battery": "电池", "rapl": "RAPL"}.get(p["source"], p["source"]) + extra = f" (电池 {p['battery']}W)" if p["battery"] is not None and p["source"] == "rapl" else "" + return f"{p['total_watts']}W [{src}]{extra}" + if p.get("battery_percent") is not None: + state = "充电中" if p.get("plugged") else "放电中" + return f"电量 {p['battery_percent']}% ({state}),功率不可测" + return "不支持(需要电池或Intel RAPL)" def _hottest(temps: dict): @@ -78,15 +81,18 @@ def fmt_status(s: dict) -> str: def fmt_power(s: dict) -> str: p = s["power"] - if p["total_watts"] is None and p["battery"] is None: + if p["total_watts"] is None and p["battery"] is None and p.get("battery_percent") is None: return templates.render( "power", - power=("未获取到功耗数据。支持的来源:笔记本电池 /sys/class/power_supply," - "Intel RAPL /sys/class/powercap (AMD/多数虚拟机不支持)。"), + power=("未获取到功耗数据。支持的来源:笔记本电池 /sys/class/power_supply 或系统电池接口," + "Intel RAPL /sys/class/powercap (AMD/多数虚拟机/台式机不支持)。"), ) lines = [] if p["battery"] is not None: lines.append(f"电池功率: {p['battery']}W") + if p.get("battery_percent") is not None: + state = "充电中" if p.get("plugged") else "放电中" + lines.append(f"电量: {p['battery_percent']}% ({state})") if p["domains"]: lines.append("RAPL 域:") lines += [f" {name}: {watts}W" for name, watts in p["domains"].items()] diff --git a/monitor.py b/monitor.py index d1740e4..4f6358c 100644 --- a/monitor.py +++ b/monitor.py @@ -65,6 +65,16 @@ def get_power() -> dict: # 累加所有域(通常 dom0 已包含子域,取最大 dom0) result["total_watts"] = max(result["domains"].values()) + # 3. Windows/通用兜底:电池电量百分比(无功率数据) + if result["total_watts"] is None: + try: + bat = psutil.sensors_battery() + except (AttributeError, OSError): + bat = None + if bat is not None: + result["battery_percent"] = round(bat.percent, 1) + result["plugged"] = bat.power_plugged + return result @@ -98,11 +108,32 @@ def get_memory() -> dict: } +def _wmi_temps() -> dict: + """Windows: 通过 WMI 热区读取温度(部分主板支持,可能需要管理员权限)。""" + import subprocess + try: + out = subprocess.run( + ["powershell", "-NoProfile", "-Command", + "Get-CimInstance -Namespace root/wmi -ClassName MSAcpi_ThermalZoneTemperature " + "| Select-Object -ExpandProperty CurrentTemperature"], + capture_output=True, text=True, timeout=10, + encoding="utf-8", errors="replace", + ) + values = [float(x) for x in out.stdout.split() if x.replace(".", "", 1).isdigit()] + except (OSError, subprocess.SubprocessError, ValueError): + return {} + # WMI 单位是 0.1 开尔文 + return {"ThermalZone": [ + {"label": f"zone{i}", "current": round(v / 10 - 273.15, 1)} + for i, v in enumerate(values) + ]} + + def get_temps() -> dict: try: sensors = psutil.sensors_temperatures() except (AttributeError, OSError): - return {} + sensors = {} out = {} for name, entries in sensors.items(): out[name] = [ @@ -110,6 +141,8 @@ def get_temps() -> dict: for i, e in enumerate(entries) if e.current is not None ] + if not out and os.name == "nt": + out = _wmi_temps() return out