"""08 — Actual household load (Hardwick Electric) vs system capacity.
Real billed kWh from the owner's Hardwick Electric Dept account, finally closing the
"load baseline" open item (Key Decisions §46, Swarm §1). Compares actual usage against
the supportable-load envelope (07) and production (02).
The raw billing series is saved in data/hardwick_usage.csv (provenance in its header)
and loaded via common.load_usage() so any calc can reuse it — this script no longer
embeds the numbers.
Run: ~/.venvs/prsm-electrical/bin/python scripts/08_load_actual.py
"""
from common import banner, result, dump_result, load_result, load_usage
DIM = {"01": 31, "02": 28, "03": 31, "04": 30, "05": 31, "06": 30,
"07": 31, "08": 31, "09": 30, "10": 31, "11": 30, "12": 31}
SEASON = {"12": "winter", "01": "winter", "02": "winter",
"06": "summer", "07": "summer", "08": "summer",
"03": "shoulder", "04": "shoulder", "05": "shoulder",
"09": "shoulder", "10": "shoulder", "11": "shoulder"}
def mm(entry):
return entry["bill_date"][:2]
def daily_avg(entries):
kwh = sum(e["net_kwh"] for e in entries)
days = sum(DIM[mm(e)] for e in entries)
return kwh, days, kwh / days
def main():
banner("08 ACTUAL LOAD (Hardwick Electric) vs CAPACITY")
p07 = load_result("07_load_capacity")
p02 = load_result("02_production")
ledger = load_usage() # data/hardwick_usage.csv, newest first
last12 = ledger[:12]
kwh12, days12, dpd12 = daily_avg(last12)
result("source", "data/hardwick_usage.csv", "", f"{len(ledger)} months on file")
result("last 12 mo total", round(kwh12), "kWh/yr", "Jun 2025 – May 2026")
result("monthly average", round(kwh12 / 12), "kWh/mo")
result("DAILY average", round(dpd12, 1), "kWh/day")
months = [e["net_kwh"] for e in last12]
result("min / max month", f"{min(months)} / {max(months)}", "kWh",
"Jan 744 / Aug 1677 — SUMMER-peaking")
print("\n MONTHLY DETAIL (last 12 mo) — kWh · net cost · avg $/kWh:")
print(f" {'month':>9}{'kWh':>7}{'net $':>10}{'$/kWh':>9}")
for e in last12:
print(f" {e['bill_date']:>9}{e['net_kwh']:>7}{e['bill_amount']:>10.2f}"
f"{e['avg_cost_per_kwh']:>9.3f}")
seas = {}
for s in ("winter", "shoulder", "summer"):
grp = [e for e in last12 if SEASON[mm(e)] == s]
_, _, dpd = daily_avg(grp)
seas[s] = round(dpd, 1)
aug = next(e["net_kwh"] for e in last12 if mm(e) == "08") / 31
print()
result("winter daily avg (D/J/F)", seas["winter"], "kWh/day")
result("shoulder daily avg", seas["shoulder"], "kWh/day")
result("summer daily avg (J/J/A)", seas["summer"], "kWh/day")
result(" August peak", round(aug, 1), "kWh/day", "the binding month for THIS load")
sup = {r["season"]: r["supportable_kwh_day"] for r in p07["supportable_daily_load_kwh"]}
prod = p07["production_kwh_day"]
pairs = [("winter", seas["winter"], sup["Winter avg (Dec-Jan)"], prod["Winter avg (Dec-Jan)"]),
("shoulder", seas["shoulder"], sup["Shoulder (Mar/Apr/Sep/Oct)"], prod["Shoulder (Mar/Apr/Sep/Oct)"]),
("summer", seas["summer"], sup["Summer (Jun)"], prod["Summer (Jun)"]),
("August", aug, sup["Summer (Jun)"], prod["Summer (Jun)"])]
print("\n ACTUAL vs SUPPORTABLE (solar-only):")
print(f" {'season':<10}{'actual':>9}{'supportable':>14}{'solar prod':>12}{'solar covers':>14}")
cmp = {}
for name, act, s, pr in pairs:
cover = 100 * pr / act
print(f" {name:<10}{act:>7.1f} {f'{s[0]}-{s[1]}':>12} {pr:>9.1f} {cover:>10.0f}% ")
cmp[name] = {"actual_kwh_day": round(act, 1), "supportable": s,
"solar_prod_kwh_day": pr, "solar_covers_pct": round(cover)}
ann_prod = p02["ac_annual_kwh"]
bal = 100 * ann_prod / kwh12
print()
result("annual production (02)", ann_prod, "kWh/yr")
result("annual consumption", round(kwh12), "kWh/yr")
result("array = % of annual load", round(bal), "%", "8 kW array UNDERSIZED vs full load")
annual_cost = sum(e["bill_amount"] for e in last12)
wavg = annual_cost / kwh12
costs = [e["bill_amount"] for e in last12]
result("annual electric cost", round(annual_cost, 2), "$/yr", "sum of last 12 bills")
result("weighted avg cost", round(wavg, 3), "$/kWh", "annual cost / annual kWh")
result("min / max monthly bill", f"{min(costs):.2f} / {max(costs):.2f}", "$")
verdict = ("Phase-1 (~8 kW / 16 kWh) offsets ~half (Aug) to ~90% (shoulder) of this load "
"by season; full off-grid coverage needs load reduction (propane/shedding) + the "
"Phase-2 PV expansion + 2nd battery. Confirms Swarm 'Phase 2 = more PV'.")
print("\n VERDICT:", verdict)
dump_result("08_load_actual", {
"source": "data/hardwick_usage.csv",
"last12_kwh_yr": round(kwh12), "monthly_avg_kwh": round(kwh12 / 12),
"daily_avg_kwh": round(dpd12, 1),
"min_month_kwh": min(months), "max_month_kwh": max(months),
"seasonal_daily_kwh": seas, "august_peak_kwh_day": round(aug, 1),
"vs_supportable": cmp,
"annual_production_kwh": ann_prod, "array_pct_of_load": round(bal),
"annual_cost_usd": round(annual_cost, 2),
"weighted_avg_cost_per_kwh": round(wavg, 3),
"min_monthly_bill_usd": min(costs), "max_monthly_bill_usd": max(costs),
"verdict": verdict,
})
if __name__ == "__main__":
main()