--:--
卡农
使用国际互联网体验更佳
Project: 打造 macOS 终极监控脚本:M 芯片温度解锁 + 硬盘 0E 预警 + 电池体检

打造 macOS 终极监控脚本:M 芯片温度解锁 + 硬盘 0E 预警 + 电池体检

在 macOS 上,我们通常使用 iStat Menus 或 Stats 等工具来监控系统。但作为开发者,我想要一个更轻量、更硬核、完全可控的方案:

  • 拒绝 Electron:不想要臃肿的后台进程。
  • 拒绝未知:我需要知道具体的底层数据(如 SSD 的 0E 致命错误)。
  • 全自动:外接硬盘插拔后自动追踪,无需手动修改配置。
  • M3 适配:解决 Apple Silicon M 芯片隐藏 CPU 温度传感器的问题。

经过一番折腾,我编写了一个 All-in-One 的 Shell 脚本,集成了 CPU 温度/压力、硬盘健康度(含 0E 检测)、外接硬盘自动追踪以及电池深度体检。

下面是实现方案。

🛠️ 前置准备

我们需要几个轻量级的命令行工具来获取底层数据。

1. 安装依赖

打开终端,使用 Homebrew 安装:

# 安装 smartmon tools (用于读取硬盘 SMART 信息)
brew install smartmontools

# 安装 terminal-notifier (用于发送原生系统通知)
brew install terminal-notifier

2. 配置 Sudo 免密

因为读取 smartctlpowermetrics 需要 root 权限,为了让脚本在后台静默运行,我们需要配置 sudo 白名单。

运行 sudo visudo,在文件最后添加以下内容(将 your_username 替换为你的用户名):

your_username ALL=(ALL) NOPASSWD: /opt/homebrew/bin/smartctl, /usr/bin/powermetrics

🔓 突破 M 芯片限制:自制 CPU 温度读取工具

在 M 芯片上,普通的 powermetrics 命令不再直接输出 CPU 核心温度。我们需要利用 C 语言调用 Apple 的私有 HID 接口来获取。

1. 创建源码 get_temp.c

新建一个文件 get_temp.c,填入以下代码:

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>

// 声明 Apple 私有 HID 接口
typedef struct __IOHIDEventSystemClient * IOHIDEventSystemClientRef;
typedef struct __IOHIDServiceClient * IOHIDServiceClientRef;
typedef struct __IOHIDEvent * IOHIDEventRef;
#define kIOHIDEventTypeTemperature 15

extern IOHIDEventSystemClientRef IOHIDEventSystemClientCreate(CFAllocatorRef allocator);
extern int IOHIDEventSystemClientSetMatching(IOHIDEventSystemClientRef client, CFDictionaryRef match);
extern CFArrayRef IOHIDEventSystemClientCopyServices(IOHIDEventSystemClientRef client);
extern CFStringRef IOHIDServiceClientCopyProperty(IOHIDServiceClientRef service, CFStringRef key);
extern IOHIDEventRef IOHIDServiceClientCopyEvent(IOHIDServiceClientRef service, int64_t type, int32_t options, int64_t timestamp);
extern double IOHIDEventGetFloatValue(IOHIDEventRef event, int32_t field);

int main() {
    IOHIDEventSystemClientRef system = IOHIDEventSystemClientCreate(kCFAllocatorDefault);
    if (!system) return 1;

    int page = 0xff00; int usage = 0x0005;
    CFNumberRef pageNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &page);
    CFNumberRef usageNum = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &usage);
    const void *keys[2] = { CFSTR("PrimaryUsagePage"), CFSTR("PrimaryUsage") };
    const void *values[2] = { pageNum, usageNum };
    CFDictionaryRef matchDict = CFDictionaryCreate(kCFAllocatorDefault, keys, values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    IOHIDEventSystemClientSetMatching(system, matchDict);
    CFArrayRef services = IOHIDEventSystemClientCopyServices(system);
    if (!services) return 1;

    CFIndex count = CFArrayGetCount(services);
    double maxTemp = 0.0;
    int found = 0;

    for (CFIndex i = 0; i < count; i++) {
        IOHIDServiceClientRef service = (IOHIDServiceClientRef)CFArrayGetValueAtIndex(services, i);
        CFStringRef nameRef = IOHIDServiceClientCopyProperty(service, CFSTR("Product"));
        if (!nameRef) continue;
        char name[256];
        CFStringGetCString(nameRef, name, 256, kCFStringEncodingUTF8);
        CFRelease(nameRef);

        if (strstr(name, "SOC MDI") || strstr(name, "Die") || strstr(name, "PMU tdev") || strstr(name, "Cluster")) {
            IOHIDEventRef event = IOHIDServiceClientCopyEvent(service, kIOHIDEventTypeTemperature, 0, 0);
            if (event) {
                double temp = IOHIDEventGetFloatValue(event, (kIOHIDEventTypeTemperature << 16));
                if (temp > 20.0 && temp < 120.0) {
                    if (temp > maxTemp) maxTemp = temp;
                    found = 1;
                }
                CFRelease(event);
            }
        }
    }
    if (found) printf("%.1f\n", maxTemp); else printf("0.0\n");
    return 0;
}

2. 编译并安装

使用系统自带的 clang 编译,并移动到系统目录:

clang -framework CoreFoundation -framework IOKit -o get_temp get_temp.c
sudo mv get_temp /usr/local/bin/get_temp

现在,你在终端输入 get_temp 就能拿到精确的 CPU 温度了。


📜 终极监控脚本

这是脚本的最终形态。它不仅会在后台静默监控,如果手动运行,还会输出一份极其漂亮的 “系统体检报告”

保存以下代码为 ~/script/temp_monitor.sh

#!/bin/bash

# ================= 配置区域 =================
EXTERNAL_DRIVE_NAME="disk"  # 你的移动硬盘卷名 (在 /Volumes 下的名字)

THRESHOLD_CPU=85      # CPU 报警阈值 (°C)
THRESHOLD_DISK=55     # 硬盘报警阈值 (°C)
THRESHOLD_BATT=40     # 电池报警阈值 (°C)
THRESHOLD_HEALTH=90   # 硬盘健康度报警阈值 (%)

# 环境变量
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"

# ================= 1. 获取基础数据 =================

# --- A. CPU ---
# 调用我们自制的 C 工具
CPU_TEMP=$(/usr/local/bin/get_temp)
if [[ -n "$CPU_TEMP" && "$CPU_TEMP" != "0.0" ]]; then
    CPU_TEMP_INT=${CPU_TEMP%.*}
else
    CPU_TEMP_INT=0; CPU_TEMP="N/A"
fi
# 获取系统热压力
CPU_PRESSURE=$(sudo /usr/bin/powermetrics -n 1 --samplers thermal 2>/dev/null | grep "Current pressure level" | awk '{print $4}')
if [[ -z "$CPU_PRESSURE" ]]; then CPU_PRESSURE="Unknown"; fi

# --- B. 电池 (深度数据) ---
BATT_INFO=$(ioreg -rn AppleSmartBattery)
# 温度
RAW_BATT_TEMP=$(echo "$BATT_INFO" | grep "\"Temperature\" =" | awk '{print $3}')
if [[ -n "$RAW_BATT_TEMP" ]]; then
    if [[ "$RAW_BATT_TEMP" -gt 100 ]]; then BATT_TEMP=$(($RAW_BATT_TEMP / 100)); else BATT_TEMP=$RAW_BATT_TEMP; fi
else BATT_TEMP=0; fi
# 循环与容量
BATT_CYCLES=$(echo "$BATT_INFO" | grep "\"CycleCount\" =" | awk '{print $3}')
BATT_MAX_CAP=$(echo "$BATT_INFO" | grep "\"AppleRawMaxCapacity\" =" | awk '{print $3}')
BATT_DESIGN_CAP=$(echo "$BATT_INFO" | grep "\"DesignCapacity\" =" | awk '{print $3}')
if [[ -n "$BATT_MAX_CAP" && -n "$BATT_DESIGN_CAP" && "$BATT_DESIGN_CAP" -gt 0 ]]; then
    BATT_HEALTH=$(( 100 * BATT_MAX_CAP / BATT_DESIGN_CAP ))
else BATT_HEALTH="?"; fi

# --- C. 内存 ---
RAM_LEVEL=$(sysctl -n kern.memorystatus_vm_pressure_level)
case "$RAM_LEVEL" in
    1) RAM_TXT="正常" ;; 2) RAM_TXT="⚠️ 压力高" ;; *) RAM_TXT="🔥 严重" ;;
esac

# ================= 2. 报告生成函数 =================
generate_disk_report() {
    local DISK_DEV=$1
    local DISK_TYPE=$2

    echo "正在读取 $DISK_DEV ($DISK_TYPE) ..."
    SMART_DATA=$(sudo /opt/homebrew/bin/smartctl -a $DISK_DEV 2>/dev/null)

    # 提取关键指标
    MODEL=$(echo "$SMART_DATA" | grep "Model Number:" | awk -F: '{print $2}' | xargs)
    if [[ -z "$MODEL" ]]; then MODEL="Unknown"; fi
    
    TEMP=$(echo "$SMART_DATA" | grep -i "Temperature:" | awk '{print $2}')
    if [[ -z "$TEMP" ]]; then TEMP="0"; fi

    # 计算健康度
    USED_PCT=$(echo "$SMART_DATA" | grep "Percentage Used" | awk '{print $3}' | tr -d '%')
    if [[ -n "$USED_PCT" ]]; then HEALTH=$((100 - USED_PCT)); else HEALTH="N/A"; USED_PCT="?"; fi

    # 统计数据
    WRITTEN=$(echo "$SMART_DATA" | grep "Data Units Written" | awk -F'[][]' '{print $2}')
    HOURS=$(echo "$SMART_DATA" | grep "Power On Hours" | awk '{print $4}' | tr -d ',')
    if [[ -n "$HOURS" ]]; then DAYS=$((HOURS / 24)); TIME_TXT="${HOURS}小时(${DAYS}天)"; else TIME_TXT="N/A"; fi
    UNSAFE=$(echo "$SMART_DATA" | grep "Unsafe Shutdowns" | awk '{print $3}' | tr -d ',')
    if [[ -z "$UNSAFE" ]]; then UNSAFE="0"; fi

    # 0E 致命错误检测
    ERR_0E=$(echo "$SMART_DATA" | grep "Media and Data Integrity Errors" | awk '{print $NF}')
    if [[ -z "$ERR_0E" ]]; then ERR_0E="N/A"; fi

    # 打印可视化报告
    echo "----------------------------------------"
    if [[ "$DISK_TYPE" == "Internal" ]]; then echo " 🍎 本地硬盘 (macOS System)"; else echo " 💾 移动硬盘 ($DISK_DEV)"; fi
    echo "----------------------------------------"
    echo "📦 型号: $MODEL"
    echo "🌡️ 温度: ${TEMP}°C"
    echo "❤️ 寿命: ${HEALTH}% (已用 ${USED_PCT}%)"
    echo "📝 写入: $WRITTEN"
    echo "⏳ 时间: $TIME_TXT"
    echo "🔌 掉电: $UNSAFE"
    echo "✨ 0E错: $ERR_0E (数据完整性)"
    echo ""

    # 回传数据给主逻辑
    eval "${DISK_TYPE}_TEMP=$TEMP"
    eval "${DISK_TYPE}_0E=$ERR_0E"
    eval "${DISK_TYPE}_HEALTH=$HEALTH"
}

# ================= 3. 执行输出 =================

echo ""
echo "========================================"
echo "      📟 系统全维体检报告      "
echo "========================================"
echo ""

# 核心算力
echo "🧠 核心算力"
echo "----------------------------------------"
echo "CPU 温度: ${CPU_TEMP}°C"
echo "CPU 压力: ${CPU_PRESSURE}"
echo "内存状态: ${RAM_TXT}"
echo ""

# 电池健康
echo "🔋 电池健康"
echo "----------------------------------------"
echo "🌡️ 温度: ${BATT_TEMP}°C"
echo "🔄 循环: ${BATT_CYCLES} 次"
echo "❤️ 寿命: ${BATT_HEALTH}%"
echo "🔋 容量: ${BATT_MAX_CAP} mAh (当前) / ${BATT_DESIGN_CAP} mAh (出厂)"
echo ""

# 硬盘检测
generate_disk_report "/dev/disk0" "Internal"

# 自动追踪移动硬盘 (APFS 兼容)
MOUNT_DEV=$(df "/Volumes/$EXTERNAL_DRIVE_NAME" 2>/dev/null | awk 'NR==2 {print $1}')
if [[ -n "$MOUNT_DEV" ]]; then
    DISK_DEVICE=$(echo "$MOUNT_DEV" | sed 's/s[0-9]*$//')
    generate_disk_report "$DISK_DEVICE" "External"
else
    echo "----------------------------------------"
    echo " 💾 移动硬盘 ($EXTERNAL_DRIVE_NAME)"
    echo "----------------------------------------"
    echo "⚠️  状态: 未连接"
    echo ""
    External_TEMP=0; External_0E=-1; External_HEALTH="N/A"
fi

# ================= 4. 告警逻辑 (通知中心) =================
MSG=""
TITLE="🔥 系统严重告警"

# 0E 错误 (最高优先级)
if [[ "$Internal_0E" != "N/A" && "$Internal_0E" -gt 0 ]]; then MSG="${MSG}💥系统盘0E错误(${Internal_0E})"; fi
if [[ "$External_0E" != "N/A" && "$External_0E" -ge 0 && "$External_0E" -gt 0 ]]; then
    if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
    MSG="${MSG}💥移动盘0E错误(${External_0E})"
fi

# 寿命预警
if [[ "$Internal_HEALTH" != "N/A" && "$Internal_HEALTH" -lt "$THRESHOLD_HEALTH" ]]; then
    if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
    MSG="${MSG}系统盘寿命低(${Internal_HEALTH}%)"
fi

# 过热预警
if [[ "$External_TEMP" -ge "$THRESHOLD_DISK" && "$External_TEMP" -gt 0 ]]; then
    if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
    MSG="${MSG}移动盘热:${External_TEMP}°C"
fi
if [[ "$CPU_TEMP_INT" -ge "$THRESHOLD_CPU" ]]; then
    if [[ -n "$MSG" ]]; then MSG="${MSG}, "; fi
    MSG="${MSG}CPU过热:${CPU_TEMP}°C"
fi

# 发送通知
if [[ -n "$MSG" ]]; then
    terminal-notifier -message "$MSG" -title "$TITLE" -sound default -group "system_monitor"
fi

📊 运行效果

在终端手动运行 bash temp_monitor.sh,你会得到一份极其详细的报告:

========================================
      📟 系统全维体检报告      
========================================

🧠 核心算力
----------------------------------------
CPU 温度: 39.5°C
CPU 压力: Nominal
内存状态: 正常

🔋 电池健康
----------------------------------------
🌡️ 温度: 31°C
🔄 循环: 156 次
❤️ 寿命: 98%
🔋 容量: 4350 mAh (当前) / 4436 mAh (出厂)

----------------------------------------
 🍎 本地硬盘 (macOS System)
----------------------------------------
📦 型号: APPLE SSD AP0512Z
🌡️ 温度: 35°C
❤️ 寿命: 100% (已用 0%)
📝 写入: 20.5 TB
⏳ 时间: 1500小时(62天)
🔌 掉电: 12 次
✨ 0E错: 0 (数据完整性)

----------------------------------------
 💾 移动硬盘 (/dev/disk4)
----------------------------------------
📦 型号: WDC WDS960G2G0C
🌡️ 温度: 46°C
❤️ 寿命: 98% (已用 2%)
✨ 0E错: 0 (数据完整性)

⚙️ 自动化运行

如果希望它每分钟自动在后台检查(只在异常时弹窗),可以使用 crontablaunchd

简单方法,在 crontab 中添加:

* * * * * /bin/bash /Users/你的用户名/script/temp_monitor.sh >/dev/null 2>&1

现在,你的 Mac 拥有了一套完全私有、透明且硬核的健康监控系统。只要通知中心安安静静,就说明你的 Mac 依旧健康!

NORMAL docs/打造 macOS 终极监控脚本:M 芯片温度解锁 + 硬盘 0E 预警 + 电池体检.md 0%
```