Files
Home_assistant/backend/app/api/routes/fitness.py
T
2026-06-11 12:22:37 +03:00

224 lines
6.5 KiB
Python

from datetime import date
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.db.base import get_db
from app.fitness.service import FitnessService
from app.fitness.structuring import structure_meal, structure_workout
from app.integrations.openfoodfacts import OpenFoodFactsClient
from app.integrations.wger import WgerClient
router = APIRouter()
class ProfileUpdate(BaseModel):
sex: str | None = None
age: int | None = None
height_cm: float | None = None
weight_kg: float | None = None
activity_level: str | None = None
goal: str | None = None
target_weight_kg: float | None = None
weekly_workouts: int | None = None
class MealCreate(BaseModel):
text: str = Field(min_length=1)
meal_type: str | None = None
class WaterCreate(BaseModel):
amount_ml: int = Field(gt=0)
class WeightCreate(BaseModel):
weight_kg: float = Field(gt=0)
body_fat_pct: float | None = None
chest_cm: float | None = None
waist_cm: float | None = None
notes: str = ""
class WorkoutCreate(BaseModel):
text: str = Field(min_length=1)
class ReminderUpdate(BaseModel):
enabled: bool | None = None
hour: int | None = Field(default=None, ge=0, le=23)
minute: int | None = Field(default=None, ge=0, le=59)
interval_hours: int | None = Field(default=None, ge=1, le=12)
@router.get("/fitness")
def get_snapshot(db: Session = Depends(get_db)) -> dict[str, Any]:
return FitnessService(db).snapshot()
@router.get("/fitness/summary")
def get_summary(
day: str | None = None,
db: Session = Depends(get_db),
) -> dict[str, Any]:
d = date.fromisoformat(day) if day else None
return FitnessService(db).get_daily_summary(d)
@router.get("/fitness/history")
def get_history(
days: int = 7,
end: str | None = None,
db: Session = Depends(get_db),
) -> dict[str, Any]:
end_day = date.fromisoformat(end) if end else None
return FitnessService(db).get_history(days=days, end_day=end_day)
@router.get("/fitness/profile")
def get_profile(db: Session = Depends(get_db)) -> dict[str, Any]:
profile = FitnessService(db).get_profile()
return profile or {"configured": False}
@router.put("/fitness/profile")
def update_profile(
payload: ProfileUpdate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return FitnessService(db).set_profile(payload.model_dump(exclude_none=True))
@router.post("/fitness/profile/calc")
def calc_targets(
payload: ProfileUpdate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
params = payload.model_dump(exclude_none=True)
if not params:
raise HTTPException(status_code=400, detail="No parameters")
return FitnessService(db).calc_targets(params)
@router.post("/fitness/meals")
async def create_meal(
payload: MealCreate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
service = FitnessService(db)
try:
structured = await structure_meal(payload.text)
except Exception as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return service.log_meal(
description=structured.get("description") or payload.text,
meal_type=payload.meal_type or structured.get("meal_type") or "snack",
calories=float(structured.get("calories") or 0),
protein_g=float(structured.get("protein_g") or 0),
fat_g=float(structured.get("fat_g") or 0),
carbs_g=float(structured.get("carbs_g") or 0),
source="llm",
estimated=bool(structured.get("estimated", True)),
)
@router.post("/fitness/water")
def create_water(
payload: WaterCreate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return FitnessService(db).log_water(payload.amount_ml)
@router.post("/fitness/weight")
def create_weight(
payload: WeightCreate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return FitnessService(db).log_weight(
payload.weight_kg,
body_fat_pct=payload.body_fat_pct,
chest_cm=payload.chest_cm,
waist_cm=payload.waist_cm,
notes=payload.notes,
)
@router.post("/fitness/workouts")
async def create_workout(
payload: WorkoutCreate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
service = FitnessService(db)
try:
structured = await structure_workout(payload.text)
except Exception as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return service.log_workout(
title=structured.get("title") or "Тренировка",
notes=structured.get("notes") or payload.text,
duration_min=structured.get("duration_min"),
exercises=structured.get("exercises"),
)
@router.get("/fitness/body-metrics")
def list_metrics(
limit: int = 30,
db: Session = Depends(get_db),
) -> list[dict[str, Any]]:
return FitnessService(db).list_body_metrics(limit=limit)
@router.delete("/fitness/meals/{log_id}")
def delete_meal(log_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
if not FitnessService(db).delete_food_log(log_id):
raise HTTPException(status_code=404, detail="Not found")
return {"ok": True}
@router.delete("/fitness/water/{log_id}")
def delete_water(log_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
if not FitnessService(db).delete_water_log(log_id):
raise HTTPException(status_code=404, detail="Not found")
return {"ok": True}
@router.delete("/fitness/workouts/{log_id}")
def delete_workout(log_id: int, db: Session = Depends(get_db)) -> dict[str, bool]:
if not FitnessService(db).delete_workout_log(log_id):
raise HTTPException(status_code=404, detail="Not found")
return {"ok": True}
@router.get("/fitness/reminders")
def list_reminders(db: Session = Depends(get_db)) -> list[dict[str, Any]]:
return FitnessService(db).list_reminders()
@router.put("/fitness/reminders/{kind}")
def update_reminder(
kind: str,
payload: ReminderUpdate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
return FitnessService(db).set_reminder(
kind,
enabled=payload.enabled,
hour=payload.hour,
minute=payload.minute,
interval_hours=payload.interval_hours,
)
@router.get("/fitness/lookup/food")
def lookup_food(q: str, limit: int = 5) -> list[dict[str, Any]]:
return OpenFoodFactsClient().search(q, limit=limit)
@router.get("/fitness/lookup/exercise")
def lookup_exercise(q: str, limit: int = 8) -> list[dict[str, Any]]:
return WgerClient().search_exercises(q, limit=limit)