from __future__ import annotations from app.core_settings import AppSettings, get_settings from app.services.catalog import MetricCatalog, get_catalog from app.services.energy import EnergyService from app.services.influx_http import InfluxHTTPService from app.services.metrics import compare_delta_pct from app.utils.time import resolve_window, shift_window class AnalyticsService: def __init__( self, settings: AppSettings | None = None, catalog: MetricCatalog | None = None, influx: InfluxHTTPService | None = None, energy: EnergyService | None = None, ) -> None: self.settings = settings or get_settings() self.catalog = catalog or get_catalog() self.influx = influx or InfluxHTTPService(self.settings) self.energy = energy or EnergyService(self.settings, self.catalog, self.influx) def production( self, range_key: str | None = None, bucket: str | None = None, compare_mode: str = "none", start: str | None = None, end: str | None = None, compare_ranges: list[dict] | None = None, ) -> dict: bucket = bucket or self.settings.analytics["default_bucket"] if bucket not in self.settings.analytics["bucket_labels"]: raise ValueError(f"Unsupported bucket: {bucket}") window = resolve_window(range_key=range_key, start=start, end=end) current_days = self.energy.daily_records_for_window(window.start, window.end, persist_missing=True) current = self.energy.bucketize_daily(current_days, bucket) total = round(sum(item.value for item in current), 2) comparison = [] comparison_total = None comparison_delta_pct = None comparisons = [] if compare_mode == "custom_multi": for index, item in enumerate(compare_ranges or []): compare_start = item.get("start") compare_end = item.get("end") if not compare_start or not compare_end: continue compare_window = resolve_window(start=compare_start, end=compare_end) comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True) comparison_series = self.energy.bucketize_daily(comparison_days, bucket) comparison_total_value = round(sum(point.value for point in comparison_series), 2) comparisons.append({ "key": item.get("key") or f"custom_{index + 1}", "label": item.get("label") or f"Custom {index + 1}", "start": compare_window.start, "end": compare_window.end, "total": comparison_total_value, "delta_pct": compare_delta_pct(total, comparison_total_value), "points": comparison_series, }) if comparisons: comparison = comparisons[0]["points"] comparison_total = comparisons[0]["total"] comparison_delta_pct = comparisons[0]["delta_pct"] elif compare_mode != "none": compare_window = shift_window(window, compare_mode) comparison_days = self.energy.daily_records_for_window(compare_window.start, compare_window.end, persist_missing=True) comparison = self.energy.bucketize_daily(comparison_days, bucket) comparison_total = round(sum(item.value for item in comparison), 2) comparison_delta_pct = compare_delta_pct(total, comparison_total) comparisons.append({ "key": compare_mode, "label": compare_mode, "start": compare_window.start, "end": compare_window.end, "total": comparison_total, "delta_pct": comparison_delta_pct, "points": comparison, }) average_bucket = round(total / len(current), 2) if current else 0.0 best_bucket = max(current, key=lambda item: item.value, default=None) return { "unit": "kWh", "bucket": bucket, "compare_mode": compare_mode, "current": current, "comparison": comparison, "comparisons": comparisons, "summary": { "total": total, "unit": "kWh", "average_bucket": average_bucket, "best_bucket_label": best_bucket.label if best_bucket else "", "best_bucket_value": best_bucket.value if best_bucket else 0.0, "co2_saved_kg": round(total * self.settings.co2_factor, 2), "comparison_total": comparison_total, "comparison_delta_pct": comparison_delta_pct, }, "meta": { "window": { "start": window.start, "end": window.end, "range_key": window.key, }, "source": "sqlite_cache_plus_live_influx", }, } def distribution( self, range_key: str | None = None, bucket: str | None = None, start: str | None = None, end: str | None = None, ) -> dict: payload = self.production(range_key=range_key, bucket=bucket, compare_mode="none", start=start, end=end) current = payload["current"] total = round(sum(item.value for item in current), 2) denominator = total or 1.0 return { "unit": payload["unit"], "bucket": payload["bucket"], "total": total, "slices": [ { "label": item.label, "value": item.value, "share": round((item.value / denominator) * 100.0, 2), } for item in current if item.value > 0 ], "meta": payload["meta"], }