Add support for UPS information
This commit is contained in:
parent
56f717450c
commit
6b4d6c4489
@ -27,6 +27,7 @@ BACKUP_DIR=""
|
|||||||
|
|
||||||
DEBUG_REMOTE=false
|
DEBUG_REMOTE=false
|
||||||
DEBUG_JSON_FILE="/tmp/sensordata.json"
|
DEBUG_JSON_FILE="/tmp/sensordata.json"
|
||||||
|
DEBUG_UPS_FILE="/tmp/upsc.txt"
|
||||||
|
|
||||||
# This script's working directory
|
# This script's working directory
|
||||||
SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
|
SCRIPT_CWD="$(dirname "$(readlink -f "$0")")"
|
||||||
@ -240,6 +241,51 @@ function configure {
|
|||||||
esac
|
esac
|
||||||
fi
|
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:
|
# DMI Type:
|
||||||
# 1 ... System Information
|
# 1 ... System Information
|
||||||
# 2 ... Base Board Information (for self-made PC)
|
# 2 ... Base Board Information (for self-made PC)
|
||||||
@ -317,6 +363,24 @@ function install_mod {
|
|||||||
msg "Sensors' output added to \"$NODES_PM_FILE\"."
|
msg "Sensors' output added to \"$NODES_PM_FILE\"."
|
||||||
fi
|
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
|
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/ |$//')
|
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"
|
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
|
# 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
|
if [ $ENABLE_HDD_TEMP = true ]; then
|
||||||
sed -i "/^Ext.define('PVE.node.StatusView',/ {
|
sed -i "/^Ext.define('PVE.node.StatusView',/ {
|
||||||
:a;
|
:a;
|
||||||
@ -876,7 +956,9 @@ Ext.define('PVE.mod.TempHelper', {\n\
|
|||||||
# Add an empty line to separate modified items as a visual group
|
# 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
|
# NOTE: Check for the presence of items in the reverse order of display
|
||||||
local lastItemId=""
|
local lastItemId=""
|
||||||
if [ $ENABLE_HDD_TEMP = true ]; then
|
if [ $ENABLE_UPS = true ]; then
|
||||||
|
lastItemId="upsc"
|
||||||
|
elif [ $ENABLE_HDD_TEMP = true ]; then
|
||||||
lastItemId="thermalHdd"
|
lastItemId="thermalHdd"
|
||||||
elif [ $ENABLE_NVME_TEMP = true ]; then
|
elif [ $ENABLE_NVME_TEMP = true ]; then
|
||||||
lastItemId="thermalNvme"
|
lastItemId="thermalNvme"
|
||||||
@ -944,6 +1026,223 @@ Ext.define('PVE.mod.TempHelper', {\n\
|
|||||||
fi
|
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 to uninstall the modification
|
||||||
function uninstall_mod {
|
function uninstall_mod {
|
||||||
check_root_privileges
|
check_root_privileges
|
||||||
|
|||||||
35
readme.md
35
readme.md
@ -7,24 +7,35 @@ If you find this helpful, a small donation is appreciated, [, use git version from Apr 6th 2025)
|
(Tested compatibility: 9.x. Using older version (7.x-8.x), use git version from Apr 6th 2025)
|
||||||

|

|
||||||
|
|
||||||
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:
|
### How it works
|
||||||
|
The modification involves the following 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.
|
1. Backup original files in the home directory/backup
|
||||||
2. Add a new code to the `Nodes.pm` file that enables host system sensor readings using the `sensors` command.
|
- `/usr/share/pve-manager/js/pvemanagerlib.js`
|
||||||
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.
|
- `/usr/share/perl5/PVE/API2/Nodes.pm`
|
||||||
4. Finally, the script also restarts the `pveproxy` service to apply the changes.
|
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:
|
The script provides three options:
|
||||||
| **Option** | **Description** |
|
| **Option** | **Description** |
|
||||||
|-------------------------|-----------------------------------------------------------------------------|
|
|-------------------------|-----------------------------------------------------------------------------|
|
||||||
| `install` | Installs the modification by applying the necessary changes. |
|
| `install` | Apply the modification. |
|
||||||
| `uninstall` | Removes the modification by restoring the original files from backups. |
|
| `uninstall` | Restore original files from backups. |
|
||||||
| `save-sensors-data` | Saves a local copy of your sensor data for reference or backup. |
|
| `save-sensors-data` | Save a local copy of detected sensor data for reference or troubleshooting. |
|
||||||
|
|
||||||
Note:
|
Notes:
|
||||||
For HDDs/SSDs readings to work, the kernel module *drivetemp* must be installed.
|
- 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
|
### Install
|
||||||
Instructions be performed as 'root', as normal users do not have access to the files.
|
Instructions be performed as 'root', as normal users do not have access to the files.
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user