all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [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

only message in thread, other threads:[~2025-10-14 21:22 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
     [not found] <20251014193601.34285-1-davide.guerri@gmail.com>
2025-10-14 19:36 ` [pve-devel] [PATCH container 1/1] sensors: add hardware sensor module for CPUs, disks, and fans Davide Guerri via pve-devel

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal