我把 macOS 的功耗数据做成了一个能在手机浏览器上实时看的仪表盘,含北京阶梯电价估
引子
我桌上摆着一台 M4 Mac mini 32GB,从 2026 年 6 月初开始就没关过机——跑 Docker、跑 ComfyUI 训练、跑后台服务。某天我突然好奇:它 7×24 小时开着,一个月到底花我多少钱电费?
这种问题,最朴素的做法是去查电表。但电表是整户楼的,单独拎出一台机器的耗电得自己测。我花了半个周末,从一行 powermetrics 命令开始,把它搭成了一个通过外网可以访问的实时 Web 仪表盘。
23 天实测下来:日均耗电 0.184 kWh,日均电费 0.09 元,月预估 2.70 元(北京阶梯电价)——这是我做这个项目之前完全没想到的数字。
这篇文章不是 step-by-step 教程(代码全开源在 GitHub),而是讲清楚整个过程中的技术决策和踩坑实录。如果你也想给自己的 Mac 装一个,可以照着流程做;如果你只是想看看"从命令行工具演进到可视化系统"是怎么个过程,也许能从我的判断里捞到一些东西。
- 最终成品在线看:https://hubianluanma.com/power/
- 完整代码:https://github.com/hubianluanma/mac-power-monitor
一、为什么需要自己做
macOS 上能拿到实时功耗数据的工具不多,我挨个对比过:
| 工具 | 类型 | 能拿到的数据 | 取舍 |
|---|---|---|---|
powermetrics | 系统 CLI(root) | CPU/GPU/ANE 毫瓦级功耗 | 数据最准,但要 sudo |
pmset | 系统 CLI | 电池放电率、电源事件 | 桌面机没电池,用不上 |
| Stats(开源) | 菜单栏 app | 实时瓦数 + 温度 | 装一个看瞬时值 |
| Watts / iGlance | 菜单栏 app | 瓦数 | 同 Stats,没特别优势 |
| Activity Monitor → 能耗 | 系统 GUI | app 级影响 | 不直观,看不到总瓦数 |
为什么没直接买 iStat Menus?它确实是好软件,但有两个问题:
- 一次性付费后是"看不见摸不着"的数字,没有历史曲线
- 我想把数据外网可访问——出门在外也能刷一下看家里 Mac 在干嘛
结论:底层用 powermetrics 采样,加一层自己的 Web 展示层做历史和成本估算。菜单栏的 Stats 我顺便装一个看瞬时值,但不参与数据流。
二、架构:四层足够,不需要更多
演化到最后的完整结构是这样的:
┌─────────────────────┐
│ powermetrics (sudo) │ ← 每分钟采样 5s
└──────────┬──────────┘
↓
┌─────────────────────┐
│ SQLite 单文件 DB │ ← /power/data/power.db
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Flask (127.0.0.1) │ ← 单文件服务,端口 7654
└──────────┬──────────┘
↓ proxy_pass /power/
┌─────────────────────┐
│ nginx (Docker) :80 │
└──────────┬──────────┘
↓
┌─────────────────────┐
│ Cloudflare Tunnel │ ← HTTPS 终止
└──────────┬──────────┘
↓
浏览器手机访问为什么这样分层:采集层只关心拿数据,存储层只关心怎么存,Web 层只关心怎么展示。每层都能独立替换或测试。这个项目我做了一周,每晚只动其中一层。
三、采集器:powermetrics + NOPASSWD sudoers
powermetrics 需要 root 权限,但每分钟采一次还要人输密码就太蠢了。给当前用户配 NOPASSWD sudoers 是常规做法:
# /etc/sudoers.d/hermes-powermetrics
spiral ALL=(root) NOPASSWD: /usr/bin/powermetrics
Defaults!/usr/bin/powermetrics !logfile, !syslog两个细节很关键:
- 限定到具体路径,不给完整 root NOPASSWD(最小权限原则)
!logfile, !syslog让这个命令不写系统日志,避免 syslog 被噪声淹没
采集器是个简单的 Python 循环,对齐整分钟采样 5 秒,解析三个 Power 字段:
# CPU Power: 123 mW
# GPU Power: 45 mW
# ANE Power: 0 mW
# Combined Power (CPU + GPU + ANE): 168 mW正则取出最后一次值(最稳定),写 SQLite。表结构只有 7 个字段,单文件 DB,零运维。
一个小坑:powermetrics 只给 SoC package 的功耗(CPU+GPU+ANE),不算屏幕、SSD、USB、Wi-Fi、DRAM 这些系统外设。M4 Mac mini 闲置时整机功耗实测 ~7-9W,但 SoC 端只有 0.1W 不到。差额主要是"系统开销",我硬编码了一个 SYSTEM_BIAS_W = 7.0 加进去当估算——比裸 SoC 数据更接近电表读数。这个参数通过环境变量可覆盖,不同 Mac 机型可以自己调。
四、Web 仪表盘:Flask + Chart.js 单文件
前端只有一页,没有用 React/Vue 这种重武器——单文件 HTML + Chart.js CDN 引一个就够。理由:
- 项目定位是"自用工具",不是产品,不需要组件化
- 一个 HTML 文件加 Chart.js 比 React + build pipeline 少 95% 的依赖
- 真要做复杂交互(比如 KPI 卡片点击切换图表),几十行 JS 就够
实际仪表盘长这样:

主视图包含四块:
- 顶部 4 个 KPI 卡片:当前功耗、今日耗电、今日平均、本月估算
- 实时功耗曲线:24 小时窗口,5 秒刷新
- 本月累计柱状图:按天累计电费
- 阶梯电价分档:北京年累计度数分档显示
一个交互细节:4 张 KPI 卡片是可点击的——点不同卡片会切换实时曲线的指标(瞬时功率 vs 今日累计 vs 5 点滑动平均 vs 月累计),这个交互我做了小动画,配上 localStorage 持久化用户选择。
五、电价模块:北京阶梯电价算法
中国阶梯电价是按年累计度数分档逐档累加的,不是简单的"超过 X 度就贵"。比如北京 2026 年标准:
| 年累计度数 | 单价 |
|---|---|
| 0 - 2520 度 | 0.4883 元/kWh |
| 2521 - 4800 度 | 0.5383 元/kWh |
| 4801+ 度 | 0.7883 元/kWh |
所以代码不能写 if kwh > 2520: price = 0.5383,而要按档逐档累加:
def beijing_cost(kwh):
if kwh <= 2520:
return kwh * 0.4883
elif kwh <= 4800:
return 2520 * 0.4883 + (kwh - 2520) * 0.5383
else:
return 2520 * 0.4883 + 2280 * 0.5383 + (kwh - 4800) * 0.7883我把它做成了一个字典驱动模块——加一个城市只要在 pricing.py 里加一行配置就行,不动核心代码。现在支持北京、上海、深圳、广州、成都五座城市。
六、踩坑实录(重点)
这部分是全文最值得看的。
坑 1:Flask SCRIPT_NAME vs 反代路径
Flask 默认认为自己在根路径跑,但 nginx 反代在 /power/ 下。直接 url_for('static', ...) 会生成 /static/... 而不是 /power/static/...,导致静态文件 404。
解决方案:在 nginx 把 SCRIPT_NAME 通过 proxy_set_header 传过去,Flask 读 request.script_root:
location /power/ {
proxy_set_header X-Script-Name /power;
proxy_pass http://127.0.0.1:7654/;
}坑 2:Hermes 沙盒启动 Flask 服务的诡异行为
我原本想用 Hermes 的 delegate_task 让 cron 帮我拉起 Flask,结果发现 Hermes 沙盒环境里 Flask 启动后会"假死"——端口 listen 着但请求不响应。原因是沙盒的 stdout/stderr 缓冲和真实终端不一样。
最终方案:老老实实 launchd 拉起,不碰任何 agent 框架。launchd plist 模板我放在 repo 的 examples/ 下,复制改路径就能用。
坑 3:JS fetch().json() 异步链
前端每 5 秒拉一次 /api/summary,但忘了 await 直接 .json(),控制台一片红。最后改成 async/await 才稳定。这种坑在原生 JS 里特别隐蔽。
坑 4:Flask 模板缓存(Py 3.14 + Werkzeug 3.1)
我用的 Python 3.14 + Werkzeug 3.1,改了 HTML 模板刷新浏览器不生效——缓存异常激进。临时方案:开发时设 TEMPLATES_AUTO_RELOAD=True;生产部署靠 release 时清缓存目录。
坑 5:powermetrics 偶发输出截断
采样 5 秒但偶发只输出 3 秒就退出,导致正则取不到 Combined Power 字段。容错做法:取最后一次出现的 Combined Power,找不到就跳过这一分钟(数据缺失但不报错)。
坑 6:Cloudflare Tunnel 的免费额度
外网访问走 Cloudflare Tunnel 免费版,每月 100 万次请求额度。我的访问量不大完全够,但如果做成公开服务被刷流量就不行了。
七、实测数据(23 天)
跑了 23 天(2026-06-22 到 2026-07-15),总共 34,186 个采样点。结果有点出乎意料:
| 指标 | 数值 |
|---|---|
| 平均功耗 | 7.68 W |
| 最低功耗 | 7.02 W(闲置态) |
| 最高功耗 | 30.39 W(6/26 那次 ComfyUI 训练) |
| 日均耗电 | 0.184 kWh |
| 日均电费 | ¥0.09 |
| 月预估电费 | ¥2.70 |
| 累计耗电 | 4.25 kWh |
| 累计电费 | ¥2.07 |
几个反直觉的发现:
- 闲置功耗远低于想象:7W 是什么概念?相当于一个普通 LED 灯泡。Mac mini 的待机能耗控制是真不错。
- 高负载占比极低:>30W 的样本只占总样本的 0.05%(ComfyUI 训练那次 16 个样本)。平时即使开着 IDE + Docker + 浏览器,功耗也很少突破 15W。
- 峰值时段在凌晨 4-9 点:一开始我以为是 bug,后来查发现是 macOS 的
powermetrics在系统空闲时反而采样更频繁(默认行为)——不是真的功耗高。 - 网络请求占总功耗的隐性大头:每分钟一次的 fetch + 5 秒刷新图表,单这一项就让"完全闲置"的功耗从 7.0W 升到 7.7W 左右。
八、做这事值不值?
我从两个维度算过账:
时间成本:
- 写采集器:2 小时
- 写 Web 仪表盘:4 小时
- 配置 nginx + Cloudflare Tunnel:1 小时
- 调试踩坑:3 小时
- 写文档 + 截图:2 小时
- 总计:约 12 小时
电费节省:
- 这个项目没有节省任何电费——Mac 该跑还是跑。
- 但让我重新评估了"7×24 开机"的成本——2.7 元/月完全可接受,比想象中便宜得多。以前我因为怕电费每天下班关机的习惯,现在是不必要的内耗。
情感收益:
- 看着 dashboard 上曲线平稳流动、CPU/SoC 一起跑长任务,有点莫名其妙的安全感。
- 偶尔出门在外刷一下
/power/,确认家里 Mac 还在正常工作,也是一种小确幸。
九、给也想做的人
如果你也想给自己的 Mac 装一个,不要先看代码,先想清楚你要回答什么问题。是"今天耗电多少"?是"哪个 app 最耗电"?是"Mac 关不关机划算"?这三个问题对应三个完全不同的设计。
我的项目只回答第一个问题。如果你想要"app 级耗电",要么买 iStat Menus,要么自己扩采集器加 ps -ax 关联 PID——这个我下个版本可能会做。
代码放在 https://github.com/hubianluanma/mac-power-monitor,MIT 协议,欢迎 fork。如果你的 Mac 不是 M4 mini,可以调 SYSTEM_BIAS_W 这个环境变量适配你的机型。
如果你部署过程中遇到问题,欢迎评论区交流,我看到会回。
