diff --git a/pve-mod-gui-sensors.sh b/pve-mod-gui-sensors.sh index 5eb3ecb..73a9345 100644 --- a/pve-mod-gui-sensors.sh +++ b/pve-mod-gui-sensors.sh @@ -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 '
N/A
'; + } + + // 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 = `${upsModel}`; + } else { + modelLine = `N/A`; + } + 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: ${statusText}`; + } else { + statusLine += `Status: N/A`; + } + + // Battery charge + if (statusLine) statusLine += ' | '; + if (batteryCharge) { + const chargeColor = getPercentageColor(batteryCharge, false); + statusLine += `Battery: ${batteryCharge}%`; + } else { + statusLine += `Battery: N/A`; + } + + // Load percentage + if (statusLine) statusLine += ' | '; + if (upsLoad) { + const loadColor = getPercentageColor(upsLoad, true); + statusLine += `Load: ${upsLoad}%`; + } else { + statusLine += `Load: N/A`; + } + + // 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: ${formatRuntime(runtime)}`; + } else { + statusLine += `Runtime: N/A`; + } + + // Input voltage + if (statusLine) statusLine += ' | '; + if (inputVoltage) { + statusLine += `Input: ${parseFloat(inputVoltage).toFixed(0)}V`; + } else { + statusLine += `Input: N/A`; + } + + // 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: ${actualWattage}W`; + } else { + statusLine += `Output: N/A`; + } + + displayItems.push(statusLine); + + // Combined battery and test line + let batteryTestLine = ''; + if (batteryMfrDate) { + batteryTestLine += `Battery MFD: ${batteryMfrDate}`; + } else { + batteryTestLine += `Battery MFD: N/A`; + } + + if (testResult && !testResult.toLowerCase().includes('no test')) { + const testColor = testResult.toLowerCase().includes('passed') ? 'white' : '#d9534f'; + batteryTestLine += ` | Test: ${testResult}`; + } else { + batteryTestLine += ` | Test: N/A`; + } + + displayItems.push(batteryTestLine); + + // Format the final output + return '
' + displayItems.join('
') + '
'; + } + }, +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 diff --git a/readme.md b/readme.md index 931ed31..29d59f7 100644 --- a/readme.md +++ b/readme.md @@ -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.