from __future__ import annotations from collections import defaultdict from datetime import datetime, timedelta, timezone from zoneinfo import ZoneInfo from fastapi import APIRouter, HTTPException, Query from app.db.repositories import get_earliest_reading_ts, get_usage_series from app.db.session import get_session router = APIRouter(prefix="/api/usage", tags=["usage"]) BNE = ZoneInfo("Australia/Brisbane") def _resolve_start(session, days: str, end: datetime) -> datetime: if days == "all": return get_earliest_reading_ts(session) or end try: n = int(days) except ValueError: raise HTTPException(400, "days must be an integer or 'all'") if n < 1: raise HTTPException(400, "days must be >= 1") return end - timedelta(days=n) def _bucket_key(local_date, granularity: str) -> str: if granularity == "month": return local_date.replace(day=1).isoformat() if granularity == "week": return (local_date - timedelta(days=local_date.weekday())).isoformat() return local_date.isoformat() @router.get("/daily") def daily_usage(days: str = Query("30")) -> list[dict]: end = datetime.now(timezone.utc) with get_session() as session: start = _resolve_start(session, days, end) rows = get_usage_series(session, start, end) # Plotting one bar per day is unreadable (and slow) once the range # stretches into years, so roll up to weekly/monthly buckets for long # ranges -- this is a display concern only, the underlying stored data # stays at full 5-minute resolution. span_days = (end - start).days granularity = "month" if span_days > 550 else "week" if span_days > 120 else "day" by_bucket: dict[str, dict[str, float]] = defaultdict(lambda: {"generation_kwh": 0.0, "load_kwh": 0.0, "import_kwh": 0.0, "export_kwh": 0.0}) for row in rows: key = _bucket_key(row.ts.astimezone(BNE).date(), granularity) bucket = by_bucket[key] bucket["generation_kwh"] += row.generation_kwh bucket["load_kwh"] += row.load_kwh bucket["import_kwh"] += row.import_kwh bucket["export_kwh"] += row.export_kwh return [{"date": day, **{k: round(v, 3) for k, v in totals.items()}} for day, totals in sorted(by_bucket.items())] @router.get("/summary") def usage_summary(days: str = Query("30")) -> dict: end = datetime.now(timezone.utc) with get_session() as session: start = _resolve_start(session, days, end) rows = get_usage_series(session, start, end) if not rows: return {"days_with_data": 0} dates_seen: set[str] = set() totals = {"generation_kwh": 0.0, "load_kwh": 0.0, "import_kwh": 0.0, "export_kwh": 0.0} for row in rows: dates_seen.add(row.ts.astimezone(BNE).date().isoformat()) totals["generation_kwh"] += row.generation_kwh totals["load_kwh"] += row.load_kwh totals["import_kwh"] += row.import_kwh totals["export_kwh"] += row.export_kwh return { "days_with_data": len(dates_seen), "first_day": min(dates_seen), "last_day": max(dates_seen), **{k: round(v, 2) for k, v in totals.items()}, }