技术博客:MacBook Pro 电池信息检测脚本优化之旅 🎉
嘿,大家好!今天我们来聊聊如何用 Python 编写一个脚本,检测 MacBook Pro 的电池信息,并解决一系列棘手问题!😄 以下是本次优化的总结,附上代码分析、流程图和时序图,最后还有思维导图帮你梳理思路。准备好了吗?🚀
优化总结:关键数据一览 📊
字段 | 值 | 单位 | 含义 | 备注 |
---|---|---|---|---|
CurrentCapacity | 100% (~6254 mAh) | % (mAh) | 当前电池容量 | 参考 AppleRawCurrentCapacity (苹果原始当前容量) |
MaxCapacity | 100% (~6398 mAh) | % (mAh) | 最大可用容量 | 参考 AppleRawMaxCapacity (苹果原始最大容量) |
StateOfCharge | 98% | % | 当前电量百分比 | 从 BatteryData 获取 |
DesignCapacity | 6249 | mAh | 出厂标称容量 | 硬件设计值 |
Temperature | 30.34 | °C | 电池温度 | 正常范围 (0-35°C) |
Voltage | 13.107 | V | 电池电压 | 正常范围 (~12-13.2V) |
CycleCount | 44 | 次 | 充放电循环次数 | 与 system_profiler 一致 |
FullyCharged | Yes | - | 电池是否充满 | 与 system_profiler 一致 |
BatteryData | ||||
- CellVoltage | [4369, 4369, 4369] | mV | 各电池单体电压 | 正常 (~4.3V/单体) |
- Qmax | [6803, 6802, 6804] | mAh | 单体最大容量估计 | 接近设计容量 |
- Serial | F8YHCJG01C700000E4 | - | 电池唯一标识 | 硬件序列号 |
- ManufactureDate | 54083053433651 | - | 电池生产日期 | 需解码,推测 2021-07 |
- Ra03 | 58 | mΩ | 内部电阻参数 | 低值表示健康 |
- Ra10 | 75 | mΩ | 内部电阻参数 | 低值表示健康 |
以上数据基于 MacBook Pro (14英寸, 2024年11月) 运行 macOS Sequoia 15.2,搭载 Apple M4 芯片,16GB 内存和 1TB SSD。电池状态显示充满(FullyCharged=Yes
),StateOfCharge=98%
与 system_profiler
的 100% 差异属正常校准。🌟
优化过程:Mermaid 流程图 🖼️
以下是脚本优化的流程图,帮助你直观了解每个步骤:
这个流程图展示了从数据采集到输出的完整逻辑!🔧
交互时序:Sequence Diagrams 🕒
以下是脚本与系统交互的时序图,展示数据流转过程:
时序图清晰展示了脚本如何与 macOS 和 I/O Registry (输入/输出注册表) 协作!⏳
技术博客正文:从问题到完美解决方案 📝
背景介绍 🎬
最近,我在使用 MacBook Pro (14英寸, 2024年11月) 开发一个电池信息检测脚本时,遇到了不少挑战!😓 最初,脚本通过 ioreg
命令获取电池数据,但 StateOfCharge
(电池充电状态) 经常显示“缺失”,且 JSON (JavaScript Object Notation,JavaScript对象表示法) 序列化因 bytes
类型失败,抛出 TypeError
。经过多次优化,终于搞定!这次博客带你回顾整个过程,分享代码和经验。💡
问题分析 🕵️♂️
- StateOfCharge 缺失:
- 调试日志显示
顶级 StateOfCharge = 缺失
,但BatteryData StateOfCharge = 98
。 - 原因:macOS Sequoia 15.2 的 SMC (System Management Controller,系统管理控制器) 动态调整了
ioreg
输出结构,StateOfCharge
移至BatteryData
字典。
- 调试日志显示
- JSON 序列化错误:
- 旧脚本无法处理
BatteryData
中的bytes
数据(如MfgData=<00...>
),导致json.dump
崩溃。 - 新增
BytesEncoder
将bytes
转为十六进制,解决问题。
- 旧脚本无法处理
- 数据不一致:
StateOfCharge=98%
(来自ioreg
) vs.100%
(来自system_profiler
),因 macOS 满电校准。
优化方案 🛠️
代码实现
以下是优化后的完整代码,重点解决上述问题:
import subprocess
import plistlib
import json
import time
from datetime import datetime
# 自定义 JSON 编码器处理 bytes
class BytesEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, bytes):
return obj.hex()
return json.JSONEncoder.default(self, obj)
# 执行 ioreg 命令,尝试多次
def get_ioreg_data(retries=5, delay=2):
cmd = "ioreg -rw0 -c AppleSmartBattery -a"
for attempt in range(retries):
try:
output = subprocess.check_output(cmd, shell=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
with open(f"battery_raw_{timestamp}.plist", "wb") as f:
f.write(output)
print(f"调试:原始 ioreg 输出已保存至 battery_raw_{timestamp}.plist")
return output
except subprocess.CalledProcessError as e:
print(f"尝试 {attempt+1}/{retries} 失败:{e} 😓")
if attempt < retries - 1:
time.sleep(delay)
print("错误:无法执行 ioreg 命令 😢")
exit(1)
# 获取 ioreg 数据
output = get_ioreg_data()
# 解析 XML 属性列表
try:
plist = plistlib.loads(output)
except Exception as e:
print(f"错误:无法解析 XML 输出: {e} 😵")
exit(1)
# 查找 AppleSmartBattery 数据
battery = None
if isinstance(plist, list) and len(plist) > 0:
battery = next((item for item in plist if isinstance(item, dict) and item.get('IOObjectClass') == 'AppleSmartBattery'), None)
if not battery:
print("错误:未找到 AppleSmartBattery 数据 😢")
exit(1)
# 调试:输出 battery 信息
print("调试:battery 键列表:", list(battery.keys()))
print("调试:顶级 StateOfCharge =", battery.get('StateOfCharge', '缺失'))
print("调试:BatteryData StateOfCharge =", battery.get('BatteryData', {}).get('StateOfCharge', '缺失'))
print("调试:StateOfCharge 类型 =", type(battery.get('BatteryData', {}).get('StateOfCharge', None)))
# 保存 battery 字典为 JSON
try:
with open("battery_debug.json", "w") as f:
json.dump(battery, f, indent=2, cls=BytesEncoder, ensure_ascii=False)
print("调试:battery 字典已保存至 battery_debug.json")
except Exception as e:
print(f"调试:保存 battery_debug.json 失败: {e} 😔")
# 提取关键字段
data = {
'CurrentCapacity': battery.get('CurrentCapacity', 'N/A'),
'AppleRawCurrentCapacity': battery.get('AppleRawCurrentCapacity', 'N/A'),
'MaxCapacity': battery.get('MaxCapacity', 'N/A'),
'AppleRawMaxCapacity': battery.get('AppleRawMaxCapacity', 'N/A'),
'StateOfCharge': battery.get('BatteryData', {}).get('StateOfCharge', battery.get('StateOfCharge', 'N/A')),
'DesignCapacity': battery.get('DesignCapacity', 'N/A'),
'Temperature': battery.get('Temperature', 'N/A') / 100.0 if isinstance(battery.get('Temperature'), (int, float)) else 'N/A',
'Voltage': battery.get('Voltage', 'N/A') / 1000.0 if isinstance(battery.get('Voltage'), (int, float)) else 'N/A',
'CycleCount': battery.get('CycleCount', 'N/A'),
'FullyCharged': 'Yes' if battery.get('FullyCharged', False) else 'No',
'BatteryData': {
'CellVoltage': battery.get('BatteryData', {}).get('CellVoltage', 'N/A'),
'Qmax': battery.get('BatteryData', {}).get('Qmax', 'N/A'),
'Serial': battery.get('BatteryData', {}).get('Serial', 'N/A'),
'ManufactureDate': battery.get('BatteryData', {}).get('ManufactureDate', 'N/A'),
'Ra03': battery.get('BatteryData', {}).get('Ra03', 'N/A'),
'Ra10': battery.get('BatteryData', {}).get('Ra10', 'N/A')
}
}
# 修正容量单位
max_capacity_display = f"{data['MaxCapacity']}% (~{data['AppleRawMaxCapacity']} mAh)" if data['MaxCapacity'] == 100 else f"{data['MaxCapacity']} mAh"
current_capacity_display = f"{data['CurrentCapacity']}% (~{data['AppleRawCurrentCapacity']} mAh)" if data['CurrentCapacity'] == 100 else f"{data['CurrentCapacity']} mAh"
# 修正 StateOfCharge
soc_display = data['StateOfCharge']
if isinstance(soc_display, (int, float)) and soc_display >= 0:
soc_display = f"{soc_display}%"
else:
# 推算 SoC
if isinstance(data['AppleRawCurrentCapacity'], int) and isinstance(data['AppleRawMaxCapacity'], int) and data['AppleRawMaxCapacity'] > 0:
soc_estimated = round((data['AppleRawCurrentCapacity'] / data['AppleRawMaxCapacity']) * 100)
soc_display = f"{soc_estimated}% (推算)"
else:
soc_display = "N/A% (无法推算)"
# 准备表格数据
table_data = [
["当前容量", current_capacity_display, "% (mAh)", "当前电池容量"],
["最大容量", max_capacity_display, "% (mAh)", "最大可用容量"],
["充电状态 (SoC)", soc_display, "%", "当前电量百分比"],
["设计容量", f"{data['DesignCapacity']}", "mAh", "出厂标称容量"],
["温度", f"{data['Temperature']}", "°C", "电池温度"],
["电压", f"{data['Voltage']}", "V", "电池电压"],
["循环次数", f"{data['CycleCount']}", "次", "充放电循环次数"],
["是否充满", f"{data['FullyCharged']}", "-", "电池是否充满"],
]
battery_data_table = [
["单体电压", f"{data['BatteryData']['CellVoltage']}", "mV", "各电池单体电压"],
["单体容量 (Qmax)", f"{data['BatteryData']['Qmax']}", "mAh", "单体最大容量估计"],
["序列号", f"{data['BatteryData']['Serial']}", "-", "电池唯一标识"],
["制造日期", f"{data['BatteryData']['ManufactureDate']}", "-", "电池生产日期"],
["电阻 (Ra03)", f"{data['BatteryData']['Ra03']}", "mΩ", "内部电阻参数"],
["电阻 (Ra10)", f"{data['BatteryData']['Ra10']}", "mΩ", "内部电阻参数"],
]
# 自定义表格格式化函数
def print_table(data, headers):
col_widths = [max(len(str(item)) for item in col) for col in zip(headers, *data)]
header_row = " | ".join(f"{header:<{width}}" for header, width in zip(headers, col_widths))
print(header_row)
print("-" * len(header_row))
for row in data:
print(" | ".join(f"{str(item):<{width}}" for item, width in zip(row, col_widths)))
# 输出表格
print("\n=== 电池信息 ===")
print_table(table_data, ["字段", "值", "单位", "含义"])
print("\n=== BatteryData 关键字段 ===")
print_table(battery_data_table, ["字段", "值", "单位", "含义"])
# 添加验证提示
print("\n=== 数据验证 ===")
print("与 system_profiler 对比:")
print(f"- StateOfCharge: {soc_display} (ioreg) vs. 100% (system_profiler)")
print(f"- MaxCapacity: {max_capacity_display} (ioreg) vs. 100% (system_profiler)")
print(f"- CycleCount: {data['CycleCount']} (ioreg) vs. 44 (system_profiler)")
- 核心改动:
BytesEncoder
:处理bytes
类型,解决 JSON 序列化问题。get_ioreg_data
:重试 5 次,增强数据采集稳定性。StateOfCharge
逻辑:优先从BatteryData
获取,备选顶级值或推算。
- 运行方式:
输出将保存python3 battery_info.py
battery_raw_YYYYMMDD_HHMMSS.plist
和battery_debug.json
,并打印表格。
输出示例
=== 电池信息 ===
字段 | 值 | 单位 | 含义
-------------------------------------------------
当前容量 | 100% (~6254 mAh) | % (mAh) | 当前电池容量
最大容量 | 100% (~6398 mAh) | % (mAh) | 最大可用容量
充电状态 (SoC) | 98% | % | 当前电量百分比
...
验证与改进 ✅
- 验证:
- 检查
battery_raw_20250514_181722.plist
:cat battery_raw_20250514_181722.plist | grep -A 5 -i StateOfCharge
- 确认
battery_debug.json
:cat battery_debug.json | grep -i StateOfCharge
- 检查
- 改进建议:
- 若
StateOfCharge
仍不稳定,可增加重试次数或延长时间。 - 可扩展解析
SystemPower
等字段,满足更多需求。
- 若
总结与展望 🌈
通过优化,脚本成功解析 StateOfCharge=98%
,解决 JSON 错误,并适配 macOS Sequoia 15.2 的动态输出。未来可加入图形界面或实时监控功能,让电池管理更直观!😎 有问题或需求?欢迎留言!💬
思维导图:电池检测优化全景 🧠
这个思维导图帮你一网打尽优化过程!📚