MacBook Pro电池检测脚本优化全解析!!!

技术博客:MacBook Pro 电池信息检测脚本优化之旅 🎉

嘿,大家好!今天我们来聊聊如何用 Python 编写一个脚本,检测 MacBook Pro 的电池信息,并解决一系列棘手问题!😄 以下是本次优化的总结,附上代码分析、流程图和时序图,最后还有思维导图帮你梳理思路。准备好了吗?🚀


优化总结:关键数据一览 📊

字段单位含义备注
CurrentCapacity100% (~6254 mAh)% (mAh)当前电池容量参考 AppleRawCurrentCapacity (苹果原始当前容量)
MaxCapacity100% (~6398 mAh)% (mAh)最大可用容量参考 AppleRawMaxCapacity (苹果原始最大容量)
StateOfCharge98%%当前电量百分比BatteryData 获取
DesignCapacity6249mAh出厂标称容量硬件设计值
Temperature30.34°C电池温度正常范围 (0-35°C)
Voltage13.107V电池电压正常范围 (~12-13.2V)
CycleCount44充放电循环次数system_profiler 一致
FullyChargedYes-电池是否充满system_profiler 一致
BatteryData
- CellVoltage[4369, 4369, 4369]mV各电池单体电压正常 (~4.3V/单体)
- Qmax[6803, 6802, 6804]mAh单体最大容量估计接近设计容量
- SerialF8YHCJG01C700000E4-电池唯一标识硬件序列号
- ManufactureDate54083053433651-电池生产日期需解码,推测 2021-07
- Ra0358内部电阻参数低值表示健康
- Ra1075内部电阻参数低值表示健康

以上数据基于 MacBook Pro (14英寸, 2024年11月) 运行 macOS Sequoia 15.2,搭载 Apple M4 芯片,16GB 内存和 1TB SSD。电池状态显示充满(FullyCharged=Yes),StateOfCharge=98%system_profiler 的 100% 差异属正常校准。🌟


优化过程:Mermaid 流程图 🖼️

以下是脚本优化的流程图,帮助你直观了解每个步骤:

执行 ioreg 命令(重试 5 次)
保存原始 plist
解析 XML 到 Python 列表
查找 IOObjectClass = AppleSmartBattery
提取 BatteryData.StateOfCharge 或顶级 StateOfCharge
StateOfCharge 是 int/float 且 ≥0?
输出 StateOfCharge(如 98%)
推算 SoC = Current / Max
推算有效?
输出推算值(如 98% 推算)
输出 N/A% (无法推算)
保存 battery 字典为 JSON(BytesEncoder)

这个流程图展示了从数据采集到输出的完整逻辑!🔧


交互时序:Sequence Diagrams 🕒

以下是脚本与系统交互的时序图,展示数据流转过程:

用户 Python脚本 系统 IORegistry 运行 battery_info.py 执行 ioreg(重试 5 次) 查询 AppleSmartBattery 返回 XML(BatteryData.StateOfCharge=98) 提供输出 保存原始 plist 解析 BatteryData.StateOfCharge = 98 保存 battery_debug.json 显示 98% 用户 Python脚本 系统 IORegistry

时序图清晰展示了脚本如何与 macOS 和 I/O Registry (输入/输出注册表) 协作!⏳


技术博客正文:从问题到完美解决方案 📝

背景介绍 🎬

最近,我在使用 MacBook Pro (14英寸, 2024年11月) 开发一个电池信息检测脚本时,遇到了不少挑战!😓 最初,脚本通过 ioreg 命令获取电池数据,但 StateOfCharge (电池充电状态) 经常显示“缺失”,且 JSON (JavaScript Object Notation,JavaScript对象表示法) 序列化因 bytes 类型失败,抛出 TypeError。经过多次优化,终于搞定!这次博客带你回顾整个过程,分享代码和经验。💡

问题分析 🕵️‍♂️

  1. StateOfCharge 缺失
    • 调试日志显示 顶级 StateOfCharge = 缺失,但 BatteryData StateOfCharge = 98
    • 原因:macOS Sequoia 15.2 的 SMC (System Management Controller,系统管理控制器) 动态调整了 ioreg 输出结构,StateOfCharge 移至 BatteryData 字典。
  2. JSON 序列化错误
    • 旧脚本无法处理 BatteryData 中的 bytes 数据(如 MfgData=<00...>),导致 json.dump 崩溃。
    • 新增 BytesEncoderbytes 转为十六进制,解决问题。
  3. 数据不一致
    • 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.plistbattery_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 的动态输出。未来可加入图形界面或实时监控功能,让电池管理更直观!😎 有问题或需求?欢迎留言!💬


思维导图:电池检测优化全景 🧠

在这里插入图片描述

这个思维导图帮你一网打尽优化过程!📚

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值