Add pump profile fetching and Insulineprofiel page #29
@@ -252,3 +252,21 @@ def fetch_treatments_range(start_date: date, end_date: date) -> pd.DataFrame:
|
|||||||
since = datetime.combine(start_date, time.min, tzinfo=timezone.utc)
|
since = datetime.combine(start_date, time.min, tzinfo=timezone.utc)
|
||||||
until = datetime.combine(end_date, time.max, tzinfo=timezone.utc)
|
until = datetime.combine(end_date, time.max, tzinfo=timezone.utc)
|
||||||
return _treatments_since(since, until)
|
return _treatments_since(since, until)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_profile() -> dict:
|
||||||
|
"""Haal het meest recent gebruikte pompprofiel op.
|
||||||
|
|
||||||
|
Nightscout's /api/v1/profile.json geeft profieldocumenten terug, nieuwste
|
||||||
|
eerst; met count=1 krijgen we alleen het actief gebruikte document. Een
|
||||||
|
profiel is geen tijdreeks (in tegenstelling tot entries/treatments) dus
|
||||||
|
geen paginering nodig — één klein verzoek volstaat.
|
||||||
|
|
||||||
|
Geeft {} terug als er geen profiel bestaat (bijv. lege Nightscout-instantie).
|
||||||
|
Het resultaat is het rauwe Nightscout-documument; zie profile_data.parse_profile()
|
||||||
|
voor de genormaliseerde vorm (basaalschema, ISF, koolhydraatratio, DIA, streefwaarden).
|
||||||
|
"""
|
||||||
|
data = _get("/api/v1/profile.json", {"count": 1})
|
||||||
|
if not data:
|
||||||
|
return {}
|
||||||
|
return data[0]
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import plotly.graph_objects as go
|
||||||
|
import requests
|
||||||
|
import streamlit as st
|
||||||
|
|
||||||
|
from auth import require_auth
|
||||||
|
from nightscout_client import NightscoutConfigError
|
||||||
|
from profile_data import segments_table, segments_to_hourly_bars, total_daily_basal
|
||||||
|
from ui_common import get_colors, load_profile
|
||||||
|
|
||||||
|
st.set_page_config(page_title="Glucose dashboard — Insulineprofiel", page_icon="📊", layout="wide")
|
||||||
|
require_auth()
|
||||||
|
|
||||||
|
st.title("Insulineprofiel")
|
||||||
|
st.caption("Het meest recent gebruikte pompprofiel: basaalschema, ISF, koolhydraatratio en DIA.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
profile = load_profile()
|
||||||
|
except NightscoutConfigError as exc:
|
||||||
|
st.error(str(exc))
|
||||||
|
st.stop()
|
||||||
|
except (requests.RequestException, ValueError) as exc:
|
||||||
|
st.error(f"Kon geen profiel ophalen van Nightscout: {exc}")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
if not profile:
|
||||||
|
st.info("Geen pompprofiel gevonden op deze Nightscout-instantie.")
|
||||||
|
st.stop()
|
||||||
|
|
||||||
|
colors = get_colors()
|
||||||
|
|
||||||
|
col1, col2 = st.columns(2)
|
||||||
|
col1.metric("DIA (werkingsduur insuline)", f"{profile['dia']:.1f} uur")
|
||||||
|
col2.caption(f"Profielnaam: {profile['name'] or '—'}")
|
||||||
|
|
||||||
|
st.subheader("Basaalschema")
|
||||||
|
bars = segments_to_hourly_bars(profile["basal"])
|
||||||
|
fig = go.Figure(
|
||||||
|
go.Scatter(
|
||||||
|
x=[b["hour"] for b in bars],
|
||||||
|
y=[b["value"] for b in bars],
|
||||||
|
mode="lines",
|
||||||
|
line={"shape": "hv", "color": colors["basal"], "width": 2},
|
||||||
|
fill="tozeroy",
|
||||||
|
fillcolor=colors["basal"],
|
||||||
|
opacity=0.7,
|
||||||
|
name="Basaal",
|
||||||
|
hovertemplate="%{x:.0f}:00 · %{y:.2f} E/uur<extra></extra>",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
fig.update_layout(
|
||||||
|
paper_bgcolor="rgba(0,0,0,0)",
|
||||||
|
plot_bgcolor="rgba(0,0,0,0)",
|
||||||
|
font_color=colors["text"],
|
||||||
|
height=320,
|
||||||
|
margin={"l": 10, "r": 10, "t": 10, "b": 10},
|
||||||
|
xaxis={"title": "Uur van de dag", "range": [0, 24], "dtick": 2, "gridcolor": colors["grid"]},
|
||||||
|
yaxis={"title": "E/uur", "gridcolor": colors["grid"], "rangemode": "tozero"},
|
||||||
|
showlegend=False,
|
||||||
|
)
|
||||||
|
st.plotly_chart(fig, width="stretch")
|
||||||
|
|
||||||
|
basal_rows = segments_table(profile["basal"])
|
||||||
|
st.dataframe(
|
||||||
|
pd.DataFrame(basal_rows).rename(
|
||||||
|
columns={"start": "Van", "end": "Tot", "value": "Basaalsnelheid (E/uur)"}
|
||||||
|
),
|
||||||
|
width="stretch",
|
||||||
|
hide_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
col_isf, col_ratio = st.columns(2)
|
||||||
|
with col_isf:
|
||||||
|
st.subheader("Insulinegevoeligheid (ISF)")
|
||||||
|
isf_rows = segments_table(profile["sens"])
|
||||||
|
st.dataframe(
|
||||||
|
pd.DataFrame(isf_rows).rename(
|
||||||
|
columns={"start": "Van", "end": "Tot", "value": "ISF (mmol/L per E)"}
|
||||||
|
),
|
||||||
|
width="stretch",
|
||||||
|
hide_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
with col_ratio:
|
||||||
|
st.subheader("Koolhydraatratio")
|
||||||
|
ratio_rows = segments_table(profile["carbratio"])
|
||||||
|
st.dataframe(
|
||||||
|
pd.DataFrame(ratio_rows).rename(
|
||||||
|
columns={"start": "Van", "end": "Tot", "value": "Ratio (g koolhydraten per E)"}
|
||||||
|
),
|
||||||
|
width="stretch",
|
||||||
|
hide_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
st.caption(
|
||||||
|
f"Totale dagelijkse basaal: {total_daily_basal(profile['basal']):.1f} E "
|
||||||
|
"(volgens profielschema, exclusief tijdelijke aanpassingen)."
|
||||||
|
)
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
Parsing van het Nightscout-pompprofiel (/api/v1/profile): basaalschema,
|
||||||
|
insulinegevoeligheid (ISF), koolhydraatratio, DIA en streefwaarden.
|
||||||
|
|
||||||
|
Framework-vrije module (geen streamlit-import), zelfde opzet als reporting.py:
|
||||||
|
puur functies op dicts, zodat dit los van de UI te unit-testen is. Zie
|
||||||
|
nightscout_client.fetch_profile() voor de ruwe fetch; deze module normaliseert
|
||||||
|
alleen (uren -> minuten sinds middernacht, mg/dl -> mmol/L waar nodig).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from nightscout_client import mgdl_to_mmol
|
||||||
|
|
||||||
|
SEGMENT_FIELDS = ("basal", "sens", "carbratio", "target_low", "target_high")
|
||||||
|
|
||||||
|
|
||||||
|
def _time_to_minutes(time_str: str) -> int:
|
||||||
|
"""Zet 'HH:MM' (Nightscout's segmenttijd-notatie) om naar minuten sinds middernacht."""
|
||||||
|
hours, minutes = time_str.split(":")
|
||||||
|
return int(hours) * 60 + int(minutes)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_segments(raw_segments: list[dict], *, to_mmol: bool = False) -> list[dict]:
|
||||||
|
"""Zet Nightscout-segmenten ([{'time': 'HH:MM', 'value': ...}, ...]) om naar
|
||||||
|
[{'minutes': int, 'value': float}, ...], oplopend gesorteerd op tijd."""
|
||||||
|
segments = []
|
||||||
|
for seg in raw_segments:
|
||||||
|
value = float(seg["value"])
|
||||||
|
if to_mmol:
|
||||||
|
value = mgdl_to_mmol(value)
|
||||||
|
segments.append({"minutes": _time_to_minutes(seg["time"]), "value": value})
|
||||||
|
return sorted(segments, key=lambda s: s["minutes"])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_profile(raw: dict) -> dict:
|
||||||
|
"""Parse één Nightscout-profieldocument (zoals teruggegeven door
|
||||||
|
nightscout_client.fetch_profile()) naar een genormaliseerde dict:
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": str,
|
||||||
|
"units": "mmol", # altijd genormaliseerd, ongeacht bronformaat
|
||||||
|
"dia": float, # uur
|
||||||
|
"basal": [{"minutes": int, "value": float}, ...], # E/uur
|
||||||
|
"sens": [{"minutes": int, "value": float}, ...], # mmol/L per E (ISF)
|
||||||
|
"carbratio": [{"minutes": int, "value": float}, ...], # g koolhydraten per E
|
||||||
|
"target_low": [{"minutes": int, "value": float}, ...], # mmol/L
|
||||||
|
"target_high": [{"minutes": int, "value": float}, ...], # mmol/L
|
||||||
|
}
|
||||||
|
|
||||||
|
Geeft {} terug als er geen (bruikbaar) profiel is.
|
||||||
|
"""
|
||||||
|
if not raw:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
default_name = raw.get("defaultProfile")
|
||||||
|
store = raw.get("store", {})
|
||||||
|
profile = store.get(default_name) if default_name else None
|
||||||
|
if profile is None and store:
|
||||||
|
# defaultProfile ontbreekt of matcht niet met een sleutel in store:
|
||||||
|
# val terug op het enige/eerste beschikbare profiel.
|
||||||
|
profile = next(iter(store.values()))
|
||||||
|
if profile is None:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
units = str(profile.get("units", "mg/dl")).strip().lower()
|
||||||
|
to_mmol = units != "mmol"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": default_name or "",
|
||||||
|
"units": "mmol",
|
||||||
|
"dia": float(profile.get("dia", 0) or 0),
|
||||||
|
"basal": _parse_segments(profile.get("basal", [])),
|
||||||
|
"sens": _parse_segments(profile.get("sens", []), to_mmol=to_mmol),
|
||||||
|
"carbratio": _parse_segments(profile.get("carbratio", [])),
|
||||||
|
"target_low": _parse_segments(profile.get("target_low", []), to_mmol=to_mmol),
|
||||||
|
"target_high": _parse_segments(profile.get("target_high", []), to_mmol=to_mmol),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def value_at(segments: list[dict], minutes: int) -> float | None:
|
||||||
|
"""Geef de waarde terug die actief is op het gegeven tijdstip (minuten
|
||||||
|
sinds middernacht) — het laatste segment met `minutes <= t`. Ligt `t` vóór
|
||||||
|
het eerste segment van de dag, dan geldt het laatste segment (dat loopt
|
||||||
|
door vanaf de vorige dag, want een profiel dekt altijd de volle 24 uur).
|
||||||
|
|
||||||
|
Geeft None terug als `segments` leeg is.
|
||||||
|
"""
|
||||||
|
if not segments:
|
||||||
|
return None
|
||||||
|
active = None
|
||||||
|
for seg in segments:
|
||||||
|
if seg["minutes"] <= minutes:
|
||||||
|
active = seg
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if active is None:
|
||||||
|
active = segments[-1]
|
||||||
|
return active["value"]
|
||||||
|
|
||||||
|
|
||||||
|
def _format_time(minutes: int) -> str:
|
||||||
|
"""Zet minuten sinds middernacht om naar 'HH:MM'."""
|
||||||
|
return f"{minutes // 60:02d}:{minutes % 60:02d}"
|
||||||
|
|
||||||
|
|
||||||
|
def segments_table(segments: list[dict]) -> list[dict]:
|
||||||
|
"""Zet segmenten om naar rijen met een start- en eindtijd voor tabelweergave:
|
||||||
|
[{'start': 'HH:MM', 'end': 'HH:MM', 'value': float}, ...]. Het laatste
|
||||||
|
segment loopt door tot 24:00 (een profiel dekt altijd de volle dag)."""
|
||||||
|
rows = []
|
||||||
|
for i, seg in enumerate(segments):
|
||||||
|
end_minutes = segments[i + 1]["minutes"] if i + 1 < len(segments) else 24 * 60
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"start": _format_time(seg["minutes"]),
|
||||||
|
"end": "24:00" if end_minutes == 24 * 60 else _format_time(end_minutes),
|
||||||
|
"value": seg["value"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def total_daily_basal(segments: list[dict]) -> float:
|
||||||
|
"""Totale basaaldosis per dag in E: som van elk segment se duur (uur) x snelheid (E/uur)."""
|
||||||
|
if not segments:
|
||||||
|
return 0.0
|
||||||
|
total = 0.0
|
||||||
|
for i, seg in enumerate(segments):
|
||||||
|
end_minutes = segments[i + 1]["minutes"] if i + 1 < len(segments) else 24 * 60
|
||||||
|
total += seg["value"] * (end_minutes - seg["minutes"]) / 60
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
def segments_to_hourly_bars(segments: list[dict]) -> list[dict]:
|
||||||
|
"""Zet segmenten om naar een lijst geschikt voor een staafgrafiek per uur van
|
||||||
|
de dag: [{'hour': float, 'value': float}, ...], met een extra punt op uur 24
|
||||||
|
(gelijk aan het eerste segment) zodat een staaf/stap-grafiek de volle dag dekt.
|
||||||
|
|
||||||
|
Gebruikt door de basaalgrafiek op de Insulineprofiel-pagina.
|
||||||
|
"""
|
||||||
|
if not segments:
|
||||||
|
return []
|
||||||
|
bars = [{"hour": seg["minutes"] / 60, "value": seg["value"]} for seg in segments]
|
||||||
|
bars.append({"hour": 24.0, "value": segments[0]["value"]})
|
||||||
|
return bars
|
||||||
@@ -357,6 +357,29 @@ def test_fetch_treatments_range_retrieves_full_history_even_with_a_hidden_server
|
|||||||
assert result["timestamp"].max().date() == end_date
|
assert result["timestamp"].max().date() == end_date
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_profile_returns_first_document(monkeypatch):
|
||||||
|
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
|
||||||
|
payload = [{"defaultProfile": "Default", "store": {"Default": {"dia": "5"}}}]
|
||||||
|
|
||||||
|
seen_params = []
|
||||||
|
monkeypatch.setattr(
|
||||||
|
ns.requests,
|
||||||
|
"get",
|
||||||
|
lambda *a, **k: (seen_params.append(k["params"]), FakeResponse(payload))[1],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = ns.fetch_profile()
|
||||||
|
assert result == payload[0]
|
||||||
|
assert seen_params[0]["count"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_fetch_profile_handles_empty_response(monkeypatch):
|
||||||
|
monkeypatch.setenv("NIGHTSCOUT_URL", "https://example.com")
|
||||||
|
monkeypatch.setattr(ns.requests, "get", lambda *a, **k: FakeResponse([]))
|
||||||
|
|
||||||
|
assert ns.fetch_profile() == {}
|
||||||
|
|
||||||
|
|
||||||
def test_paginated_get_uses_created_at_field_for_treatments_cursor(monkeypatch):
|
def test_paginated_get_uses_created_at_field_for_treatments_cursor(monkeypatch):
|
||||||
"""Losstaande, gerichte assertie op het daadwerkelijke queryparameter-veld
|
"""Losstaande, gerichte assertie op het daadwerkelijke queryparameter-veld
|
||||||
— moet `find[created_at][$lte]` zijn, nooit `find[date][$lte]`, voor het
|
— moet `find[created_at][$lte]` zijn, nooit `find[date][$lte]`, voor het
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
import profile_data as pd_
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_profile(units="mg/dl", default_name="Default"):
|
||||||
|
return {
|
||||||
|
"defaultProfile": default_name,
|
||||||
|
"store": {
|
||||||
|
"Default": {
|
||||||
|
"units": units,
|
||||||
|
"dia": "5",
|
||||||
|
"basal": [
|
||||||
|
{"time": "00:00", "value": "0.8"},
|
||||||
|
{"time": "06:00", "value": "1.2"},
|
||||||
|
{"time": "22:00", "value": "0.7"},
|
||||||
|
],
|
||||||
|
"sens": [{"time": "00:00", "value": "50"}],
|
||||||
|
"carbratio": [{"time": "00:00", "value": "10"}],
|
||||||
|
"target_low": [{"time": "00:00", "value": "90"}],
|
||||||
|
"target_high": [{"time": "00:00", "value": "120"}],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_returns_empty_dict_for_empty_input():
|
||||||
|
assert pd_.parse_profile({}) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_converts_mgdl_to_mmol():
|
||||||
|
profile = pd_.parse_profile(_raw_profile(units="mg/dl"))
|
||||||
|
assert profile["units"] == "mmol"
|
||||||
|
# 90 mg/dl -> ~5.0 mmol/L, 120 mg/dl -> ~6.7 mmol/L, 50 mg/dl (ISF) -> ~2.8 mmol/L
|
||||||
|
assert profile["target_low"][0]["value"] == pytest.approx(5.0, abs=0.05)
|
||||||
|
assert profile["target_high"][0]["value"] == pytest.approx(6.7, abs=0.05)
|
||||||
|
assert profile["sens"][0]["value"] == pytest.approx(2.8, abs=0.05)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_leaves_mmol_profiles_unconverted():
|
||||||
|
profile = pd_.parse_profile(_raw_profile(units="mmol"))
|
||||||
|
assert profile["target_low"][0]["value"] == pytest.approx(90.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_does_not_convert_basal_or_carbratio():
|
||||||
|
profile = pd_.parse_profile(_raw_profile(units="mg/dl"))
|
||||||
|
assert profile["basal"][0]["value"] == pytest.approx(0.8)
|
||||||
|
assert profile["carbratio"][0]["value"] == pytest.approx(10.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_sorts_basal_segments_by_time():
|
||||||
|
raw = _raw_profile()
|
||||||
|
raw["store"]["Default"]["basal"] = [
|
||||||
|
{"time": "22:00", "value": "0.7"},
|
||||||
|
{"time": "00:00", "value": "0.8"},
|
||||||
|
{"time": "06:00", "value": "1.2"},
|
||||||
|
]
|
||||||
|
profile = pd_.parse_profile(raw)
|
||||||
|
assert [seg["minutes"] for seg in profile["basal"]] == [0, 360, 1320]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_falls_back_to_first_profile_when_default_name_missing():
|
||||||
|
raw = _raw_profile(default_name="DoesNotExist")
|
||||||
|
profile = pd_.parse_profile(raw)
|
||||||
|
assert profile["dia"] == pytest.approx(5.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_profile_dia_and_name():
|
||||||
|
profile = pd_.parse_profile(_raw_profile())
|
||||||
|
assert profile["dia"] == pytest.approx(5.0)
|
||||||
|
assert profile["name"] == "Default"
|
||||||
|
|
||||||
|
|
||||||
|
def test_value_at_returns_active_segment():
|
||||||
|
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}, {"minutes": 1320, "value": 0.7}]
|
||||||
|
assert pd_.value_at(segments, 100) == pytest.approx(0.8)
|
||||||
|
assert pd_.value_at(segments, 360) == pytest.approx(1.2)
|
||||||
|
assert pd_.value_at(segments, 1000) == pytest.approx(1.2)
|
||||||
|
assert pd_.value_at(segments, 1350) == pytest.approx(0.7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_value_at_wraps_to_last_segment_before_first():
|
||||||
|
segments = [{"minutes": 360, "value": 1.2}, {"minutes": 1320, "value": 0.7}]
|
||||||
|
assert pd_.value_at(segments, 100) == pytest.approx(0.7)
|
||||||
|
|
||||||
|
|
||||||
|
def test_value_at_empty_segments_returns_none():
|
||||||
|
assert pd_.value_at([], 100) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_segments_table_computes_end_times_from_next_segment():
|
||||||
|
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}]
|
||||||
|
rows = pd_.segments_table(segments)
|
||||||
|
assert rows == [
|
||||||
|
{"start": "00:00", "end": "06:00", "value": 0.8},
|
||||||
|
{"start": "06:00", "end": "24:00", "value": 1.2},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_segments_to_hourly_bars_closes_the_24h_loop():
|
||||||
|
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}]
|
||||||
|
bars = pd_.segments_to_hourly_bars(segments)
|
||||||
|
assert bars[-1] == {"hour": 24.0, "value": 0.8}
|
||||||
|
|
||||||
|
|
||||||
|
def test_segments_to_hourly_bars_empty_input():
|
||||||
|
assert pd_.segments_to_hourly_bars([]) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_daily_basal_weights_by_segment_duration():
|
||||||
|
# 6h @ 0.8 + 18h @ 1.2 = 4.8 + 21.6 = 26.4
|
||||||
|
segments = [{"minutes": 0, "value": 0.8}, {"minutes": 360, "value": 1.2}]
|
||||||
|
assert pd_.total_daily_basal(segments) == pytest.approx(26.4)
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_daily_basal_empty_input():
|
||||||
|
assert pd_.total_daily_basal([]) == 0.0
|
||||||
+21
-1
@@ -14,7 +14,8 @@ import pandas as pd
|
|||||||
import plotly.graph_objects as go
|
import plotly.graph_objects as go
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
|
||||||
from nightscout_client import fetch_entries_range, fetch_treatments_range
|
from nightscout_client import fetch_entries_range, fetch_profile, fetch_treatments_range
|
||||||
|
from profile_data import parse_profile
|
||||||
from reporting import (
|
from reporting import (
|
||||||
HIGH,
|
HIGH,
|
||||||
LOW,
|
LOW,
|
||||||
@@ -57,6 +58,11 @@ PALETTE = {
|
|||||||
"tir_in_range": "#0ca30c",
|
"tir_in_range": "#0ca30c",
|
||||||
"tir_high": "#fab219",
|
"tir_high": "#fab219",
|
||||||
"tir_very_high": "#c76f0a",
|
"tir_very_high": "#c76f0a",
|
||||||
|
"basal": "#7b5ea3",
|
||||||
|
"bolus_food": "#1baf7a",
|
||||||
|
"bolus_correction": "#2a78d6",
|
||||||
|
"bolus_smb": "#898781",
|
||||||
|
"target": "#52514e",
|
||||||
},
|
},
|
||||||
"dark": {
|
"dark": {
|
||||||
"glucose": "#3987e5",
|
"glucose": "#3987e5",
|
||||||
@@ -76,6 +82,11 @@ PALETTE = {
|
|||||||
"tir_in_range": "#0ca30c",
|
"tir_in_range": "#0ca30c",
|
||||||
"tir_high": "#fab219",
|
"tir_high": "#fab219",
|
||||||
"tir_very_high": "#d9852e",
|
"tir_very_high": "#d9852e",
|
||||||
|
"basal": "#9b7ec2",
|
||||||
|
"bolus_food": "#199e70",
|
||||||
|
"bolus_correction": "#3987e5",
|
||||||
|
"bolus_smb": "#898781",
|
||||||
|
"target": "#c3c2b7",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +140,15 @@ def load_range_data(start: date, end: date):
|
|||||||
return glucose, treatments
|
return glucose, treatments
|
||||||
|
|
||||||
|
|
||||||
|
@st.cache_data(ttl=21600, show_spinner="Profiel ophalen van Nightscout...")
|
||||||
|
def load_profile() -> dict:
|
||||||
|
"""Gecachete profiel-fetch. Langere TTL dan load_range_data (6 uur i.p.v.
|
||||||
|
1 uur): een pompprofiel wijzigt zelden, in tegenstelling tot glucose-/
|
||||||
|
behandeldata van een lopende periode."""
|
||||||
|
raw = fetch_profile()
|
||||||
|
return parse_profile(raw)
|
||||||
|
|
||||||
|
|
||||||
def add_threshold_lines(fig: go.Figure, colors: dict, row: int | None = None) -> None:
|
def add_threshold_lines(fig: go.Figure, colors: dict, row: int | None = None) -> None:
|
||||||
"""Teken de 4 klinische drempellijnen (3.0/3.9/10.0/13.9 mmol/L) op een grafiek.
|
"""Teken de 4 klinische drempellijnen (3.0/3.9/10.0/13.9 mmol/L) op een grafiek.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user