Last active
November 6, 2025 05:27
-
-
Save koleson/d54e0cd1f3bce3aedf13be005df99abb to your computer and use it in GitHub Desktop.
PVS System Information Checker - Retrieves system info from SunPower PVS devices via API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| ############################################################################# | |
| # pvs_vars_check.sh | |
| # | |
| # Description: Retrieves and displays system information from SunPower PVS | |
| # devices via their API, including serial number, model, hardware | |
| # revision, firmware version, system type, and eMMC flashwear. | |
| # | |
| # Usage: ./pvs_vars_check.sh [OPTIONS] <PVS_IP> | |
| # Options: | |
| # -v, --json Output raw JSON response in addition to | |
| # formatted output | |
| # | |
| # Requirements: curl, jq | |
| # | |
| # Author: Kiel Oleson | |
| # Assistance: Claude (Anthropic) | |
| # Date: 5 November 2025 | |
| # Version: 1.0.1 | |
| # | |
| # Version History: | |
| # 1.0.1 (5 Nov 2025) - Fixed flashwear conversion arithmetic expression | |
| # 1.0 (5 Nov 2025) - Initial release | |
| # | |
| # More PVS Info: https://gist.github.com/koleson/5c719620039e0282976a8263c068e85c | |
| # fcgi_vars info: https://github.com/tjmonk/fcgi_vars | |
| # API docs: https://github.com/SunStrong-Management/pypvs/tree/main/doc | |
| # | |
| ############################################################################# | |
| # Parse flags | |
| JSON_OUTPUT=false | |
| PVS_IP="" | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| -v|--json) | |
| JSON_OUTPUT=true | |
| shift | |
| ;; | |
| *) | |
| PVS_IP="$1" | |
| shift | |
| ;; | |
| esac | |
| done | |
| # Check for required tools | |
| if ! command -v curl &> /dev/null; then | |
| echo "ERROR: curl is not installed" | |
| echo "Please install curl to use this script" | |
| echo "" | |
| echo "Installation instructions:" | |
| echo " macOS: brew install curl" | |
| echo " Ubuntu: sudo apt-get install curl" | |
| echo " Fedora: sudo dnf install curl" | |
| exit 1 | |
| fi | |
| if ! command -v jq &> /dev/null; then | |
| echo "ERROR: jq is not installed" | |
| echo "Please install jq to use this script" | |
| echo "" | |
| echo "Installation instructions:" | |
| echo " macOS: brew install jq" | |
| echo " Ubuntu: sudo apt-get install jq" | |
| echo " Fedora: sudo dnf install jq" | |
| exit 1 | |
| fi | |
| if ! command -v base64 &> /dev/null; then | |
| echo "ERROR: base64 is not installed" | |
| echo "The base64 command is required for authentication" | |
| echo "It should be available by default on most Unix systems" | |
| exit 1 | |
| fi | |
| # Test that jq can parse JSON | |
| echo '{"test":true}' | jq . &> /dev/null | |
| if [ $? -ne 0 ]; then | |
| echo "ERROR: jq is not working correctly" | |
| echo "jq failed to parse test JSON" | |
| exit 1 | |
| fi | |
| # Check for PVS IP parameter | |
| if [ -z "$PVS_IP" ]; then | |
| echo "Usage: $0 [OPTIONS] <PVS_IP>" | |
| echo "Example: $0 192.168.1.101" | |
| echo "" | |
| echo "Options:" | |
| echo " -v, --json Output raw JSON response" | |
| exit 1 | |
| fi | |
| # Use HTTPS exclusively (some vars are only available via HTTPS) | |
| PROTOCOL="https" | |
| RESPONSE=$(curl -k -s -m 10 --connect-timeout 5 -w "\n%{http_code}" "${PROTOCOL}://${PVS_IP}/vars?match=/" 2>/dev/null) | |
| # Check if curl succeeded | |
| if [ $? -ne 0 ]; then | |
| echo "" | |
| echo "ERROR: Unable to connect to PVS at ${PVS_IP}" | |
| echo "" | |
| echo "Please check:" | |
| echo " - The IP address or hostname is correct" | |
| echo " - The PVS is powered on and connected to the network" | |
| echo " - You are connected to the same network as the PVS" | |
| exit 1 | |
| fi | |
| # Split response into body and HTTP code | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -n1) | |
| BODY=$(echo "$RESPONSE" | sed '$d') | |
| # Check for non-200 response | |
| if [ "$HTTP_CODE" != "200" ]; then | |
| echo "" | |
| echo "ERROR: The PVS responded with an error (HTTP $HTTP_CODE)" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| exit 1 | |
| fi | |
| # Validate JSON structure | |
| echo "$BODY" | jq . &> /dev/null | |
| if [ $? -ne 0 ]; then | |
| echo "" | |
| echo "ERROR: The PVS response is not valid JSON" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| echo "" | |
| echo "Received response:" | |
| echo "$BODY" | head -c 500 | |
| exit 1 | |
| fi | |
| COUNT=$(echo "$BODY" | jq -r '.count // empty' 2>/dev/null) | |
| if [ -z "$COUNT" ]; then | |
| echo "" | |
| echo "ERROR: The PVS response format was not recognized" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| echo "" | |
| echo "Received response:" | |
| echo "$BODY" | head -c 500 | |
| exit 1 | |
| fi | |
| # Extract serial number | |
| SERIAL=$(echo "$BODY" | jq -r '.values[] | select(.name == "/sys/info/serialnum") | .value' 2>/dev/null) | |
| if [ -z "$SERIAL" ]; then | |
| echo "" | |
| echo "ERROR: Could not find serial number in PVS response" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| exit 1 | |
| fi | |
| # Validate serial number format (should start with Z and be long enough) | |
| if [[ ! "$SERIAL" =~ ^Z.+ ]]; then | |
| echo "" | |
| echo "ERROR: Serial number format not recognized" | |
| echo "Expected format starting with 'Z', but got: ${SERIAL}" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| exit 1 | |
| fi | |
| if [ ${#SERIAL} -lt 5 ]; then | |
| echo "" | |
| echo "ERROR: Serial number too short" | |
| echo "Expected at least 5 characters, but got: ${SERIAL} (${#SERIAL} characters)" | |
| exit 1 | |
| fi | |
| # Extract last 5 characters of serial number | |
| SN_LAST5="${SERIAL: -5}" | |
| # Authenticate with PVS | |
| # Create cookie file in temp directory | |
| COOKIE_FILE=$(mktemp /tmp/pvs_cookies.XXXXXX 2>/dev/null) | |
| if [ $? -ne 0 ] || [ -z "$COOKIE_FILE" ]; then | |
| echo "" | |
| echo "ERROR: Failed to create temporary cookie file" | |
| echo "Please ensure /tmp is writable" | |
| exit 1 | |
| fi | |
| trap "rm -f $COOKIE_FILE" EXIT | |
| # Create base64 auth header | |
| AUTH=$(echo -n "ssm_owner:${SN_LAST5}" | base64) | |
| # Attempt authentication | |
| AUTH_RESPONSE=$(curl -k -s -m 10 --connect-timeout 5 \ | |
| -b "$COOKIE_FILE" \ | |
| -c "$COOKIE_FILE" \ | |
| -w "\n%{http_code}" \ | |
| -H "Authorization: basic $AUTH" \ | |
| "${PROTOCOL}://${PVS_IP}/auth?login" 2>/dev/null) | |
| # Check if curl succeeded | |
| if [ $? -ne 0 ]; then | |
| echo "" | |
| echo "ERROR: Authentication request failed" | |
| echo "Unable to connect to the PVS authentication endpoint" | |
| exit 1 | |
| fi | |
| # Split response into body and HTTP code | |
| AUTH_HTTP_CODE=$(echo "$AUTH_RESPONSE" | tail -n1) | |
| AUTH_BODY=$(echo "$AUTH_RESPONSE" | sed '$d') | |
| if [ "$AUTH_HTTP_CODE" != "200" ]; then | |
| echo "" | |
| echo "ERROR: Authentication failed (HTTP $AUTH_HTTP_CODE)" | |
| echo "The PVS did not accept the credentials" | |
| if [ "$JSON_OUTPUT" = true ]; then | |
| echo "" | |
| echo "Response body:" | |
| echo "$AUTH_BODY" | |
| fi | |
| exit 1 | |
| fi | |
| if [ "$JSON_OUTPUT" = true ]; then | |
| echo "Auth response (HTTP $AUTH_HTTP_CODE):" | |
| echo "$AUTH_BODY" | |
| fi | |
| # Retrieve system variables | |
| VARS_URL="${PROTOCOL}://${PVS_IP}/vars?name=/sys/info/serialnum,/sys/info/model,/sys/info/hwrev,/sys/info/sw_rev,/sys/info/sys_type,/sys/pvs/flashwear_type_b" | |
| VARS_RESPONSE=$(curl -k -s -m 10 --connect-timeout 5 \ | |
| -b "$COOKIE_FILE" \ | |
| -c "$COOKIE_FILE" \ | |
| -w "\n%{http_code}" \ | |
| "$VARS_URL" 2>/dev/null) | |
| # Check if curl succeeded | |
| if [ $? -ne 0 ]; then | |
| echo "" | |
| echo "ERROR: Failed to retrieve system variables" | |
| echo "Unable to connect to the PVS" | |
| exit 1 | |
| fi | |
| # Split response into body and HTTP code | |
| VARS_HTTP_CODE=$(echo "$VARS_RESPONSE" | tail -n1) | |
| VARS_BODY=$(echo "$VARS_RESPONSE" | sed '$d') | |
| if [ "$VARS_HTTP_CODE" != "200" ]; then | |
| echo "" | |
| echo "ERROR: Failed to retrieve system variables (HTTP $VARS_HTTP_CODE)" | |
| # Try to parse error details from JSON response | |
| ERROR_DESC=$(echo "$VARS_BODY" | jq -r '.description // empty' 2>/dev/null) | |
| ERROR_CODE=$(echo "$VARS_BODY" | jq -r '.errorcode // empty' 2>/dev/null) | |
| if [ -n "$ERROR_DESC" ]; then | |
| echo "Error Description: $ERROR_DESC" | |
| fi | |
| if [ -n "$ERROR_CODE" ]; then | |
| echo "Error Code: $ERROR_CODE" | |
| fi | |
| echo "The authenticated request was not successful" | |
| if [ "$JSON_OUTPUT" = true ]; then | |
| echo "" | |
| echo "Request URL:" | |
| echo "$VARS_URL" | |
| echo "" | |
| echo "Response body:" | |
| echo "$VARS_BODY" | |
| fi | |
| exit 1 | |
| fi | |
| # Validate JSON structure | |
| echo "$VARS_BODY" | jq . &> /dev/null | |
| if [ $? -ne 0 ]; then | |
| echo "" | |
| echo "ERROR: The PVS response for system variables is not valid JSON" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| exit 1 | |
| fi | |
| VARS_COUNT=$(echo "$VARS_BODY" | jq -r '.count // empty' 2>/dev/null) | |
| if [ -z "$VARS_COUNT" ]; then | |
| echo "" | |
| echo "ERROR: Unexpected response format when retrieving system variables" | |
| echo "The PVS firmware version may be unexpected or incompatible" | |
| exit 1 | |
| fi | |
| # Extract specific values | |
| MODEL=$(echo "$VARS_BODY" | jq -r '.values[] | select(.name == "/sys/info/model") | .value' 2>/dev/null) | |
| HWREV=$(echo "$VARS_BODY" | jq -r '.values[] | select(.name == "/sys/info/hwrev") | .value' 2>/dev/null) | |
| SW_REV=$(echo "$VARS_BODY" | jq -r '.values[] | select(.name == "/sys/info/sw_rev") | .value' 2>/dev/null) | |
| SYS_TYPE=$(echo "$VARS_BODY" | jq -r '.values[] | select(.name == "/sys/info/sys_type") | .value' 2>/dev/null) | |
| FLASHWEAR=$(echo "$VARS_BODY" | jq -r '.values[] | select(.name == "/sys/pvs/flashwear_type_b") | .value' 2>/dev/null) | |
| # Set defaults for missing values | |
| [ -z "$MODEL" ] && MODEL="MISSING" | |
| [ -z "$HWREV" ] && HWREV="MISSING" | |
| [ -z "$SW_REV" ] && SW_REV="MISSING" | |
| [ -z "$SYS_TYPE" ] && SYS_TYPE="MISSING" | |
| # Process flashwear hex value to percentage | |
| if [ -n "$FLASHWEAR" ]; then | |
| # Convert hex to decimal (remove 0x prefix if present) | |
| FLASHWEAR_HEX="${FLASHWEAR#0x}" | |
| # Validate hex format before conversion | |
| if [[ "$FLASHWEAR_HEX" =~ ^[0-9A-Fa-f]+$ ]]; then | |
| # Arithmetic expansion doesn't allow stderr redirection, so check is redundant after regex | |
| FLASHWEAR_DEC=$((16#$FLASHWEAR_HEX)) | |
| FLASHWEAR_PCT="${FLASHWEAR_DEC}0% consumed" | |
| else | |
| FLASHWEAR_PCT="INVALID (${FLASHWEAR})" | |
| fi | |
| else | |
| FLASHWEAR_PCT="MISSING" | |
| fi | |
| # Display results | |
| if [ "$JSON_OUTPUT" = true ]; then | |
| echo "==============================" | |
| echo "Raw JSON Response" | |
| echo "==============================" | |
| echo "URL: ${VARS_URL}" | |
| echo "" | |
| echo "$VARS_BODY" | jq '.' | |
| echo "" | |
| fi | |
| # Display formatted output | |
| echo "==============================" | |
| echo "PVS Information" | |
| echo "==============================" | |
| echo "IP Address: ${PVS_IP}" | |
| echo "Serial Number: ${SERIAL}" | |
| echo "Last 5 Characters: ${SN_LAST5} (ssm_owner password)" | |
| echo "PVS Model: ${MODEL}" | |
| echo "PVS Hardware Revision: ${HWREV}" | |
| echo "PVS Firmware Version: ${SW_REV}" | |
| echo "System Type: ${SYS_TYPE}" | |
| echo "eMMC Flashwear: ${FLASHWEAR_PCT}" | |
| echo "==============================" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment