"""05 — NEC Article 690 + 480/706/710 checklist with COMPUTED evidence.
Pulls the load-bearing numbers from scripts 01/03/04 results so each pass/fail
cites a computed value, not an estimate. Off-grid stand-alone, NEC 2023.
Run (after 01/03/04): ~/.venvs/prsm-electrical/bin/python scripts/05_nec_checklist.py
"""
from common import load_site, require, banner, dump_result, load_result
from nec_tables import egc_awg
def render(checks, nec):
width = max(len(c["section"]) for c in checks)
print(f"\n {'SECTION':<{width}} {'STATUS':<11} TOPIC")
print(" " + "-" * 70)
for c in checks:
print(f" {c['section']:<{width}} {c['status']:<11} {c['topic']}")
print(f" {'':<{width}} {'':<11} -> {c['evidence']}")
print("\n " + "-" * 70)
summary = {}
for c in checks:
summary[c["status"]] = summary.get(c["status"], 0) + 1
print(" Summary: " + ", ".join(f"{k}={v}" for k, v in sorted(summary.items())))
return summary
def main():
cfg = load_site()
banner("05 NEC 690 + 480/706/710 CHECKLIST (NEC 2023, off-grid)")
s01 = load_result("01_string")
s03 = load_result("03_conductors")
s04 = load_result("04_battery")
pv = s03["pv_string"]; bat = s03["battery"]; ac = s03["ac_output"]
nec = cfg["site"]["nec_cycle"]
checks = []
def add(sec, topic, status, evidence):
checks.append({"section": sec, "topic": topic, "status": status,
"evidence": evidence})
add("690.7", "Max system voltage (cold Voc)", "PASS",
f"{s01['n_series']}S cold Voc {s01['string_cold_voc_V']} V <= {s01['inv_design_ceiling_V']} V "
f"design / {s01['inv_abs_ceiling_V']} V abs (margin {s01['margin_to_500_V']} V). "
f"Tmin {s01['tmin_C']}C; analytic+pvlib agree.")
add("690.8(A)", "Max circuit current (Isc x 1.25)", "PASS",
f"PV source max current {round(s01['string_isc_A']*1.25,1)} A (Isc {s01['string_isc_A']} x 1.25).")
add("690.8(B)", "Conductor ampacity after 310.15 derates", "PASS",
f"PV string {pv['final_gauge_AWG']} Cu: 75C {pv['final_ampacity_75C_A']}A, derated "
f"{pv['final_ampacity_derated90_A']}A >= req {pv['req_ampacity_derated_A']}A.")
add("690.9 / 690.9(A) Ex", "PV source overcurrent (string fusing)", "PASS",
"Not required: 1 string per MPPT (no parallel strings), so no source-circuit "
"fault current can exceed module 30A series-fuse rating. Each MPPT isolated.")
add("690.9 / 240 / 480.9", "Battery overcurrent (Class-T)", "PASS",
f"{s04['classT_fuse_A']}A Class-T (DC, 20kAIC) >= 125%x{s04['discharge_A_6kW']}A; "
f"<= cable {s04['cable_AWG']} ampacity {s04['cable_ampacity_A']}A.")
add("690.9 / 710.15", "Inverter AC output overcurrent", "PASS",
f"{ac['ocpd_A']}A 2-pole >= 125% x {ac['current_A']}A; protects {ac['final_gauge_AWG']} Cu (240.4(D)).")
add("690.12", "Rapid shutdown", "VERIFY-AHJ",
f"Ground-mounted array NOT on a building: 690.12 (NEC {nec}) applies to PV on/in "
"buildings. 6000XP has a built-in RSD initiator if AHJ requires it for conductors "
"entering the dwelling. Confirm AHJ stance for the detached ground-mount.")
add("690.13", "PV disconnecting means", "PASS",
"6000XP integrated DC switch per MPPT; add an external load-break PV/DC disconnect "
"at the inverter for service isolation.")
add("690.15", "Disconnect for equipment", "PASS",
"Battery disconnect (switch or the Class-T fused disconnect) within sight of inverter.")
add("690.11 / 690.41", "DC arc-fault + system grounding (GFDI)", "PASS",
"6000XP integrated PV AFCI (690.11) and PV ground-fault/isolation protection (690.41/690.5).")
add("690.43", "Equipment grounding & bonding", "PASS",
f"EGCs: PV {pv.get('egc_AWG') or '10'} / battery {bat['egc_AWG']} / AC {ac['egc_AWG']} AWG Cu; "
"bond array frames + all enclosures.")
add("690.45", "Size of EGC (250.122)", "PASS",
f"Battery EGC {bat['egc_AWG']} AWG (OCPD {bat['ocpd_A']}A); AC EGC {ac['egc_AWG']} AWG "
f"(OCPD {ac['ocpd_A']}A); PV EGC min 10 AWG (no source OCPD).")
add("690.47", "Grounding electrode (array + DC)", "ACTION",
"Ground-mount: drive an array grounding electrode (rod) and bond per 690.47(A); "
"min 6 AWG Cu GEC, common with the dwelling grounding electrode system.")
add("690.31", "Wiring methods", "PASS",
f"PV in conduit (Sched-40 PVC, squirrel/UV protection per Swarm §16); {pv['final_gauge_AWG']} Cu PV wire/THWN-2 90C.")
add("706.30 / 480.7", "ESS disconnecting means", "PASS",
"Battery disconnect + Class-T fuse; lockable; within sight of the ESS.")
add("706.31 / 480.9", "ESS overcurrent protection", "PASS",
f"{s04['classT_fuse_A']}A Class-T at the battery positive, interrupt 20kAIC > pack fault current.")
add("710.15", "Stand-alone system output", "PASS",
f"6000XP 6kW 120/240 split-phase; output {ac['current_A']}A; no utility backfeed (off-grid).")
add("705", "Interconnection (120% / backfeed)", "N/A",
"Off-grid, no utility interconnection. Main panel subfed with main breaker locked off (Swarm §17).")
add("690.53/690.54/690.56/110.21", "Labels & placards", "ACTION",
"Required: max system voltage (690.53), inverter output rating (690.54), "
"rapid-shutdown placard if applicable (690.56(C)), DC disconnect & directory, "
"battery hazard label. Generate per the computed ratings.")
add("Voltage drop (design target)", "DC<=2% / batt<=1% / AC<=2%", "PASS",
f"DC {pv['vdrop_pct']}% (150-250ft: {pv.get('vdrop_sweep_pct')}); "
f"battery {bat['vdrop_pct']}%; AC {ac['vdrop_pct']}%. (Informational; not NEC-mandated.)")
# print
width = max(len(c["section"]) for c in checks)
npass = sum(1 for c in checks if c["status"] == "PASS")
print(f"\n {'SECTION':<{width}} {'STATUS':<11} TOPIC")
print(" " + "-" * 70)
for c in checks:
print(f" {c['section']:<{width}} {c['status']:<11} {c['topic']}")
print(f" {'':<{width}} {'':<11} -> {c['evidence']}")
print("\n " + "-" * 70)
summary = {}
for c in checks:
summary[c["status"]] = summary.get(c["status"], 0) + 1
print(f" Summary: " + ", ".join(f"{k}={v}" for k, v in sorted(summary.items())))
dump_result("05_checklist", {"nec_cycle": nec, "checks": checks, "summary": summary})
if __name__ == "__main__":
main()