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>
This commit is contained in:
Charles N Wyble
2026-02-27 16:45:41 -05:00
commit 400764a9ff
22 changed files with 3587 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
#!/usr/bin/env python3
# DEPRECATED: This script is deprecated. Use siter-solar-analysis.sh instead.
# This file is kept for reference only.
"""Find optimal tilt/azimuth for ground mount at SITER location"""
import requests
import time
LAT = float(os.environ.get("SITER_LAT", "30.44")) # Central Texas
import os
LON = float(os.environ.get("SITER_LON", "-97.62")) # Central Texas
API_URL = "https://developer.nrel.gov/api/pvwatts/v6.json"
def query(capacity, tilt, azimuth, losses=8):
params = {
"api_key": os.environ.get("NREL_API_KEY", "DEMO_KEY"),
"lat": LAT, "lon": LON,
"system_capacity": capacity,
"array_type": 0, # Ground mount
"tilt": tilt, "azimuth": azimuth,
"module_type": 0,
"losses": losses,
}
try:
r = requests.get(API_URL, params=params, timeout=30)
data = r.json()
return data.get("outputs", {}).get("ac_annual", 0)
except:
return 0
print("=" * 70)
print("OPTIMAL ORIENTATION ANALYSIS - SITER Solar (Ground Mount)")
print("Location: SITER")
print("System: 16 panels × 250W = 4.0 kW DC")
print("Losses: 8% (optimized - no shade, good airflow)")
print("=" * 70)
# Test different tilts (latitude = 30.44°N)
print("\nTilt Analysis (Azimuth = 180° South):")
print("-" * 40)
best_tilt = 0
best_prod = 0
for tilt in [0, 15, 20, 25, 30, 35, 40, 45, 50, 60, 90]:
prod = query(4.0, tilt, 180)
if prod > best_prod:
best_prod = prod
best_tilt = tilt
print(f" Tilt {tilt:>2}°: {prod:>6,.0f} kWh/yr")
time.sleep(0.5)
print(f"\n Best tilt: {best_tilt}° ({best_prod:,.0f} kWh/yr)")
# Test azimuth variations
print("\nAzimuth Analysis (Tilt = 30°):")
print("-" * 40)
best_az = 180
best_prod = 0
for az in [90, 120, 150, 180, 210, 240, 270]:
prod = query(4.0, 30, az)
if prod > best_prod:
best_prod = prod
best_az = az
direction = {90: "E", 120: "ESE", 150: "SSE", 180: "S", 210: "SSW", 240: "WSW", 270: "W"}
print(f" {az:>3}° ({direction.get(az, ''):<3}): {prod:>6,.0f} kWh/yr")
time.sleep(0.5)
print(f"\n Best azimuth: {best_az}° ({best_prod:,.0f} kWh/yr)")
# Final optimized production
print("\n" + "=" * 70)
print("OPTIMIZED SYSTEM PERFORMANCE")
print("=" * 70)
opt_prod = query(4.0, best_tilt, best_az)
value = (opt_prod * 0.60 * 0.085) + (opt_prod * 0.40 * 0.04)
payback = 4100 / (value / 12)
print(f"\n Optimal Config: {best_tilt}° tilt, {best_az}° azimuth")
print(f" Annual Production: {opt_prod:,.0f} kWh")
print(f" Monthly Average: {opt_prod/12:,.0f} kWh")
print(f" Monthly Value: ${value/12:.2f}")
print(f" Bill Offset: {(value/12/301.08)*100:.1f}%")
print(f" Payback: {payback/12:.1f} years")
print("\n" + "=" * 70)
print("PANEL WATTAGE OPTIONS (Optimized Orientation)")
print("=" * 70)
print(f"\n{'Panels':<12} {'Capacity':>10} {'kWh/yr':>10} {'$/mo':>8} {'Offset':>8} {'Payback':>10}")
print("-" * 60)
for watts in [250, 300, 350, 400, 450]:
capacity = (16 * watts) / 1000
prod = query(capacity, best_tilt, best_az)
if prod:
value = (prod * 0.60 * 0.085) + (prod * 0.40 * 0.04)
payback = 4100 / (value / 12)
offset = (value / 12 / 301.08) * 100
print(f"16 × {watts}W {capacity:>8.1f}kW {prod:>10,.0f} ${value/12:>6.2f} {offset:>6.1f}% {payback/12:>8.1f} yrs")
time.sleep(0.5)