""" Health data service: read endpoints + upsert helpers used by the Garmin sync. Mirrors the original Node HealthService, including the camelCase JSON mapping. Upserts use backend-specific SQL because SQLite does not support `ON DUPLICATE KEY UPDATE` (it uses `ON CONFLICT ... DO UPDATE`). """ import uuid from db import execute, query_one, query_all from config import DB_TYPE def _range_sql(user_id, start=None, end=None): params = [user_id] sql = "WHERE user_id = ?" if start: sql += " AND date >= ?" params.append(start) if end: sql += " AND date <= ?" params.append(end) return sql, params def get_summary(user_id, start=None, end=None): sql, params = _range_sql(user_id, start, end) rows = query_all( "SELECT date, steps, heart_rate, heart_rate_variability, " "sleep_duration, sleep_quality, stress, calories_burned " f"FROM health_data {sql} ORDER BY date ASC", params, ) return [ { "date": r["date"], "steps": r["steps"], "heartRate": r["heart_rate"], "heartRateVariability": r["heart_rate_variability"], "sleep": ( {"duration": r["sleep_duration"], "quality": r["sleep_quality"]} if r["sleep_duration"] is not None else None ), "stress": r["stress"], "caloriesBurned": r["calories_burned"], } for r in rows ] def get_steps(user_id, start=None, end=None): sql, params = _range_sql(user_id, start, end) rows = query_all( f"SELECT date, steps FROM health_data {sql} AND steps IS NOT NULL ORDER BY date ASC", params, ) return [{"date": r["date"], "steps": r["steps"]} for r in rows] def get_heart_rate(user_id, start=None, end=None): sql, params = _range_sql(user_id, start, end) rows = query_all( f"SELECT date, heart_rate, heart_rate_variability FROM health_data {sql} " "AND heart_rate IS NOT NULL ORDER BY date ASC", params, ) return [ { "date": r["date"], "heartRate": r["heart_rate"], "heartRateVariability": r["heart_rate_variability"], } for r in rows ] def get_sleep(user_id, start=None, end=None): sql, params = _range_sql(user_id, start, end) rows = query_all( f"SELECT date, sleep_duration, sleep_quality FROM health_data {sql} " "AND sleep_duration IS NOT NULL ORDER BY date ASC", params, ) return [ {"date": r["date"], "duration": r["sleep_duration"], "quality": r["sleep_quality"]} for r in rows ] def get_activities(user_id, start=None, end=None): sql, params = _range_sql(user_id, start, end) rows = query_all( "SELECT id, activity_type, start_time, end_time, duration, distance, " "calories, heart_rate_average, heart_rate_max " f"FROM activities {sql} ORDER BY start_time DESC", params, ) return rows def upsert_health_daily(user_id, record): hid = f"{user_id}-{record['date']}" cols = [ "id", "user_id", "date", "steps", "heart_rate", "heart_rate_variability", "blood_pressure_systolic", "blood_pressure_diastolic", "sleep_duration", "sleep_quality", "stress", "calories_burned", ] placeholders = ", ".join(["?"] * len(cols)) vals = [ hid, user_id, record.get("date"), record.get("steps"), record.get("heartRate"), record.get("heartRateVariability"), record.get("bloodPressureSystolic"), record.get("bloodPressureDiastolic"), record.get("sleepDuration"), record.get("sleepQuality"), record.get("stress"), record.get("caloriesBurned"), ] if DB_TYPE == "mariadb": update_cols = [c for c in cols if c not in ("id", "user_id")] updates = ", ".join([f"{c}=VALUES({c})" for c in update_cols]) sql = ( f"INSERT INTO health_data ({', '.join(cols)}) VALUES ({placeholders}) " f"ON DUPLICATE KEY UPDATE {updates}, updated_at=CURRENT_TIMESTAMP" ) else: update_cols = [c for c in cols if c not in ("id", "user_id")] updates = ", ".join([f"{c}=excluded.{c}" for c in update_cols]) sql = ( f"INSERT INTO health_data ({', '.join(cols)}) VALUES ({placeholders}) " f"ON CONFLICT(user_id, date) DO UPDATE SET {updates}, updated_at=CURRENT_TIMESTAMP" ) execute(sql, vals) return hid def insert_activity(user_id, activity): aid = str(uuid.uuid4()) cols = [ "id", "user_id", "activity_type", "start_time", "end_time", "duration", "distance", "calories", "heart_rate_average", "heart_rate_max", ] placeholders = ", ".join(["?"] * len(cols)) vals = [ aid, user_id, activity.get("activityType"), activity.get("startTime"), activity.get("endTime"), activity.get("duration"), activity.get("distance"), activity.get("calories"), activity.get("heartRateAverage"), activity.get("heartRateMax"), ] execute( f"INSERT INTO activities ({', '.join(cols)}) VALUES ({placeholders})", vals, ) return aid