From: Davide Guerri via pve-devel <pve-devel@lists.proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Davide Guerri <davide.guerri@gmail.com>
Subject: [pve-devel] [PATCH container 1/1] api: add hardware sensors endpoint and API support
Date: Tue, 14 Oct 2025 21:36:09 +0200 [thread overview]
Message-ID: <mailman.910.1760476899.390.pve-devel@lists.proxmox.com> (raw)
In-Reply-To: <20251014193609.34452-1-davide.guerri@gmail.com>
[-- Attachment #1: Type: message/rfc822, Size: 20516 bytes --]
From: Davide Guerri <davide.guerri@gmail.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH container 1/1] api: add hardware sensors endpoint and API support
Date: Tue, 14 Oct 2025 21:36:09 +0200
Message-ID: <20251014193609.34452-2-davide.guerri@gmail.com>
This change adds support for querying hardware sensors via the API,
enhancing system monitoring capabilities.
API hierarchy:
/sensors -> GET index (returns: temperature, fan)
/temperature -> GET index (returns: cpu, disk, other)
/cpu -> GET CPU temperature data
/disk -> GET disk temperature data
/other -> GET other temperature data
/fan -> GET fan speed data
Signed-off-by: Davide Guerri <davide.guerri@gmail.com>
---
PVE/API2/Hardware.pm | 8 +-
PVE/API2/Hardware/Makefile | 1 +
PVE/API2/Hardware/Sensors.pm | 420 +++++++++++++++++++++++++++++++++++
3 files changed, 428 insertions(+), 1 deletion(-)
create mode 100644 PVE/API2/Hardware/Sensors.pm
diff --git a/PVE/API2/Hardware.pm b/PVE/API2/Hardware.pm
index 48b0765a..31391d92 100644
--- a/PVE/API2/Hardware.pm
+++ b/PVE/API2/Hardware.pm
@@ -7,6 +7,7 @@ use PVE::JSONSchema qw(get_standard_option);
use PVE::RESTHandler;
use PVE::API2::Hardware::PCI;
+use PVE::API2::Hardware::Sensors;
use PVE::API2::Hardware::USB;
use base qw(PVE::RESTHandler);
@@ -16,6 +17,11 @@ __PACKAGE__->register_method({
path => 'pci',
});
+__PACKAGE__->register_method({
+ subclass => "PVE::API2::Hardware::Sensors",
+ path => 'sensors',
+});
+
__PACKAGE__->register_method({
subclass => "PVE::API2::Hardware::USB",
path => 'usb',
@@ -47,7 +53,7 @@ __PACKAGE__->register_method({
my ($param) = @_;
my $res = [
- { type => 'pci' }, { type => 'usb' },
+ { type => 'pci' }, { type => 'sensors' }, { type => 'usb' },
];
return $res;
diff --git a/PVE/API2/Hardware/Makefile b/PVE/API2/Hardware/Makefile
index 026a8dd6..36f359cc 100644
--- a/PVE/API2/Hardware/Makefile
+++ b/PVE/API2/Hardware/Makefile
@@ -2,6 +2,7 @@ include ../../../defines.mk
PERLSOURCE= \
PCI.pm \
+ Sensors.pm \
USB.pm \
all:
diff --git a/PVE/API2/Hardware/Sensors.pm b/PVE/API2/Hardware/Sensors.pm
new file mode 100644
index 00000000..4509b88b
--- /dev/null
+++ b/PVE/API2/Hardware/Sensors.pm
@@ -0,0 +1,420 @@
+package PVE::API2::Hardware::Sensors;
+
+use v5.36;
+
+use PVE::JSONSchema qw(get_standard_option);
+use PVE::RESTHandler;
+use PVE::SensorInfo;
+
+use base qw(PVE::RESTHandler);
+
+# Helper function to add optional fields to an entry
+sub add_optional_fields {
+ my ($entry, $info, $fields) = @_;
+
+ for my $field (@$fields) {
+ if (exists $info->{$field}) {
+ $entry->{$field} = $info->{$field};
+ }
+ }
+
+ return $entry;
+}
+
+__PACKAGE__->register_method({
+ name => 'sensor_index',
+ path => '',
+ method => 'GET',
+ description => "Index of available sensor methods.",
+ permissions => {
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => { subdir => { type => 'string' } },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ return [
+ { subdir => 'temperature' }, { subdir => 'fan' },
+ ];
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'temperature_index',
+ path => 'temperature',
+ method => 'GET',
+ description => "Index of temperature sensor types.",
+ permissions => {
+ user => 'all',
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => { subdir => { type => 'string' } },
+ },
+ links => [{ rel => 'child', href => "{subdir}" }],
+ },
+ code => sub {
+ my ($param) = @_;
+
+ return [
+ { subdir => 'cpu' }, { subdir => 'disk' }, { subdir => 'other' },
+ ];
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read_cpu_temperature_values',
+ path => 'temperature/cpu',
+ method => 'GET',
+ description => "Read current CPU temperature values.",
+ protected => 1,
+ proxyto => "node",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit'], any => 1],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ sensor => {
+ type => 'string',
+ description => 'Sensor identifier (e.g., coretemp/Core 0).',
+ },
+ temperature => {
+ type => 'number',
+ description => 'Current temperature in degrees Celsius.',
+ },
+ unit => {
+ type => 'string',
+ description => 'Temperature unit (always celsius).',
+ },
+ driver => {
+ type => 'string',
+ description => 'Human-readable driver description.',
+ },
+ max => {
+ type => 'number',
+ optional => 1,
+ description => 'Maximum operating temperature before throttling.',
+ },
+ critical => {
+ type => 'number',
+ optional => 1,
+ description => 'Critical temperature threshold.',
+ },
+ logical_core => {
+ type => 'integer',
+ optional => 1,
+ description => 'Logical core number.',
+ },
+ physical_core => {
+ type => 'string',
+ optional => 1,
+ description => 'Physical core ID.',
+ },
+ package => {
+ type => 'integer',
+ optional => 1,
+ description => 'CPU package/socket number.',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $temps = PVE::SensorInfo::read_temperatures('cpu');
+
+ my $result = [];
+ foreach my $sensor (sort keys %$temps) {
+ my $info = $temps->{$sensor};
+
+ my $entry = {
+ sensor => $sensor,
+ temperature => $info->{temperature},
+ unit => $info->{unit},
+ driver => $info->{driver},
+ };
+
+ add_optional_fields(
+ $entry,
+ $info,
+ [qw(max critical logical_core physical_core package)],
+ );
+
+ push @$result, $entry;
+ }
+
+ return $result;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read_disk_temperature_values',
+ path => 'temperature/disk',
+ method => 'GET',
+ description => "Read current disk temperature values.",
+ protected => 1,
+ proxyto => "node",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit'], any => 1],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ sensor => {
+ type => 'string',
+ description => 'Sensor identifier (e.g., hwmon1/Composite).',
+ },
+ device => {
+ type => 'string',
+ optional => 1,
+ description => 'Block device name (e.g., nvme0n1, sda).',
+ },
+ temperature => {
+ type => 'number',
+ description => 'Current temperature in degrees Celsius.',
+ },
+ unit => {
+ type => 'string',
+ description => 'Temperature unit (always celsius).',
+ },
+ driver => {
+ type => 'string',
+ description => 'Human-readable driver description.',
+ },
+ max => {
+ type => 'number',
+ optional => 1,
+ description => 'Maximum operating temperature.',
+ },
+ critical => {
+ type => 'number',
+ optional => 1,
+ description => 'Critical temperature threshold.',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $temps = PVE::SensorInfo::read_temperatures('disk');
+
+ my $result = [];
+ foreach my $sensor (sort keys %$temps) {
+ my $info = $temps->{$sensor};
+
+ my $entry = {
+ sensor => $sensor,
+ temperature => $info->{temperature},
+ unit => $info->{unit},
+ driver => $info->{driver},
+ };
+
+ add_optional_fields($entry, $info, [qw(device max critical)]);
+
+ push @$result, $entry;
+ }
+
+ return $result;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read_other_temperature_values',
+ path => 'temperature/other',
+ method => 'GET',
+ description => "Read current temperature values from other sensors.",
+ protected => 1,
+ proxyto => "node",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit'], any => 1],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ sensor => {
+ type => 'string',
+ description => 'Sensor identifier.',
+ },
+ temperature => {
+ type => 'number',
+ description => 'Current temperature in degrees Celsius.',
+ },
+ unit => {
+ type => 'string',
+ description => 'Temperature unit (always celsius).',
+ },
+ driver => {
+ type => 'string',
+ description => 'Human-readable driver description.',
+ },
+ max => {
+ type => 'number',
+ optional => 1,
+ description => 'Maximum operating temperature.',
+ },
+ critical => {
+ type => 'number',
+ optional => 1,
+ description => 'Critical temperature threshold.',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $temps = PVE::SensorInfo::read_temperatures('other');
+
+ my $result = [];
+ foreach my $sensor (sort keys %$temps) {
+ my $info = $temps->{$sensor};
+
+ my $entry = {
+ sensor => $sensor,
+ temperature => $info->{temperature},
+ unit => $info->{unit},
+ driver => $info->{driver},
+ };
+
+ add_optional_fields($entry, $info, [qw(max critical)]);
+
+ push @$result, $entry;
+ }
+
+ return $result;
+ },
+});
+
+__PACKAGE__->register_method({
+ name => 'read_fan_speeds',
+ path => 'fan',
+ method => 'GET',
+ description => "Read current fan speed values.",
+ protected => 1,
+ proxyto => "node",
+ permissions => {
+ check => ['perm', '/', ['Sys.Audit'], any => 1],
+ },
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ },
+ },
+ returns => {
+ type => 'array',
+ items => {
+ type => "object",
+ properties => {
+ fan => {
+ type => 'string',
+ description => 'Fan identifier (e.g., nct6775/CPU Fan).',
+ },
+ speed => {
+ type => 'integer',
+ description => 'Current fan speed in RPM.',
+ },
+ unit => {
+ type => 'string',
+ description => 'Speed unit (always rpm).',
+ },
+ driver => {
+ type => 'string',
+ description => 'Human-readable driver description.',
+ },
+ min => {
+ type => 'integer',
+ optional => 1,
+ description => 'Minimum fan speed in RPM.',
+ },
+ max => {
+ type => 'integer',
+ optional => 1,
+ description => 'Maximum fan speed in RPM.',
+ },
+ target => {
+ type => 'integer',
+ optional => 1,
+ description => 'Target fan speed in RPM.',
+ },
+ alarm => {
+ type => 'integer',
+ optional => 1,
+ description => 'Fan alarm status (0=OK, 1=Alarm).',
+ },
+ },
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ my $speeds = PVE::SensorInfo::read_fan_speeds();
+
+ my $result = [];
+ foreach my $fan (sort keys %$speeds) {
+ my $info = $speeds->{$fan};
+
+ my $entry = {
+ fan => $fan,
+ speed => $info->{speed},
+ unit => $info->{unit},
+ driver => $info->{driver},
+ };
+
+ add_optional_fields($entry, $info, [qw(min max target alarm)]);
+
+ push @$result, $entry;
+ }
+
+ return $result;
+ },
+});
+
+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
parent reply other threads:[~2025-10-14 21:21 UTC|newest]
Thread overview: expand[flat|nested] mbox.gz Atom feed
[parent not found: <20251014193609.34452-1-davide.guerri@gmail.com>]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=mailman.910.1760476899.390.pve-devel@lists.proxmox.com \
--to=pve-devel@lists.proxmox.com \
--cc=davide.guerri@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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.