Aller au contenu

Open Wearables (audit, #216 / #217)

Cette page trace l'audit d'Open Wearables (OW) et la décision prise pour le projet. Audit réalisé le 5 octobre 2026 dans le code source (pas dans la plaquette), puis revérifié le 10 octobre 2026 pour cette page. Légende utilisée partout : vérifié = lu dans le code ou la documentation d'OW au commit épinglé, ou dans une page publique que nous avons effectivement récupérée (URL et date données) ; rapporté = affirmé par un tiers, étiqueté comme tel avec sa date et son URL.

Commit épinglé de tout ce qui suit : tag 0.9.0 = ff8527a52ad8a96cd1ebe8c19344295c934ae9dc (17 septembre 2026). La branche main a aussi été relue au commit 12941a4cb10fd77755d5b0007d8f267c5fea451b (2 octobre 2026), uniquement là où c'est dit.

Version testée par le client : ff8527a52ad8a96cd1ebe8c19344295c934ae9dc (tag 0.9.0) — dernier tag publié, endpoints et champs relus dans les sources à ce commit par le client scripts/arc_openwearables.py (#219, OW_REF). Le client, l'installation (--health-source, #220) et l'usage par les agents (bilan matinal, #221) sont livrés.

Décision

Décision : OW n'est jamais une source d'entraînement ; Garmin reste en direct ; la santé du bilan matinal est une option

  1. OW comme source d'entraînement ([data].source) : non. Les trois marques clés du trail lui sont inaccessibles (ou inexistantes) pour un particulier, et ce qui reste est trop pauvre pour l'analyse trail.
  2. Garmin reste séparé et en direct (garmin-mcp). OW ne peut pas obtenir les données Garmin d'un particulier, et même avec un accès entreprise il n'exposerait ni Training Readiness, ni FC de récupération, ni D−.
  3. « Beaucoup d'appareils » pour l'entraînement : la réponse existe déjà, c'est Intervals.icu — voir le guide Quelle source pour mon appareil ?.
  4. La valeur d'OW est étroite : la santé quotidienne (HRV, FC de repos, sommeil, scores de récupération natifs) venant d'appareils que le projet ne lit pas en direct. Cette lecture est livrée (#218 à #221) comme option stricte [health].source = "openwearables" : sans elle (valeur par défaut primary), rien dans le projet n'appelle OW, et aucun agent n'en parle.

Ce qu'est Open Wearables

  • Plateforme libre sous licence MIT (LICENSE), auto-hébergée : API FastAPI, Postgres, Redis, workers Celery (README.md, docker-compose.yml).
  • Une API unifiée qui normalise les données de plusieurs fournisseurs, et un serveur MCP intégré (README.md, dossier mcp/).
  • Pré-1.0 : le tag audité est 0.9.0 ; la version 1.0 est annoncée pour fin octobre 2026 (docs/roadmap.mdx). Les endpoints et formes de réponse peuvent encore bouger : toute intégration devra être revérifiée au dernier tag publié avant d'être écrite (c'est la « porte d'entrée » de #219 à #221).

Accès par fournisseur pour un particulier

Preuves : fichiers d'OW au commit épinglé (chemins relatifs à la racine du dépôt). « Utile ici » = ce que le projet pourrait en tirer pour le bilan matinal ; les cases de couverture viennent de docs/providers/coverage.mdx.

Fournisseur Accès pour un particulier Ce qu'OW en tire d'utile ici Preuve
Garmin ❌ entités légales seulement (« Personal-use applications are rejected ») ; programme en pause (voir plus bas) ; OW ne reçoit Garmin qu'en push (URL HTTPS publique) — docs/providers/garmin-api-integration.mdx ; backend/app/services/providers/garmin/strategy.py (ProviderCapabilities(webhook_stream=True, webhook_callback=True, max_historical_days=30), pas de lecture par sondage)
Suunto ❌ entreprises / organisations seulement, pas d'usage personnel — docs/providers/suunto-api-integration.mdx (renvoie à https://apizone.suunto.com/faq, relue le 10 octobre 2026 : « we do not provide this for personal use »)
COROS ❌ non pris en charge — absent de la liste des fournisseurs (docs/providers/, coverage.mdx, backend/app/schemas/enums/provider.py : ProviderName sans COROS) ; docs/roadmap.mdx : « we cannot promise new integrations on a timeline »
Polar ✅ libre-service (compte Polar Flow → AccessLink) séances très pauvres ; HRV : moyenne de nuit seulement, comme composante du score Nightly Recharge (heart_rate_variability_avg) ; série HRV RMSSD uniquement avec un appareil Polar Elixir (« not available from standard devices ») ; sommeil, scores natifs : readiness 0–10, recovery 1–6 (Nightly Recharge) ; pas de FC de repos docs/providers/polar-api-integration.mdx ; docs/providers/coverage.mdx (section Polar) ; backend/app/services/providers/polar/coverage.py (WORKOUT_FIELDS = heart_rate_max, heart_rate_avg, energy_burned, distance ; commentaire de TIMESERIES : RMSSD via spo2 et wrist-ecg) ; backend/app/services/providers/polar/data_247.py (normalize_nightly_recharge, composante heart_rate_variability_avg ; RMSSD en sections « Elixir — SpO2 » et « Elixir — Wrist ECG ») ; backend/app/constants/health_scores.py
Oura ✅ ; l'URL de redirection OAuth doit être en HTTPS public HRV RMSSD, FC de repos (FC minimale de la nuit), sommeil avec stades, readiness 1–100 ; pas de FC pendant les séances docs/providers/oura-api-integration.mdx, docs/providers/coverage.mdx
WHOOP ✅ (abonnement WHOOP actif) recovery 0–100, HRV RMSSD, FC de repos, sommeil (totaux de stades seulement, pas d'hypnogramme) docs/providers/whoop-api-integration.mdx, coverage.mdx
Ultrahuman ✅ (compte personnel, bague Ring Air ; API « Partnership ») HRV en SDNN, sommeil avec stades ; pas de FC de repos dans la matrice ; aucune séance docs/providers/ultrahuman-api-integration.mdx, coverage.mdx
Withings ✅ ; l'URL de redirection ne peut être ni localhost ni une IP nue poids et composition, tension, FC ponctuelle, sommeil (totaux) ; HRV non importée, pas de FC de repos docs/providers/withings-api-integration.mdx, coverage.mdx
Google Health (Pixel Watch, Fitbit) ✅ (projet Google Cloud avec l'API Health activée) HRV RMSSD et SDNN, FC de repos, sommeil avec stades docs/providers/google-api-integration.mdx, coverage.mdx
Apple Health, Health Connect, Samsung ⚠️ via l'app mobile d'OW, en bêta (TestFlight / APK sur demande Discord) ; Samsung Health exige le mode développeur ; Apple Health aussi par import XML Apple : HRV en SDNN seulement ; Health Connect : RMSSD ; FC de repos dans les deux docs/app/introduction.mdx (application mobile, bêta), coverage.mdx ; import XML Apple Health : README.md (« Core Features », ligne 128)
Strava ✅, mais un abonnement Strava est requis pour l'API depuis juin 2026 (doc OW) flux FC / vitesse / cadence / puissance, sans altitude ni GPS ; pas de santé docs/providers/strava-api-integration.mdx ; backend/app/services/providers/strava/coverage.py (STREAM_KEY_SERIES_TYPE)

Écarts avec le texte de l'épopée, corrigés ici

Ultrahuman : la HRV est en SDNN (et non RMSSD) et la FC de repos n'est pas importée. Withings : pas de FC de repos (FC ponctuelle seulement). Ce sont les cases de coverage.mdx au commit épinglé qui font foi.

Faits externes (datés)

  • Pause du programme développeur Garmin — rapporté : billet de Momentum (la société qui édite OW), publié le 15 juillet 2026, mis à jour le 27 août 2026, relu le 10 octobre 2026 : https://www.themomentum.ai/blog/garmin-developer-program-closed-roadmap. Selon ce billet, les nouvelles demandes sont suspendues, sans date de reprise annoncée, et les connexions existantes continuent de fonctionner. Un second article (the5krunner, 14 septembre 2026, https://the5krunner.com/2026/09/14/garmin-developer-api-access-paused/) a été cité par l'audit mais n'a pas pu être relu le 10 octobre 2026 (réponse HTTP 403) : non vérifié par nous. La règle d'entités légales, elle, est dans la doc d'OW au commit épinglé.
  • Fitbit — Google : le support de l'ancienne API Fitbit Web prend fin le 30 septembre 2026 et l'API sera arrêtée le 30 octobre 2026 (page relue le 10 octobre 2026 : https://developers.google.com/health/about). L'API encore active à la date de cette page ne le sera donc plus à la fin du mois ; OW renvoie vers son intégration Google Health.
  • Abonnement Strava — rapporté par la doc d'OW (strava-api-integration.mdx, « as of June 2026 »), non vérifié chez Strava.
  • Appareils synchronisés par Intervals.icu — page relue le 10 octobre 2026 : https://www.intervals.icu/features/wellness/ (« Auto-sync from Garmin, Polar, Suunto, Coros, Huawei, Amazfit, Oura, WHOOP » ; « Sync from Apple Health and others using 3rd party apps »).

Comparaison avec Garmin en direct

Besoin du projet Garmin direct (aujourd'hui) OW 0.9.0 Preuve (commit épinglé)
D− d'une séance ✅ ❌ : seul elevation_gain_meters backend/app/schemas/responses/activity/events.py
FC de récupération (HRR) ✅ recovery_hr_bpm ❌ (seule heart_rate_recovery_one_minute existe, côté Apple) docs/providers/coverage.mdx
Training Readiness ✅ ❌ : Garmin via OW = sommeil, stress, Body Battery backend/app/services/providers/garmin/coverage.py (HEALTH_SCORES)
Splits / tours ✅ ⚠️ champ segments (tours issus de fichiers FIT) seulement si les fichiers FIT Garmin arrivent backend/app/schemas/responses/activity/events.py, backend/app/services/fit_parser.py
Flux par seconde ✅ (download_fit.py) ⚠️ désactivés par défaut backend/app/config.py : ingest_workout_samples: bool = False, store_fit_files: bool = False
Calories totales + BMR kcal actives seulement pour Garmin backend/app/services/providers/garmin/workouts.py (energy_burned = activeKilocalories)
Push de séances, parcours, matériel, nutrition ✅ ❌ lecture seule ; « Workout planner » classé « Exploring », sans échéance docs/roadmap.mdx
Récupération multi-fabricants — ⚠️ GET /users/{user_id}/summaries/recovery ne renvoie aujourd'hui que WHOOP (« This is a bug », docstring) ; le champ recovery_score est déprécié (deprecated=True) au profit de GET /api/v1/users/{user_id}/health-scores backend/app/api/routes/v1/summaries.py ; backend/app/schemas/responses/activity/summaries.py (champ recovery_score)
Échelles des scores Garmin 0–100 variables selon le fabricant (readiness Oura 1–100, Polar 0–10 ; recovery WHOOP 0–100, Polar 1–6) backend/app/constants/health_scores.py (HEALTH_SCORE_RANGES)

Le serveur MCP d'Open Wearables

Six outils, tous en lecture (mcp/app/tools/) : get_users, get_activity_summary, get_sleep_summary, get_workout_events, get_timeseries, get_menstrual_cycles.

  • get_sleep_summary renvoie date, début, fin, durée et source : ni HRV, ni stades, ni FC de repos.
  • get_workout_events renvoie 12 champs par séance (dont elevation_gain_meters, avg_pace_sec_per_km), sans D−, cadence ni zones.
  • get_timeseries pagine par 100 échantillons avec un plafond _MAX_PAGES = 100 : jusqu'à environ 10 000 échantillons peuvent arriver dans le contexte d'une seule réponse.
  • Aucun outil pour les scores de readiness ou de récupération.

Le projet ne l'utilisera donc pas pour la santé du bilan matinal. L'option lit l'API REST d'OW par un script du dépôt (même précédent que download_fit.py, qui contourne un MCP inadapté).

Coût d'hébergement

  • Services : docker-compose.yml en déclare huit (db, app, celery-worker, celery-beat, flower, redis, svix-server, frontend). Le minimum fonctionnel (base, Redis, API, worker et beat) est une estimation de l'audit, non testée ici.
  • Une application développeur par fournisseur : identifiant et secret à créer soi-même chez Polar, Oura, WHOOP, Ultrahuman, Withings, Google, Strava.
  • HTTPS public pour l'OAuth d'Oura et de Withings (un tunnel suffit le temps de la connexion, selon la doc d'OW). Ensuite, hors Garmin, la synchronisation se fait par sondage : la doc Ultrahuman indique un sondage toutes les heures par Celery Beat.
  • Télémétrie anonyme : sur la branche main, telemetry_enabled: bool = True (aussi coupée par DO_NOT_TRACK=1) depuis le commit db55ea3 du 25 septembre 2026, postérieur à 0.9.0 : absente du commit épinglé, elle arrivera avec la prochaine version. Pour la couper : TELEMETRY_ENABLED=false ou DO_NOT_TRACK=1 (docs/dev-guides/telemetry.mdx à main, commit 12941a4cb10fd77755d5b0007d8f267c5fea451b).

Installation

On n'installe pas Open Wearables (OW) : vous le déployez vous-même en suivant sa documentation, puis le projet s'y branche (comme à un compte intervals.icu ou Strava). Aucun serveur MCP n'est déclaré ni retiré : .mcp.json, opencode.json et les configurations des IDE restent intacts, le projet lit l'API REST d'OW par scripts/arc_openwearables.py (lecture seule, aucune écriture chez OW ni chez le fournisseur).

  1. Déployer OW selon sa documentation, avec des images épinglées sur un tag stable (themomentum/open-wearables-backend:<tag> et themomentum/open-wearables-frontend:<tag>, jamais nightly). Coupez la télémétrie anonyme (TELEMETRY_ENABLED=false ou DO_NOT_TRACK=1) : elle sera activée par défaut à partir de la version qui suivra 0.9.0 (voir « Coût d'hébergement »). Changez le mot de passe administrateur par défaut.
  2. Créer une clé d'API dans le portail d'OW. Elle ne sera jamais écrite dans le dépôt.
  3. Créer l'application développeur chez le fournisseur de santé (Oura, WHOOP, Polar, Withings…) puis le connecter à OW. Oura et Withings exigent une redirection HTTPS publique : un tunnel suffit, le temps de l'OAuth seulement ; ensuite la synchronisation se fait par sondage horaire, sans URL publique à garder ouverte.
  4. Régler les priorités dans le portail (Settings → Priorities) pour que le fournisseur choisi l'emporte. Sinon le client signale provider_mismatch et n'utilise pas la nuit (une seule source de santé par jour).
  5. Garder OW sur le réseau local ou derrière un VPN (Tailscale…), sans l'exposer publiquement.
  6. Brancher le projet :
./install.sh --health-source openwearables --ow-url http://127.0.0.1:8000 --ow-provider oura
  • --ow-url : l'adresse de l'API (pas celle du portail) ; --ow-provider : UN fabricant parmi oura, whoop, polar, ultrahuman, withings, google_health, apple, health_connect, samsung, suunto. garmin (Garmin passe en direct) et strava (aucune donnée de santé) sont refusés.
  • La clé d'API est demandée sans écho (sur un terminal) et écrite dans ~/.config/ai-running-coach/openwearables.key en mode 600, hors dépôt ; elle n'est jamais affichée, ni passée en argument, ni écrite dans .mcp.json ou le workspace. Sans terminal (ou avec --no-auth), importez-la avec --ow-key-file FICHIER ; un fichier déjà présent est conservé et n'est jamais redemandé.
  • L'installation écrit seulement [health].source, [health.openwearables].base_url et .provider dans config/workspace.user.toml ; un ./install.sh relancé sans l'option ne touche à rien. --dry-run affiche ce qui serait fait.
  • Pour revenir en arrière : ./install.sh --health-source primary remet [health].source = "primary" et ne supprime ni l'URL, ni le fournisseur, ni la clé.
  • Vérifier : python3 scripts/coach_doctor.py --check openwearables (ou /coach-doctor). Une fois la configuration complète, le contrôle appelle votre instance (voir « Diagnostic ») ; --no-probe-ow pour rester hors ligne.

Synchronisation automatique (daily-sync)

Avec [health].source = "openwearables" et [data].source = "garmin", scripts/daily-sync.sh refuse à l'exécuteur les quatre outils santé Garmin (get_hrv_data, get_rhr_day, get_sleep_data, get_training_readiness) ainsi que ceux de body battery et de stress (get_body_battery, get_body_battery_events, get_stress_data, get_stress_summary, get_all_day_stress : le contrat refuse body_battery_* et stress_avg dans un fichier santé Open Wearables ; absents de la liste blanche livrée, ils ne sont refusés que par précaution) : la règle « une seule source de santé par jour » est imposée, pas seulement demandée (avec la passerelle leanproxy, seule la consigne du skill protège, comme pour les écritures). Limites par exécuteur : claude, copilot, opencode et gemini refusent ces outils par nom ; cursor les refuse par Mcp(garmin:…) dans .cursor/cli.json (écrit par install.sh, retiré au retour à primary) — s'ils manquent, le run continue avec un avertissement (seule la consigne du skill protège) ; codex n'a aucun filtrage par outil ; la passerelle leanproxy n'en a aucun non plus. Avec [data].source = "intervals", rien n'est refusé : icu_get_wellness_for_date sert aussi à la phase du cycle. Le fichier de clé est en outre interdit en lecture à l'exécuteur claude (Read(<fichier>)).

Avant chaque run, le script fait un seul arc_openwearables.py check. Si l'instance est morte ou la clé refusée, la synchronisation Garmin continue : le problème est journalisé et ajouté en une ligne au résumé (au plus une fois par jour). Les activités sont toujours lues chez Garmin.

[sync].mode = "watch" reste réservé à [data].source = "garmin". Avec une santé Open Wearables, le déclencheur morning de scripts/watch.py s'appuie toujours sur le sommeil Garmin comme signal de réveil, ce qui peut précéder la synchronisation chez OW : la nuit peut donc ne pas être encore arrivée. Dans ce cas, le skill /garmin-daily-sync n'écrit aucun fichier santé tant que la nuit n'est pas arrivée chez OW (le run suivant réessaie, il ne récupère que les fichiers manquants) et le résumé le dit ; une session interactive le dit aussi et planifie sur la charge et le ressenti déclaré.

Diagnostic

/coach-doctor (contrôle openwearables) vérifie le fournisseur, l'adresse, la présence et le mode 600 du fichier de clé (valeur jamais lue ni affichée) ; puis, automatiquement quand la configuration est complète, il fait un appel à l'instance — la vôtre, jamais un tiers : joignable, clé acceptée, fournisseur connecté et synchronisé récemment, forme de réponse compatible avec la version testée. L'appel est borné : 3 s par requête, sous-processus plafonné à 12 s. --no-probe-ow (hors ligne) s'en tient à la configuration ; l'ancien --probe-ow est accepté sans effet. Le cron ne lance que --check garmin_token : il ne contacte jamais OW par ce biais. Hors de l'option [health].source = "openwearables", le contrôle dit seulement « non utilisé ». Pannes courantes : voir Dépannage.

Le bilan matinal avec Open Wearables

Avec [health].source = "openwearables", coach, medical et /garmin-daily-sync lisent la santé du jour par un seul appel : python3 scripts/arc_openwearables.py health-day --date AAAA-MM-JJ --json. Les outils santé Garmin (get_hrv_data, get_rhr_day, get_sleep_data, get_training_readiness) ne sont pas appelés, et avec intervals.icu comme source d'entraînement icu_get_wellness_for_date n'est pas utilisé pour la HRV, la FC de repos ni le sommeil : une seule source de santé par jour. Les séances, elles, viennent toujours de [data].source. Avec Strava comme source d'entraînement, le message « HRV / FC de repos / readiness indisponibles — source Strava » disparaît au profit des données d'OW.

[health].morning_check Ce que l'agent fait
full HRV (RMSSD) + FC de repos + score natif du fabricant s'il existe. Sinon : « readiness indisponible — FABRICANT via Open Wearables », jamais autre chose à la place.
minimal Une ligne : le score natif (« Readiness Oura 82/100 », « Nightly Recharge Polar 4/6 ») ou « readiness indisponible (Open Wearables, FABRICANT) ».
off Rien n'est lu, aucune mention.
  • Nommer, jamais convertir. Un score est toujours dit « CATÉGORIE FABRICANT VALEUR/BORNE » et recopié tel que le script le rend : jamais « Training Readiness », jamais remis sur 100, jamais comparé aux seuils Garmin. Il est écrit dans provider_scores du fichier santé, jamais dans readiness_score ni sleep_score (que le contrat refuse pour cette source).
  • HRV en SDNN (Apple Health). Affichée à titre indicatif (« HRV SDNN 52 ms (Apple Health — non comparable à une RMSSD) »), jamais comme tendance HRV du verdict ; le bilan complet est dit incomplet (« HRV RMSSD indisponible »). Elle est écrite dans hrv_sdnn_ms, jamais dans hrv_overnight_ms.
  • Ligne de base. arc_index.py hrv-baseline est calculée par source (et par fabricant). Après un changement d'appareil elle peut rendre en_construction : l'agent le dit et décide sur la FC de repos, le score natif et le ressenti déclaré, jamais sur la bande de l'ancienne source.
  • Pannes et retards, toujours dits. Instance injoignable ou clé refusée : « santé Open Wearables indisponible » (en interactif, /coach-doctor est proposé ; en headless, une mention dans la ligne Alerte : du résumé). Configuration incomplète : « santé Open Wearables : configuration incomplète », même traitement. « Nuit pas encore arrivée » veut dire : ni durée de sommeil, ni HRV (même si une FC de repos du jour est là) ; en headless, aucun fichier santé n'est alors écrit (pesée du jour comprise, relue au run suivant) et le résumé le dit. Données périmées (message d'avertissement recopié, avec l'âge), fabricant non connecté ou connexion inactive (à reconnecter dans le portail Open Wearables), score natif écarté faute d'échelle connue : dits aussi. Jamais un « tout va bien » par défaut, et une donnée manquante ne fait jamais passer un verdict orange ou rouge au vert ; une douleur déclarée ou une annulation médicale garde son protocole.
  • Rien de propre à Garmin. Ni body battery, ni stress, ni score de sommeil Garmin, ni readiness_* dans un fichier santé Open Wearables (le contrat les refuse). Les outils correspondants sont refusés par nom en headless pour la plupart des exécuteurs ; en session interactive, avec codex et avec la passerelle leanproxy, rien ne filtre : la consigne des agents est la protection.
  • Fichier santé. medical/AAAA-MM-JJ_health.md porte l'objet arc rendu par le script (health_source, health_provider, health_device…), morning_check, verdict, verdict_reason et missing_reason (recopié des mesures indisponibles), validé par arc_index.py --validate. Tout fichier santé qui porte une mesure dit sa source (garmin, intervals ou openwearables) ; un fichier de douleur seule n'en a pas.

Ce qui ferait changer la décision

  • Garmin rouvre son programme aux particuliers (aujourd'hui : entités légales seulement, et en pause).
  • OW ajoute COROS, ou ouvre Suunto aux particuliers.
  • Le modèle de séance d'OW gagne le D− et un endpoint de flux par séance, avec ingestion activée par défaut.
  • Le MCP d'OW expose HRV, FC de repos et scores de readiness.

Références

Toutes au commit épinglé ff8527a52ad8a96cd1ebe8c19344295c934ae9dc : https://github.com/the-momentum/open-wearables/tree/ff8527a52ad8a96cd1ebe8c19344295c934ae9dc

  • Couverture : docs/providers/coverage.mdx
  • Fournisseurs : docs/providers/{garmin,suunto,polar,oura,whoop,ultrahuman,withings,google,strava}-api-integration.mdx
  • Application mobile : docs/app/introduction.mdx
  • Feuille de route : docs/roadmap.mdx
  • Code : backend/app/services/providers/*/coverage.py, backend/app/services/providers/polar/data_247.py, backend/app/services/providers/garmin/{strategy,workouts}.py, backend/app/services/fit_parser.py, backend/app/api/routes/v1/summaries.py, backend/app/schemas/responses/activity/{events,summaries}.py, backend/app/schemas/enums/provider.py, backend/app/constants/health_scores.py, backend/app/config.py, mcp/app/tools/*.py
  • Import Apple Health XML : README.md
  • Hors dépôt : pages citées plus haut, relues le 10 octobre 2026.

Voir aussi : Quelle source pour mon appareil ?, Montres COROS, Configuration Intervals.icu, Configuration Strava.