Skip to content

Commit

Permalink
Merge pull request #2 from Zitt/pwm_picofan
Browse files Browse the repository at this point in the history
Additional sensors to Summary page
  • Loading branch information
Zitt authored Aug 7, 2024
2 parents ffabca4 + a017488 commit 8d291d2
Show file tree
Hide file tree
Showing 9 changed files with 715 additions and 3 deletions.
51 changes: 49 additions & 2 deletions v8.2.4/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ To access the fan speeds and other temperatures on the motherboard; you will nee
```shell
sensors-detect
```
You will need to reboot your pve enviorment after making the sensors-detect changes.
You will need to reboot your pve environment after making the sensors-detect changes.

To confirm the SMART temperature readings are working, run the sensors command on the
hypervisor:
Expand All @@ -47,6 +47,33 @@ sensors

Verify that fan speeds are detected.

### Rev 2 Requirements

In addition to the previous requirements; Rev 2 requires python 3.11 or better be installed on your proxmox node. You may need to do:

```shell
apt install python3.11
```

Python3 is used by the picofan.py and the smartctl.py scripts included in Rev 2.

[Picofan](https://github.com/tjko/fanpico) is installed in the Authors ProxMox box to provide additional sensors and more fan control as he ran out of fan headers on the X299 board. The picofan scripts use paho-mqtt to communicate with a broker running on the Proxmox node. The Picofan controller talks over Wifi to the broker running at a static IP running [EMQX](https://tteck.github.io/Proxmox/#emqx-lxc). The broker is then queried by the new content in Node.pm. You will probably need to install:
```shell
apt install python3-paho-mqtt
```
for the proper mqtt libraries. The author is using the latest available in proxmox 8.2.4: python3-paho-mqtt/stable,now 1.6.1-1
Edit picofan.py and insert the broker's username and password:
```python
userName = "<put data here>"
password = ""
```

[Smartctl](https://www.smartmontools.org/) is used by the smartctl.py script to query for NVME SSD health. I believe this is already installed in proxmox. If not you may be able to install with:
```shell
apt install smartmontools
```
Due to linux permissions; the smartctl.py must be run as root. See the Architectural decisions below for more detail.

## Usage

The simple way to apply these modifications is by examining the patch files in one of the patches
Expand All @@ -56,10 +83,10 @@ done if the PVE packages on your system match the version these patches were gen
Adjust your manual patches as appropriate.
Make backups of your PVE files before patching and I recommend you manually apply the patches with a text editor yourself so that you understand the changes that are being made.

Order of operations is to install the .patch files first... verify correct operations then apply the r2.patch files. If you don't want the features provided by Rev 2; simply skip or don't do the patches. You should do all the Rev2 patches if you want a single feature or use your software brain to pull out the relevant pieces.

The patch directories and the versions of the Proxmox packages they were generated against are:


[v8.2.4](v8.2.4/patches)
* pve-manager 8.2.4
* proxmox-widget-toolkit 4.2.1
Expand All @@ -78,3 +105,23 @@ The patch directories and the versions of the Proxmox packages they were generat
### Making manual modifications

See original [Readme](../README.md) for more information.

## Rev 2 Architectural Decisions

Rev 2 uses helper python scripts to collect data. I'm sure there are better ways to do this; however, I'm not well versed in proxmox code / capabilities.

### Picofan
[Picofan](https://github.com/tjko/fanpico) is collected via a MQTT broker with rentetion. It saves new data (when received) from the broker in the */tmp/ProxFanPico.json* file. If the picofan.py is called and no data is available in the broker; it will read and return the data stored previously in */tmp/ProxFanPico.json*. If the data is received by the broker; it is written to the json file and returned to the caller. This is done to prevent GUI hangs in proxmox's summary page if no new data is present in the broker. The picofan.py script makes sure the json file is readable by all users on the system via a chmod. Picofan sensors are reported in the gui.

Data from picofan.py is merged with the sensors data from lmsensors. They are treated similarly.

### Smartctl
smartctl.py can only be run as a root as certain permissions are needed. Because of this the smartctl.py program writes a publicly readable json file to */tmp/smartctl.*%s*.json* where %s is the name of the nvme drive. This program is run once as part of a systemd unit file. See smartctl-nvme0n1.export.service for a template. How to install unit files is beyond the scope of this document. Additionally; The author has setup a crontab every six hours to re-capture the nvme life periodically. Node.pm in proxmox reads the json file for it's data. An example crontab from the author's root user:
```crontab
33 */6 * * * PATH=/usr/sbin:$PATH /usr/bin/python3 /opt/smartctl.py >> /var/log/smartctl.log 2>&1
```
%s in the filename is replaced with the nvme drive as detected by the regular expression on or about line 16. The */proc/mounts* file is interrogated looking for */boot* entries. smartctl is then run to capture the data for that drive. The author's system currently only has one nvme ssd; so only one drive of data is captured.

### sys file system
Rev 2 uses the pwm readings presented in /sys/devices/platform/nct6775.656/hwmon/hwmon4/pwm* to calculate the maximum RPM speed of a given fan for the bar graphs. It does simple algebra to calculate. If you do not have the author's exact motherboard; you will need to change this code.

181 changes: 181 additions & 0 deletions v8.2.4/patches/Nodes.pm.r2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
diff --git a/usr/share/perl5/PVE/API2/Nodes.pm b/../Nodes.pm
index ed117843..57acbcad 100644
--- a/usr/share/perl5/PVE/API2/Nodes.pm
+++ b/../Nodes.pm
@@ -495,6 +495,51 @@ __PACKAGE__->register_method({
free => $dinfo->{blocks} - $dinfo->{used},
};

+ $res->{'nvme0n1'} = { used => '25', total=>'98', error => 'no proc/mounts' };
+
+ open my $mnt, "/proc/mounts";
+ while ( my $line = <$mnt> ) {
+ if ($line =~ m/^(.+nvme\d.\d)p\d+\s+(.boot\S+)/i ) {
+ my $nvme = $1;
+
+ my @p = split('/', $nvme);
+ my $bnvme = $p[-1];
+
+ my $content = "";
+ my $fname = '/tmp/smartctl.'.$bnvme.'.json';
+ open(my $fh, '<', $fname ) or $res->{$bnvme} = { used => '26', total=>'99', error => 'cannot open file: ' . $fname };
+ {
+ local $/;
+ $content = <$fh>;
+ }
+ close($fh);
+ my $sensors = eval { decode_json($content) };
+
+ if ( !defined($sensors) ) {
+ $res->{$bnvme} = { used => '50', total=>'101', error => 'no sensors' };
+ }
+
+ if ( exists $sensors->{nvme_smart_health_information_log} ) {
+ if ( exists $sensors->{nvme_smart_health_information_log}{percentage_used} ) {
+ my $left = 100.0 - $sensors->{nvme_smart_health_information_log}{percentage_used};
+
+ $res->{$bnvme} = {
+ used => sprintf("%.1f", $left),
+ total => '100',
+ mntpt => $2,
+ dev => $nvme
+ };
+ last;
+ }
+ } else {
+ $res->{$bnvme} = { used => '75.0', total=>'102.2', error => 'no nvme_smart_health_information_log',
+ sensors => $sensors, username => scalar getpwuid($<) };
+ }
+ }
+ }
+ close($mnt);
+
my %sensors_config = (
cputemp => {
jsonpath => ['coretemp-isa-0000', 'Package id 0'],
@@ -526,6 +571,12 @@ __PACKAGE__->register_method({
valkey => 'fan2_input',
critkey => 'fan2_alarm',
},
+ fan3rpm => {
+ jsonpath => ['fans', 8],
+ valkey => 'rpm',
+ pwmkey => 'pwm',
+ picofan => 1,
+ },
fan4rpm => {
jsonpath => ['nct6798-isa-0290', 'fan4'],
valkey => 'fan4_input',
@@ -540,34 +591,105 @@ __PACKAGE__->register_method({
jsonpath => ['nct6798-isa-0290', 'fan7'],
valkey => 'fan7_input',
critkey => 'fan7_min',
- }
+ },
+ inletTemp => {
+ jsonpath => ['sensors', 1],
+ valkey => 'temp',
+ picofan => 1,
+ },
+ outletTemp => {
+ jsonpath => ['sensors', 2],
+ valkey => 'temp',
+ picofan => 1,
+ },
+ picoTemp => {
+ jsonpath => ['sensors', 3],
+ valkey => 'temp',
+ picofan => 1,
+ }
);

my $temp_default_val = 0;
my $temp_default_crit = 80;

my $sensors = eval { decode_json(`sensors -j`); };
+ my $sensors2 = eval { decode_json(`/usr/bin/python3 /opt/picofan.py`); };
+ if ( exists $sensors2->{fans} ) {
+ $sensors->{fans} = $sensors2->{fans};
+ }
+ if ( exists $sensors2->{sensors} ) {
+ $sensors->{sensors} = $sensors2->{sensors};
+ }
+ undef $sensors2;
+
if (defined($sensors)) {
keys %sensors_config;
while (my ($k, $v) = each %sensors_config) {
if (!defined($v->{jsonpath})) { next; }
my $currref = $sensors;
my $pathdefined = 1;
- for my $pathseg (@{$v->{jsonpath}}) {
- if (defined($currref->{$pathseg})) {
- $currref = $currref->{$pathseg}
- } else {
- $pathdefined = 0;
+ if ( exists $v->{picofan} ) {
+ my $entry = $v->{jsonpath}[0];
+ my $id = $v->{jsonpath}[1];
+
+ if ( exists $sensors->{$entry} ) {
+ foreach my $a ( @{ $sensors->{$entry} } ) {
+ if ( $a->{id} == $id ) {
+ $res->{$k} = {
+ used => $a->{$v->{valkey}},
+ total => $temp_default_crit
+ };
+
+ if ( exists $v->{pwmkey} ) {
+ my $pwm = $a->{ $v->{pwmkey} };
+ my $rpm = $res->{$k}->{used};
+ if ($pwm > 1) {
+ my $max = $rpm/$pwm * 100;
+ $res->{$k}->{total} = ($max == int $max) ? $max : int($max + 1);
+ }
+ }
last;
+ }
+ }
+ }
+ $pathdefined = 0;
+ } else {
+ for my $pathseg (@{$v->{jsonpath}}) {
+ if ( defined $v->{jsonpath}[1] &&
+ $v->{jsonpath}[1] =~ m/fan(\d+)/i &&
+ !defined $res->{$k}->{pwm} ) {
+ my $hwmon = "/sys/devices/platform/nct6775.656/hwmon/hwmon4/pwm".$1;
+ my $pwm;
+ if (-r $hwmon) {
+ open(my $fh, '<', $hwmon );
+ { local $/; $pwm = <$fh>; }
+ close($fh);
+ $res->{$k}->{pwm} = $pwm;
+ }
+ }
+ if (defined($currref->{$pathseg})) {
+ $currref = $currref->{$pathseg}
+ } else {
+ $pathdefined = 0;
+ last;
+ }
}
}
if (!$pathdefined) { next; }
+ my $pwm = -1;
+ if ( defined( $res->{$k}->{pwm} ) ) { $pwm = $res->{$k}->{pwm}; chomp($pwm); }
$res->{$k} = {
used => defined($v->{valkey}) && defined($currref->{$v->{valkey}})
? $currref->{$v->{valkey}} : $temp_default_val,
total => defined($v->{critkey}) && defined($currref->{$v->{critkey}})
? $currref->{$v->{critkey}} : $temp_default_crit,
};
+ if ($pwm > 0) {
+ my $rpm = $res->{$k}->{used};
+ my $max = $rpm/$pwm * 255;
+ $res->{$k}->{total} = ($max == int $max) ? $max : int($max + 1);
+ $res->{$k}->{pwm} = $pwm;
+ }
}
}


24 changes: 24 additions & 0 deletions v8.2.4/patches/proxmoxlib.js.r2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
diff --git a/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js b/../proxmoxlib.js
index defc3300..949b0b77 100644
--- a/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
+++ b/../proxmoxlib.js
@@ -1105,6 +1105,33 @@ utilities: {
}
return record.used.toFixed(0) + ' RPM';
},
+ render_node_nvme_life: function(record) {
+ //console.table( record );
+ if (!record || !Ext.isNumeric(record.used) || !Ext.isNumeric(record.total)) {
+ return '-';
+ }
+ let u = parseFloat(record.used);
+ let t = parseFloat(record.total);
+
+ let ratio = (u * 100 / t).toFixed(0);
+
+ return ratio + '% remaining';
+ },

loadTextFromFile: function(file, callback, maxBytes) {
let maxSize = maxBytes || 8192;

Loading

0 comments on commit 8d291d2

Please sign in to comment.