diff --git a/clusters/homelab/apps/garmin/configmap.yaml b/clusters/homelab/apps/garmin/configmap.yaml index d1971be..9e37b07 100644 --- a/clusters/homelab/apps/garmin/configmap.yaml +++ b/clusters/homelab/apps/garmin/configmap.yaml @@ -34,13 +34,11 @@ data: today = date.today() start = today - timedelta(days=365 * 3) - activities = client.get_activities_by_date( - start.isoformat(), today.isoformat() - ) - conn = psycopg2.connect(db_url) cur = conn.cursor() + # ── Schema ──────────────────────────────────────────────────────────────── + cur.execute(""" CREATE TABLE IF NOT EXISTS activities ( id BIGINT PRIMARY KEY, @@ -66,20 +64,41 @@ data: """) cur.execute(""" - ALTER TABLE activities - ADD COLUMN IF NOT EXISTS max_hr INTEGER, - ADD COLUMN IF NOT EXISTS vo2max FLOAT, - ADD COLUMN IF NOT EXISTS calories FLOAT, - ADD COLUMN IF NOT EXISTS elevation_gain FLOAT, - ADD COLUMN IF NOT EXISTS steps INTEGER, - ADD COLUMN IF NOT EXISTS cadence FLOAT, - ADD COLUMN IF NOT EXISTS hr_zone_1 FLOAT, - ADD COLUMN IF NOT EXISTS hr_zone_2 FLOAT, - ADD COLUMN IF NOT EXISTS hr_zone_3 FLOAT, - ADD COLUMN IF NOT EXISTS hr_zone_4 FLOAT, - ADD COLUMN IF NOT EXISTS hr_zone_5 FLOAT + CREATE TABLE IF NOT EXISTS daily_health ( + date DATE PRIMARY KEY, + resting_hr INTEGER, + total_steps INTEGER, + total_distance_meters FLOAT, + active_calories FLOAT, + total_calories FLOAT, + stress_avg INTEGER, + stress_max INTEGER, + body_battery_high INTEGER, + body_battery_low INTEGER, + spo2_avg FLOAT, + spo2_min FLOAT, + respiration_avg FLOAT, + hrv_weekly_avg FLOAT, + hrv_last_night FLOAT, + hrv_status TEXT, + sleep_duration_seconds INTEGER, + sleep_deep_seconds INTEGER, + sleep_light_seconds INTEGER, + sleep_rem_seconds INTEGER, + sleep_awake_seconds INTEGER, + sleep_score INTEGER, + sleep_feedback TEXT + ) """) + conn.commit() + + # ── Activities ──────────────────────────────────────────────────────────── + + activities = client.get_activities_by_date( + start.isoformat(), today.isoformat() + ) + for a in activities: cur.execute(""" INSERT INTO activities @@ -89,20 +108,20 @@ data: VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ON CONFLICT (id) DO UPDATE SET duration_seconds = EXCLUDED.duration_seconds, - distance_meters = EXCLUDED.distance_meters, - avg_hr = EXCLUDED.avg_hr, - max_hr = EXCLUDED.max_hr, - avg_pace = EXCLUDED.avg_pace, - vo2max = EXCLUDED.vo2max, - calories = EXCLUDED.calories, - elevation_gain = EXCLUDED.elevation_gain, - steps = EXCLUDED.steps, - cadence = EXCLUDED.cadence, - hr_zone_1 = EXCLUDED.hr_zone_1, - hr_zone_2 = EXCLUDED.hr_zone_2, - hr_zone_3 = EXCLUDED.hr_zone_3, - hr_zone_4 = EXCLUDED.hr_zone_4, - hr_zone_5 = EXCLUDED.hr_zone_5 + distance_meters = EXCLUDED.distance_meters, + avg_hr = EXCLUDED.avg_hr, + max_hr = EXCLUDED.max_hr, + avg_pace = EXCLUDED.avg_pace, + vo2max = EXCLUDED.vo2max, + calories = EXCLUDED.calories, + elevation_gain = EXCLUDED.elevation_gain, + steps = EXCLUDED.steps, + cadence = EXCLUDED.cadence, + hr_zone_1 = EXCLUDED.hr_zone_1, + hr_zone_2 = EXCLUDED.hr_zone_2, + hr_zone_3 = EXCLUDED.hr_zone_3, + hr_zone_4 = EXCLUDED.hr_zone_4, + hr_zone_5 = EXCLUDED.hr_zone_5 """, ( a.get("activityId"), a.get("activityName"), @@ -126,6 +145,118 @@ data: )) conn.commit() + print(f"Synced {len(activities)} activities") + + # ── Daily Health (letzte 7 Tage, inkrementell) ──────────────────────────── + + def safe_get(fn, *args): + try: + return fn(*args) + except Exception as e: + print(f" Warnung {fn.__name__}: {e}") + return None + + health_start = today - timedelta(days=7) + check_date = health_start + synced = 0 + + while check_date <= today: + d = check_date.isoformat() + + stats = safe_get(client.get_stats, d) or {} + stress = safe_get(client.get_stress_data, d) or {} + bb_list = safe_get(client.get_body_battery, d, d) or [] + spo2 = safe_get(client.get_spo2_data, d) or {} + resp = safe_get(client.get_respiration_data, d) or {} + hrv_raw = safe_get(client.get_hrv_data, d) or {} + sleep_raw = safe_get(client.get_sleep_data, d) or {} + + hrv = hrv_raw.get("hrvSummary", {}) + sleep = sleep_raw.get("dailySleepDTO", {}) + + bb_high = max((e.get("charged", 0) for e in bb_list), default=None) + bb_low = min((e.get("drained", 0) for e in bb_list), default=None) + + cur.execute(""" + INSERT INTO daily_health ( + date, + resting_hr, total_steps, total_distance_meters, + active_calories, total_calories, + stress_avg, stress_max, + body_battery_high, body_battery_low, + spo2_avg, spo2_min, + respiration_avg, + hrv_weekly_avg, hrv_last_night, hrv_status, + sleep_duration_seconds, sleep_deep_seconds, + sleep_light_seconds, sleep_rem_seconds, sleep_awake_seconds, + sleep_score, sleep_feedback + ) VALUES ( + %s, + %s,%s,%s, + %s,%s, + %s,%s, + %s,%s, + %s,%s, + %s, + %s,%s,%s, + %s,%s, + %s,%s,%s, + %s,%s + ) + ON CONFLICT (date) DO UPDATE SET + resting_hr = EXCLUDED.resting_hr, + total_steps = EXCLUDED.total_steps, + total_distance_meters = EXCLUDED.total_distance_meters, + active_calories = EXCLUDED.active_calories, + total_calories = EXCLUDED.total_calories, + stress_avg = EXCLUDED.stress_avg, + stress_max = EXCLUDED.stress_max, + body_battery_high = EXCLUDED.body_battery_high, + body_battery_low = EXCLUDED.body_battery_low, + spo2_avg = EXCLUDED.spo2_avg, + spo2_min = EXCLUDED.spo2_min, + respiration_avg = EXCLUDED.respiration_avg, + hrv_weekly_avg = EXCLUDED.hrv_weekly_avg, + hrv_last_night = EXCLUDED.hrv_last_night, + hrv_status = EXCLUDED.hrv_status, + sleep_duration_seconds = EXCLUDED.sleep_duration_seconds, + sleep_deep_seconds = EXCLUDED.sleep_deep_seconds, + sleep_light_seconds = EXCLUDED.sleep_light_seconds, + sleep_rem_seconds = EXCLUDED.sleep_rem_seconds, + sleep_awake_seconds = EXCLUDED.sleep_awake_seconds, + sleep_score = EXCLUDED.sleep_score, + sleep_feedback = EXCLUDED.sleep_feedback + """, ( + check_date, + stats.get("restingHeartRate"), + stats.get("totalSteps"), + stats.get("totalDistanceMeters"), + stats.get("activeKilocalories"), + stats.get("totalKilocalories"), + stress.get("avgStressLevel"), + stress.get("maxStressLevel"), + bb_high, + bb_low, + spo2.get("avgSpo2"), + spo2.get("lowestSpo2"), + resp.get("avgWakingRespirationValue"), + hrv.get("weeklyAvg"), + hrv.get("lastNight"), + hrv.get("status"), + sleep.get("sleepTimeSeconds"), + sleep.get("deepSleepSeconds"), + sleep.get("lightSleepSeconds"), + sleep.get("remSleepSeconds"), + sleep.get("awakeSleepSeconds"), + (sleep.get("sleepScores") or {}).get("overall", {}).get("value"), + (sleep.get("sleepScores") or {}).get("overall", {}).get("qualifierKey"), + )) + + conn.commit() + synced += 1 + check_date += timedelta(days=1) + time.sleep(0.5) + cur.close() conn.close() - print(f"Synced {len(activities)} activities") + print(f"Synced {synced} daily health records") \ No newline at end of file