feat: remove power monitoring feature
This commit is contained in:
1 parent
593b8011c6
commit
e49a2f8f03
6 files changed
+3
-123
No files matched your search
@@ -55,8 +55,7 @@ uv run pytest
|
|||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/status` 或 `状态` | 总览:功耗、CPU、负载、内存、最高温度、网速、运行时间 |
|
| `/status` 或 `状态` | 总览:CPU、负载、内存、最高温度、网速、运行时间 |
|
||||||
| `/power` 或 `功耗` | 功耗详情(电池功率 / Intel RAPL 各域瓦数) |
|
|
||||||
| `/top` 或 `进程` | CPU 占用 TOP10 进程 |
|
| `/top` 或 `进程` | CPU 占用 TOP10 进程 |
|
||||||
| `/temp` 或 `温度` | 各传感器温度 |
|
| `/temp` 或 `温度` | 各传感器温度 |
|
||||||
| `/net` 或 `网络` | 网络上下行速率 |
|
| `/net` 或 `网络` | 网络上下行速率 |
|
||||||
@@ -68,19 +67,12 @@ uv run pytest
|
|||||||
消息包含当前 CPU%、负载和 TOP5 高负载进程(PID/名称/CPU/内存)。同一轮高负载期间
|
消息包含当前 CPU%、负载和 TOP5 高负载进程(PID/名称/CPU/内存)。同一轮高负载期间
|
||||||
冷却 10 分钟不重复告警;回落到阈值以下会发送恢复通知。以上均可在 `config.toml` 的 `[alert]` 中调整。
|
冷却 10 分钟不重复告警;回落到阈值以下会发送恢复通知。以上均可在 `config.toml` 的 `[alert]` 中调整。
|
||||||
|
|
||||||
## 功耗说明
|
|
||||||
|
|
||||||
- 笔记本:读取 `/sys/class/power_supply/BAT*/power_now`
|
|
||||||
- Intel CPU:读取 RAPL `/sys/class/powercap/intel-rapl:*/energy_uj`,两次采样差分计算瓦数
|
|
||||||
- AMD CPU / 多数虚拟机 / 云主机:无硬件计数器,`/power` 会提示不支持(属正常)
|
|
||||||
|
|
||||||
## 平台兼容性
|
## 平台兼容性
|
||||||
|
|
||||||
| 功能 | Linux | Windows |
|
| 功能 | Linux | Windows |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| CPU/内存/进程/磁盘/网络 | 完整支持 | 完整支持 |
|
| CPU/内存/进程/磁盘/网络 | 完整支持 | 完整支持 |
|
||||||
| 负载 (load average) | 内核提供 | psutil 模拟值 |
|
| 负载 (load average) | 内核提供 | psutil 模拟值 |
|
||||||
| 功耗 (瓦数) | RAPL / 电池功率 | 不可测,笔记本显示电池电量与充放电状态 |
|
|
||||||
| 温度 | lm-sensors / sysfs | WMI 热区(部分主板支持,可能需管理员权限) |
|
| 温度 | lm-sensors / sysfs | WMI 热区(部分主板支持,可能需管理员权限) |
|
||||||
|
|
||||||
机器人本体(OneBot 反向 WS、命令、告警)在两个平台行为一致。
|
机器人本体(OneBot 反向 WS、命令、告警)在两个平台行为一致。
|
||||||
|
|||||||
@@ -43,17 +43,6 @@ def _fmt_uptime(seconds: float) -> str:
|
|||||||
return f"{d}天{h}小时{m}分" if d else f"{h}小时{m}分"
|
return f"{d}天{h}小时{m}分" if d else f"{h}小时{m}分"
|
||||||
|
|
||||||
|
|
||||||
def _power_str(p: dict) -> str:
|
|
||||||
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):
|
def _hottest(temps: dict):
|
||||||
flat = [t for entries in temps.values() for t in entries]
|
flat = [t for entries in temps.values() for t in entries]
|
||||||
return max(flat, key=lambda t: t["current"]) if flat else None
|
return max(flat, key=lambda t: t["current"]) if flat else None
|
||||||
@@ -66,7 +55,6 @@ def fmt_status(s: dict) -> str:
|
|||||||
d = s["disk"][0] if s["disk"] else None
|
d = s["disk"][0] if s["disk"] else None
|
||||||
return templates.render(
|
return templates.render(
|
||||||
"status",
|
"status",
|
||||||
power=_power_str(s["power"]),
|
|
||||||
cpu_total=f"{sum(c['per_core']):.1f}%",
|
cpu_total=f"{sum(c['per_core']):.1f}%",
|
||||||
cpu_max=f"{c['cores'] * 100}%",
|
cpu_max=f"{c['cores'] * 100}%",
|
||||||
cpu_avg=f"{c['percent']}",
|
cpu_avg=f"{c['percent']}",
|
||||||
@@ -83,26 +71,6 @@ 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 and p.get("battery_percent") is None:
|
|
||||||
return templates.render(
|
|
||||||
"power",
|
|
||||||
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()]
|
|
||||||
return templates.render("power", power="\n".join(lines) or "无数据")
|
|
||||||
|
|
||||||
|
|
||||||
def fmt_top(s: dict) -> str:
|
def fmt_top(s: dict) -> str:
|
||||||
if not s["top"]:
|
if not s["top"]:
|
||||||
return templates.render("top", top_n=10, top_list="无数据")
|
return templates.render("top", top_n=10, top_list="无数据")
|
||||||
@@ -133,7 +101,6 @@ def fmt_net(net: dict) -> str:
|
|||||||
# 命令别名 -> 动作;只有此表内的词会触发,其他消息一律忽略
|
# 命令别名 -> 动作;只有此表内的词会触发,其他消息一律忽略
|
||||||
COMMANDS = {
|
COMMANDS = {
|
||||||
"status": "status", "状态": "status",
|
"status": "status", "状态": "status",
|
||||||
"power": "power", "功耗": "power",
|
|
||||||
"top": "top", "进程": "top",
|
"top": "top", "进程": "top",
|
||||||
"temp": "temp", "温度": "temp",
|
"temp": "temp", "温度": "temp",
|
||||||
"net": "net", "网络": "net",
|
"net": "net", "网络": "net",
|
||||||
@@ -234,8 +201,6 @@ class Bot:
|
|||||||
try:
|
try:
|
||||||
if action == "status":
|
if action == "status":
|
||||||
return fmt_status(monitor.get_status())
|
return fmt_status(monitor.get_status())
|
||||||
if action == "power":
|
|
||||||
return fmt_power({"power": monitor.get_power()})
|
|
||||||
if action == "top":
|
if action == "top":
|
||||||
return fmt_top({"top": monitor.get_top_processes(10)})
|
return fmt_top({"top": monitor.get_top_processes(10)})
|
||||||
if action == "temp":
|
if action == "temp":
|
||||||
|
|||||||
+2
-75
@@ -1,4 +1,4 @@
|
|||||||
"""Linux 系统监控:功耗(RAPL/电池)、CPU、内存、温度、磁盘、网络、进程。"""
|
"""Linux 系统监控:CPU、内存、温度、磁盘、网络、进程。"""
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -6,79 +6,7 @@ from pathlib import Path
|
|||||||
import psutil
|
import psutil
|
||||||
|
|
||||||
|
|
||||||
# ---------- 功耗 ----------
|
# ---------- 指标 ----------
|
||||||
|
|
||||||
def _read_energy_uj(path: Path) -> int | None:
|
|
||||||
try:
|
|
||||||
return int(path.read_text().strip())
|
|
||||||
except (OSError, ValueError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_power() -> dict:
|
|
||||||
"""返回功耗信息,单位瓦(W)。优先电池,其次 Intel RAPL。"""
|
|
||||||
result = {"source": None, "total_watts": None, "domains": {}, "battery": None}
|
|
||||||
|
|
||||||
# 1. 电池功率(笔记本放电/充电)
|
|
||||||
for p in Path("/sys/class/power_supply").glob("BAT*/power_now"):
|
|
||||||
try:
|
|
||||||
# power_now 单位微瓦
|
|
||||||
watts = int(p.read_text().strip()) / 1_000_000
|
|
||||||
result["battery"] = round(watts, 2)
|
|
||||||
result["source"] = "battery"
|
|
||||||
result["total_watts"] = round(watts, 2)
|
|
||||||
except (OSError, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 2. Intel RAPL energy 差分
|
|
||||||
rapl_dir = Path("/sys/class/powercap")
|
|
||||||
domains_now = {}
|
|
||||||
for i, d in enumerate(sorted(rapl_dir.glob("intel-rapl:*"))):
|
|
||||||
name_file = d / "name"
|
|
||||||
energy_file = d / "energy_uj"
|
|
||||||
if not energy_file.exists():
|
|
||||||
continue
|
|
||||||
domain = name_file.read_text().strip() if name_file.exists() else f"rapl{i}"
|
|
||||||
energy = _read_energy_uj(energy_file)
|
|
||||||
if energy is not None:
|
|
||||||
domains_now[domain] = (energy, time.monotonic())
|
|
||||||
|
|
||||||
if domains_now:
|
|
||||||
# 用上一次读数差分计算瓦数
|
|
||||||
prev = getattr(get_power, "_prev_rapl", None)
|
|
||||||
get_power._prev_rapl = domains_now
|
|
||||||
if prev:
|
|
||||||
for domain, (energy, ts) in domains_now.items():
|
|
||||||
if domain in prev:
|
|
||||||
p_energy, p_ts = prev[domain]
|
|
||||||
dt = ts - p_ts
|
|
||||||
if dt > 0:
|
|
||||||
delta = energy - p_energy
|
|
||||||
if delta < 0: # 计数器回绕
|
|
||||||
max_uj = 1 << 32
|
|
||||||
delta += max_uj
|
|
||||||
watts = delta / 1_000_000 / dt
|
|
||||||
result["domains"][domain] = round(watts, 2)
|
|
||||||
if result["domains"]:
|
|
||||||
result["source"] = result["source"] or "rapl"
|
|
||||||
if result["total_watts"] is None:
|
|
||||||
# 累加所有域(通常 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
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- 其他指标 ----------
|
|
||||||
|
|
||||||
def get_cpu() -> dict:
|
def get_cpu() -> dict:
|
||||||
load1, load5, load15 = _safe_getloadavg()
|
load1, load5, load15 = _safe_getloadavg()
|
||||||
@@ -249,7 +177,6 @@ def get_uptime() -> float:
|
|||||||
def get_status(top_n: int | None = None) -> dict:
|
def get_status(top_n: int | None = None) -> dict:
|
||||||
"""系统总览。top_n 为 None 时不枚举进程(Windows 上较慢)。"""
|
"""系统总览。top_n 为 None 时不枚举进程(Windows 上较慢)。"""
|
||||||
data = {
|
data = {
|
||||||
"power": get_power(),
|
|
||||||
"cpu": get_cpu(),
|
"cpu": get_cpu(),
|
||||||
"memory": get_memory(),
|
"memory": get_memory(),
|
||||||
"temps": get_temps(),
|
"temps": get_temps(),
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
功耗详情
|
|
||||||
{power}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
服务器状态
|
服务器状态
|
||||||
功耗: {power}
|
|
||||||
CPU: {cpu_total} / {cpu_max} (单核均值 {cpu_avg}%) 核心 {cores} 负载 {load}
|
CPU: {cpu_total} / {cpu_max} (单核均值 {cpu_avg}%) 核心 {cores} 负载 {load}
|
||||||
内存: {memory}
|
内存: {memory}
|
||||||
最高温度: {temp}
|
最高温度: {temp}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ async def wait_reply(fake: FakeOneBotClient, timeout: float = 30) -> str:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("cmd,expect", [
|
@pytest.mark.parametrize("cmd,expect", [
|
||||||
("状态", "服务器状态"),
|
("状态", "服务器状态"),
|
||||||
("功耗", "功耗"),
|
|
||||||
("top", "TOP10"),
|
("top", "TOP10"),
|
||||||
("温度", "温度"),
|
("温度", "温度"),
|
||||||
("网络", "网络速率"),
|
("网络", "网络速率"),
|
||||||
|
|||||||
Reference in new issue
Block a user