#!/usr/bin/env python3 """ fetch_chargelab.py — pull completed transactions from the ChargeLab API and save to data/ChargeLab Exports/chargelab_transactions_.csv Usage: python3 fetch_chargelab.py [--from YYYY-MM-DD] [--to YYYY-MM-DD] Defaults to fetching all available history (no --from) through today (--to). Requires CHARGELAB_API_KEY in a .env file (or environment variable). """ import csv import os import sys import argparse from datetime import datetime, timezone from pathlib import Path try: import urllib.request import urllib.parse import json except ImportError: print("ERROR: Python standard library missing — check your Python install.") sys.exit(1) BASE = Path(__file__).parent ENV_FILE = BASE / ".env" # ─── Load .env ──────────────────────────────────────────────────────────────── def load_env(): if ENV_FILE.exists(): with open(ENV_FILE) as f: for line in f: line = line.strip() if line and not line.startswith("#") and "=" in line: k, _, v = line.partition("=") os.environ.setdefault(k.strip(), v.strip().strip('"').strip("'")) load_env() API_KEY = os.environ.get("CHARGELAB_API_KEY", "") BASE_URL = "https://api.chargelab.io/core/v1" if not API_KEY: print("ERROR: CHARGELAB_API_KEY not set. Add it to .env:") print(" CHARGELAB_API_KEY=your_key_here") sys.exit(1) # ─── HTTP helper ────────────────────────────────────────────────────────────── def get(path: str, params: dict) -> dict: qs = urllib.parse.urlencode(params) url = f"{BASE_URL}{path}?{qs}" req = urllib.request.Request(url, headers={"Authorization": f"x-auth {API_KEY}"}) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) # ─── Fetch all transactions (paginated) ─────────────────────────────────────── def fetch_transactions(from_dt: str | None, to_dt: str | None) -> list[dict]: params = {"role": "COMPANY", "limit": 1000, "offset": 0} if from_dt: params["filter_ge[startTime]"] = from_dt if to_dt: params["filter_le[stopTime]"] = to_dt all_txns = [] total = None while True: print(f" Fetching offset={params['offset']} ...", end=" ", flush=True) data = get("/completedTransactions", params) if total is None: total = data.get("totalCount", "?") print(f"(totalCount={total})", flush=True) else: print(flush=True) entities = data.get("entities", []) all_txns.extend(entities) if len(entities) < params["limit"]: break params["offset"] += params["limit"] print(f" Fetched {len(all_txns)} transactions total.") return all_txns # ─── Flatten to rows ────────────────────────────────────────────────────────── COLUMNS = [ "transactionId", "chargerId", "chargerName", "locationId", "locationName", "portId", "startTime", "stopTime", "consumedEnergyKilowattHours", "pluggedInTimeSeconds", "effectiveChargingTimeSeconds", "billedTotalAmount", "billedCurrency", ] def flatten(txn: dict) -> dict: charger = txn.get("charger", {}) loc = charger.get("location", {}) return { "transactionId": txn.get("transactionId", ""), "chargerId": charger.get("chargerId", ""), "chargerName": charger.get("name", ""), "locationId": loc.get("locationId", ""), "locationName": loc.get("name", ""), "portId": txn.get("portId", ""), "startTime": txn.get("startTime", ""), "stopTime": txn.get("stopTime", ""), "consumedEnergyKilowattHours": txn.get("consumedEnergyKilowattHours", ""), "pluggedInTimeSeconds": txn.get("pluggedInTimeSeconds", ""), "effectiveChargingTimeSeconds": txn.get("effectiveChargingTimeSeconds", ""), "billedTotalAmount": txn.get("billedTotalAmount", ""), "billedCurrency": txn.get("billedCurrency", ""), } # ─── Main ───────────────────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser(description="Fetch ChargeLab completed transactions.") parser.add_argument("--from", dest="from_dt", metavar="YYYY-MM-DD", help="Start date (inclusive). Defaults to all history.") parser.add_argument("--to", dest="to_dt", metavar="YYYY-MM-DD", help="End date (inclusive). Defaults to today.") args = parser.parse_args() # Convert bare dates to ISO-8601 timestamps (UTC) from_iso = f"{args.from_dt}T00:00:00Z" if args.from_dt else None to_iso = f"{args.to_dt}T23:59:59Z" if args.to_dt else None print(f"ChargeLab API pull") print(f" from : {from_iso or '(all history)'}") print(f" to : {to_iso or '(today)'}") txns = fetch_transactions(from_iso, to_iso) if not txns: print("No transactions returned.") return # Sort by startTime txns.sort(key=lambda t: t.get("startTime", "")) # Write CSV today = datetime.now(timezone.utc).strftime("%Y-%m-%d") out_dir = BASE / "data" / "ChargeLab Exports" out_dir.mkdir(parents=True, exist_ok=True) out_path = out_dir / f"chargelab_transactions_{today}.csv" with open(out_path, "w", newline="") as f: writer = csv.DictWriter(f, fieldnames=COLUMNS) writer.writeheader() for txn in txns: writer.writerow(flatten(txn)) print(f"Saved {len(txns)} rows → {out_path.relative_to(BASE)}") # Quick summary kwh_total = sum( float(r.get("consumedEnergyKilowattHours") or 0) for r in (flatten(t) for t in txns) if r["consumedEnergyKilowattHours"] != "" ) chargers = sorted({flatten(t)["chargerName"] for t in txns if t.get("charger", {}).get("name")}) print(f" Total kWh: {kwh_total:.1f}") print(f" Chargers : {', '.join(chargers) or '(none named)'}") if __name__ == "__main__": main()