"""02 — Production / per-MPPT energy sanity (PVWatts v8, fixed open-rack).
Annual + monthly AC kWh for the ~8 kW array, with the winter (Dec/Jan) worst-case
that actually governs an off-grid system. Per-MPPT DC power check vs the inverter.
Ground truth: NREL PVWatts V8 REST API. The host is VERIFIED empirically here
(the project docs claim a 2026-05-29 migration to developer.nlr.gov; we test the
canonical developer.nrel.gov first and only fall back if it fails).
Run: ~/.venvs/prsm-electrical/bin/python scripts/02_production.py
Uses NREL_API_KEY if set, else DEMO_KEY (rate-limited but works for PVWatts).
"""
import os
import requests
from common import load_site, require, banner, result, dump_result, load_result
HOSTS = ["https://developer.nrel.gov", "https://developer.nlr.gov"]
PATH = "/api/pvwatts/v8.json"
MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def call_pvwatts(params):
"""Try each host; return (json, host, err)."""
last = None
for host in HOSTS:
try:
r = requests.get(host + PATH, params=params, timeout=30)
if r.status_code == 200 and "outputs" in r.json():
return r.json(), host, None
last = f"{host}: HTTP {r.status_code} {r.text[:120]}"
except Exception as e: # noqa
last = f"{host}: {type(e).__name__}: {e}"
return None, None, last
def main():
cfg = load_site()
banner("02 PRODUCTION (PVWatts v8, fixed open-rack array_type=0)")
s01 = load_result("01_string")
modules = int(require(cfg, "array.module_count"))
pmp = require(cfg, "panel.pmp_w")
kw = round(modules * pmp / 1000.0, 2)
tilt = require(cfg, "array.tilt_deg")
az = require(cfg, "array.azimuth_deg")
lat = require(cfg, "site.latitude")
lon = require(cfg, "site.longitude")
bif = require(cfg, "panel.bifaciality") # BOM panel = 0.80 (bifacial)
gcr = require(cfg, "array.gcr") # Phase-1 single-row = 0.30 (flagged in site.yaml)
key = os.environ.get("NREL_API_KEY", "DEMO_KEY")
result("system_capacity", kw, "kW DC", f"{modules} x {pmp}W (BOM Waaree BiN-08-575 bifacial)")
result("tilt / azimuth", f"{tilt} / {az}", "deg")
result("bifaciality / gcr", f"{bif} / {gcr}", "", "BOM bifacial rear-gain (PVWatts v8); single-row Phase 1")
result("api key", "NREL_API_KEY" if key != "DEMO_KEY" else "DEMO_KEY (rate-limited)")
params = {"api_key": key, "system_capacity": kw, "module_type": 0,
"array_type": 0, "tilt": tilt, "azimuth": az, "losses": 14,
"lat": lat, "lon": lon, "dc_ac_ratio": round(kw / 6.0, 2),
"bifaciality": bif, "gcr": gcr}
data, host, err = call_pvwatts(params)
if data is None:
result("PVWatts", "UNAVAILABLE", "", err)
result("note", "production is a sanity check; safety calcs (01/03/04) do not depend on it")
dump_result("02_production", {"status": "unavailable", "error": err,
"system_kw": kw})
raise SystemExit("PVWatts call failed; rerun with a valid NREL_API_KEY.")
out = data["outputs"]
result("VERIFIED host", host, "", "developer.nrel.gov canonical"
if "nrel" in host else "fell back to alt host")
ac_annual = out["ac_annual"]
ac_monthly = out["ac_monthly"]
cf = out.get("capacity_factor")
result("AC annual", round(ac_annual), "kWh/yr")
result("capacity factor", round(cf, 1) if cf else "n/a", "%")
print("\n Monthly AC (kWh) + kWh/day:")
for i, m in enumerate(MONTHS):
print(f" {m} {ac_monthly[i]:8.0f} {ac_monthly[i]/DAYS[i]:5.1f}/day")
dec_day = ac_monthly[11] / DAYS[11]
jan_day = ac_monthly[0] / DAYS[0]
jun_day = ac_monthly[5] / DAYS[5]
print()
result("WINTER Dec / Jan kWh/day", f"{dec_day:.1f} / {jan_day:.1f}", "",
"off-grid binding case")
result("Summer Jun kWh/day", f"{jun_day:.1f}", "", "for contrast")
result("winter:summer ratio", f"{dec_day/jun_day:.2f}", "",
"NEK winter PV starvation -> generator + Phase-2 PV (Swarm §2,§6)")
# per-MPPT DC power check (from 01)
print("\n Per-MPPT loading (DC nameplate, from 01):")
result("string DC power", s01["string_power_W"], "W",
f"vs 4000 usable / 5000 oversize cap per MPPT")
result("array DC total", s01["array_dc_W"], "W", "vs 8000 usable / 10000 oversize")
result("DC:AC ratio", round(kw / 6.0, 2), "", "winter-favorable oversizing")
dump_result("02_production", {
"status": "ok", "verified_host": host, "system_kw": kw,
"panel": "Waaree BiN-08-575 bifacial (Grade B) — BOM",
"bifaciality": bif, "gcr": gcr,
"ac_annual_kwh": round(ac_annual), "capacity_factor_pct": cf,
"ac_monthly_kwh": [round(x) for x in ac_monthly],
"dec_kwh_day": round(dec_day, 1), "jan_kwh_day": round(jan_day, 1),
"jun_kwh_day": round(jun_day, 1),
"string_dc_W": s01["string_power_W"], "array_dc_W": s01["array_dc_W"],
"dc_ac_ratio": round(kw / 6.0, 2),
})
if __name__ == "__main__":
main()