【Proxmox VE】PVE 首页显示 CPU、主板、NVME、硬盘 温度等信息

网上各种版本的 PVE 首页温度 DIY 脚本,但萝卜青菜各有所爱,总是难得遇到钟情的那一款
自行 DIY 的话,正则 + js 入门确实需要点门槛
所以这里提供另一种实现方式 json,清晰明了,便于阅读,修改简单
无论是 温度、风扇转速、硬盘温度、硬盘信息都可以轻松搞定



出错原因及处理方式

  1. 首页转圈,只显示一部分数据
    一般都是由于参数值读取错误造成的
    假设代码片段中有如下参数
    var temperature = value['temperature']['current'].toFixed(1);
    但硬盘 smartctl 返回值中没有温度值、或键值名称不一致,就会造成参数读取错误,首页转圈
  2. 首页转圈,所有自定义数据都不能显示
    检查 CPU 温度代码段,是否有代码错误(参照第一条)
    如修改了 Nodes.pm 文件,需要使用 systemctl restart pveproxy 命令重载 PVE 界面
    仅修改 js 文件的情况下,无需重载 PVE 界面,浏览器强制刷新就能看到修改后的结果
  3. 首页白屏,什么都不显示
    一般都是代码有误引起的,请还原文件重新操作,或检查代码是否缺少相对应的 {} , () 等符号
  4. 中文乱码
    请不要将文件拖到本地使用 记事本 操作,直接在 WinSCP 中操作,或在本地使用 Notepad 等软件进行编辑
    如操作无误依然乱码,请检查终端软件及 WinSCP 的编码设置,并在 PVE 终端中输入
    export LC_ALL=en_US.UTF-8
  5. 无报错,但显示不全
    参考文末,修改显示范围

前置命令

# 更新软件包列表:

1
apt-get update

# 安装lm-sensors:

1
apt-get install lm-sensors patch

# 初始化 sensors (一路yes,回车):

1
sensors-detect

# 给予 smartctl 权限 (如不需要硬盘信息可以忽略):

1
chmod +s /usr/sbin/smartctl

# 设置 PVE 编码为 UTF-8 (如 PVE 安装时正确选择了 china 地区可以忽略):

1
export LC_ALL=en_US.UTF-8

# 获取温度信息,查看可以设置的数据:

1
sensors

# 这个时候是没有风扇等信息的,需要重启 (如不需要风扇转速信息可以忽略):

1
reboot

备份原文件

如命令未错误输入,文件备份在原文件相同目录下
如 : /usr/share/perl5/PVE/API2/Nodes.pm 备份为 /usr/share/perl5/PVE/API2/Nodes.pmbak

1
2
3
4
5
6
proxmoxlib_js="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
Nodes_pm="/usr/share/perl5/PVE/API2/Nodes.pm"
pvemanagerlib_js="/usr/share/pve-manager/js/pvemanagerlib.js"
cp ${proxmoxlib_js} ${proxmoxlib_js}bak
cp ${Nodes_pm} ${Nodes_pm}bak
cp ${pvemanagerlib_js} ${pvemanagerlib_js}bak

懒人补丁法(若未修改过原文件,已包含高度修改、去除订阅提示,宽度未更改)

将补丁文件放到 /tmp/ 文件夹
大版本升级可能依然需要手动修改部分代码,补丁失败的文件及代码行数会在 patch 返回值中显示,请留意
如遇页面显示错误,参照上文
不保证可用性

2022/06/16
为每一个参数加入了判断语句,应该不会再出错了,但是大概可能会拖慢读取速度 0.xx 秒:
强迫症、完美主义者、需要自行添加数据等,自行参照注释版本添加修改
M.2 为系统盘,无法休眠调试,代码未处理,需要自行添加
PVE_7.2_temperatures.zip
PVE_8.0_temperatures.zip

1
2
3
4
5
6
# 应用补丁(请确认已经备份原文件)
patch ${proxmoxlib_js} < /tmp/proxmoxlib_js.patch
patch ${Nodes_pm} < /tmp/Nodes_pm.patch
patch ${pvemanagerlib_js} < /tmp/pvemanagerlib_js.patch
# 重载 PVE 界面
systemctl restart pveproxy

自行制作补丁

可以在重装或升级后快速修改文件(大版本升级可能需要手动修改部分代码)
生成的补丁文件在 /tmp/ 文件夹:

1
2
3
4
5
6
proxmoxlib_js="/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js"
Nodes_pm="/usr/share/perl5/PVE/API2/Nodes.pm"
pvemanagerlib_js="/usr/share/pve-manager/js/pvemanagerlib.js"
diff -uN ${proxmoxlib_js}bak ${proxmoxlib_js} > /tmp/proxmoxlib_js.patch
diff -uN ${Nodes_pm}bak ${Nodes_pm} > /tmp/Nodes_pm.patch
diff -uN ${pvemanagerlib_js}bak ${pvemanagerlib_js} > /tmp/pvemanagerlib_js.patch

自行修改文件

修改 Nodes.pm 文件
/usr/share/perl5/PVE/API2/Nodes.pm
搜索:$res->{pveversion} = PVE::pvecfg::package()
下方添加

1
2
3
4
5
$res->{sensors_json} = `sensors -j`; # 获取 CPU 、主板温度及风扇转速
$res->{smartctl_nvme_json} = `smartctl -a -j /dev/nvme?`; # 读取 nvme 硬盘 S.M.A.R.T. 值,获取硬盘寿命、容量、温度等
$res->{smartctl_sda_json} = `smartctl -i -n standby /dev/sda|grep "STANDBY" || smartctl -i -n standby /dev/sda|grep "No such device" || smartctl -a -j /dev/sda`; #先检测硬盘是否为休眠状态,若否,则检查磁盘是否存在,若否,则返回 S.M.A.R.T. 值
$res->{smartctl_sdb_json} = `smartctl -i -n standby /dev/sdb|grep "STANDBY" || smartctl -i -n standby /dev/sdb|grep "No such device" || smartctl -a -j /dev/sdb`; #先检测硬盘是否为休眠状态,若否,则检查磁盘是否存在,若否,则返回 S.M.A.R.T. 值
$res->{cpusensors} = `lscpu | grep MHz`; # 读取 CPU 频率

关于获取硬盘名称

1
lsblk | awk '$NF=="disk" {print $1}' | sort -u

终端输入 sensors -j 命令,我们可以得到类似下面的结果(以下为 CPU 温度值截取)
先眼熟一下 JSON 格式的返回值,等下会用到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"coretemp-isa-0000":{
"Adapter": "ISA adapter",
"Package id 0":{
"temp1_input": 38.000,
"temp1_max": 84.000,
"temp1_crit": 100.000,
"temp1_crit_alarm": 0.000
},
"Core 0":{
"temp2_input": 34.000,
"temp2_max": 84.000,
"temp2_crit": 100.000,
"temp2_crit_alarm": 0.000
},
"Core 1":{
"temp3_input": 38.000,
"temp3_max": 84.000,
"temp3_crit": 100.000,
"temp3_crit_alarm": 0.000
}

修改 pvemanagerlib.js 文件
/usr/share/pve-manager/js/pvemanagerlib.js
搜索 PVE Manager Version
下方添加

原始版本及注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
itemId: 'thermal', // thermal 代表了这一段代码的 id ,随便改一下不要重复就行了
colspan: 2,
printBar: false,
title: gettext('温度'), // 这里表示你想在页面中显示的左侧标题名
textField: 'sensors_json', // 这里需要填写 Nodes.pm 文件中对应的命令,也就是从哪一个返回值中获取数据
renderer: function(value) {
value = JSON.parse(value); // 使用 JavaScript 内置函数 JSON.parse() 将字符串转换为 JavaScript 对象
const cpu0 = value['coretemp-isa-0000']['Package id 0']['temp1_input'].toFixed(1); // 这里表示读取 CPU 温度,对应 sensors -j 输出的 JSON 格式数据,toFixed(1) 表示将数字转换为字符,只保留 (1) 位小数
const PECI0 = value['nct6798-isa-0290']['PECI Agent 0']['temp7_input'].toFixed(1); // 同上,自行修改
const pch = value['pch_cometlake-virtual-0']['temp1']['temp1_input'].toFixed(1); // 同上,自行修改
return `CPU: ${cpu0}°C || 南桥: ${pch} ℃ | 网卡: ${PECI0} ℃`; // return 表示输出值,也就是最后显示在 WEB 页面中的值,{}中填入上几行中定义的变量,格式自行调整
}
},

加一点细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
{
itemId: 'thermal',
colspan: 2,
printBar: false,
title: gettext('温度'),
textField: 'sensors_json',
renderer: function(value) {
value = value.replace(/temp([0-9]{1,})_input/g,'input');
// Intel
if (value.indexOf("coretemp-isa") != -1 ) {
value = value.replace(/coretemp-isa-(.{4})/g,'coretemp-isa');
value = value.replace(/nct6798-isa-(.{4})/g,'nct6798-isa');
value = JSON.parse(value);
try {var cpu_Intel = 'CPU: ' + value['coretemp-isa']['Package id 0']['input'].toFixed(1) + '°C';} catch(e) {var cpu_Intel = '';}
try {var acpi = ' || 主板: ' + value['acpitz-acpi-0']['temp1']['input'].toFixed(1) + '°C';} catch(e) {var acpi = '';}
try {var pch = ' || 南桥: ' + value['pch_cometlake-virtual-0']['temp1']['input'].toFixed(1) + '°C';} catch(e) {var pch = '';}
try {var pci0 = ' || 网卡: ' + value['nct6798-isa']['PECI Agent 0']['input'].toFixed(1) + '°C';} catch(e) {var pci0 = '';}
// 如果存在主板、PCI网卡温度,优先显示
if (cpu_Intel.length > 0 && pch.length + acpi.length + pci0.length > 0) {
return `${cpu_Intel}${acpi}${pch}${pci0}`;
// 如果不存在,显示 CPU 全核温度,最高支持 8 核心
} else if (cpu_Intel.length > 0) {
try {var cpu0 = ' || 核心 0 : ' + value['coretemp-isa']['Core 0']['input'].toFixed(1) + '°C';} catch(e) {var cpu0 = '';}
try {var cpu1 = ' | 核心 1 : ' + value['coretemp-isa']['Core 1']['input'].toFixed(1) + '°C';} catch(e) {var cpu1 = '';}
try {var cpu2 = ' | 核心 2 : ' + value['coretemp-isa']['Core 2']['input'].toFixed(1) + '°C';} catch(e) {var cpu2 = '';}
try {var cpu3 = ' | 核心 3 : ' + value['coretemp-isa']['Core 3']['input'].toFixed(1) + '°C';} catch(e) {var cpu3 = '';}
try {var cpu4 = ' | 核心 4 : ' + value['coretemp-isa']['Core 4']['input'].toFixed(1) + '°C';} catch(e) {var cpu4 = '';}
try {var cpu5 = ' | 核心 5 : ' + value['coretemp-isa']['Core 5']['input'].toFixed(1) + '°C';} catch(e) {var cpu5 = '';}
try {var cpu6 = ' | 核心 6 : ' + value['coretemp-isa']['Core 6']['input'].toFixed(1) + '°C';} catch(e) {var cpu6 = '';}
try {var cpu7 = ' | 核心 7 : ' + value['coretemp-isa']['Core 7']['input'].toFixed(1) + '°C';} catch(e) {var cpu7 = '';}
return `${cpu_Intel}${cpu0}${cpu1}${cpu2}${cpu3}${cpu4}${cpu5}${cpu6}${cpu7}`;
}
// AMD
} else if (value.indexOf("amdgpu-pci") != -1 ) {
value = value.replace(/k10temp-pci-(.{4})/g,'k10temp-pci');
value = value.replace(/zenpower-pci-(.{4})/g,'zenpower-pci');
value = value.replace(/amdgpu-pci-(.{4})/g,'amdgpu-pci');
value = JSON.parse(value);
try {var cpu_amd_k10 = 'CPU: ' + value['k10temp-pci']['Tctl']['input'].toFixed(1) + '°C';} catch(e) {var cpu_amd_k10 = '';}
try {var cpu_amd_zen = 'CPU: ' + value['zenpower-pci']['Tctl']['input'].toFixed(1) + '°C';} catch(e) {var cpu_amd_zen = '';}
try {var amdgpu = ' | GPU: ' + value['amdgpu-pci']['edge']['input'].toFixed(1) + '°C';} catch(e) {var amdgpu = '';}
return `${cpu_amd_k10}${cpu_amd_zen}${amdgpu}`;
} else {
return `提示: CPU 及 主板 温度读取异常`;
}
}
},
{
itemId: 'nvme_ssd',
colspan: 2,
printBar: false,
title: gettext('NVME'),
textField: 'smartctl_nvme_json',
renderer: function(value) {
value = JSON.parse(value);
if (value['model_name']) {
try {var model_name = value['model_name'];} catch(e) {var model_name = '';}
try {var percentage_used = ' | 使用寿命: ' + value['nvme_smart_health_information_log']['percentage_used'].toFixed(0) + '% ';} catch(e) {var percentage_used = '';}
try {var data_units_read = value['nvme_smart_health_information_log']['data_units_read']*512/1024/1024/1024;var data_units_read = '(读: ' + data_units_read.toFixed(2) + 'TB, ';} catch(e) {var data_units_read = '';}
try {var data_units_written = value['nvme_smart_health_information_log']['data_units_written']*512/1024/1024/1024;var data_units_written = '写: ' + data_units_written.toFixed(2) + 'TB)';} catch(e) {var data_units_written = '';}
try {var power_on_time = ' | 通电: ' + value['power_on_time']['hours'].toFixed(0) + '小时';} catch(e) {var power_on_time = '';}
try {var temperature = ' | 温度: ' + value['temperature']['current'].toFixed(1) + '°C';} catch(e) {var temperature = '';}
return `${model_name}${percentage_used}${data_units_read}${data_units_written}${power_on_time}${temperature}`;
} else {
return `提示: 未安装硬盘或已直通硬盘控制器`;
}
}
},
{
itemId: 'SATA_sda',
colspan: 2,
printBar: false,
title: gettext('SATA_sda'),
textField: 'smartctl_sda_json',
renderer: function(value) {
if (value.indexOf("Device is in STANDBY mode") != -1 ) {
return `提示: 磁盘休眠中`;
} else if (value.indexOf("No such device") != -1 ) {
return `提示: 未安装硬盘或已直通硬盘控制器`;
} else {
value = JSON.parse(value);
try {var model_name = value['model_name'];} catch(e) {var model_name = '';}
try {var user_capacity = value['user_capacity']['bytes']/1024/1024/1024;var user_capacity = ' | 容量: ' + user_capacity.toFixed(2) + ' GB';} catch(e) {var user_capacity = '';}
try {var power_on_time = ' | 已通电: ' + value['power_on_time']['hours'].toFixed(0) + ' 小时';} catch(e) {var power_on_time = '';}
try {var error_count = value['ata_smart_error_log']['summary']['count'].toFixed(0);if (error_count != 0){error_count = ' | 磁盘错误: ' + error_count;} else {var error_count = '';} } catch(e) {var error_count = '';}
try {var self_count = value['ata_smart_self_test_log']['standard']['error_count_total'].toFixed(0);if (self_count != 0){self_count = ' | 自检错误: ' + self_count;} else {var self_count = '';} } catch(e) {var self_count = '';}
try {var temperature = ' | 温度: ' + value['temperature']['current'].toFixed(1) + '°C';} catch(e) {var temperature = '';}
return `${model_name}${user_capacity}${power_on_time}${error_count}${self_count}${temperature}`;
}
}
},
{
itemId: 'SATA_sdb',
colspan: 2,
printBar: false,
title: gettext('SATA_sdb'),
textField: 'smartctl_sdb_json',
renderer: function(value) {
if (value.indexOf("Device is in STANDBY mode") != -1 ) {
return `提示: 磁盘休眠中`;
} else if (value.indexOf("No such device") != -1 ) {
return `提示: 未安装硬盘或已直通硬盘控制器`;
} else {
value = JSON.parse(value);
try {var model_name = value['model_name'];} catch(e) {var model_name = '';}
try {var user_capacity = value['user_capacity']['bytes']/1024/1024/1024;var user_capacity = ' | 容量: ' + user_capacity.toFixed(2) + ' GB';} catch(e) {var user_capacity = '';}
try {var power_on_time = ' | 已通电: ' + value['power_on_time']['hours'].toFixed(0) + ' 小时';} catch(e) {var power_on_time = '';}
try {var error_count = value['ata_smart_error_log']['summary']['count'].toFixed(0);if (error_count != 0){error_count = ' | 磁盘错误: ' + error_count;} else {var error_count = '';} } catch(e) {var error_count = '';}
try {var self_count = value['ata_smart_self_test_log']['standard']['error_count_total'].toFixed(0);if (self_count != 0){self_count = ' | 自检错误: ' + self_count;} else {var self_count = '';} } catch(e) {var self_count = '';}
try {var temperature = ' | 温度: ' + value['temperature']['current'].toFixed(1) + '°C';} catch(e) {var temperature = '';}
return `${model_name}${user_capacity}${power_on_time}${error_count}${self_count}${temperature}`;
}
}
},
{
itemId: 'MHz',
colspan: 2,
printBar: false,
title: gettext('CPU频率'),
textField: 'cpusensors',
renderer:function(value){
var f0 = value.match(/CPU MHz.*?([\d]+)/)[1];
var f1 = value.match(/CPU min MHz.*?([\d]+)/)[1];
var f2 = value.match(/CPU max MHz.*?([\d]+)/)[1];
return `实时: ${f0} MHz || 最小: ${f1} MHz | 最大: ${f2} MHz `
}
},

如果是 PVE 8.0,CPU 不再返回实时频率,需要修改为下列代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
itemId: 'MHz',
colspan: 2,
printBar: false,
title: gettext('CPU频率'),
textField: 'cpusensors',
renderer:function(value){
var f1 = value.match(/CPU min MHz.*?([\d]+)/)[1];
var f2 = value.match(/CPU max MHz.*?([\d]+)/)[1];
var f0 = value.match(/CPU.*scaling MHz.*?([\d]+)/)[1];
var f0 = f0*f2/100;
return `实时: ${f0} MHz || 最小: ${f1} MHz | 最大: ${f2} MHz `
}
},

修改显示范围
依然是 pvemanagerlib.js 文件
搜索 widget.pveNodeStatus
将 height: 300 (默认值) 改大为 420,或者更大,然后保存(每多一行大概增大 20~25)

重载 PVE 界面
修改完成后重载 PVE 界面
如无把握,建议不要一次加入太多代码,修改一段就重载一次

1
systemctl restart pveproxy
Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.
  • Copyrights © 2022-2023 tty228
  • Visitors: | Views:

请我喝杯咖啡吧~

支付宝
微信