Spaces:
Running
Running
| # core/tools/build_glucose_plot.py | |
| from __future__ import annotations | |
| import io | |
| from datetime import datetime, timedelta | |
| from typing import List, Dict, Any, Tuple | |
| import matplotlib | |
| matplotlib.use("Agg") # Headless für HF | |
| import matplotlib.pyplot as plt | |
| from core.tools.nightscout import get_sgv_entries | |
| def _parse_entries(entries: List[Dict[str, Any]]) -> Tuple[List[datetime], List[int]]: | |
| times: List[datetime] = [] | |
| values: List[int] = [] | |
| for e in entries or []: | |
| sgv = e.get("sgv") | |
| ts = e.get("date") # ms epoch | |
| if sgv is None or ts is None: | |
| continue | |
| try: | |
| times.append(datetime.utcfromtimestamp(float(ts) / 1000.0)) | |
| values.append(int(float(sgv))) | |
| except Exception: | |
| continue | |
| return times, values | |
| def build_glucose_plot_png(hours: int = 3) -> bytes: | |
| """ | |
| PNG Glukoseverlauf der letzten `hours`. | |
| Zielbereich: 80–180 (grün) | |
| Zonen: | |
| < 80 rot | |
| 80–95 gelb | |
| 95–180 grün | |
| 180–250 gelb | |
| > 250 rot | |
| """ | |
| # ---- Nightscout Daten ---- | |
| entries: List[Dict[str, Any]] = get_sgv_entries(hours=hours) # <<< WICHTIG: hours=..., nicht since_hours | |
| if not entries: | |
| raise RuntimeError("No Nightscout data available") | |
| times, values = _parse_entries(entries) | |
| if not times: | |
| raise RuntimeError("No valid SGV points") | |
| # ---- Plot ---- | |
| fig, ax = plt.subplots(figsize=(9, 4.8)) | |
| # Achsenhintergrund explizit weiß (verhindert "alles blau" durch Styles) | |
| ax.set_facecolor("white") | |
| fig.patch.set_facecolor("white") | |
| # Y-Limits so, dass alle Zonen sichtbar sind | |
| ymin = min(40, min(values) - 20) | |
| ymax = max(300, max(values) + 20) | |
| ax.set_ylim(ymin, ymax) | |
| # Deutlichere Farben + höheres Alpha (Telegram komprimiert!) | |
| RED = "#ff6b6b" | |
| YELLOW = "#ffd166" | |
| GREEN = "#2ecc71" | |
| # Zonen (zorder klein -> liegen "hinten") | |
| ax.axhspan(ymin, 80, facecolor=RED, alpha=0.35, zorder=0) | |
| ax.axhspan(80, 95, facecolor=YELLOW, alpha=0.35, zorder=0) | |
| ax.axhspan(95, 180, facecolor=GREEN, alpha=0.28, zorder=0) | |
| ax.axhspan(180, 250, facecolor=YELLOW, alpha=0.35, zorder=0) | |
| ax.axhspan(250, ymax, facecolor=RED, alpha=0.35, zorder=0) | |
| # Zielbereich als dünne Kontur (hilft visuell) | |
| ax.axhline(80, linewidth=1, alpha=0.4, zorder=1) | |
| ax.axhline(180, linewidth=1, alpha=0.4, zorder=1) | |
| # Verlauf (zorder hoch -> immer sichtbar oben) | |
| ax.plot(times, values, linewidth=2.2, marker="o", markersize=3.2, zorder=5) | |
| ax.set_ylabel("mg/dL") | |
| ax.set_title(f"Glukoseverlauf – letzte {hours}h (Ziel: 80–180)") | |
| ax.grid(True, alpha=0.25, zorder=2) | |
| fig.autofmt_xdate() | |
| buf = io.BytesIO() | |
| plt.tight_layout() | |
| plt.savefig(buf, format="png", dpi=140) | |
| plt.close(fig) | |
| buf.seek(0) | |
| return buf.read() |