"""Health data routes: summary / steps / heart-rate / sleep / activities.""" import datetime from flask import Blueprint, request, g, jsonify from auth import require_auth from services import health as health_svc from services import settings as settings_svc from services import fitness_age from services import garmin_extras as extras bp = Blueprint("health", __name__) def _range(): return request.args.get("startDate"), request.args.get("endDate") @bp.route("/summary", methods=["GET"]) @require_auth def summary(): s, e = _range() return jsonify(health_svc.get_summary(g.user_id, s, e)) @bp.route("/steps", methods=["GET"]) @require_auth def steps(): s, e = _range() return jsonify(health_svc.get_steps(g.user_id, s, e)) @bp.route("/heart-rate", methods=["GET"]) @require_auth def heart_rate(): s, e = _range() return jsonify(health_svc.get_heart_rate(g.user_id, s, e)) @bp.route("/sleep", methods=["GET"]) @require_auth def sleep(): s, e = _range() return jsonify(health_svc.get_sleep(g.user_id, s, e)) @bp.route("/activities", methods=["GET"]) @require_auth def activities(): s, e = _range() return jsonify(health_svc.get_activities(g.user_id, s, e)) @bp.route("/fitness-age", methods=["GET"]) @require_auth def body_age(): """身体年龄, computed from the profile plus the most recent readings. VO2max only refreshes after an outdoor run or ride, so the search window is wide: the last recorded value is still the current one. """ profile = settings_svc.get_raw(g.user_id) start = (datetime.date.today() - datetime.timedelta(days=180)).isoformat() days = health_svc.get_summary(g.user_id, start, None) def latest(key): for day in reversed(days): if day.get(key): return day[key] return None return jsonify(fitness_age.estimate( age=settings_svc.age_from(profile["birth_date"]), sex=profile["sex"], vo2max=latest("vo2max"), resting_hr=latest("heartRate"), bmi=settings_svc.bmi_from(profile["height_cm"], profile["weight_kg"]), )) @bp.route("/badges", methods=["GET"]) @require_auth def badges(): """Earned badges (奖励), most recent first.""" return jsonify(health_svc.get_badges(g.user_id)) @bp.route("/personal-records", methods=["GET"]) @require_auth def personal_records(): return jsonify(health_svc.get_personal_records(g.user_id)) @bp.route("/body-composition", methods=["GET"]) @require_auth def body_composition(): """Weight and everything a connected scale reports with it.""" s, e = _range() return jsonify(extras.get_body_composition(g.user_id, s, e)) @bp.route("/blood-pressure", methods=["GET"]) @require_auth def blood_pressure(): return jsonify(extras.get_blood_pressure(g.user_id)) @bp.route("/race-predictions", methods=["GET"]) @require_auth def race_predictions(): """Garmin's predicted 5K / 10K / half / marathon times, in seconds.""" return jsonify(extras.get_race_predictions(g.user_id)) @bp.route("/series", methods=["GET"]) @require_auth def daily_series(): """Within-day curves for one date: heart rate, stress, body battery, respiration, SpO2.""" date = request.args.get("date") if not date: return jsonify({"error": "缺少 date 参数"}), 400 return jsonify(extras.get_daily_series(g.user_id, date)) @bp.route("/challenges", methods=["GET"]) @require_auth def challenges(): return jsonify(extras.get_challenges(g.user_id)) @bp.route("/devices", methods=["GET"]) @require_auth def devices(): return jsonify(extras.get_devices(g.user_id))