OverviewFilesCommitsIssuesDocsReleasesPeers

electrical-review / scripts / 12_chargeverter.py

106 lines · python ref:
"""12 — Transition grid-charging via an EG4 Chargeverter GC (DC-coupled, islanded).

Phase-1→off-grid TRANSITION option: a UL-listed AC battery charger (EG4 Chargeverter GC)
fed from shore/grid power, outputting DC onto the SELECTED Path-A battery busbar. The grid
only ever feeds the charger (a branch-circuit load) — it never parallels the inverter AC
output, so the system stays a stand-alone (NEC Art. 710) island: no 705 interconnection,
no backfeed, no synchronization. This script sizes the charger's AC input + DC output
circuits and — the safety-critical bit — checks the COMBINED PV + charger current against
the 2-pack BMS charge limit.

EG4 Chargeverter GC (datasheet, eg4electronics.com): 48 V nominal, DC out 0-100 A @240V
(0-45 A @120V / 0-50 A @208V), 5120 W, AC in max 26 A @240V, charge V 43-57 (def 54), 94% eff,
built-in breaker, UL listed.  Conductor sizing reuses the validated size_run() from 03.

Run:  ~/.venvs/prsm-electrical/bin/python scripts/12_chargeverter.py
"""
import importlib
from common import load_site, require, banner, result, dump_result
from nec_tables import std_ocpd_at_least, T310_16

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

# --- EG4 Chargeverter GC (CONFIRMED from datasheet) ---
CV_DC_A_MAX = 100        # DC output @ 240 VAC (adjustable 0-100)
CV_AC_A_MAX = 26         # AC input current @ 240 VAC (max)
CV_W = 5120              # max output @ 240 VAC
CV_AC_V = 240.0
CV_V_CHG_DEFAULT = 54.0  # default charge voltage (adjustable 43-57)


def main():
    cfg = load_site()
    banner("12  TRANSITION — EG4 Chargeverter GC (DC-coupled grid charging, islanded)")

    vnom = require(cfg, "battery.nominal_v")
    cap_ah = require(cfg, "battery.capacity_ah")
    bms_chg = require(cfg, "battery.bms_continuous_charge_a")       # 150 A per pack
    inv_pv_chg = require(cfg, "inverter.batt_max_charge_a")         # 125 A per 6000XP from PV
    npack, n_inv = 2, 2                                             # selected Path-A topology
    runs = (cfg.get("phase2") or {}).get("runs_ft", cfg["runs_ft"])
    tgt = cfg["targets"]

    result("Chargeverter GC", f"DC 0-{CV_DC_A_MAX}A / AC {CV_AC_A_MAX}A @240V / {CV_W}W", "",
           "UL-listed battery charger; grid feeds ONLY this (branch-circuit load)")
    result("on the Path-A bank", f"{npack} packs, {n_inv}x 6000XP", "",
           "charger lands on the same 250A DC busbar as the packs + inverters")

    # ---------- THE safety-critical check: combined charge current vs BMS limit ----------
    bank_chg_limit = npack * bms_chg               # 300 A
    pv_chg_max = n_inv * inv_pv_chg                # 250 A (2x 6000XP from PV)
    cv = CV_DC_A_MAX                               # 100 A charger
    print("\n  COMBINED charge current (PV chargers + Chargeverter) vs the 2-pack BMS limit:")
    result("bank BMS charge limit", f"{bank_chg_limit}", "A", f"{npack} x {bms_chg}A/pack")
    result("PV charge max (2x 6000XP)", f"{pv_chg_max}", "A", "both inverters charging from PV")
    for label, total in [("Chargeverter ALONE (PV~0, night/winter)", cv),
                         ("Chargeverter @100A + PV @250A (worst)", cv + pv_chg_max),
                         ("Chargeverter @50A + PV @250A", 50 + pv_chg_max)]:
        per_pack = total / npack
        crate = per_pack / cap_ah
        ok = per_pack <= bms_chg
        result(f"  {label}", f"{total}A bank -> {per_pack:.0f}A/pack ({crate:.2f}C)", "",
               f"{'OK' if ok else 'EXCEEDS '+str(bms_chg)+'A/pack BMS'}")
    cv_max_with_full_pv = bank_chg_limit - pv_chg_max
    result("max Chargeverter if run WITH full PV", f"{cv_max_with_full_pv}", "A",
           f"so PV{pv_chg_max}+CV <= {bank_chg_limit}A bank (150A/pack)")
    print("  -> Rule: grid-charge when PV is low (the natural transition case) at up to 100A; if the")
    print("     charger may run alongside MAX PV, cap it at ~50A. The JK-BMS (150A/pack) is the backstop.")

    # ---------- AC input branch circuit (ordinary NEC 210/240, NOT interconnection) ----------
    print("\n  AC INPUT branch circuit (utility -> charger; a LOAD, not a source):")
    r_ac = size_run("Chargeverter AC input (240V branch)", mode="continuous",
                    current=CV_AC_A_MAX, vref=CV_AC_V, length_ft=runs.get("inverter_to_loads", 10),
                    target_pct=tgt["vdrop_ac_pct_max"], ambient_c=30, ccc=2)
    report(r_ac)

    # ---------- DC output to the battery busbar (a charge source on the ESS bus) ----------
    print("\n  DC OUTPUT to the battery busbar (690.9/706.31 charge-source OCPD + disconnect):")
    r_dc = size_run("Chargeverter DC output (-> busbar)", mode="continuous",
                    current=CV_DC_A_MAX, vref=vnom, length_ft=runs.get("inverter_to_battery", 10),
                    target_pct=tgt["vdrop_battery_pct_max"], ambient_c=30, ccc=2)
    report(r_dc)
    result("  note", "2/0 matches the bank cabling (175A) for one cable type", "",
           f"computed minimum is {r_dc['final_gauge_AWG']}; fuse {r_dc['ocpd_A']}A at the busbar tap")

    verdict = (cv / npack <= bms_chg) and ((cv_max_with_full_pv + pv_chg_max) <= bank_chg_limit)
    print()
    result("OVERALL", "PASS (with charge-current coordination)" if verdict else "REVIEW", "",
           "islanded DC-coupled charging; grid never touches the inverter or AC panel")

    dump_result("12_chargeverter", {
        "charger": "EG4 Chargeverter GC", "dc_out_a_max": CV_DC_A_MAX, "ac_in_a_max": CV_AC_A_MAX,
        "bank_charge_limit_a": bank_chg_limit, "pv_charge_max_a": pv_chg_max,
        "max_cv_with_full_pv_a": cv_max_with_full_pv,
        "cv_alone_per_pack_a": cv / npack, "cv_alone_crate": round((cv / npack) / cap_ah, 3),
        "ac_input": {"gauge": r_ac["final_gauge_AWG"], "ocpd_a": r_ac["ocpd_A"], "egc": r_ac["egc_AWG"]},
        "dc_output": {"gauge": r_dc["final_gauge_AWG"], "ocpd_a": r_dc["ocpd_A"], "egc": r_dc["egc_AWG"]},
        "compliance": "Art.710 stand-alone; grid feeds only the charger (branch-circuit load); "
                      "no 705 interconnection, no AC paralleling, no backfeed.",
        "verdict": "PASS (coordinate charge current)" if verdict else "REVIEW",
    })


if __name__ == "__main__":
    main()