Fix bolus/basal insulin categorization: split SMB from manual corrections, add basal calc

AndroidAPS records an automatic SMB micro-bolus as eventType "Correction
Bolus" with type "SMB", not as its own eventType. bolus_stats() was
grouping by eventType alone, so every SMB was silently counted as a manual
correction bolus. Verified against production data: 100% of "Correction
Bolus" treatments over 90 days were actually type=SMB.

Also adds basal_stats(), computing delivered basal directly from Temp
Basal treatments (rate x duration) without needing the pump profile.

Closes gitea #20.
This commit is contained in:
2026-08-01 22:52:54 +02:00
parent 441bb84212
commit 9e11dd8d73
5 changed files with 184 additions and 26 deletions
+20 -10
View File
@@ -176,29 +176,39 @@ def _entries_since(since: datetime, until: datetime | None = None) -> pd.DataFra
return df[["timestamp", "mmol"]].sort_values("timestamp").reset_index(drop=True)
_TREATMENT_COLUMNS = ["timestamp", "eventType", "insulin", "carbs", "type", "absolute", "duration"]
def _treatments_since(since: datetime, until: datetime | None = None) -> pd.DataFrame:
"""Haal treatments op sinds `since` (en optioneel tot `until`), lokaal gefilterd."""
"""Haal treatments op sinds `since` (en optioneel tot `until`), lokaal gefilterd.
`type` en `absolute`/`duration` komen erbij naast de oorspronkelijke
kolommen: AndroidAPS registreert een SMB-microbolus niet als eigen
`eventType`, maar als `eventType == "Correction Bolus"` met
`type == "SMB"` (zie reporting.bolus_stats() / gitea #20) — en
`absolute` (E/uur) + `duration` (minuten) op Temp Basal-treatments maken
het mogelijk afgeleverde basaalinsuline te berekenen zonder het
pompprofiel te hoeven ophalen (zie reporting.basal_stats()).
"""
data = _paginated_get("/api/v1/treatments.json", since, until, TREATMENTS_CURSOR_FIELD)
if not data:
return pd.DataFrame(columns=["timestamp", "eventType", "insulin", "carbs"])
return pd.DataFrame(columns=_TREATMENT_COLUMNS)
df = pd.DataFrame(data)
df["timestamp"] = pd.to_datetime(df["created_at"], utc=True).dt.tz_localize(None)
for col in ("insulin", "carbs"):
for col in ("insulin", "carbs", "absolute", "duration"):
if col not in df.columns:
df[col] = pd.NA
df[col] = pd.to_numeric(df[col], errors="coerce")
if "eventType" not in df.columns:
df["eventType"] = ""
for col in ("eventType", "type"):
if col not in df.columns:
df[col] = ""
df[col] = df[col].fillna("")
df = df[df["timestamp"] >= since.replace(tzinfo=None)]
if until is not None:
df = df[df["timestamp"] <= until.replace(tzinfo=None)]
return (
df[["timestamp", "eventType", "insulin", "carbs"]]
.sort_values("timestamp")
.reset_index(drop=True)
)
return df[_TREATMENT_COLUMNS].sort_values("timestamp").reset_index(drop=True)
def fetch_entries(hours: int = 24) -> pd.DataFrame: