OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / scripts / 14_shore_acin.py

103 lines · python ref:
"""14 — Transition ALTERNATIVE: shore power straight into the 6000XP AC-IN (no Chargeverter).

If the AHJ permits grid into the inverter's AC input, Phase 1 can charge the single Docan
pack through the 6000XP's OWN onboard AC charger instead of the DC-coupled Chargeverter.
This sizes the shore -> AC-IN branch and checks the limits. Single 6000XP, single pack.

Inputs (site.yaml, CONFIRMED unless noted):
  - inverter.batt_max_charge_a       125 A DC from PV; note says 115 A from AC -> AC-charge cap
  - inverter.ac_input_passthrough_a   50 A  (grid/gen bypass = branch design basis)
  - battery.charge_cutoff_v          58.4 V (worst-case AC-draw voltage, conservative)
  - battery.bms_continuous_charge_a  150 A (pack BMS charge limit)
  - AC 240 V split-phase
ASSUMPTION (flagged, not from datasheet): AC->DC onboard-charger efficiency = 0.92
  (Chargeverter GC is 94%; 6000XP onboard charger taken conservatively at 92% -> higher AC draw).

Run:  ~/.venvs/prsm-electrical/bin/python scripts/14_shore_acin.py
"""
import importlib
from common import load_site, require, get, banner, result, dump_result

_c = importlib.import_module("03_conductors")
size_run, report = _c.size_run, _c.report

AC_CHARGE_EFF = 0.92          # ASSUMPTION (see header) — conservative for AC-draw sizing
V_AC = 240.0


def main():
    cfg = load_site()
    banner("14  TRANSITION ALT — shore -> 6000XP AC-IN (no Chargeverter)")

    ac_charge_dc_a = 115                                       # site.yaml note: 115 A from AC
    pv_charge_dc_a = require(cfg, "inverter.batt_max_charge_a")  # 125 (PV)
    ac_in_cap_a = require(cfg, "inverter.ac_input_passthrough_a")  # 50
    v_charge = require(cfg, "battery.charge_cutoff_v")          # 58.4 (worst case)
    vnom = require(cfg, "battery.nominal_v")
    cap_ah = require(cfg, "battery.capacity_ah")
    bms_charge_lim = require(cfg, "battery.bms_continuous_charge_a")  # 150
    runs = get(cfg, "phase2.runs_ft") or cfg["runs_ft"]
    tgt = cfg["targets"]
    L = runs.get("inverter_to_loads", 10)

    result("inverter AC-charge cap", f"{ac_charge_dc_a} A DC", "", "site.yaml: 125 A from PV / 115 A from AC")
    result("6000XP AC input cap", f"{ac_in_cap_a} A @ 240V", "", "grid/gen passthrough (branch design basis)")

    # --- AC draw at the full 115 A DC AC-charge limit (conservative worst-case voltage) ---
    p_dc = ac_charge_dc_a * v_charge
    i_ac_charge = p_dc / (AC_CHARGE_EFF * V_AC)
    result("charge power (DC, worst-case)", f"{p_dc:.0f} W", "", f"{ac_charge_dc_a} A x {v_charge} V (absorption ceiling)")
    result("AC draw for full charge", f"{i_ac_charge:.1f} A @ 240V", "",
           f"= {p_dc:.0f} W / ({AC_CHARGE_EFF} eff x 240 V); <= {ac_in_cap_a} A input cap "
           f"({'PASS' if i_ac_charge <= ac_in_cap_a else 'OVER'}, {ac_in_cap_a - i_ac_charge:.0f} A spare for loads)")

    # --- pack-side checks (single Docan pack) ---
    c_rate = ac_charge_dc_a / cap_ah
    result("pack charge vs BMS limit", f"{ac_charge_dc_a} / {bms_charge_lim} A", "",
           f"{'PASS' if ac_charge_dc_a <= bms_charge_lim else 'FAIL'} ({100*ac_charge_dc_a/bms_charge_lim:.0f}%); "
           f"C-rate {c_rate:.2f}C (<= 0.5C)")

    # --- branch sized to the CHARGE DRAW (continuous), the realistic transition feed.
    # OCPD stays <= the inverter's 50 A input rating; the inverter's internal breaker also protects the input.
    print("\n  SHORE -> AC-IN BRANCH (NEC 210/215, 240.4(D); sized to the ~30 A charge draw):")
    r = size_run("Shore -> 6000XP AC-IN (240V)", mode="continuous", current=i_ac_charge,
                 vref=V_AC, length_ft=L, target_pct=tgt["vdrop_ac_pct_max"], ambient_c=30, ccc=2)
    report(r)
    result("branch result", f"{r['final_gauge_AWG']} AWG Cu + {r['ocpd_A']} A 2-pole + {r['egc_AWG']} AWG EGC", "",
           f"Vdrop {r['vdrop_pct']}%; carries the {i_ac_charge:.0f} A charge draw; OCPD {r['ocpd_A']} A <= {ac_in_cap_a} A input rating. "
           f"240 V branch like an EV charger (NEC 210/240, not 705). Upsize to 6 AWG/50 A to also pass full loads from shore.")

    # --- shore-source scenarios: what a given feed allows ---
    print("\n  SHORE-SOURCE SCENARIOS (program AC-charge current to the feed):")
    rows = []
    for feed_a in (30, 50):
        feed_w = feed_a * V_AC
        dc_a_at_feed = min(ac_charge_dc_a, (feed_w * AC_CHARGE_EFF) / v_charge)
        loads_spare_a = max(0.0, feed_a - i_ac_charge) if feed_a >= i_ac_charge else 0.0
        note = (f"full {ac_charge_dc_a} A DC charge (~{i_ac_charge:.0f} A AC) + ~{loads_spare_a:.0f} A spare for loads"
                if feed_a >= i_ac_charge else
                f"charge limited to ~{dc_a_at_feed:.0f} A DC; program inverter AC-charge <= {feed_a} A")
        result(f"  {feed_a} A / 240 V shore", f"~{dc_a_at_feed:.0f} A DC achievable", "", note)
        rows.append({"shore_feed_a": feed_a, "dc_charge_a_achievable": round(dc_a_at_feed, 1),
                     "loads_spare_a": round(loads_spare_a, 1)})

    dump_result("14_shore_acin", {
        "scope": "Phase-1 transition alternative: shore -> single 6000XP AC-IN (permitted scenario)",
        "ac_charge_dc_a": ac_charge_dc_a, "pv_charge_dc_a": pv_charge_dc_a,
        "ac_input_cap_a": ac_in_cap_a, "charge_voltage_worstcase_V": v_charge,
        "ac_charge_eff_assumed": AC_CHARGE_EFF,
        "ac_draw_full_charge_a": round(i_ac_charge, 1),
        "ac_draw_le_input_cap": bool(i_ac_charge <= ac_in_cap_a),
        "spare_for_loads_a": round(ac_in_cap_a - i_ac_charge, 1),
        "pack_c_rate": round(c_rate, 3), "pack_charge_le_bms": bool(ac_charge_dc_a <= bms_charge_lim),
        "branch": {"gauge_AWG": r["final_gauge_AWG"], "ocpd_A": r["ocpd_A"],
                   "egc_AWG": r["egc_AWG"], "vdrop_pct": r["vdrop_pct"], "length_ft": L},
        "shore_scenarios": rows,
        "vs_chargeverter": "Skips the EG4 Chargeverter GC + its DC tap; reuses the AC-IN branch (also needed for the generator).",
        "nec_basis": "Grid -> inverter AC input = NEC 210/240 branch + the inverter's transfer switch; off-grid inverter does NOT export (no 705). AHJ must permit grid-on-AC-IN (the scenario premise).",
    })


if __name__ == "__main__":
    main()