Files
SITER-Solar/solar-analysis/solar_estimate.py
Charles N Wyble 400764a9ff feat: initial project setup with bash-based NREL analysis
- Add bash script (siter-solar-analysis.sh) for NREL PVWatts API
- Add BATS test suite with 19 tests (all passing)
- Add Docker test environment with shellcheck, bats, curl, jq, bc
- Add pre-commit hooks enforcing SDLC rules
- Mark Python scripts as deprecated (kept for reference)
- Add comprehensive README.md and AGENTS.md documentation
- Add .env.example for configuration template
- Add .gitignore excluding private data (base-bill/, .env)
- Add SVG diagrams for presentation
- Redact all private location data (use SITER placeholder)

All work done following SDLC: Docker-only development, TDD approach,
conventional commits, code/docs/tests synchronized.

Generated with Crush

Assisted-by: GLM-5 via Crush <crush@charm.land>
2026-02-27 16:45:41 -05:00

300 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# DEPRECATED: This script is deprecated. Use siter-solar-analysis.sh instead.
# This file is kept for reference only.
"""
NREL PVWatts Solar Production Estimator for SITER Solar Project
Location: SITER
Usage:
python solar_estimate.py # Use defaults (16 panels @ 400W = 6.4kW)
python solar_estimate.py YOUR_API_KEY # With your NREL API key
python solar_estimate.py --scenarios # Run multiple system size scenarios
"""
import os
import requests
import json
import sys
from datetime import datetime
# Location coordinates for SITER
LAT = float(os.environ.get("SITER_LAT", "30.44")) # Central Texas
LON = float(os.environ.get("SITER_LON", "-97.62")) # Central Texas
# Default system parameters
NUM_PANELS = 16
DEFAULT_PANEL_WATTS = 400 # Modern panels are typically 350-450W
ARRAY_TYPE = 0 # 0 = Fixed - Open Rack (ground mount)
TILT = 30 # degrees - optimal for Texas latitude (~30°N)
AZIMUTH = 180 # South-facing
MODULE_TYPE = 0 # 0 = Standard
LOSSES = 14 # System losses percentage (typical)
# Financial parameters
PROJECT_COST = 4100
CURRENT_MONTHLY_BILL = 301.08
CURRENT_ANNUAL_CONSUMPTION = 23952 # kWh/year (from actual bills: ~1996 kWh/month avg)
BASE_POWER_CONSUMPTION_RATE = 0.085 # $/kWh avoided
BASE_POWER_EXPORT_RATE = 0.04 # $/kWh buyback
# API endpoint
API_URL = "https://developer.nrel.gov/api/pvwatts/v6.json"
def get_solar_estimate(api_key, system_capacity_kw):
"""Query NREL PVWatts API for solar production estimate."""
params = {
"api_key": api_key,
"lat": LAT,
"lon": LON,
"system_capacity": system_capacity_kw,
"array_type": ARRAY_TYPE,
"tilt": TILT,
"azimuth": AZIMUTH,
"module_type": MODULE_TYPE,
"losses": LOSSES,
"timeframe": "monthly"
}
try:
response = requests.get(API_URL, params=params, timeout=30)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"API request failed: {e}")
return None
def calculate_financials(annual_kwh):
"""Calculate financial projections based on annual production."""
monthly_avg = annual_kwh / 12
# Estimate self-consumption vs export
# During peak solar hours, home may not consume all production
# Conservative estimate: 60% self-consumed, 40% exported
self_consumed_pct = 0.60
self_consumed = annual_kwh * self_consumed_pct
exported = annual_kwh * (1 - self_consumed_pct)
self_consumption_value = self_consumed * BASE_POWER_CONSUMPTION_RATE
export_value = exported * BASE_POWER_EXPORT_RATE
total_annual_value = self_consumption_value + export_value
monthly_savings = total_annual_value / 12
offset_pct = (monthly_savings / CURRENT_MONTHLY_BILL) * 100
payback_months = PROJECT_COST / monthly_savings if monthly_savings > 0 else float('inf')
return {
"annual_kwh": annual_kwh,
"monthly_avg_kwh": monthly_avg,
"self_consumed_kwh": self_consumed,
"exported_kwh": exported,
"self_consumption_value": self_consumption_value,
"export_value": export_value,
"total_annual_value": total_annual_value,
"monthly_savings": monthly_savings,
"offset_pct": offset_pct,
"payback_months": payback_months,
"payback_years": payback_months / 12
}
def format_output(data, system_capacity_kw, panel_watts):
"""Format API response into readable report."""
if not data or data.get("errors"):
print("Error:", data.get("errors", "Unknown error"))
return None
outputs = data.get("outputs", {})
station = data.get("station_info", {})
annual_kwh = outputs.get("ac_annual", 0)
fin = calculate_financials(annual_kwh)
# Self-consumption percentage used in calculations
self_consumed_pct = 0.60
print("=" * 70)
print("SITER SOLAR PROJECT - NREL PVWATTS ANALYSIS")
print("=" * 70)
print(f"\nLocation: SITER")
print(f" Coordinates: {LAT}, {LON}")
print(f" Station: {station.get('city', 'N/A')}, {station.get('state', 'Texas')}")
print(f" Distance: {station.get('distance', 'N/A')}m | Elevation: {station.get('elev', 'N/A')}m")
print(f"\nSystem Configuration:")
print(f" Panels: {NUM_PANELS} × {panel_watts}W = {system_capacity_kw} kW DC")
print(f" Array Type: Fixed Open Rack (Ground Mount)")
print(f" Orientation: {TILT}° tilt, {AZIMUTH}° azimuth (South-facing)")
print(f" System Losses: {LOSSES}%")
print(f"\n{'='*70}")
print("MONTHLY PRODUCTION ESTIMATE (NREL PVWatts)")
print(f"{'='*70}")
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
ac_monthly = outputs.get("ac_monthly", [])
solrad_monthly = outputs.get("solrad_monthly", [])
print(f"\n{'Month':<6} {'AC Output':<14} {'Daily Avg':<12} {'Solar Rad':<15}")
print(f"{'':6} {'(kWh)':<14} {'(kWh/day)':<12} {'(kWh/m²/day)':<15}")
print("-" * 50)
for i, month in enumerate(months):
ac = ac_monthly[i] if i < len(ac_monthly) else 0
daily_avg = ac / 30.5 if ac else 0
solrad = solrad_monthly[i] if i < len(solrad_monthly) else 0
print(f"{month:<6} {ac:>10.0f} {daily_avg:>10.0f} {solrad:>14.2f}")
print("-" * 50)
print(f"{'ANNUAL':<6} {fin['annual_kwh']:>10.0f} {fin['monthly_avg_kwh']:>10.0f}")
print(f"\n{'='*70}")
print("ANNUAL PERFORMANCE SUMMARY")
print(f"{'='*70}")
print(f" Annual AC Output: {fin['annual_kwh']:,.0f} kWh")
print(f" Monthly Average: {fin['monthly_avg_kwh']:,.0f} kWh")
print(f" Daily Average: {fin['annual_kwh']/365:,.0f} kWh")
print(f" Capacity Factor: {outputs.get('capacity_factor', 'N/A')}%")
print(f" Avg Solar Radiation: {outputs.get('solrad_annual', 'N/A')} kWh/m²/day")
print(f"\n{'='*70}")
print("FINANCIAL ANALYSIS")
print(f"{'='*70}")
print(f"\n Current Consumption: {CURRENT_ANNUAL_CONSUMPTION:,} kWh/year")
print(f" Solar Production: {fin['annual_kwh']:,.0f} kWh/year")
print(f" Self-Sufficiency: {(fin['annual_kwh']/CURRENT_ANNUAL_CONSUMPTION)*100:.1f}%")
print(f"\n Self-Consumed ({self_consumed_pct*100:.0f}%): {fin['self_consumed_kwh']:,.0f} kWh")
print(f" Value @ ${BASE_POWER_CONSUMPTION_RATE}/kWh: ${fin['self_consumption_value']:,.2f}/year")
print(f"\n Exported to Grid ({(1-self_consumed_pct)*100:.0f}%): {fin['exported_kwh']:,.0f} kWh")
print(f" Value @ ${BASE_POWER_EXPORT_RATE}/kWh: ${fin['export_value']:,.2f}/year")
print(f"\n TOTAL ANNUAL VALUE: ${fin['total_annual_value']:,.2f}")
print(f" MONTHLY SAVINGS: ${fin['monthly_savings']:,.2f}")
print(f"\n{'='*70}")
print("ROI ANALYSIS")
print(f"{'='*70}")
print(f"\n Project Cost: ${PROJECT_COST:,.2f}")
print(f" Current Monthly Bill: ${CURRENT_MONTHLY_BILL:,.2f}")
print(f" Projected Monthly Savings: ${fin['monthly_savings']:,.2f}")
print(f" Bill Offset: {fin['offset_pct']:.1f}%")
print(f"\n PAYBACK PERIOD: {fin['payback_months']:.1f} months ({fin['payback_years']:.1f} years)")
# 5-year projection
print(f"\n{'='*70}")
print("5-YEAR FINANCIAL PROJECTION")
print(f"{'='*70}")
print(f"\n {'Year':<6} {'Cumulative Savings':<20} {'Net Position':<15}")
print(f" {'-'*40}")
for year in range(6):
cum_savings = fin['total_annual_value'] * year
net = cum_savings - PROJECT_COST
print(f" {year:<6} ${cum_savings:>17,.2f} ${net:>12,.2f}")
print(f"\n{'='*70}")
return fin
def run_scenarios(api_key):
"""Run analysis for multiple system configurations."""
print("=" * 70)
print("SITER SOLAR - SYSTEM SIZE SCENARIOS")
print("=" * 70)
print(f"\nLocation: SITER")
print(f"Current Annual Consumption: {CURRENT_ANNUAL_CONSUMPTION:,} kWh")
print(f"Current Monthly Bill: ${CURRENT_MONTHLY_BILL:,.2f}")
print()
scenarios = [
# (panels, watts_per_panel, description)
(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)"),
]
results = []
print(f"{'System':<25} {'kW':>6} {'kWh/yr':>8} {'$/mo':>8} {'Offset':>8} {'Payback':>10}")
print("-" * 70)
for panels, watts, desc in scenarios:
capacity_kw = (panels * watts) / 1000
data = get_solar_estimate(api_key, capacity_kw)
if data and not data.get("errors"):
annual_kwh = data.get("outputs", {}).get("ac_annual", 0)
fin = calculate_financials(annual_kwh)
results.append({
"description": desc,
"panels": panels,
"watts": watts,
"capacity_kw": capacity_kw,
**fin
})
print(f"{desc:<25} {capacity_kw:>6.1f} {fin['annual_kwh']:>8,.0f} "
f"${fin['monthly_savings']:>6.2f} {fin['offset_pct']:>6.1f}% "
f"{fin['payback_years']:>8.1f} yrs")
print("-" * 70)
# Find system size for ~100% offset
target_monthly_savings = CURRENT_MONTHLY_BILL
target_annual_value = target_monthly_savings * 12
# Back-calculate required production
# annual_value = (production * 0.60 * 0.085) + (production * 0.40 * 0.04)
# annual_value = production * (0.051 + 0.016) = production * 0.067
required_production = target_annual_value / 0.067 # ~$4,016 annual value needed
required_kw = required_production / 1500 # Rough: 1kW produces ~1500 kWh/year in Texas
print(f"\nTo achieve 100% bill offset, estimated system size needed:")
print(f" ~{required_kw:.0f} kW ({int(required_kw/0.4)} panels @ 400W each)")
return results
def main():
global NUM_PANELS
api_key = os.environ.get("NREL_API_KEY", "DEMO_KEY")
run_scenarios_mode = False
panel_watts = DEFAULT_PANEL_WATTS
for arg in sys.argv[1:]:
if arg == "--scenarios":
run_scenarios_mode = True
elif arg.startswith("--panels="):
NUM_PANELS = int(arg.split("=")[1])
elif arg.startswith("--watts="):
panel_watts = int(arg.split("=")[1])
elif not arg.startswith("--"):
api_key = arg
system_capacity_kw = (NUM_PANELS * panel_watts) / 1000
if run_scenarios_mode:
run_scenarios(api_key)
else:
print(f"Querying NREL PVWatts API (system: {system_capacity_kw} kW)...")
data = get_solar_estimate(api_key, system_capacity_kw)
if data:
result = format_output(data, system_capacity_kw, panel_watts)
# Save JSON for further analysis
with open("nrel_solar_data.json", "w") as f:
json.dump(data, f, indent=2)
print(f"\nRaw data saved to nrel_solar_data.json")
if __name__ == "__main__":
main()