目录
家里那台 ESXi 用的是消费级主板,一直想把 CPU 温度接到 Home Assistant 里看看曲线。本以为是个半小时的活,结果从"温度到底怎么读"到"数据怎么送进 HA"连着踩了四个坑,折腾了小半天。这篇把过程完整记下来,包括那些走不通的路------知道哪条路是死的,比知道哪条路能走同样有用。
先说结论:消费级主板上,ESXi 的风扇转速读不到,但 CPU 温度可以读,办法是绕过主板、直接读 CPU 内部的 MSR 寄存器。
环境:
- ESXi 6.7,主板 MSI MS-7A74(B250 芯片组),CPU Pentium G4600(2 核 4 线程)
- Home Assistant 2026.7(HAOS),装了 NGINX SSL proxy 插件
- 文中 IP 均为示例:ESXi
192.168.1.10,HA192.168.1.20,域名ha.example.com
一、先把走不通的路堵死
服务器主板的温度监控一般走 IPMI 或者厂商的 CIM provider。消费级主板这两样都没有。我把四个可能的通道全试了一遍,结论是全军覆没:
IPMI / BMC
bash
[root@esxi:~] esxcli hardware platform get
Platform Information
Product Name: MS-7A74
Vendor Name: MSI
IPMI Supported: false
[root@esxi:~] esxcli hardware ipmi bmc get
Retrieve IPMI Baseboard Management Configuration failed : No BMC Device found
[root@esxi:~] esxcli hardware ipmi sdr list
(空,一条都没有)
CIM 健康传感器
bash
[root@esxi:~] vim-cmd hostsvc/hosthardware | grep -icE "numericSensorInfo|fan|rpm"
0
一条传感器信息都没有。这个接口在服务器主板上会列出一堆温度、风扇、电压项,这里是空的。
WBEM 服务
bash
[root@esxi:~] esxcli system wbem get
Enabled: false
CIMObject Manager PID: 0
服务本身没开。而且就算开了也没用------它需要厂商提供 CIM provider 来喂数据,消费级主板不会有。
vsish 的 IO 端口节点
bash
[root@esxi:~] vsish -e ls /hardware/port/
size/
只有一个 size/,不是能读写 IO 端口的接口。
顺便说清楚:风扇转速为什么没戏
很多人(包括当初的我)会想:温度能读,转速应该也能吧?不能。
风扇转速是由主板上的 Super I/O 芯片(常见的是 Nuvoton NCT6xxx 系列)通过 0x2E/0x4E 这两个 IO 端口提供的,需要 nct6775 这类 lm-sensors 驱动去解码。ESXi 是闭源的 VMkernel,既不带这个驱动,也不开放用户态的 IO 端口读写能力------它的设计里,风扇监控只走 IPMI/BMC 或者厂商 CIM,那是服务器主板才有的东西。
所以在 ESXi 上,消费级主板的风扇转速就是读不到。想监控只能走物理外挂:把 4pin 风扇的转速信号线(第 3 根)接到 ESP32,用 ESPHome 的 pulse_counter 组件读 RPM。
那 CPU 温度凭什么能读?因为它压根不经过主板。 温度传感器在 CPU 硅片内部,读数通过 CPU 自己的 MSR(Model Specific Register)暴露,跟主板有没有 BMC、有没有 Super I/O 完全无关。
二、用 vsish 读 MSR
ESXi 自带 vsish,可以直接读 MSR。用到两个寄存器:
0x1a2(MSR_TEMPERATURE_TARGET):里面存着 TjMax,也就是这颗 CPU 的温度上限0x1b1(IA32_PACKAGE_THERM_STATUS):存的是"距离 TjMax 还差多少度",注意它是差值不是绝对温度
实际操作:
bash
[root@esxi:~] vsish -e get /hardware/msr/pcpu/0/addr/0x1a2
0x641400
[root@esxi:~] vsish -e get /hardware/msr/pcpu/0/addr/0x1b1
0x88390000
换算规则:
TjMax = (0x1a2 的值 >> 16) & 0xFF
温度 = TjMax - ((0x1b1 的值 >> 16) & 0x7F)
代入上面的数:
- TjMax =
0x641400 >> 16=0x64= 100 °C - 差值 =
0x88390000 >> 16=0x8839,再& 0x7F=0x39= 57 - 温度 = 100 − 57 = 43 °C
两个容易踩的点
第一,只用 package 温度(0x1b1),别用单核的 0x19c。
0x19c(IA32_THERM_STATUS)是每个核心自己的温度。问题在于核心进入 C6 深度睡眠后,它的数字温度传感器会停止更新,读出来是 34 °C 这种明显偏低的假值。我一开始同时采了单核和 package,画出来的曲线单核那几条在 34 和 43 之间反复横跳,看着像散热出了问题,实际上只是核心在睡觉。
package 温度是整个 CPU 封装的温度,不受单核睡眠影响,稳定可靠。
第二,pcpu 编号别越界。
bash
[root@esxi:~] vsish -e get /hardware/msr/pcpu/4/addr/0x19c
VSISHCmdGetInt():Get failed: Not found
G4600 是 2 核 4 线程,所以只有 pcpu0~3,读 pcpu4 报 Not found 是正常的,不是故障。
顺手做个压力测试
温度能读了,正好验证一下散热。用 md5sum /dev/zero 起 4 个进程压满 CPU(纯计算,不涉及磁盘 IO):
bash
# 注意:这会让宿主机上所有虚拟机卡顿,生产环境慎用
P=""
for i in 1 2 3 4; do md5sum /dev/zero >/dev/null 2>&1 & P="$P $!"; done
# 一定要加个看门狗兜底,防止 SSH 断开后进程留在后台
(sleep 200; kill -9 $P 2>/dev/null) &
用 vim-cmd hostsvc/hostsummary | grep overallCpuUsage 确认真的压满了(我这里从空载的 1039 MHz 涨到 6845 MHz,满载 95%)。
实测数据(室温 27 °C):
| 工况 | package 温度 |
|---|---|
| 空载(跑着 n 台虚拟机) | 42~44 °C |
| 满载稳态 | 50~52 °C |
| 停止负载 5 秒后 | 47 °C |
| TjMax | 100 °C |
满载和空载只差 9 °C,停载后 5 秒就掉 4 °C,说明散热器接触良好、导热路径通畅。如果硅脂没压开或者扣具没锁紧,这个温差会明显放大,停载后的回落也会变慢很多。
三、数据怎么送进 Home Assistant
温度能读了,接下来是怎么把它变成 HA 里的一个实体。
对比下几种方案:
| Webhook(选用) | MQTT | REST API | |
|---|---|---|---|
| 凭证权限 | 只是一个随机 ID,泄露最多能伪造温度读数 | 需要 MQTT 账号 | 全权限 token |
| 实体质量 | 有 unique_id,可 UI 管理,重启不丢 | 同左 | 无 unique_id,重启短暂消失 |
HA 侧配置
在 configuration.yaml 里确认有这行(大多数人的配置里已经有了):
yaml
template: !include template.yaml
然后在 template.yaml 末尾追加:
yaml
- trigger:
- trigger: webhook
webhook_id: !secret esxi_temp_webhook
allowed_methods:
- POST
local_only: true # 只接受内网请求,公网打不进来
sensor:
- unique_id: esxi_cpu_package_temp
default_entity_id: sensor.esxi_cpu_temp
name: ESXi CPU 温度
state: "{{ trigger.json.temp_c | float }}"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement # 加上它才会进入长期统计,能看月/年趋势
attributes:
tjmax: "{{ trigger.json.tjmax | int }}"
raw_msr: "{{ trigger.json.raw }}"
secrets.yaml 里加一行,值用随机串(openssl rand -hex 24 生成):
yaml
esxi_temp_webhook: 你生成的随机字符串
改完不用重启 HA ,reload 一下 template 集成就行。在 SSH 插件里 SUPERVISOR_TOKEN 是现成的:
bash
ha core check # 先验证配置语法,别改坏了
curl -X POST -H "Authorization: Bearer $SUPERVISOR_TOKEN" \
http://supervisor/core/api/services/template/reload
四、网络上的两个坑
这部分花的时间比前面所有加起来都多。
坑一:ESXi 防火墙默认丢弃出站流量
脚本写好,一跑就超时:
urllib.error.URLError: <urlopen error timed out>
ping 得通,但端口不通。查了一下防火墙:
bash
[root@esxi:~] esxcli network firewall get
Default Action: DROP
Enabled: true
ESXi 防火墙的默认动作是 DROP,而且对出站同样生效,只有被规则明确放行的端口才能出去。实测:
bash
[root@esxi:~] nc -z -w 3 192.168.1.20 8123 # HA 默认端口
(不通)
[root@esxi:~] nc -z -w 3 192.168.1.20 443
Connection to 192.168.1.20 443 port [tcp/https] succeeded!
8123 被丢弃,443 通。
我没有去改防火墙规则。ESXi 加自定义规则要往 /etc/vmware/firewall/ 写 XML,重启和升级之后都会丢失,得额外维护一套恢复机制,不值当。443 是现成的通路,上面跑着 HA 的 NGINX SSL proxy 插件,它会把请求转发给 HA core。
如果你的 HA 没装 SSL proxy 插件,也可以考虑在别的机器上做转发,思路是一样的:别跟 ESXi 防火墙较劲。
坑二:用 IP 访问会被 nginx 拒绝
改成 https://192.168.1.20/api/webhook/xxx,换了个错误:
ssl.SSLError: [SSL: TLSV1_UNRECOGNIZED_NAME] tlsv1 unrecognized name
原因是 nginx 按 SNI(TLS 握手时携带的域名)来分流。用 IP 直连时,SNI 里带的不是 ha.example.com,nginx 找不到匹配的 server 块,直接在 TLS 层就拒了。
解决办法是用域名访问但让它解析到内网 IP 。注意 ESXi 6.7 没有 esxcli network ip hosts 这个命名空间:
bash
[root@esxi:~] esxcli network ip hosts list
Error: Unknown command or namespace network ip hosts list
直接写文件:
bash
echo "192.168.1.20 ha.example.com" >> /etc/hosts
这样既走了域名(SNI 正确),又不依赖内网 DNS 能否解析到内网地址。
至于证书校验,脚本里关掉了。ESXi 6.7 自带的 CA bundle 不含 Let's Encrypt 的根证书,验不过。考虑到是同一个交换机下的内网直连,而且 webhook_id 本身就是凭证,这个取舍可以接受。
五、采集脚本
ESXi 上没有 curl,也没有 base64,但自带 Python 3.5,urllib 够用了。
/vmfs/volumes/datastore1/scripts/esxi_cputemp.py:
python
#!/bin/python3
# -*- coding: utf-8 -*-
import json
import ssl
import subprocess
import urllib.request
HA_WEBHOOK = "https://ha.example.com/api/webhook/你的webhook_id"
VSISH = "/bin/vsish"
TIMEOUT = 10
SSL_CTX = ssl._create_unverified_context()
def read_msr(addr):
"""读 pcpu0 的指定 MSR。vsish 输出形如 0x883a0000。"""
out = subprocess.check_output(
[VSISH, "-e", "get", "/hardware/msr/pcpu/0/addr/" + addr])
return int(out.decode().strip(), 16)
def main():
tjmax = (read_msr("0x1a2") >> 16) & 0xFF
raw = read_msr("0x1b1")
temp_c = tjmax - ((raw >> 16) & 0x7F)
payload = json.dumps({
"temp_c": temp_c,
"tjmax": tjmax,
"raw": hex(raw),
}).encode("utf-8")
req = urllib.request.Request(
HA_WEBHOOK,
data=payload,
headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=TIMEOUT, context=SSL_CTX).read()
if __name__ == "__main__":
main()
脚本放在 datastore 上(/vmfs/volumes/datastore1/),那是 VMFS,重启不会丢。权限给 700。
cron 条目写进 /var/spool/cron/crontabs/root:
* * * * * /bin/python3 /vmfs/volumes/datastore1/scripts/esxi_cputemp.py >/dev/null 2>&1
六、让配置扛住重启
这是 ESXi 特有的麻烦:crontab 和 /etc/hosts 在重启后会被重置 。要靠 /etc/rc.local.d/local.sh 在开机时重建。
编辑 local.sh,在末尾的 exit 0 之前插入:
sh
grep -q "ha.example.com" /etc/hosts || echo "192.168.1.20 ha.example.com" >> /etc/hosts
grep -q esxi_cputemp /var/spool/cron/crontabs/root || echo "* * * * * /bin/python3 /vmfs/volumes/datastore1/scripts/esxi_cputemp.py >/dev/null 2>&1" >> /var/spool/cron/crontabs/root
改完用 sh -n /etc/rc.local.d/local.sh 验证语法,这是开机脚本,写坏了影响启动。
不需要重启 crond:busybox 的 crond 每分钟会检查 crontab 文件的 mtime,有变化会自动重新加载。
还有最后一步容易漏:local.sh 和 /etc/hosts 这些改动要写进 bootbank 才算真正持久化。ESXi 自带的 cron 里有一条 1 * * * * /sbin/auto-backup.sh,每小时会做一次。如果你改完就打算重启 ESXi,先手动跑一次 /sbin/auto-backup.sh,否则改动会丢。
七、验证
ESXi 侧手动跑一次,正常情况下无输出、退出码 0:
bash
python3 /vmfs/volumes/datastore1/scripts/esxi_cputemp.py
echo $?
HA 侧查实体:
bash
curl -s -H "Authorization: Bearer $SUPERVISOR_TOKEN" \
http://supervisor/core/api/states/sensor.esxi_cpu_temp
返回:
json
{
"entity_id": "sensor.esxi_cpu_temp",
"state": "43.0",
"attributes": {
"state_class": "measurement",
"tjmax": 100,
"raw_msr": "0x88390000",
"unit_of_measurement": "°C",
"device_class": "temperature",
"friendly_name": "ESXi CPU 温度"
},
"last_updated": "2026-07-30T15:17:00.376483+00:00"
}
验收看三点:state 是合理温度、last_updated 在一分钟以内、多查几次 raw_msr 会变(证明是新数据不是缓存)。
八、这个方案的局限
ESXi 宕机时,实体会一直保持最后一个温度值,不会变成 unavailable。
trigger-based template sensor 没有 TTL 机制,收不到新数据就一直显示旧值。也就是说,如果 ESXi 挂了,你在 HA 里看到的还是它挂之前那个温度,看起来一切正常。
要做"数据过期"检测,得再加一个基于 last_updated 的 binary_sensor,配合一个心跳实体(比如 time_date 集成提供的 sensor.time)来触发重新计算。我暂时没做,先记在这里。
小结
几个可以直接拿走的结论:
- 消费级主板跑 ESXi,CPU 温度能读(走 MSR),风扇转速读不到,别在后者上浪费时间。
- 读温度只用 package 的
0x1b1,单核的0x19c在核心睡眠时会给假值。 - ESXi 防火墙默认 DROP 出站,8123 这类非标端口出不去;别去加自定义防火墙 XML,重启就没了,走现成放行的 443 更省事。
- nginx 按 SNI 分流,所以要用域名 +
/etc/hosts静态映射,不能用 IP 直连。 - ESXi 上没有 curl 和 base64,但有 Python 3.5。
- crontab 和
/etc/hosts重启会丢,要靠local.sh重建,再靠auto-backup.sh落盘。
整套跑下来,HA 里就有了一条每分钟更新的温度曲线,还能进长期统计看趋势。对于家里放着的这种"没有带外管理"的机器,算是补上了一块比较重要的可观测性。