Security: - Remove -k/--api-key CLI option (prevents process list exposure) - API key now only accepted via NREL_API_KEY environment variable Features: - Add API timeout (30s) and retry logic with exponential backoff - Add rate limit detection and graceful test skipping Documentation: - Add AGPLv3 LICENSE file - Add CONTRIBUTING.md with development guidelines - Add CHANGELOG.md following Keep a Changelog format - Add copyright headers to all source files Tests: - Expand test suite from 19 to 52 tests - Add edge case tests (negative values, boundaries) - Add input validation tests - Add financial calculation verification tests - Add rate limit handling to skip tests gracefully - Remove skip-on-failure logic - tests now properly fail All 52 tests pass (19 skipped when API rate limited). 🤖 Generated with [Crush](https://crush.cli.software) Assisted-by: GLM-5 via Crush <crush@charm.land>
704 lines
22 KiB
Bash
Executable File
704 lines
22 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
#
|
||
# SITER Solar Analysis - NREL PVWatts Solar Production Estimator
|
||
# Copyright (C) 2026 Known Element
|
||
#
|
||
# This program is free software: you can redistribute it and/or modify
|
||
# it under the terms of the GNU Affero General Public License as published
|
||
# by the Free Software Foundation, either version 3 of the License, or
|
||
# (at your option) any later version.
|
||
#
|
||
# This program is distributed in the hope that it will be useful,
|
||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
# GNU Affero General Public License for more details.
|
||
#
|
||
# You should have received a copy of the GNU Affero General Public License
|
||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||
# shellcheck source=/dev/null
|
||
#
|
||
# siter-solar-analysis - NREL PVWatts Solar Production Estimator
|
||
#
|
||
# Usage:
|
||
# ./siter-solar-analysis.sh # Basic analysis
|
||
# ./siter-solar-analysis.sh --scenarios # Compare system sizes
|
||
# ./siter-solar-analysis.sh --help # Show help
|
||
#
|
||
# Environment variables:
|
||
# NREL_API_KEY - Your NREL API key (default: DEMO_KEY)
|
||
# SITER_LAT - Site latitude (default: 30.44)
|
||
# SITER_LON - Site longitude (default: -97.62)
|
||
#
|
||
# Get your free API key at: https://developer.nrel.gov/signup/
|
||
#
|
||
set -euo pipefail
|
||
|
||
# Version
|
||
readonly VERSION="1.0.0"
|
||
|
||
# Default configuration
|
||
readonly DEFAULT_API_KEY="DEMO_KEY"
|
||
readonly DEFAULT_LAT="30.44"
|
||
readonly DEFAULT_LON="-97.62"
|
||
readonly DEFAULT_PANELS="16"
|
||
readonly DEFAULT_PANEL_WATTS="250"
|
||
readonly DEFAULT_TILT="30"
|
||
readonly DEFAULT_AZIMUTH="180"
|
||
readonly DEFAULT_LOSSES="14"
|
||
readonly DEFAULT_ARRAY_TYPE="0"
|
||
|
||
# Financial parameters
|
||
readonly PROJECT_COST="4100"
|
||
readonly CURRENT_MONTHLY_BILL="301.08"
|
||
readonly CURRENT_ANNUAL_CONSUMPTION="23952"
|
||
readonly CONSUMPTION_RATE="0.085"
|
||
readonly EXPORT_RATE="0.04"
|
||
readonly SELF_CONSUMPTION_PCT="0.60"
|
||
|
||
# API endpoint
|
||
readonly API_URL="https://developer.nrel.gov/api/pvwatts/v6.json"
|
||
# API configuration
|
||
readonly API_TIMEOUT="30"
|
||
readonly API_MAX_RETRIES="3"
|
||
readonly API_RETRY_DELAY="2"
|
||
|
||
# Colors for output
|
||
readonly RED='\033[0;31m'
|
||
readonly GREEN='\033[0;32m'
|
||
readonly YELLOW='\033[0;33m'
|
||
readonly NC='\033[0m' # No Color
|
||
|
||
# Output control
|
||
VERBOSE="${VERBOSE:-false}"
|
||
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
|
||
|
||
#######################################
|
||
# Print error message and exit
|
||
# Arguments:
|
||
# $1 - Error message
|
||
# Outputs:
|
||
# Writes error to stderr
|
||
#######################################
|
||
error() {
|
||
echo -e "${RED}ERROR: ${1}${NC}" >&2
|
||
exit 1
|
||
}
|
||
|
||
#######################################
|
||
# Print warning message
|
||
# Arguments:
|
||
# $1 - Warning message
|
||
#######################################
|
||
warn() {
|
||
echo -e "${YELLOW}WARN: ${1}${NC}" >&2
|
||
}
|
||
|
||
#######################################
|
||
# Print info message
|
||
# Arguments:
|
||
# $1 - Info message
|
||
#######################################
|
||
info() {
|
||
if [[ "$VERBOSE" == "true" ]]; then
|
||
echo -e "${GREEN}INFO: ${1}${NC}" >&2
|
||
fi
|
||
}
|
||
|
||
#######################################
|
||
# Print usage information
|
||
#######################################
|
||
usage() {
|
||
cat << EOF
|
||
SITER Solar Analysis - NREL PVWatts Solar Production Estimator v${VERSION}
|
||
|
||
USAGE:
|
||
$(basename "$0") [OPTIONS]
|
||
|
||
OPTIONS:
|
||
--lat VALUE Site latitude (default: ${DEFAULT_LAT})
|
||
--lon VALUE Site longitude (default: ${DEFAULT_LON})
|
||
-p, --panels NUM Number of panels (default: ${DEFAULT_PANELS})
|
||
-w, --watts WATTS Watts per panel (default: ${DEFAULT_PANEL_WATTS})
|
||
-t, --tilt DEGREES Array tilt in degrees (default: ${DEFAULT_TILT})
|
||
-a, --azimuth DEGREES Array azimuth (default: ${DEFAULT_AZIMUTH})
|
||
-l, --losses PERCENT System losses percent (default: ${DEFAULT_LOSSES})
|
||
--scenarios Run multiple system size scenarios
|
||
--json Output as JSON
|
||
--verbose Enable verbose output
|
||
-h, --help Show this help message
|
||
-v, --version Show version
|
||
|
||
EXAMPLES:
|
||
# Basic analysis with defaults
|
||
$(basename "$0")
|
||
|
||
# Compare different system sizes
|
||
$(basename "$0") --scenarios
|
||
|
||
# Custom configuration
|
||
$(basename "$0") -p 20 -w 400 --lat 30.5 --lon -97.7
|
||
|
||
ENVIRONMENT VARIABLES:
|
||
NREL_API_KEY Your NREL API key
|
||
SITER_LAT Site latitude
|
||
SITER_LON Site longitude
|
||
|
||
Get your free API key at: https://developer.nrel.gov/signup/
|
||
EOF
|
||
}
|
||
|
||
#######################################
|
||
# Check required dependencies
|
||
# Globals:
|
||
# None
|
||
# Arguments:
|
||
# None
|
||
# Outputs:
|
||
# Error if dependencies missing
|
||
#######################################
|
||
check_dependencies() {
|
||
local missing=()
|
||
|
||
command -v curl >/dev/null 2>&1 || missing+=("curl")
|
||
command -v jq >/dev/null 2>&1 || missing+=("jq")
|
||
command -v bc >/dev/null 2>&1 || missing+=("bc")
|
||
|
||
if [[ ${#missing[@]} -gt 0 ]]; then
|
||
error "Missing required dependencies: ${missing[*]}\nInstall with: apt-get install ${missing[*]}"
|
||
fi
|
||
}
|
||
|
||
#######################################
|
||
# Validate numeric value
|
||
# Arguments:
|
||
# $1 - Value to validate
|
||
# $2 - Parameter name for error message
|
||
# Returns:
|
||
# 0 if valid, 1 if invalid
|
||
#######################################
|
||
validate_numeric() {
|
||
local value="$1"
|
||
local name="$2"
|
||
|
||
if ! [[ "$value" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
||
error "Invalid ${name}: '${value}' must be a number"
|
||
fi
|
||
}
|
||
|
||
#######################################
|
||
# Validate range
|
||
# Arguments:
|
||
# $1 - Value to validate
|
||
# $2 - Minimum
|
||
# $3 - Maximum
|
||
# $4 - Parameter name
|
||
# Returns:
|
||
# 0 if valid, exits with error if invalid
|
||
#######################################
|
||
validate_range() {
|
||
local value="$1"
|
||
local min="$2"
|
||
local max="$3"
|
||
local name="$4"
|
||
|
||
validate_numeric "$value" "$name"
|
||
|
||
if (( $(echo "$value < $min" | bc -l) )) || (( $(echo "$value > $max" | bc -l) )); then
|
||
error "Invalid ${name}: ${value} (must be between ${min} and ${max})"
|
||
fi
|
||
}
|
||
|
||
#######################################
|
||
# Query NREL PVWatts API
|
||
# Globals:
|
||
# API_URL
|
||
# Arguments:
|
||
# $1 - API key
|
||
# $2 - System capacity (kW)
|
||
# $3 - Latitude
|
||
# $4 - Longitude
|
||
# $5 - Tilt
|
||
# $6 - Azimuth
|
||
# $7 - Losses
|
||
# Outputs:
|
||
# JSON response from API
|
||
#######################################
|
||
query_nrel_api() {
|
||
local api_key="$1"
|
||
local capacity="$2"
|
||
local lat="$3"
|
||
local lon="$4"
|
||
local tilt="$5"
|
||
local azimuth="$6"
|
||
local losses="$7"
|
||
local array_type="${8:-$DEFAULT_ARRAY_TYPE}"
|
||
|
||
local url="${API_URL}?api_key=${api_key}&lat=${lat}&lon=${lon}"
|
||
url+="&system_capacity=${capacity}&array_type=${array_type}"
|
||
url+="&tilt=${tilt}&azimuth=${azimuth}&module_type=0"
|
||
url+="&losses=${losses}&timeframe=monthly"
|
||
|
||
info "Querying NREL API: capacity=${capacity}kW"
|
||
|
||
local response
|
||
local http_code
|
||
local retry_count=0
|
||
local delay=$API_RETRY_DELAY
|
||
|
||
while [[ $retry_count -lt $API_MAX_RETRIES ]]; do
|
||
# shellcheck disable=SC2086
|
||
response=$(curl -s -S --max-time "$API_TIMEOUT" -w "\n%{http_code}" ${url} 2>&1)
|
||
http_code=$(echo "$response" | tail -n1)
|
||
response=$(echo "$response" | sed '$d')
|
||
|
||
# Check for timeout or network errors
|
||
if [[ "$http_code" == "000" ]] || [[ "$response" == *"timed out"* ]] || [[ "$response" == *"connection"* ]]; then
|
||
retry_count=$((retry_count + 1))
|
||
if [[ $retry_count -lt $API_MAX_RETRIES ]]; then
|
||
warn "API request failed (attempt $retry_count/$API_MAX_RETRIES), retrying in ${delay}s..."
|
||
sleep "$delay"
|
||
delay=$((delay * 2)) # Exponential backoff
|
||
continue
|
||
fi
|
||
error "API request failed after $API_MAX_RETRIES attempts: timeout or connection error"
|
||
fi
|
||
|
||
# Success - break out of retry loop
|
||
break
|
||
done
|
||
|
||
if [[ "$http_code" != "200" ]]; then
|
||
error "API request failed with HTTP ${http_code}: ${response}"
|
||
fi
|
||
|
||
# Check for API errors
|
||
local api_errors
|
||
api_errors=$(echo "$response" | jq -r '.errors // [] | if length > 0 then .[] else empty end')
|
||
if [[ -n "$api_errors" ]]; then
|
||
error "API returned errors: ${api_errors}"
|
||
fi
|
||
|
||
echo "$response"
|
||
}
|
||
|
||
#######################################
|
||
# Calculate financial projections
|
||
# Arguments:
|
||
# $1 - Annual production (kWh)
|
||
# Outputs:
|
||
# Financial metrics
|
||
#######################################
|
||
calculate_financials() {
|
||
local annual_kwh="$1"
|
||
|
||
local self_consumed
|
||
local exported
|
||
local self_consumption_value
|
||
local export_value
|
||
local total_annual_value
|
||
local monthly_savings
|
||
local offset_pct
|
||
local payback_months
|
||
local payback_years
|
||
|
||
self_consumed=$(echo "$annual_kwh * $SELF_CONSUMPTION_PCT" | bc -l)
|
||
exported=$(echo "$annual_kwh * (1 - $SELF_CONSUMPTION_PCT)" | bc -l)
|
||
self_consumption_value=$(echo "$self_consumed * $CONSUMPTION_RATE" | bc -l)
|
||
export_value=$(echo "$exported * $EXPORT_RATE" | bc -l)
|
||
total_annual_value=$(echo "$self_consumption_value + $export_value" | bc -l)
|
||
monthly_savings=$(echo "$total_annual_value / 12" | bc -l)
|
||
offset_pct=$(echo "$monthly_savings / $CURRENT_MONTHLY_BILL * 100" | bc -l)
|
||
|
||
if (( $(echo "$monthly_savings > 0" | bc -l) )); then
|
||
payback_months=$(echo "$PROJECT_COST / $monthly_savings" | bc -l)
|
||
else
|
||
payback_months="999999"
|
||
fi
|
||
payback_years=$(echo "$payback_months / 12" | bc -l)
|
||
|
||
cat << EOF
|
||
{
|
||
"annual_kwh": $(printf "%.0f" "$annual_kwh"),
|
||
"monthly_avg_kwh": $(printf "%.0f" "$(echo "$annual_kwh / 12" | bc -l)"),
|
||
"self_consumed_kwh": $(printf "%.0f" "$self_consumed"),
|
||
"exported_kwh": $(printf "%.0f" "$exported"),
|
||
"self_consumption_value": $(printf "%.2f" "$self_consumption_value"),
|
||
"export_value": $(printf "%.2f" "$export_value"),
|
||
"total_annual_value": $(printf "%.2f" "$total_annual_value"),
|
||
"monthly_savings": $(printf "%.2f" "$monthly_savings"),
|
||
"offset_pct": $(printf "%.1f" "$offset_pct"),
|
||
"payback_months": $(printf "%.1f" "$payback_months"),
|
||
"payback_years": $(printf "%.1f" "$payback_years")
|
||
}
|
||
EOF
|
||
}
|
||
|
||
#######################################
|
||
# Format and display analysis results
|
||
# Arguments:
|
||
# $1 - API response JSON
|
||
# $2 - System capacity (kW)
|
||
# $3 - Panel watts
|
||
# $4 - Number of panels
|
||
# $5 - Latitude
|
||
# $6 - Longitude
|
||
# Outputs:
|
||
# Formatted report
|
||
#######################################
|
||
format_output() {
|
||
local data="$1"
|
||
local capacity="$2"
|
||
local panel_watts="$3"
|
||
local num_panels="$4"
|
||
local lat="$5"
|
||
local lon="$6"
|
||
|
||
local annual_kwh
|
||
local capacity_factor
|
||
local solrad_annual
|
||
local station_city
|
||
local station_state
|
||
|
||
annual_kwh=$(echo "$data" | jq -r '.outputs.ac_annual')
|
||
capacity_factor=$(echo "$data" | jq -r '.outputs.capacity_factor')
|
||
solrad_annual=$(echo "$data" | jq -r '.outputs.solrad_annual')
|
||
station_city=$(echo "$data" | jq -r '.station_info.city // "N/A"')
|
||
station_state=$(echo "$data" | jq -r '.station_info.state // "Texas"')
|
||
|
||
local financials
|
||
financials=$(calculate_financials "$annual_kwh")
|
||
|
||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||
echo "$data" | jq --argjson fin "$financials" \
|
||
--arg capacity "$capacity" \
|
||
--arg panel_watts "$panel_watts" \
|
||
--arg num_panels "$num_panels" \
|
||
'{
|
||
system: {
|
||
capacity_kw: ($capacity | tonumber),
|
||
panels: ($num_panels | tonumber),
|
||
panel_watts: ($panel_watts | tonumber)
|
||
},
|
||
production: {
|
||
annual_kwh: .outputs.ac_annual,
|
||
monthly_kwh: .outputs.ac_monthly,
|
||
capacity_factor: .outputs.capacity_factor,
|
||
solrad_annual: .outputs.solrad_annual
|
||
},
|
||
financials: $fin
|
||
}'
|
||
return
|
||
fi
|
||
|
||
# Text output
|
||
echo "======================================================================"
|
||
echo "SITER SOLAR PROJECT - NREL PVWATTS ANALYSIS"
|
||
echo "======================================================================"
|
||
echo ""
|
||
echo "Location: SITER"
|
||
echo " Coordinates: ${lat}, ${lon}"
|
||
echo " Station: ${station_city}, ${station_state}"
|
||
echo ""
|
||
echo "System Configuration:"
|
||
echo " Panels: ${num_panels} × ${panel_watts}W = ${capacity} kW DC"
|
||
echo " Array Type: Fixed Open Rack (Ground Mount)"
|
||
echo " Orientation: ${DEFAULT_TILT}° tilt, ${DEFAULT_AZIMUTH}° azimuth (South-facing)"
|
||
echo " System Losses: ${DEFAULT_LOSSES}%"
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo "MONTHLY PRODUCTION ESTIMATE (NREL PVWatts)"
|
||
echo "======================================================================"
|
||
|
||
local months=("Jan" "Feb" "Mar" "Apr" "May" "Jun" "Jul" "Aug" "Sep" "Oct" "Nov" "Dec")
|
||
local ac_monthly
|
||
ac_monthly=$(echo "$data" | jq -r '.outputs.ac_monthly[]')
|
||
local solrad_monthly
|
||
solrad_monthly=$(echo "$data" | jq -r '.outputs.solrad_monthly[]')
|
||
|
||
printf "\n%-6s %-14s %-12s %-15s\n" "Month" "AC Output" "Daily Avg" "Solar Rad"
|
||
printf "%-6s %-14s %-12s %-15s\n" "" "(kWh)" "(kWh/day)" "(kWh/m²/day)"
|
||
echo "--------------------------------------------------"
|
||
|
||
local i=0
|
||
local total_kwh=0
|
||
for month in "${months[@]}"; do
|
||
local ac
|
||
ac=$(echo "$ac_monthly" | sed -n "$((i+1))p")
|
||
local solrad
|
||
solrad=$(echo "$solrad_monthly" | sed -n "$((i+1))p")
|
||
local daily_avg
|
||
daily_avg=$(echo "$ac / 30.5" | bc -l)
|
||
printf "%-6s %10.0f %10.0f %14.2f\n" "$month" "$ac" "$daily_avg" "$solrad"
|
||
total_kwh=$(echo "$total_kwh + $ac" | bc -l)
|
||
((i++))
|
||
done
|
||
|
||
echo "--------------------------------------------------"
|
||
printf "%-6s %10.0f %10.0f\n" "ANNUAL" "$annual_kwh" "$(echo "$annual_kwh / 12" | bc -l)"
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo "ANNUAL PERFORMANCE SUMMARY"
|
||
echo "======================================================================"
|
||
printf " Annual AC Output: %'.0f kWh\n" "$annual_kwh"
|
||
printf " Monthly Average: %'.0f kWh\n" "$(echo "$annual_kwh / 12" | bc -l)"
|
||
printf " Daily Average: %'.0f kWh\n" "$(echo "$annual_kwh / 365" | bc -l)"
|
||
echo " Capacity Factor: ${capacity_factor}%"
|
||
echo " Avg Solar Radiation: ${solrad_annual} kWh/m²/day"
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo "FINANCIAL ANALYSIS"
|
||
echo "======================================================================"
|
||
|
||
local self_consumed_kwh
|
||
local exported_kwh
|
||
local total_annual_value
|
||
local monthly_savings
|
||
local offset_pct
|
||
local payback_years
|
||
|
||
self_consumed_kwh=$(echo "$financials" | jq -r '.self_consumed_kwh')
|
||
exported_kwh=$(echo "$financials" | jq -r '.exported_kwh')
|
||
total_annual_value=$(echo "$financials" | jq -r '.total_annual_value')
|
||
monthly_savings=$(echo "$financials" | jq -r '.monthly_savings')
|
||
offset_pct=$(echo "$financials" | jq -r '.offset_pct')
|
||
payback_years=$(echo "$financials" | jq -r '.payback_years')
|
||
|
||
echo ""
|
||
echo " Current Consumption: ${CURRENT_ANNUAL_CONSUMPTION} kWh/year"
|
||
printf " Solar Production: %'.0f kWh/year\n" "$annual_kwh"
|
||
printf " Self-Sufficiency: %.1f%%\n" "$(echo "$annual_kwh / $CURRENT_ANNUAL_CONSUMPTION * 100" | bc -l)"
|
||
|
||
echo ""
|
||
printf " Self-Consumed (60%%): %'.0f kWh\n" "$self_consumed_kwh"
|
||
printf " Value @ \$%s/kWh: \$%'.2f/year\n" "$CONSUMPTION_RATE" "$(echo "$financials" | jq -r '.self_consumption_value')"
|
||
|
||
echo ""
|
||
printf " Exported to Grid (40%%): %'.0f kWh\n" "$exported_kwh"
|
||
printf " Value @ \$%s/kWh: \$%'.2f/year\n" "$EXPORT_RATE" "$(echo "$financials" | jq -r '.export_value')"
|
||
|
||
echo ""
|
||
printf " TOTAL ANNUAL VALUE: \$%'.2f\n" "$total_annual_value"
|
||
printf " MONTHLY SAVINGS: \$%'.2f\n" "$monthly_savings"
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
echo "ROI ANALYSIS"
|
||
echo "======================================================================"
|
||
echo ""
|
||
printf " Project Cost: \$%'.2f\n" "$PROJECT_COST"
|
||
printf " Current Monthly Bill: \$%'.2f\n" "$CURRENT_MONTHLY_BILL"
|
||
printf " Projected Monthly Savings: \$%'.2f\n" "$monthly_savings"
|
||
printf " Bill Offset: %.1f%%\n" "$offset_pct"
|
||
printf "\n PAYBACK PERIOD: %.0f months (%.1f years)\n" "$(echo "$financials" | jq -r '.payback_months')" "$payback_years"
|
||
|
||
echo ""
|
||
echo "======================================================================"
|
||
}
|
||
|
||
#######################################
|
||
# Run multiple system size scenarios
|
||
# Arguments:
|
||
# $1 - API key
|
||
# $2 - Latitude
|
||
# $3 - Longitude
|
||
# $4 - Number of panels
|
||
# Outputs:
|
||
# Comparison table
|
||
#######################################
|
||
run_scenarios() {
|
||
local api_key="$1"
|
||
local lat="$2"
|
||
local lon="$3"
|
||
local num_panels="$4"
|
||
|
||
local scenarios=(
|
||
"16:250:16 × 250W (older panels)"
|
||
"16:300:16 × 300W"
|
||
"16:350:16 × 350W"
|
||
"16:400:16 × 400W (modern standard)"
|
||
"16:450:16 × 450W (high efficiency)"
|
||
"20:400:20 × 400W (expanded)"
|
||
"24:400:24 × 400W (full offset target)"
|
||
)
|
||
|
||
echo "======================================================================"
|
||
echo "SITER SOLAR - SYSTEM SIZE SCENARIOS"
|
||
echo "======================================================================"
|
||
echo ""
|
||
echo "Location: SITER (${lat}, ${lon})"
|
||
echo "Current Annual Consumption: ${CURRENT_ANNUAL_CONSUMPTION} kWh"
|
||
printf "Current Monthly Bill: \$%'.2f\n" "$CURRENT_MONTHLY_BILL"
|
||
echo ""
|
||
|
||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||
echo "["
|
||
local first=true
|
||
else
|
||
printf "%-25s %6s %8s %8s %8s %10s\n" "System" "kW" "kWh/yr" "\$/mo" "Offset" "Payback"
|
||
echo "----------------------------------------------------------------------"
|
||
fi
|
||
|
||
for scenario in "${scenarios[@]}"; do
|
||
IFS=':' read -r panels watts desc <<< "$scenario"
|
||
local capacity
|
||
capacity=$(echo "$panels * $watts / 1000" | bc -l)
|
||
|
||
local data
|
||
data=$(query_nrel_api "$api_key" "$capacity" "$lat" "$lon" "$DEFAULT_TILT" "$DEFAULT_AZIMUTH" "$DEFAULT_LOSSES")
|
||
|
||
local annual_kwh
|
||
annual_kwh=$(echo "$data" | jq -r '.outputs.ac_annual')
|
||
|
||
local financials
|
||
financials=$(calculate_financials "$annual_kwh")
|
||
|
||
local monthly_savings
|
||
local offset_pct
|
||
local payback_years
|
||
|
||
monthly_savings=$(echo "$financials" | jq -r '.monthly_savings')
|
||
offset_pct=$(echo "$financials" | jq -r '.offset_pct')
|
||
payback_years=$(echo "$financials" | jq -r '.payback_years')
|
||
|
||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||
if [[ "$first" != "true" ]]; then
|
||
echo ","
|
||
fi
|
||
first=false
|
||
echo "$financials" | jq --arg desc "$desc" \
|
||
--arg capacity "$capacity" \
|
||
--arg panels "$panels" \
|
||
--arg watts "$watts" \
|
||
'{
|
||
description: $desc,
|
||
panels: ($panels | tonumber),
|
||
watts_per_panel: ($watts | tonumber),
|
||
capacity_kw: ($capacity | tonumber)
|
||
} + .'
|
||
else
|
||
printf "%-25s %6.1f %8,.0f \$%6.2f %6.1f%% %8.1f yrs\n" \
|
||
"$desc" "$capacity" "$annual_kwh" "$monthly_savings" "$offset_pct" "$payback_years"
|
||
fi
|
||
done
|
||
|
||
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
|
||
echo "]"
|
||
else
|
||
echo "----------------------------------------------------------------------"
|
||
echo ""
|
||
echo "Note: To achieve 100% bill offset would require ~32 kW system"
|
||
fi
|
||
}
|
||
|
||
#######################################
|
||
# Main entry point
|
||
# Globals:
|
||
# All configuration variables
|
||
# Arguments:
|
||
# Command line arguments
|
||
# Outputs:
|
||
# Analysis results
|
||
#######################################
|
||
main() {
|
||
local api_key="${NREL_API_KEY:-$DEFAULT_API_KEY}"
|
||
local lat="${SITER_LAT:-$DEFAULT_LAT}"
|
||
local lon="${SITER_LON:-$DEFAULT_LON}"
|
||
local num_panels="$DEFAULT_PANELS"
|
||
local panel_watts="$DEFAULT_PANEL_WATTS"
|
||
local tilt="$DEFAULT_TILT"
|
||
local azimuth="$DEFAULT_AZIMUTH"
|
||
local losses="$DEFAULT_LOSSES"
|
||
local scenarios_mode=false
|
||
|
||
# Parse arguments
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--lat)
|
||
lat="$2"
|
||
shift 2
|
||
;;
|
||
--lon)
|
||
lon="$2"
|
||
shift 2
|
||
;;
|
||
-p|--panels)
|
||
num_panels="$2"
|
||
shift 2
|
||
;;
|
||
-w|--watts)
|
||
panel_watts="$2"
|
||
shift 2
|
||
;;
|
||
-t|--tilt)
|
||
tilt="$2"
|
||
shift 2
|
||
;;
|
||
-a|--azimuth)
|
||
azimuth="$2"
|
||
shift 2
|
||
;;
|
||
-l|--losses)
|
||
losses="$2"
|
||
shift 2
|
||
;;
|
||
--scenarios)
|
||
scenarios_mode=true
|
||
shift
|
||
;;
|
||
--json)
|
||
OUTPUT_FORMAT="json"
|
||
shift
|
||
;;
|
||
--verbose)
|
||
VERBOSE="true"
|
||
shift
|
||
;;
|
||
-h|--help)
|
||
usage
|
||
exit 0
|
||
;;
|
||
-v|--version)
|
||
echo "siter-solar-analysis version ${VERSION}"
|
||
exit 0
|
||
;;
|
||
-*)
|
||
error "Unknown option: $1\nTry --help for usage information."
|
||
;;
|
||
*)
|
||
error "Unexpected argument: $1\nTry --help for usage information."
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Check dependencies
|
||
check_dependencies
|
||
|
||
# Validate inputs
|
||
validate_numeric "$lat" "latitude"
|
||
validate_numeric "$lon" "longitude"
|
||
validate_numeric "$num_panels" "panels"
|
||
validate_numeric "$panel_watts" "panel watts"
|
||
validate_range "$lat" "-90" "90" "latitude"
|
||
validate_range "$lon" "-180" "180" "longitude"
|
||
|
||
# Calculate system capacity
|
||
local capacity
|
||
capacity=$(echo "$num_panels * $panel_watts / 1000" | bc -l)
|
||
|
||
if [[ "$scenarios_mode" == "true" ]]; then
|
||
run_scenarios "$api_key" "$lat" "$lon" "$num_panels"
|
||
else
|
||
info "Querying NREL PVWatts API (system: ${capacity} kW)..."
|
||
local data
|
||
data=$(query_nrel_api "$api_key" "$capacity" "$lat" "$lon" "$tilt" "$azimuth" "$losses")
|
||
format_output "$data" "$capacity" "$panel_watts" "$num_panels" "$lat" "$lon"
|
||
fi
|
||
}
|
||
|
||
# Run main if not being sourced
|
||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||
main "$@"
|
||
fi
|