4.5 KiB
COROS-MCP — Historie/Backfill + Parser-Pitfalls (verifiziert 2026-08-18)
Ergänzt zu coros-mcp-data-shapes.md. Alle Befunde sind reale call-tool-Antworten, nicht geraten.
Health-Historie: days-Parameter + Server-Limit
queryDailyHealthData akzeptiert im Schema einen days-Parameter (integer, default 7).
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{"days":30}'
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{"days":365}'
WICHTIG (verifiziert 2026-08-18): Egal ob days:30, days:90 oder days:365 — die COROS-API liefert maximal ~53 Health-Tage zurück (z.B. nur 2026-05-16 .. 2026-08-15, 53 Blöcke). Das ist ein SERVER-Limit von COROS, kein Parser- oder Client-Fehler. Mehr Health-Historie gibt es über diese API nicht; für längere Geschichte bleibt nur der Sport/FIT-Pfad.
Auch bei "alles was geht" lohnt kein Versuch über 53 hinaus zu holen — wiederhole sportBackfill dafür nicht.
Sport-Historie: querySportRecords mit Datumsbereich
querySportRecords akzeptiert (anders als Health) startDate/endDate im Format yyyyMMdd (kein days) und sportTypeCodes-Array:
coros-mcp call-tool --tool querySportRecords --arguments-json \
'{"startDate":"20250501","endDate":"20260815","sportTypeCodes":[65535]}'
sportTypeCodes: [65535]= alle Sportarten (sonst numerische Codes, nie Namen).- Sport-Historie reicht deutlich weiter zurück als Health (1 Jahr+). 20 Records über 15 Monate bestätigt.
- Antwort enthält je Workout:
N. <Sportart> — YYYY-MM-DD,Time Window: startTimestamp=.. | endTimestamp=..,Duration,Avg HR: <n> bpm,Calories: <n> kcal,Distance: <x> km,LabelId: <Zahl>,SportType: <code>.
🔥 PITFALL: COROS-content[0].text enthält LITERALES \n, KEINE echten Newlines
(Wichtigster Fund — ohne Normalisierung matcht nichts.)
querySportRecords (und teils andere) liefern im Text literale 2-Zeichen-Sequenz \n (ein Backslash + n), keine echten Zeilenumbrüche. splitlines() / repr() in einem Launch zeigen das nicht zuverlässig.
Debug-Befund (2026-08-18):
text = d['content'][0]['text']
'\n' in text # False (kein echter Newline)
'\\n' in text # True (literales Backslash-n vorhanden)
Fix — normalisiere VOR jedem Zeilen-Parsing:
text = text.replace('\\n', '\n')
lines = text.splitlines() # erst jetzt funktioniert splitlines()
Übersehen → alle Regex gegen splitlines() finden 0 Zeilen (kein Fehler, nur 0 Treffer). Achte auf die richtige Backslash-Tiefe: Ersetze das 1-Backslash-\n-Literal, nicht doppel.
⏱️ Dauer-Format ist uneinheitlich: HH:MM:SS vs MM:SS vs SS
COROS-Duration:-Werte kommen gemischt:
1:00:14→ 1h = 60 min (3 Segmente)2:03:36→ 2h03m = 123 min (3 Segmente)51:35→ 51 min (2 Segmente — NICHT 51std35!)0:22→ 22 Sek → 0 min (2 Segmente)
Fehlerhafte Regex (\d+):(\d+)(?::(\d+))? macht 51:35 fälschlich zu 51*60+35 = 3095 min (absurd 63h). Robust:
m = re.search(r'Duration:\s*([\d:]+)', block)
seg = [int(x) for x in m.group(1).split(':')]
if len(seg) == 3: # HH:MM:SS
dauer_min = seg[0]*60 + seg[1]
elif len(seg) == 2: # MM:SS
dauer_min = seg[0]
else: # SS
dauer_min = seg[0] // 60
Sport-Daten → SEPARATE Tabelle (nicht health_records)
Ein Tag kann mehrere Workouts haben → passt NICHT in health_records (1 Zeile/Tag). Eigene Tabelle:
CREATE TABLE IF NOT EXISTS sport_records (
label_id TEXT PRIMARY KEY, -- COROS-LabelId je Workout (natürlicher PK)
datum TEXT NOT NULL, -- yyyyMMdd
sportart TEXT NOT NULL,
sport_type_code INTEGER, -- COROS SportType-Code
dauer_min INTEGER,
avg_hr INTEGER,
kalorien INTEGER,
distanz_km REAL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_sport_datum ON sport_records(datum);
Idempotent: INSERT OR IGNORE ... ON CONFLICT(label_id) DO NOTHING — Workouts sind nach Import unveränderlich, kein UPDATE nötig. (Direkter DB-Write, da health_records-API createRule leer = nur Superuser schreibt; siehe pocketbase-deployment.)
Reihenfolge bei Backfill in bestehende DB
- Wenn schon fehlerhafte Records mit dem alten Dauer-Parser geschrieben wurden → Flushing die Tabelle (
DELETE FROM sport_records) oder per label_id neu überschreiben, DANN Neu-Load mit korrektem Parser. - Immer vorher DB-Backup (Backup-Regel):
cp /opt/pocketbase/pb_data/data.db data.db.bak-<ts>.