feat: windows adaptation (WMI thermal zones, battery fallback)

This commit is contained in:
snowy-yang committed 2026-09-12 19:53:24 +08:00
1 parent 28add64ae0
commit 8775c022bd
3 files changed
+59 -9

No files matched your search

+11
View File
@@ -74,6 +74,17 @@ uv run pytest
- Intel CPU:读取 RAPL `/sys/class/powercap/intel-rapl:*/energy_uj`,两次采样差分计算瓦数 - Intel CPU:读取 RAPL `/sys/class/powercap/intel-rapl:*/energy_uj`,两次采样差分计算瓦数
- AMD CPU / 多数虚拟机 / 云主机:无硬件计数器,`/power` 会提示不支持(属正常) - AMD CPU / 多数虚拟机 / 云主机:无硬件计数器,`/power` 会提示不支持(属正常)
## 平台兼容性
| 功能 | Linux | Windows |
|---|---|---|
| CPU/内存/进程/磁盘/网络 | 完整支持 | 完整支持 |
| 负载 (load average) | 内核提供 | psutil 模拟值 |
| 功耗 (瓦数) | RAPL / 电池功率 | 不可测,笔记本显示电池电量与充放电状态 |
| 温度 | lm-sensors / sysfs | WMI 热区(部分主板支持,可能需管理员权限) |
机器人本体(OneBot 反向 WS、命令、告警)在两个平台行为一致。
## 本地测试(Windows 也可) ## 本地测试(Windows 也可)
## systemd 部署(Linux) ## systemd 部署(Linux)
+14 -8
View File
@@ -40,11 +40,14 @@ def _fmt_uptime(seconds: float) -> str:
def _power_str(p: dict) -> str: def _power_str(p: dict) -> str:
if p["total_watts"] is None: if p["total_watts"] is not None:
return "不支持(需要电池或Intel RAPL)" src = {"battery": "电池", "rapl": "RAPL"}.get(p["source"], p["source"])
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 ""
extra = f" (电池 {p['battery']}W)" if p["battery"] is not None and p["source"] == "rapl" else "" return f"{p['total_watts']}W [{src}]{extra}"
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): def _hottest(temps: dict):
@@ -78,15 +81,18 @@ def fmt_status(s: dict) -> str:
def fmt_power(s: dict) -> str: def fmt_power(s: dict) -> str:
p = s["power"] 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( return templates.render(
"power", "power",
power=("未获取到功耗数据。支持的来源:笔记本电池 /sys/class/power_supply," power=("未获取到功耗数据。支持的来源:笔记本电池 /sys/class/power_supply 或系统电池接口,"
"Intel RAPL /sys/class/powercap (AMD/多数虚拟机不支持)。"), "Intel RAPL /sys/class/powercap (AMD/多数虚拟机/台式机不支持)。"),
) )
lines = [] lines = []
if p["battery"] is not None: if p["battery"] is not None:
lines.append(f"电池功率: {p['battery']}W") 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"]: if p["domains"]:
lines.append("RAPL 域:") lines.append("RAPL 域:")
lines += [f" {name}: {watts}W" for name, watts in p["domains"].items()] lines += [f" {name}: {watts}W" for name, watts in p["domains"].items()]
+34 -1
View File
@@ -65,6 +65,16 @@ def get_power() -> dict:
# 累加所有域(通常 dom0 已包含子域,取最大 dom0) # 累加所有域(通常 dom0 已包含子域,取最大 dom0)
result["total_watts"] = max(result["domains"].values()) 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 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: def get_temps() -> dict:
try: try:
sensors = psutil.sensors_temperatures() sensors = psutil.sensors_temperatures()
except (AttributeError, OSError): except (AttributeError, OSError):
return {} sensors = {}
out = {} out = {}
for name, entries in sensors.items(): for name, entries in sensors.items():
out[name] = [ out[name] = [
@@ -110,6 +141,8 @@ def get_temps() -> dict:
for i, e in enumerate(entries) for i, e in enumerate(entries)
if e.current is not None if e.current is not None
] ]
if not out and os.name == "nt":
out = _wmi_temps()
return out return out