Sottotitolo: Come trasformare i dati raccolti quotidianamente da uno smartwatch in una base informativa strutturata, pronta per essere interrogata da un motore di intelligenza artificiale privato.
Ogni giorno uno smartwatch raccoglie una quantità sorprendente di informazioni.
Frequenza cardiaca, passi, attività sportive, qualità del sonno, variabilità della frequenza cardiaca (HRV), stress e recupero sono soltanto alcuni dei parametri disponibili.
Nel mio caso utilizzo un Garmin Fenix per monitorare soprattutto gli allenamenti di nuoto, sia in piscina sia in acque libere.
Garmin Connect permette di visualizzare questi dati attraverso grafici e report, ma volevo realizzare qualcosa di diverso.
Volevo che un’intelligenza artificiale potesse leggere i miei dati Garmin, comprenderne l’evoluzione nel tempo e aiutarmi a interpretare allenamenti e recupero.
Per esempio:
- Come è cambiata la mia frequenza cardiaca durante gli ultimi allenamenti?
- Sto migliorando l’efficienza della nuotata?
- Esiste una relazione tra qualità del sonno e prestazioni sportive?
- Come varia la mia HRV nei periodi di maggiore carico?
- Quanto recupero dopo un allenamento particolarmente impegnativo?
Per rispondere a queste domande, il primo problema da risolvere non era l’intelligenza artificiale; era l’accesso ai dati.
L’architettura del progetto
Ho deciso di costruire una pipeline completamente ospitata nel mio homelab.
L’architettura complessiva prevede quattro componenti principali:
- Garmin Connect,raccoglie e conserva le info provenienti dallo smartwatch.
- Garmin Collector, un’applicazione Python che interroga le API e scarica i dati.
- Uno storage persistente, nel quale vengono salvati i dati in formato JSON.
- Un server MCP, che permette al motore AI di accedere alle informazioni attraverso strumenti dedicati.
Il flusso logico è:
Garmin Fenix
|
v
Garmin Connect
|
| HTTPS / API
v
Garmin Collector (Python)
|
| Normalizzazione
v
File JSON strutturati
|
v
Storage persistente
|
v
Garmin MCP Server
|
v
OpenWebUI + Motore AI
In questo primo articolo ci concentreremo esclusivamente sulla parte iniziale: acquisizione, autenticazione, organizzazione e normalizzazione dei dati Garmin.
L’infrastruttura Kubernetes e l’integrazione MCP saranno affrontate nei prossimi articoli.
La prima difficoltà: accedere ai dati Garmin
A differenza di altri servizi, Garmin Connect non offre agli utenti privati una semplice API pubblica, liberamente utilizzabile per scaricare tutti i propri dati attraverso una chiave personale standard.
Garmin mette a disposizione programmi API ufficiali, come Garmin Health API, destinati principalmente a integrazioni approvate.
Esistono inoltre librerie e integrazioni non ufficiali che utilizzano gli endpoint di Garmin Connect.
Nel mio progetto ho utilizzato un sistema di autenticazione basato su una credenziale personale e sul rilascio di token temporanei.
È importante precisare che questa modalità di autenticazione dipende dal servizio effettivamente utilizzato e non deve essere considerata un’interfaccia pubblica Garmin universalmente disponibile.
L’implementazione concreta impiega due passaggi:
- Presentare la credenziale personale a un endpoint di autenticazione.
- Utilizzare il token ottenuto per le successive richieste API.
Questo approccio evita di trasmettere la credenziale principale a ogni richiesta.
La gestione delle credenziali
Durante lo sviluppo locale, ho utilizzato un file .env.local:
GARMIN_PERSONAL_API_KEY=<chiave_personale>
Il file deve essere escluso dal versionamento Git.
.env.local
token.json
__pycache__/
Le credenziali non devono essere inserite direttamente nel codice Python, nei manifest pubblici Kubernetes o nei repository GitHub.
Nel deployment definitivo, la credenziale viene invece gestita attraverso un Secret Kubernetes.
Il modulo Python garmin_api.py
Per separare l’autenticazione dalla raccolta dei dati, ho organizzato il codice in due moduli:
garmin_api.py: autenticazione, token e richieste HTTP.pull.py: acquisizione e salvataggio dei dati Garmin.
Questa separazione permette di modificare il sistema di autenticazione senza riscrivere tutta la logica di raccolta.
La struttura delle directory
garmin-test/
|
|-- garmin_api.py
|-- pull.py
|-- .env.local
|
`-- data/
|-- activities.json
|-- daily.json
|-- sleep.json
|-- hrv.json
|-- weight.json
|-- fitness_age.json
|-- hr_zones.json
|-- meta.json
`-- token.json
Il file token.json è un file operativo contenente informazioni di autenticazione e deve essere protetto come una credenziale.
Lettura della chiave
Il modulo individua il file .env.local nella directory dello script.
Un esempio semplificato:
import os
from pathlib import Path
HERE = Path(__file__).resolve().parent
ENV_FILE = HERE / ".env.local"
def read_key():
if not ENV_FILE.exists():
raise RuntimeError(
"File .env.local non trovato"
)
for line in ENV_FILE.read_text().splitlines():
if line.startswith("GARMIN_PERSONAL_API_KEY="):
return line.split("=", 1)[1].strip()
raise RuntimeError("Chiave Garmin mancante")
Questa funzione illustra la lettura della credenziale. Non costituisce da sola un client Garmin completo.
Nella versione eseguita su Kubernetes, lo script di avvio crea il file .env.local a partire dalla variabile d’ambiente fornita da un Secret.
Token temporanei e rinnovo automatico
Uno degli aspetti più importanti del Collector è la gestione dei token.
Nel progetto il token viene memorizzato localmente, in modo da non dover eseguire una nuova autenticazione per ogni richiesta.
La logica generale è:
def get_token(force=False):
if not force:
token = read_cached_token()
if token_is_valid(token):
return token
token = request_new_token()
save_token(token)
return token
Questo è pseudocodice illustrativo: le funzioni di cache e autenticazione devono essere implementate secondo il protocollo del servizio utilizzato.
Nel mio caso è stata configurata una durata massima locale del token di 20 ore.
La durata della cache non garantisce che il token rimanga valido per tutto quel periodo. Il server remoto può revocarlo o rifiutarlo prima della scadenza.
Per questo motivo è importante gestire anche le risposte HTTP 401.
Gestione degli errori di autenticazione
Il Collector segue questa logica:
Richiesta API
|
v
Token disponibile?
|
+-- NO --> Richiedi nuovo token
|
v
Esegui richiesta
|
+-- HTTP 200 --> Elabora i dati
|
+-- HTTP 401 --> Rinnova token
|
v
Riprova una volta
|
+-- OK
|
+-- Errore: interrompi
Il nuovo tentativo deve essere limitato: ripetere indefinitamente richieste con credenziali non valide non risolve il problema e può provocare ulteriori blocchi.
Durante il progetto abbiamo incontrato proprio un errore HTTP 401.
La causa è stata individuata confrontando la credenziale locale funzionante con quella configurata nell’ambiente Kubernetes.
Il test di autenticazione locale è riuscito, mentre la credenziale precedentemente presente nel Secret veniva rifiutata.
Questo episodio ha dimostrato l’importanza di separare la diagnosi delle credenziali dalla diagnosi dell’infrastruttura.
Quali dati abbiamo deciso di raccogliere
Una volta risolta l’autenticazione, il passo successivo è stato scegliere quali informazioni acquisire.
Ho organizzato la raccolta in diverse categorie.
| Dataset | Contenuto principale |
|---|---|
activities.json |
Allenamenti, distanze, durate, frequenza cardiaca, carico |
daily.json |
Passi, frequenza cardiaca, Body Battery e riepiloghi giornalieri |
sleep.json |
Durata del sonno e relative fasi |
hrv.json |
Variabilità della frequenza cardiaca |
weight.json |
Storico delle pesate |
fitness_age.json |
Informazioni sull’età fitness |
hr_zones.json |
Zone di frequenza cardiaca |
meta.json |
Metadati della raccolta |
Non tutti i campi sono necessariamente disponibili per ogni attività o per ogni giornata.
La disponibilità dipende dal dispositivo, dalle impostazioni Garmin e dalle informazioni effettivamente presenti nell’account.
Un esempio: le attività di nuoto
Per gli allenamenti di nuoto, i dati possono includere informazioni particolarmente interessanti:
- Distanza percorsa.
- Durata totale e tempo effettivo.
- Frequenza cardiaca media e massima.
- Velocità media.
- Cadenza delle bracciate.
- SWOLF, un indicatore combinato di tempo e numero di bracciate.
- Distanza media per bracciata.
- Training Effect aerobico e anaerobico.
- Tempo trascorso nelle zone cardiache.
Questi dati costituiscono una base utile per confrontare le prestazioni nel tempo.
Tuttavia, è necessario interpretare correttamente il contesto: una sessione in piscina non è direttamente confrontabile con una gara in acque libere, dove condizioni ambientali, navigazione e correnti possono influenzare il risultato.
Normalizzare i dati: dal dato grezzo al JSON
Le risposte API possono contenere strutture complesse, campi opzionali e unità di misura differenti.
Per poter utilizzare questi dati in un sistema AI, è utile definire uno schema stabile.
Consideriamo, per esempio, una possibile rappresentazione normalizzata di un allenamento:
{
"activity_id": "example-001",
"date": "2026-11-10",
"sport": "lap_swimming",
"distance_m": 2400,
"duration_s": 2880,
"average_hr_bpm": 128,
"average_swolf": 42,
"source": "garmin"
}
I valori riportati sono illustrativi e non rappresentano una specifica attività realmente acquisita.
La normalizzazione permette di:
- Utilizzare nomi di campo coerenti.
- Esplicitare le unità di misura.
- Gestire i valori mancanti.
- Collegare i dati tramite identificatori e date.
- Ridurre la complessità delle successive interrogazioni.
È importante conservare anche i dati originali, oppure almeno i campi necessari a ricostruire il significato delle misure.
Organizzare i dati per data
Per i riepiloghi giornalieri, una struttura indicizzata per data risulta particolarmente pratica.
{
"2026-11-09": {
"steps": 8500,
"resting_hr": 54
},
"2026-11-10": {
"steps": 10200,
"resting_hr": 52
}
}
Anche questi sono dati di esempio.
Una struttura del genere rende semplice selezionare intervalli temporali e costruire confronti.
Per un’analisi più avanzata, potremo successivamente trasferire i dati in un database relazionale o in un sistema dedicato alle serie temporali.
Per la prima versione del progetto, però, i file JSON sono stati sufficienti.
Il Collector: automatizzare la raccolta
Il secondo script, pull.py, si occupa di orchestrare la raccolta.
Il suo compito è:
- Inizializzare il client API.
- Richiedere i diversi dataset.
- Gestire gli errori.
- Salvare i risultati nei file JSON.
- Terminare con un codice di uscita che permetta di identificare eventuali fallimenti.
Il principio generale è semplice:
from pathlib import Path
import json
DATA_DIR = Path(__file__).resolve().parent / "data"
DATA_DIR.mkdir(exist_ok=True)
def save_json(filename, payload):
destination = DATA_DIR / filename
temporary = destination.with_suffix(".tmp")
temporary.write_text(
json.dumps(
payload,
ensure_ascii=False,
indent=2
),
encoding="utf-8"
)
temporary.replace(destination)
Questo esempio mostra il salvataggio tramite file temporaneo e sostituzione atomica, quando supportata dal filesystem.
Il codice completo di pull.py deve naturalmente includere le chiamate effettive alle API, la gestione dei dati mancanti e le regole di aggiornamento dei dataset.
La scrittura atomica riduce il rischio di lasciare file JSON parziali in caso di interruzione, ma non sostituisce backup e controlli di integrità.
Validare i dati raccolti
Scaricare un file non significa necessariamente aver acquisito informazioni corrette.
Per questo ho introdotto verifiche sui dataset.
Per esempio:
import json
from pathlib import Path
data_dir = Path("data")
for name in [
"activities.json",
"daily.json",
"sleep.json",
"hrv.json"
]:
path = data_dir / name
if not path.exists():
print(f"{name}: MANCANTE")
continue
with path.open(encoding="utf-8") as f:
payload = json.load(f)
print(
f"{name}: "
f"{len(payload)} elementi"
)
Nel nostro progetto abbiamo verificato la presenza di 366 chiavi per i dataset giornalieri relativi a riepiloghi, sonno e HRV durante una delle prime raccolte.
È stata una verifica utile, ma il solo numero di record non basta.
Un Collector affidabile dovrebbe controllare anche:
- Data più recente disponibile.
- Presenza di identificatori duplicati.
- Coerenza delle unità di misura.
- Integrità sintattica dei JSON.
- Completezza della finestra temporale richiesta.
La lezione più importante: distinguere raccolta e consultazione
Una delle decisioni architetturali fondamentali è stata separare il componente che raccoglie i dati da quello che li espone all’intelligenza artificiale.
Il Garmin Collector comunica con il servizio remoto.
Il Garmin MCP Server, invece, legge i dati già presenti nello storage.
Questo significa che il motore AI non deve necessariamente autenticarsi presso Garmin ogni volta che riceve una domanda.
Il vantaggio è duplice.
Da un lato, riduciamo la dipendenza dalle API esterne. Dall’altro, manteniamo un archivio locale che può essere consultato anche quando il servizio remoto non è temporaneamente disponibile.
C’è però una conseguenza importante: se il Collector smette di funzionare, il server MCP può continuare a rispondere utilizzando dati non aggiornati.
È esattamente il problema che abbiamo incontrato durante una verifica dell’infrastruttura.
Il server MCP rispondeva correttamente, ma l’ultima attività disponibile risaliva al 1° novembre.
Dopo aver ripristinato l’autenticazione del Collector, una nuova raccolta ha permesso al motore AI di leggere anche l’attività del 10 novembre.
Questo ci ha insegnato che la disponibilità del servizio e la freschezza dei dati sono due indicatori distinti.
In futuro sarà utile esporre anche la data dell’ultima raccolta riuscita, così che l’assistente AI possa dichiarare esplicitamente quanto sono aggiornate le informazioni utilizzate.
Sicurezza e protezione dei dati personali
I dati Garmin comprendono informazioni potenzialmente sensibili, soprattutto quando riguardano salute, sonno e parametri fisiologici.
Per questo motivo il progetto segue alcuni principi:
- Le credenziali non vengono inserite nel codice.
- I token sono conservati in file protetti.
- I dati vengono archiviati su infrastruttura privata.
- L’accesso agli strumenti MCP viene limitato ai componenti autorizzati.
- Le informazioni personali non vengono pubblicate nei repository.
- Le copie di backup devono essere protette quanto i dati originali.
È inoltre importante ricordare che le integrazioni non ufficiali possono cambiare comportamento o smettere di funzionare in seguito a modifiche del servizio remoto.
Un sistema di produzione dovrebbe prevedere monitoraggio, gestione delle credenziali e procedure di recupero.
Conclusioni
Il primo obiettivo del progetto è stato raggiunto: trasformare i dati raccolti da Garmin in informazioni strutturate e disponibili localmente.
Abbiamo costruito una pipeline capace di:
- Autenticarsi presso il servizio di accesso ai dati Garmin.
- Gestire token temporanei.
- Scaricare attività e parametri fisiologici.
- Organizzare i dati in file JSON.
- Conservare uno storico utilizzabile da applicazioni successive.
- Separare la raccolta dalla consultazione tramite intelligenza artificiale.
Il risultato non è ancora un coach AI.
È qualcosa di più fondamentale: una base dati personale, strutturata e interrogabile, sulla quale costruire un sistema intelligente.
Nel prossimo articolo vedremo come portare questa pipeline all’interno di un cluster Kubernetes RKE2, utilizzando CronJob per la raccolta automatica, Secret per le credenziali e volumi persistenti Synology iSCSI per la conservazione dei dati.
Affronteremo anche problemi reali incontrati durante l’implementazione, come i conflitti Multi-Attach e i Job bloccati.
