#!/usr/bin/env python3 # # 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 . # 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()