* [pve-devel] [PATCH container 1/1] sensors: add hardware sensor module for CPUs, disks, and fans
[not found] <20251014193601.34285-1-davide.guerri@gmail.com>
@ 2025-10-14 19:36 ` Davide Guerri via pve-devel
0 siblings, 0 replies; only message in thread
From: Davide Guerri via pve-devel @ 2025-10-14 19:36 UTC (permalink / raw)
To: pve-devel; +Cc: Davide Guerri
[-- Attachment #1: Type: message/rfc822, Size: 19789 bytes --]
From: Davide Guerri <davide.guerri@gmail.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH container 1/1] sensors: add hardware sensor module for CPUs, disks, and fans
Date: Tue, 14 Oct 2025 21:36:01 +0200
Message-ID: <20251014193601.34285-2-davide.guerri@gmail.com>
Add new PVE::SensorInfo.pm module providing functionality to read
temperature, fan, and other sensor data from /sys/class/hwmon.
Includes driver mappings for various hardware (Intel, AMD, ASUS, etc.)
and CPU core topology integration for accurate per-core temperature
reporting.
Signed-off-by: Davide Guerri <davide.guerri@gmail.com>
---
src/Makefile | 1 +
src/PVE/SensorInfo.pm | 385 ++++++++++++++++++++++++++++++++++++++++++
2 files changed, 386 insertions(+)
create mode 100644 src/PVE/SensorInfo.pm
diff --git a/src/Makefile b/src/Makefile
index 0ca817b..388ebbb 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -32,6 +32,7 @@ LIB_SOURCES = \
RESTHandler.pm \
SafeSyslog.pm \
SectionConfig.pm \
+ SensorInfo.pm \
SysFSTools.pm \
Syscall.pm \
Systemd.pm \
diff --git a/src/PVE/SensorInfo.pm b/src/PVE/SensorInfo.pm
new file mode 100644
index 0000000..3938a4e
--- /dev/null
+++ b/src/PVE/SensorInfo.pm
@@ -0,0 +1,385 @@
+package PVE::SensorInfo;
+
+use v5.36;
+
+use PVE::Tools qw(file_read_firstline dir_glob_foreach);
+
+my $hwmon_base = "/sys/class/hwmon";
+my $cpu_base = "/sys/devices/system/cpu";
+
+# CPU sensor driver names
+my %cpu_drivers = (
+ 'coretemp' => 'Intel',
+ 'k10temp' => 'AMD',
+ 'k8temp' => 'AMD K8',
+ 'zenpower' => 'AMD Ryzen',
+ 'fam15h_power' => 'AMD',
+ 'atk0110' => 'ASUS',
+ 'w83627ehf' => 'Winbond',
+ 'nct6775' => 'Nuvoton',
+ 'it87' => 'ITE',
+ 'hp_wmi' => 'HPE',
+ 'asus_wmi' => 'ASUS WMI',
+);
+
+# Disk sensor driver names
+my %disk_drivers = (
+ 'drivetemp' => 'SATA/SAS',
+ 'nvme' => 'NVMe',
+);
+
+# Fan sensor driver names (motherboard and chassis fans)
+my %fan_drivers = (
+ 'nct6775' => 'Nuvoton',
+ 'it87' => 'ITE',
+ 'w83627ehf' => 'Winbond',
+ 'asus_wmi_sensors' => 'ASUS WMI',
+ 'atk0110' => 'ASUS',
+ 'dell_smm' => 'Dell',
+ 'gigabyte_waterforce' => 'Gigabyte',
+ 'corsairpsu' => 'Corsair PSU',
+ 'nzxt_smart2' => 'NZXT',
+ 'drivetemp' => 'Drive',
+);
+
+my $classify_temperature_sensor = sub {
+ my ($hwmon, $name) = @_;
+
+ return 'cpu' if exists $cpu_drivers{$name};
+ return 'disk' if exists $disk_drivers{$name};
+
+ return 'other';
+};
+
+my $build_core_mapping = sub {
+ my $core_map = {};
+ my $seen_cores = {};
+ my $logical_num = 0;
+
+ dir_glob_foreach(
+ $cpu_base,
+ 'cpu\d+',
+ sub {
+ my ($cpu) = @_;
+
+ my $cpu_path = "$cpu_base/$cpu";
+ my $core_id_file = "$cpu_path/topology/core_id";
+ my $package_id_file = "$cpu_path/topology/physical_package_id";
+
+ return if !-r $core_id_file;
+
+ my $physical_core = file_read_firstline($core_id_file);
+ my $package = file_read_firstline($package_id_file);
+ $package = 0 if !defined($package);
+
+ return if !defined($physical_core);
+
+ my $key = "${package}:${physical_core}";
+
+ if (!exists $seen_cores->{$key}) {
+ my $info = {
+ logical_num => $logical_num,
+ physical_id => $physical_core,
+ package => $package,
+ };
+
+ $core_map->{$key} = $info;
+ $seen_cores->{$key} = 1;
+ $logical_num++;
+ }
+ },
+ );
+
+ return $core_map;
+};
+
+my $get_disk_device_from_hwmon = sub {
+ my ($hwmon_path, $sensor_name) = @_;
+
+ # For NVMe devices: /sys/class/hwmon/hwmonX/device -> ../../nvmeY
+ # The nvme controller directory contains nvmeYn1 subdirectory
+ # (namespace)
+ if ($sensor_name eq 'nvme') {
+ if (-l "$hwmon_path/device") {
+ my $device_link = readlink("$hwmon_path/device");
+ if (defined($device_link) && $device_link =~ m/(nvme\d+)/) {
+ my $nvme_ctrl = $1;
+ # Look for the first namespace (typically nvmeXn1)
+ my $device_path = "$hwmon_path/device";
+ if (opendir(my $dh, $device_path)) {
+ my @namespaces = ();
+ while (my $entry = readdir($dh)) {
+ if ($entry =~ m/^(nvme\d+n\d+)$/) {
+ push @namespaces, $entry;
+ }
+ }
+ closedir($dh);
+ if (@namespaces) {
+ # Return the first namespace, sorted (usually nvmeXn1)
+ return (sort @namespaces)[0];
+ }
+ }
+ }
+ }
+ }
+
+ # For drivetemp devices: name format is "drivetemp"
+ # Device path: /sys/class/hwmon/hwmonX/device -> ../../X:Y:Z:W/
+ # (SCSI host:channel:id:lun)
+ if ($sensor_name eq 'drivetemp') {
+ if (-l "$hwmon_path/device") {
+ my $device_link = readlink("$hwmon_path/device");
+ if (defined($device_link)) {
+ # Extract SCSI address (e.g., "../../6:0:0:0/")
+ if ($device_link =~ m/(\d+):(\d+):(\d+):(\d+)/) {
+ my ($host, $channel, $id, $lun) = ($1, $2, $3, $4);
+
+ my $scsi_device_path =
+ "/sys/class/scsi_disk/" . "${host}:${channel}:${id}:${lun}/device";
+
+ # Look for the block device
+ my $scsi_block_path = "${scsi_device_path}/block";
+ if (-d $scsi_block_path) {
+ # Find the block device name (e.g., "sda")
+ opendir(my $dh, $scsi_block_path) or return undef;
+ while (my $entry = readdir($dh)) {
+ next if $entry =~ /^\./;
+ closedir($dh);
+ return $entry;
+ }
+ closedir($dh);
+ }
+ }
+ }
+ }
+ }
+
+ return undef;
+};
+
+# returns hashref of current TEMPERATURE readings for given sensor type
+# type can be 'cpu', 'disk', 'other', or 'all' (default)
+#
+# return format:
+# {
+# 'coretemp/Core 0' => {
+# temperature => 45.0, # in degrees Celsius
+# unit => 'celsius',
+# type => 'cpu',
+# driver => 'Intel',
+# max => 100.0, # optional
+# critical => 100.0, # optional
+# logical_core => 0, # optional, for CPU cores
+# physical_core => 0, # optional, for CPU cores
+# package => 0, # optional, for CPU cores
+# },
+# 'hwmon4/Sensor 1' => {
+# temperature => 35.0, # in degrees Celsius
+# unit => 'celsius',
+# type => 'disk',
+# driver => 'SATA/SAS',
+# device => 'sda', # block device name (for disks)
+# },
+# 'hwmon1/Composite' => {
+# temperature => 42.0, # in degrees Celsius
+# unit => 'celsius',
+# type => 'disk',
+# driver => 'NVMe',
+# device => 'nvme0n1', # block device name (for disks)
+# },
+# 'hwmon2/Composite' => {
+# temperature => 43.0, # in degrees Celsius
+# unit => 'celsius',
+# type => 'disk',
+# driver => 'NVMe',
+# device => 'nvme1n1', # block device name (for disks)
+# },
+# ...
+# }
+#
+sub read_temperatures {
+ my ($type) = @_;
+
+ $type = 'all' if !defined($type);
+ my @types = ($type eq 'all') ? qw(cpu disk other) : ($type);
+
+ my $results = {};
+ return $results if !-d $hwmon_base;
+
+ # Build core mapping once for all sensors
+ my $core_mapping = $build_core_mapping->();
+
+ dir_glob_foreach(
+ $hwmon_base,
+ 'hwmon\d+',
+ sub {
+ my ($hwmon) = @_;
+
+ my $hwmon_path = "$hwmon_base/$hwmon";
+ my $sensor_name = file_read_firstline("$hwmon_path/name");
+ return if !defined($sensor_name);
+
+ # Determine sensor type and skip if not requested
+ my $sensor_type = $classify_temperature_sensor->($hwmon_path, $sensor_name);
+ return if !grep { $_ eq $sensor_type } @types;
+
+ my $driver = $cpu_drivers{$sensor_name} || $disk_drivers{$sensor_name} || 'Unknown';
+ my $is_cpu_sensor = exists $cpu_drivers{$sensor_name};
+ my $is_disk_sensor = exists $disk_drivers{$sensor_name};
+
+ # Get disk device information for disk sensors
+ my $disk_device;
+ if ($is_disk_sensor) {
+ $disk_device = $get_disk_device_from_hwmon->($hwmon_path, $sensor_name);
+ }
+
+ # Read all temperature sensors for this hwmon device
+ dir_glob_foreach(
+ $hwmon_path,
+ 'temp\d+_input',
+ sub {
+ my ($temp_input) = @_;
+ my ($num) = $temp_input =~ /temp(\d+)_input$/;
+ return if !defined($num);
+
+ # Read current temperature
+ my $raw_temp = file_read_firstline("$hwmon_path/$temp_input");
+ return if !defined($raw_temp);
+
+ my $temp_c = $raw_temp / 1000.0;
+
+ # Get label
+ my $label = file_read_firstline("$hwmon_path/temp${num}_label");
+ $label = "Sensor $num" if !defined($label);
+
+ # Create unique key: use hwmon ID for disk sensors, sensor name for others
+ my $key = $is_disk_sensor ? "$hwmon/$label" : "$sensor_name/$label";
+
+ $results->{$key} = {
+ temperature => $temp_c,
+ unit => 'celsius',
+ type => $sensor_type,
+ driver => $driver,
+ };
+
+ # Add CPU topology information
+ if ($is_cpu_sensor && $label =~ /^Core (\d+)$/) {
+ my $physical_id = $1;
+ foreach my $map_key (keys %$core_mapping) {
+ my $map = $core_mapping->{$map_key};
+ if ($map->{physical_id} eq $physical_id) {
+ $results->{$key}->{logical_core} = $map->{logical_num};
+ $results->{$key}->{physical_core} = $physical_id;
+ $results->{$key}->{package} = $map->{package};
+ last;
+ }
+ }
+ }
+
+ # Add disk device information
+ if ($is_disk_sensor && defined($disk_device)) {
+ $results->{$key}->{device} = $disk_device;
+ }
+
+ # Add temperature limits
+ my $max = file_read_firstline("$hwmon_path/temp${num}_max");
+ $results->{$key}->{max} = $max / 1000.0 if defined($max);
+
+ my $crit = file_read_firstline("$hwmon_path/temp${num}_crit");
+ $results->{$key}->{critical} = $crit / 1000.0 if defined($crit);
+ },
+ );
+ },
+ );
+
+ return $results;
+}
+
+# returns hashref of current FAN SPEED readings
+#
+# return format:
+# {
+# 'nct6775/CPU Fan' => {
+# speed => 1500, # in RPM
+# unit => 'rpm',
+# driver => 'Nuvoton',
+# min => 0, # optional
+# max => 2000, # optional
+# target => 1500, # optional
+# alarm => 0, # optional, 0 or 1
+# },
+# ...
+# }
+#
+sub read_fan_speeds {
+ my $results = {};
+ return $results if !-d $hwmon_base;
+
+ dir_glob_foreach(
+ $hwmon_base,
+ 'hwmon\d+',
+ sub {
+ my ($hwmon) = @_;
+
+ my $hwmon_path = "$hwmon_base/$hwmon";
+ my $sensor_name = file_read_firstline("$hwmon_path/name");
+ return if !defined($sensor_name);
+
+ my $driver = $fan_drivers{$sensor_name} || 'Unknown';
+
+ # Read all fan sensors for this hwmon device
+ dir_glob_foreach(
+ $hwmon_path,
+ 'fan\d+_input',
+ sub {
+ my ($fan_input) = @_;
+ my ($num) = $fan_input =~ /fan(\d+)_input$/;
+ return if !defined($num);
+
+ # Read current fan speed
+ my $raw_speed = file_read_firstline("$hwmon_path/$fan_input");
+ return if !defined($raw_speed);
+
+ # Get label
+ my $label = file_read_firstline("$hwmon_path/fan${num}_label");
+ $label = "Fan $num" if !defined($label);
+
+ my $key = "$sensor_name/$label";
+
+ $results->{$key} = {
+ speed => int($raw_speed),
+ unit => 'rpm',
+ driver => $driver,
+ };
+
+ # Add optional fan metadata
+ my $min = file_read_firstline("$hwmon_path/fan${num}_min");
+ $results->{$key}->{min} = int($min) if defined($min);
+
+ my $max = file_read_firstline("$hwmon_path/fan${num}_max");
+ $results->{$key}->{max} = int($max) if defined($max);
+
+ my $target = file_read_firstline("$hwmon_path/fan${num}_target");
+ $results->{$key}->{target} = int($target) if defined($target);
+
+ my $alarm = file_read_firstline("$hwmon_path/fan${num}_alarm");
+ $results->{$key}->{alarm} = int($alarm) if defined($alarm);
+ },
+ );
+ },
+ );
+
+ return $results;
+}
+
+# convenience function: read CPU temperatures only
+sub read_cpu_temps {
+ return read_temperatures('cpu');
+}
+
+# convenience function: read disk temperatures only
+sub read_disk_temps {
+ return read_temperatures('disk');
+}
+
+1;
--
2.50.1 (Apple Git-155)
[-- Attachment #2: Type: text/plain, Size: 160 bytes --]
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
^ permalink raw reply [flat|nested] only message in thread