"""04 — Battery discharge / C-rate / runtime + Class-T fuse (NEC 240.6 / 690.71 / 480).
Confirms that at full AC output the battery current is within EVERY limit (inverter,
JK-BMS trip, EVE cell-honest), checks the surge transient, computes C-rate + runtime,
and finalizes the Class-T fuse coordinated with the cable from 03.
Run: ~/.venvs/prsm-electrical/bin/python scripts/04_battery.py
"""
from common import (load_site, require, get, banner, result, dump_result, load_result)
from nec_tables import std_ocpd_at_least, T310_16
def main():
cfg = load_site()
banner("04 BATTERY (discharge current, C-rate, runtime, Class-T fuse)")
cond = load_result("03_conductors")["battery"]
vnom = require(cfg, "battery.nominal_v")
cap_ah = require(cfg, "battery.capacity_ah")
usable_kwh = require(cfg, "battery.usable_kwh")
dod = require(cfg, "battery.dod_design")
ac_w = require(cfg, "inverter.ac_continuous_w")
surge_w = require(cfg, "inverter.ac_surge_w")
eff = require(cfg, "inverter.discharge_eff")
inv_dis = require(cfg, "inverter.batt_max_discharge_a")
inv_chg = require(cfg, "inverter.batt_max_charge_a")
bms_trip = require(cfg, "battery.bms_discharge_ocp_a")
bms_trip_delay = require(cfg, "battery.bms_ocp_delay_s")
bms_sc = require(cfg, "battery.bms_short_circuit_a")
bms_market = require(cfg, "battery.bms_continuous_a")
cell_cont = require(cfg, "battery.cell_continuous_a")
# --- continuous discharge @ full AC ---
i_dis = ac_w / eff / vnom
result("inverter AC continuous", ac_w, "W")
result("battery discharge current", round(i_dis, 1), "A", "ac_w/eff/Vnom @ 6kW")
print("\n Limit stack (continuous discharge must clear ALL):")
limits = [("inverter max discharge", inv_dis),
("JK-BMS OCP trip (300s)", bms_trip),
("EVE cell-honest (0.5P)", cell_cont),
("JK-BMS marketed max", bms_market)]
binding = min(limits, key=lambda x: x[1])
for name, lim in limits:
result(f" vs {name}", f"{round(i_dis,1)} / {lim}", "A",
f"{'PASS' if i_dis <= lim else 'FAIL'} ({100*i_dis/lim:.0f}%)")
result("binding limit", f"{binding[0]} = {binding[1]} A", "",
f"headroom {binding[1]-i_dis:.0f} A ({100*(binding[1]-i_dis)/binding[1]:.0f}%)")
# --- C-rate ---
c_dis = i_dis / cap_ah
c_chg_pv = inv_chg / cap_ah
result("discharge C-rate", f"{c_dis:.2f}C", "", f"{round(i_dis,1)}A / {cap_ah}Ah (<=0.5C OK)")
result("charge C-rate (PV max)", f"{c_chg_pv:.2f}C", "", f"{inv_chg}A / {cap_ah}Ah")
# --- surge transient ---
i_surge = surge_w / eff / vnom
print("\n Surge transient (inverter peak ~3.5 s):")
result(" battery surge current", round(i_surge, 1), "A", f"{surge_w}W/eff/Vnom")
result(" vs JK-BMS short-circuit", f"{round(i_surge,1)} / {bms_sc}", "A",
f"{'under SC trip' if i_surge < bms_sc else 'OVER'} (3.5s << relevant)")
result(" vs JK-BMS OCP trip", f"{round(i_surge,1)} > {bms_trip}", "A",
f"but OCP delay {bms_trip_delay}s >> 3.5s -> no nuisance trip")
surge_note = ("transient only; one pack cannot sustain >6kW continuously — "
"Phase-2 second pack relieves surge headroom")
# --- runtime sanity ---
print("\n Runtime (sanity — winter off-grid still needs the generator):")
rt_full = usable_kwh / (ac_w / 1000.0)
rt_full_dod = usable_kwh * dod / (ac_w / 1000.0)
rt_avg_dod = usable_kwh * dod / 1.5
result(" @ full 6kW (100% / 80% DoD)", f"{rt_full:.2f} / {rt_full_dod:.2f}", "h")
result(" @ 1.5kW avg load (80% DoD)", f"{rt_avg_dod:.1f}", "h")
# --- Class-T fuse final ---
print("\n Class-T fuse selection (NEC 240.6 / 690.9 / 480.9):")
req = i_dis * 1.25
cable_awg = cond["final_gauge_AWG"]
cable_amp = T310_16[cable_awg][1]
fuse = std_ocpd_at_least(req)
# fuse must be <= cable ampacity
ok_cable = fuse <= cable_amp
result(" required (125% x cont)", round(req, 1), "A", "240.6 next size up")
result(" Class-T fuse", f"{fuse} A", "", "DC-rated, 20 kAIC")
result(" cable coordination", f"{fuse}A <= {cable_awg} ampacity {cable_amp}A",
"", "OK" if ok_cable else "UPSIZE CABLE")
result(" fuse vs BMS SC trip", f"{fuse}A < {bms_sc}A SC / interrupt 20kAIC",
"", "layered: BMS protects cells, fuse protects cable + fault current")
verdict = i_dis <= binding[1] and ok_cable
result("OVERALL battery verdict", "PASS" if verdict else "REVIEW")
dump_result("04_battery", {
"discharge_A_6kW": round(i_dis, 1),
"limits_A": {n: l for n, l in limits},
"binding_limit": {"name": binding[0], "A": binding[1],
"headroom_A": round(binding[1] - i_dis, 1)},
"discharge_C": round(c_dis, 3), "charge_C_pv": round(c_chg_pv, 3),
"surge_A": round(i_surge, 1), "surge_note": surge_note,
"runtime_full_h": round(rt_full, 2), "runtime_full_80dod_h": round(rt_full_dod, 2),
"runtime_1p5kw_80dod_h": round(rt_avg_dod, 1),
"classT_fuse_A": fuse, "cable_AWG": cable_awg, "cable_ampacity_A": cable_amp,
"fuse_cable_ok": ok_cable, "verdict": "PASS" if verdict else "REVIEW",
})
if __name__ == "__main__":
main()