Add support for UPS information

This commit is contained in:
Meliox 2025-09-06 14:32:10 +02:00 committed by GitHub
parent 56f717450c
commit 6b4d6c4489
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 323 additions and 13 deletions

View File

@ -27,6 +27,7 @@ BACKUP_DIR=""
DEBUG_REMOTE=false
DEBUG_JSON_FILE="/tmp/sensordata.json"
DEBUG_UPS_FILE="/tmp/upsc.txt"
# This script's working directory
SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
@ -240,6 +241,51 @@ function configure {
esac
fi
# Prompt user for enabling UPS
local choiseEnableUPS=$(ask "Do you wish to enable information from an attached UPS (requires configured UPS server and installed UPS client from Network UPS Tools already configured beforehand). (Y/n)")
case "$choiseEnableUPS" in
[yY] | "")
# Test the connection using upsc command
if [ $DEBUG_REMOTE = true ]; then
upsOutput=$(cat $DEBUG_UPS_FILE)
echo "Remote debugging is used. UPS readings from dump file $DEBUG_UPS_FILE will be used."
upsConnection="DEBUG_UPS"
else
# Prompt user for UPS connection details
upsConnection=$(ask "Enter connection details for the UPS (e.g., upsname[@hostname[:port]])")
if (! command -v upsc &>/dev/null); then
err "The 'upsc' command is not available. Please install the 'nut-client' package and ensure it is configured correctly. Exiting..."
fi
upsOutput=$(upsc "$upsConnection" 2>&1)
fi
# Check for device.model in the output to confirm successful connection
if (echo "$upsOutput" | grep -q "device.model:"); then
# Extract the model name
modelName=$(echo "$upsOutput" | grep "device.model:" | cut -d':' -f2- | xargs)
ENABLE_UPS=true
echo "Successfully connected to UPS model: $modelName at $upsConnection."
info "UPS information will be displayed..."
else
warn "Failed to connect to UPS at '$upsConnection'. No valid UPS model found."
warn "Error: $upsOutput"
ENABLE_UPS=false
fi
;;
[nN])
ENABLE_UPS=false
info "UPS information will NOT be displayed..."
;;
*)
warn "Invalid selection. UPS information will not be displayed."
ENABLE_UPS=false
;;
esac
echo ""
# DMI Type:
# 1 ... System Information
# 2 ... Base Board Information (for self-made PC)
@ -317,6 +363,24 @@ function install_mod {
msg "Sensors' output added to \"$NODES_PM_FILE\"."
fi
if [ $ENABLE_UPS = true ]; then
local upsCmd
if [ $DEBUG_REMOTE = true ]; then
upsCmd="cat \"$DEBUG_UPS_FILE\""
else
upsCmd="upsc \"$upsConnection\" 2>/dev/null"
fi
# Insert UPS data collection before the disk info line
sed -i "/my \$dinfo = df('\/', 1);/i\\
\\
# Collect UPS status information\\
\$res->{upsc} = \\\`$upsCmd\\\`;\\
" "$NODES_PM_FILE"
msg "UPS output added to \"$NODES_PM_FILE\"."
fi
if [ $ENABLE_SYSTEM_INFO = true ]; then
local systemInfoCmd=$(dmidecode -t ${SYSTEM_INFO_TYPE} | awk -F': ' '/Manufacturer|Product Name|Serial Number/ {print $1": "$2}' | awk '{$1=$1};1' | sed 's/$/ |/' | paste -sd " " - | sed 's/ |$//')
sed -i "/my \$dinfo = df('\/', 1);/i\\\t\t\$res->{systemInfo} = \"$(echo "$systemInfoCmd")\";\n" "$NODES_PM_FILE"
@ -568,6 +632,22 @@ Ext.define('PVE.mod.TempHelper', {\n\
#
# NOTE: The following items will be added in reverse order
#
if [ $ENABLE_UPS = true ]; then
local TEMP_JS_FILE="/tmp/ups_widget.js"
generate_ups_widget $TEMP_JS_FILE
sed -i "/^Ext.define('PVE.node.StatusView',/ {
:a
/items:/!{N;ba;}
:b
/'thermal.*},/!{N;bb;}
r /tmp/ups_widget.js
}" "$PVE_MANAGER_LIB_JS_FILE"
rm $TEMP_JS_FILE
fi
if [ $ENABLE_HDD_TEMP = true ]; then
sed -i "/^Ext.define('PVE.node.StatusView',/ {
:a;
@ -876,7 +956,9 @@ Ext.define('PVE.mod.TempHelper', {\n\
# Add an empty line to separate modified items as a visual group
# NOTE: Check for the presence of items in the reverse order of display
local lastItemId=""
if [ $ENABLE_HDD_TEMP = true ]; then
if [ $ENABLE_UPS = true ]; then
lastItemId="upsc"
elif [ $ENABLE_HDD_TEMP = true ]; then
lastItemId="thermalHdd"
elif [ $ENABLE_NVME_TEMP = true ]; then
lastItemId="thermalNvme"
@ -944,6 +1026,223 @@ Ext.define('PVE.mod.TempHelper', {\n\
fi
}
generate_ups_widget() {
#region UPS widget heredoc
cat > "$1" <<'EOF'
{
xtype: 'box',
colspan: 2,
html: gettext('UPS'),
},
{
itemId: 'upsc',
colspan: 2,
printBar: false,
title: gettext('Device'),
iconCls: 'fa fa-fw fa-battery-three-quarters',
textField: 'upsc',
renderer: function(value) {
let objValue = {};
try {
// Parse the UPS data
if (typeof value === 'string') {
const lines = value.split('\n');
lines.forEach(line => {
const colonIndex = line.indexOf(':');
if (colonIndex > 0) {
const key = line.substring(0, colonIndex).trim();
const val = line.substring(colonIndex + 1).trim();
objValue[key] = val;
}
});
} else if (typeof value === 'object') {
objValue = value || {};
}
} catch(e) {
objValue = {};
}
// If objValue is null or empty, return N/A
if (!objValue || Object.keys(objValue).length === 0) {
return '<div style="text-align: right;"><span style="color: white;">N/A</span></div>';
}
// Helper function to get status color
function getStatusColor(status) {
if (!status) return '#999';
const statusUpper = status.toUpperCase();
if (statusUpper.includes('OL')) return 'white'; // White for online
if (statusUpper.includes('OB')) return '#d9534f'; // Red for on battery
if (statusUpper.includes('LB')) return '#d9534f'; // Red for low battery
return '#f0ad4e'; // Orange for other states
}
// Helper function to get load/charge color
function getPercentageColor(value, isLoad = false) {
if (!value || isNaN(value)) return '#999';
const num = parseFloat(value);
if (isLoad) {
if (num >= 80) return '#d9534f'; // Red for high load
if (num >= 60) return '#f0ad4e'; // Orange for medium load
return 'white'; // White for low load
} else {
// For battery charge
if (num <= 20) return '#d9534f'; // Red for low charge
if (num <= 50) return '#f0ad4e'; // Orange for medium charge
return 'white'; // White for good charge
}
}
// Helper function to format runtime
function formatRuntime(seconds) {
if (!seconds || isNaN(seconds)) return 'N/A';
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs}s`;
}
// Extract key UPS information
const batteryCharge = objValue['battery.charge'];
const batteryRuntime = objValue['battery.runtime'];
const inputVoltage = objValue['input.voltage'];
const upsLoad = objValue['ups.load'];
const upsStatus = objValue['ups.status'];
const upsModel = objValue['ups.model'] || objValue['device.model'];
const testResult = objValue['ups.test.result'];
const batteryChargeLow = objValue['battery.charge.low'];
const batteryRuntimeLow = objValue['battery.runtime.low'];
const upsRealPowerNominal = objValue['ups.realpower.nominal'];
const batteryMfrDate = objValue['battery.mfr.date'];
// Build the status display
let displayItems = [];
// First line: Model info
let modelLine = '';
if (upsModel) {
modelLine = `<span style="color: white;">${upsModel}</span>`;
} else {
modelLine = `<span style="color: white;">N/A</span>`;
}
displayItems.push(modelLine);
// Main status line with all metrics
let statusLine = '';
// Status
if (upsStatus) {
const statusUpper = upsStatus.toUpperCase();
let statusText = 'Unknown';
let statusColor = '#f0ad4e';
if (statusUpper.includes('OL')) {
statusText = 'Online';
statusColor = 'white'; // White for good status
} else if (statusUpper.includes('OB')) {
statusText = 'On Battery';
statusColor = '#d9534f'; // Red for on battery
} else if (statusUpper.includes('LB')) {
statusText = 'Low Battery';
statusColor = '#d9534f'; // Red for low battery
} else {
statusText = upsStatus;
statusColor = '#f0ad4e'; // Orange for unknown status
}
statusLine += `Status: <span style="color: ${statusColor};">${statusText}</span>`;
} else {
statusLine += `Status: <span style="color: white;">N/A</span>`;
}
// Battery charge
if (statusLine) statusLine += ' | ';
if (batteryCharge) {
const chargeColor = getPercentageColor(batteryCharge, false);
statusLine += `Battery: <span style="color: ${chargeColor};">${batteryCharge}%</span>`;
} else {
statusLine += `Battery: <span style="color: white;">N/A</span>`;
}
// Load percentage
if (statusLine) statusLine += ' | ';
if (upsLoad) {
const loadColor = getPercentageColor(upsLoad, true);
statusLine += `Load: <span style="color: ${loadColor};">${upsLoad}%</span>`;
} else {
statusLine += `Load: <span style="color: white;">N/A</span>`;
}
// Runtime
if (statusLine) statusLine += ' | ';
if (batteryRuntime) {
const runtime = parseInt(batteryRuntime);
const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
let runtimeColor = 'white';
if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f'; // Red if less than half of low threshold
else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e'; // Orange if at low threshold
statusLine += `Runtime: <span style="color: ${runtimeColor};">${formatRuntime(runtime)}</span>`;
} else {
statusLine += `Runtime: <span style="color: white;">N/A</span>`;
}
// Input voltage
if (statusLine) statusLine += ' | ';
if (inputVoltage) {
statusLine += `Input: <span style="color: white;">${parseFloat(inputVoltage).toFixed(0)}V</span>`;
} else {
statusLine += `Input: <span style="color: white;">N/A</span>`;
}
// Calculate actual watt usage
if (statusLine) statusLine += ' | ';
let actualWattage = null;
if (upsLoad && upsRealPowerNominal) {
const load = parseFloat(upsLoad);
const nominal = parseFloat(upsRealPowerNominal);
if (!isNaN(load) && !isNaN(nominal)) {
actualWattage = Math.round((load / 100) * nominal);
}
}
// Real power (calculated watt usage)
if (actualWattage !== null) {
statusLine += `Output: <span style="color: white;">${actualWattage}W</span>`;
} else {
statusLine += `Output: <span style="color: white;">N/A</span>`;
}
displayItems.push(statusLine);
// Combined battery and test line
let batteryTestLine = '';
if (batteryMfrDate) {
batteryTestLine += `<span style="color: white;">Battery MFD: ${batteryMfrDate}</span>`;
} else {
batteryTestLine += `<span style="color: white;">Battery MFD: N/A</span>`;
}
if (testResult && !testResult.toLowerCase().includes('no test')) {
const testColor = testResult.toLowerCase().includes('passed') ? 'white' : '#d9534f';
batteryTestLine += ` | <span style="color: ${testColor};">Test: ${testResult}</span>`;
} else {
batteryTestLine += ` | <span style="color: white;">Test: N/A</span>`;
}
displayItems.push(batteryTestLine);
// Format the final output
return '<div style="text-align: right;">' + displayItems.join('<br>') + '</div>';
}
},
EOF
#endregion UPS widget heredoc
if [[ $? -ne 0 ]]; then
echo "Error: Failed to generate UPS widget code" >&2
exit 1
fi
}
# Function to uninstall the modification
function uninstall_mod {
check_root_privileges

View File

@ -7,24 +7,35 @@ If you find this helpful, a small donation is appreciated, [![Donate](https://ww
(Tested compatibility: 9.x. Using older version (7.x-8.x), use git version from Apr 6th 2025)
![Promxox temp mod](https://github.com/Meliox/PVE-mods/blob/main/pve-mod-sensors.png?raw=true)
This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensor readings in a flexible and readable manner. Supported are CPU, NVMe/HDD/SSD temperatures (Celsius/Fahrenheit), fan speeds, ram temperatures, mixed CPU's core nodes setups, motherboard information or system information.
This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensor readings in a flexible and readable manner.
The following readings are possible:
- CPU, NVMe/HDD/SSD temperatures (Celsius/Fahrenheit), fan speeds, ram temperatures via lm-sensors. Note: Hdds require kernel module *drivetemp* module installed.
- UPS information via Network Monitoring Tool
- Motherboard information or system information via dmidecode
The modification includes three main steps:
1. Create backups of the original files located at `/usr/share/pve-manager/js/pvemanagerlib.js` and `/usr/share/perl5/PVE/API2/Nodes.pm` in users home directory/PVE_MODS.
2. Add a new code to the `Nodes.pm` file that enables host system sensor readings using the `sensors` command.
3. Modify the `pvemanagerlib.js` file to expand the space in the node status view, add new items that display the temperature information in Celsius for CPUs, NVMe drives, HDDs/SSDs and fan speeds (the actual item list depends on the sensor readings available during the installation). The view layout is also adjusted to no longer match the column number setting and always expands to the full width of the browser window. It is also possible to collapse the panel vertically.
4. Finally, the script also restarts the `pveproxy` service to apply the changes.
### How it works
The modification involves the following steps:
1. Backup original files in the home directory/backup
- `/usr/share/pve-manager/js/pvemanagerlib.js`
- `/usr/share/perl5/PVE/API2/Nodes.pm`
Backups are saved in a `backup/` directory alongside the script.
2. Patch `Nodes.pm` to enable readings.
3. Modify `pvemanagerlib.js` to:
- Expand the node status view to full browser width.
- Add reading (depending on & selections).
- Allow collapsing the panel vertically.
4. Restart the `pveproxy` service to apply changes.
The script provides three options:
| **Option** | **Description** |
|-------------------------|-----------------------------------------------------------------------------|
| `install` | Installs the modification by applying the necessary changes. |
| `uninstall` | Removes the modification by restoring the original files from backups. |
| `save-sensors-data` | Saves a local copy of your sensor data for reference or backup. |
| `install` | Apply the modification. |
| `uninstall` | Restore original files from backups. |
| `save-sensors-data` | Save a local copy of detected sensor data for reference or troubleshooting. |
Note:
For HDDs/SSDs readings to work, the kernel module *drivetemp* must be installed.
Notes:
- UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested.
- Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required.
### Install
Instructions be performed as 'root', as normal users do not have access to the files.