feat: implement missing placeholder functions (sleep, vitals, rest)
Implementiert 6 fehlende Platzhalter-Funktionen die im Katalog waren aber keine Berechnung hatten. Neue Funktionen: - get_sleep_avg_duration(7d) → "7.5h" - get_sleep_avg_quality(7d) → "65% (Deep+REM)" - get_rest_days_count(30d) → "5 Ruhetage" - get_vitals_avg_hr(7d) → "58 bpm" - get_vitals_avg_hrv(7d) → "45 ms" - get_vitals_vo2_max() → "42.5 ml/kg/min" Datenquellen: - sleep_log (JSONB segments mit Deep/REM/Light/Awake) - rest_days (Kraft/Cardio/Entspannung) - vitals_baseline (resting_hr, hrv, vo2_max) Jetzt in PLACEHOLDER_MAP registriert → sofort nutzbar. Fixes: Platzhalter-Export zeigt jetzt alle Werte (statt "nicht verfügbar") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
555ff62b56
commit
a9114bc40a
|
|
@ -290,6 +290,138 @@ def get_trainingstyp_verteilung(profile_id: str, days: int = 14) -> str:
|
|||
return ", ".join(parts)
|
||||
|
||||
|
||||
def get_sleep_avg_duration(profile_id: str, days: int = 7) -> str:
|
||||
"""Calculate average sleep duration in hours."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
cur.execute(
|
||||
"""SELECT sleep_segments FROM sleep_log
|
||||
WHERE profile_id=%s AND date >= %s
|
||||
ORDER BY date DESC""",
|
||||
(profile_id, cutoff)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if not rows:
|
||||
return "nicht verfügbar"
|
||||
|
||||
total_minutes = 0
|
||||
for row in rows:
|
||||
segments = row['sleep_segments']
|
||||
if segments:
|
||||
# Sum duration_min from all segments
|
||||
for seg in segments:
|
||||
total_minutes += seg.get('duration_min', 0)
|
||||
|
||||
if total_minutes == 0:
|
||||
return "nicht verfügbar"
|
||||
|
||||
avg_hours = total_minutes / len(rows) / 60
|
||||
return f"{avg_hours:.1f}h"
|
||||
|
||||
|
||||
def get_sleep_avg_quality(profile_id: str, days: int = 7) -> str:
|
||||
"""Calculate average sleep quality (Deep+REM %)."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
cur.execute(
|
||||
"""SELECT sleep_segments FROM sleep_log
|
||||
WHERE profile_id=%s AND date >= %s
|
||||
ORDER BY date DESC""",
|
||||
(profile_id, cutoff)
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if not rows:
|
||||
return "nicht verfügbar"
|
||||
|
||||
total_quality = 0
|
||||
count = 0
|
||||
for row in rows:
|
||||
segments = row['sleep_segments']
|
||||
if segments:
|
||||
deep_rem_min = sum(s.get('duration_min', 0) for s in segments if s.get('stage') in ['Deep', 'REM'])
|
||||
total_min = sum(s.get('duration_min', 0) for s in segments)
|
||||
if total_min > 0:
|
||||
quality_pct = (deep_rem_min / total_min) * 100
|
||||
total_quality += quality_pct
|
||||
count += 1
|
||||
|
||||
if count == 0:
|
||||
return "nicht verfügbar"
|
||||
|
||||
avg_quality = total_quality / count
|
||||
return f"{avg_quality:.0f}% (Deep+REM)"
|
||||
|
||||
|
||||
def get_rest_days_count(profile_id: str, days: int = 30) -> str:
|
||||
"""Count rest days in the given period."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
cur.execute(
|
||||
"""SELECT COUNT(DISTINCT date) as count FROM rest_days
|
||||
WHERE profile_id=%s AND date >= %s""",
|
||||
(profile_id, cutoff)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
count = row['count'] if row else 0
|
||||
return f"{count} Ruhetage"
|
||||
|
||||
|
||||
def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str:
|
||||
"""Calculate average resting heart rate."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
cur.execute(
|
||||
"""SELECT AVG(resting_hr) as avg FROM vitals_baseline
|
||||
WHERE profile_id=%s AND date >= %s AND resting_hr IS NOT NULL""",
|
||||
(profile_id, cutoff)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if row and row['avg']:
|
||||
return f"{int(row['avg'])} bpm"
|
||||
return "nicht verfügbar"
|
||||
|
||||
|
||||
def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
|
||||
"""Calculate average heart rate variability."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
||||
cur.execute(
|
||||
"""SELECT AVG(hrv) as avg FROM vitals_baseline
|
||||
WHERE profile_id=%s AND date >= %s AND hrv IS NOT NULL""",
|
||||
(profile_id, cutoff)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if row and row['avg']:
|
||||
return f"{int(row['avg'])} ms"
|
||||
return "nicht verfügbar"
|
||||
|
||||
|
||||
def get_vitals_vo2_max(profile_id: str) -> str:
|
||||
"""Get latest VO2 Max value."""
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute(
|
||||
"""SELECT vo2_max FROM vitals_baseline
|
||||
WHERE profile_id=%s AND vo2_max IS NOT NULL
|
||||
ORDER BY date DESC LIMIT 1""",
|
||||
(profile_id,)
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if row and row['vo2_max']:
|
||||
return f"{row['vo2_max']:.1f} ml/kg/min"
|
||||
return "nicht verfügbar"
|
||||
|
||||
|
||||
# ── Placeholder Registry ──────────────────────────────────────────────────────
|
||||
|
||||
PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
||||
|
|
@ -323,6 +455,16 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
|||
'{{activity_detail}}': get_activity_detail,
|
||||
'{{trainingstyp_verteilung}}': get_trainingstyp_verteilung,
|
||||
|
||||
# Schlaf & Erholung
|
||||
'{{sleep_avg_duration}}': lambda pid: get_sleep_avg_duration(pid, 7),
|
||||
'{{sleep_avg_quality}}': lambda pid: get_sleep_avg_quality(pid, 7),
|
||||
'{{rest_days_count}}': lambda pid: get_rest_days_count(pid, 30),
|
||||
|
||||
# Vitalwerte
|
||||
'{{vitals_avg_hr}}': lambda pid: get_vitals_avg_hr(pid, 7),
|
||||
'{{vitals_avg_hrv}}': lambda pid: get_vitals_avg_hrv(pid, 7),
|
||||
'{{vitals_vo2_max}}': get_vitals_vo2_max,
|
||||
|
||||
# Zeitraum
|
||||
'{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y'),
|
||||
'{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage',
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user