Initial: Mesh-Configs vom 03.09.2026 01:23

This commit is contained in:
root
2026-09-03 01:23:55 +02:00
commit 4b65134f8c
797 changed files with 146197 additions and 0 deletions
@@ -0,0 +1,78 @@
---
name: coros-mcp-integration
description: "Use when: COROS-Sportuhr-Daten in PersoEnt anbinden."
version: 1.0.0
author: mehlzoerwer
platforms: [linux]
models:
default: mimo-v2-flash
license: MIT
related_skills: [content-creator, pocketbase-deployment]
---
# COROS-MCP — Sportuhr-Daten in PersoEnt
Nutze diesen Skill, wenn PersoEnt (oder ein anderes Projekt) **echte COROS-Sportuhr-Gesundheitsdaten** anbinden soll: Schlaf, HRV, Stress, Ruhe-Herzfrequenz, Training, Erholung, VO2max.
## Status (2026-08-10)
- **Installiert** global: `npm install -g coros-mcp` (OFFIZIELLES Paket von `coros_openplatform`, ~0.1.1)
- **Eingeloggt** — Token gecacht in `/root/.coros-mcp-skill-gateway-ts/<region>/token.json`
- **22 MCP-Tools** verfügbar, Endpoint `mcp.coros.com` (Europa: `mcpeu.coros.com`)
## CLI-Syntax (WICHTIG — Flags, nicht Positional)
Das Paket nutzt Flags, NICHT Positionsargumente:
```bash
coros-mcp list-tools # alle Tools + Schema
coros-mcp describe-tool --tool <NAME> # Schema eines Tools
coros-mcp call-tool --tool <NAME> --arguments-json '{}' # Tool aufrufen
```
- `call-tool --tool X queryDailyHealthData` (ohne `--tool`) → **Fehler "unknown argument"**
- `--args '{}'` / `--name X` / `--tool` mit Positional → **alles falsch** (richtig: `--tool NAME --arguments-json JSON`)
## Login-Flow (einmalig, nur Nutzerkonto)
```bash
coros-mcp login-start # erzeugt Browser-Login-Link (mcpeu.coros.com/cli/login/...)
# Nutzer oeffnet Link + meldet sich mit COROS-Konto an
coros-mcp login-finish # bestaetigt Session -> 'login ok'
coros-mcp login-status # 'no pending login session' = sauber/fertig
```
Login-Link ist region-/sessiongebunden. Andere COROS-Region → `--issuer`/`--mcp-url` anpassen.
## Wichtigste Tools (Daten fuer PersoEnt-Gesundheitsdetektiv)
| Tool | Daten |
|------|-------|
| `queryDailyHealthData` | 7 Tage: Schritte, Kalorien, Stress, Schlaf (Tief/Leicht/REM), Schlaf-HR |
| `querySleepHrv` | taegl. HRV + Baseline + Normbereich |
| `queryRestingHeartRate` / `queryAvgHeartRate` | Ruhe-/durchschnittl. HF |
| `queryStressLevel` / `queryStressTimeSeries` | Stress (avg/roh) |
| `querySportRecords` | Workout-Protokoll (+ labelId, sportType) |
| `getActivityDetail` / `queryActivityLapData` | Activity-Details inkl. HR |
| `queryRecoveryStatus` | Erholung % + Level |
| `queryFitnessAssessmentOverview` | VO2max, Threshold-Pace |
| `querySleepData` | Schlafscore, Dauer, Stufen |
| `queryDevices` / `queryUserInfo` | Geraete / Profil |
## Integration in PersoEnt (Architektur-Vorgabe)
- **Server-seitig auf VPS** (Master): Poller ruft `coros-mcp call-tool` auf → Daten nach **PocketBase** (`health_records`-ahnliche Struktur) → PersoEnt-Dashboard/Detektiv liest von dort.
- HRV + Schlaf + Stress + Erholung = ideale **Kausal-Merkmale** fuer den Gesundheitsdetektiv (besser als Google-Health-Fit).
- Gleiche Basis-Assets (ein COROS-Konto) fuer mehrere Kanaele/Personas.
## Edge Cases
- **Daten brauchen echten Login** — ohne `login-finish` kein Token, `call-tool` schlaegt fehl.
- Zeitfenster-Tools (`queryCustomActivityLapData`) brauchen `startTimestamp`/`endTimestamp` in Unix-Sekunden.
- Sport-Records-Details brauchen `labelId` + `sportType` aus der Records-Liste.
## Verifizierte Daten-Shapes
Siehe `references/coros-mcp-data-shapes.md` — echte Antwort-Formate (queryDailyHealthData = ein String mit `--- JJJJMMTT ---`-Tag-Abschnitten, Sport-Records mit labelId/sportType), Token-Sicherheit (root-only Cache + age-Backup, NIE die Klartext-token.json verschluesseln) und der PersoEnt-Write-Hinweis (kein datum-Feld).
## 🔥 Robustes Parsen der 7-Tage-Helth-Antwort (verifiziert 2026-08-15)
`queryDailyHealthData` liefert EINEN String mit 7 `--- 20260815 ---`-Tagbloecken. Naive Extraktion (erstes "Steps" finden) ist FEHLERHAFT — es erwischt das Datum oder einen alten Tag. Robust:
- **Letzten Tages-Block nehmen**, nicht den ersten: `re.split(r'--- (\d{8}) ---', text)` → nimm das letzte (date, body)-Paar.
- **`Stress: Avg 22`** hat "Avg" zwischen Label und Zahl → Regex `re.escape(label) + r'[^\d]*?([\d.,]+)'` (Ueberspringt Zwischenwoerter).
- **Puls zuverlaessig aus `Sleep HR: Avg 51 bpm`** im Health-Block (AVG HR-Call `queryAvgHeartRate` hat anderes/leeres Block-Format — nicht darauf stuetzen).
- **Schlaf** = `Total: 7h 46min` → `(\d+)h (\d+)min` → 7.77h.
- COROS-Werte kommen mit **Tausender-Komma** (z.B. `9,304`) → `,` entfernen.
- Nach jedem Fix: **real gegen die Live-Antwort testen** (echten `call-tool`-String parsen), nie von Schema raten — Daten-Shapes weichen ab.
@@ -0,0 +1,95 @@
# 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).
```bash
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:
```bash
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):**
```python
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:**
```python
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:**
```python
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:
```sql
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
1. 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.
2. Immer vorher DB-Backup (Backup-Regel): `cp /opt/pocketbase/pb_data/data.db data.db.bak-<ts>`.
@@ -0,0 +1,51 @@
# COROS-MCP — verifizierte Tool-Calls + Daten-Shapes (2026-08-15)
Proof-of-Concept mit echtem Login, alle Werte real. Endpoint Europa `mcpeu.coros.com`, Token-Cache `/root/.coros-mcp-skill-gateway-ts/<region>/token.json`.
## Zugriffshinweis (Token-Sicherheit, Team-Auflage)
- Token-Datei root-only (`chmod 600`), Cache `chmod 700`.
- Zusaetzlich age-verschluesseltes Backup: `age-keygen -o /root/.coros-age-key.txt; age -r <PUBKEY> -o /root/.coros-token.json.age <token.json>`.
- WICHTIG: **NICHT die Klartext-`token.json` verschluesseln/ersetzen** — die `coros-mcp`-CLI LIESST sie selbst (Klartext nötig fuer den Betrieb). Verschluesselung nur als SEPARATE Sicherungskopie ablegen.
## Korrekte Aufruf-Syntax (verifiziert)
```bash
# Achtung: Flags! Nicht positional. KEIN --args/--name/--tool-positional.
coros-mcp list-tools
coros-mcp describe-tool --tool queryDailyHealthData
coros-mcp call-tool --tool queryDailyHealthData --arguments-json '{}'
```
## Echte Ergebnis-Shapes (Anhaltspunkt fuer Parser)
### queryDailyHealthData ({} args) — Text, 7 Tage
```
"Daily Health Data — Last 7 days | Resting HR: 52 bpm | HRV Baseline: 42 ms
--- 20260815 ---
Steps: 981 | Calories: 47 kcal | Exercise: 0 min
Stress: Avg 22
Sleep Summary:
Total: 7h 46min | Deep: 1h 57min | Light: 4h 17min | REM: 1h 23min | Awake: 9 min
```
Antwort-Container: `{"content":[{"type":"text","text":"<string>"}],"isError":false}` — **Daten sind EIN String**, mit `\n`-Zeilen, pro Tag Abschnitt `--- JJJJMMTT ---`. Parser: nach Datums-Marker + Label splitten.
### querySportRecords ({}) — 1+ Records
```
"Sport Records — DATE to DATE (N records)
1. Indoor Cycling — 2026-08-11
Location: Indoor Rad
Time Window: startTimestamp=1786472136 | endTimestamp=1786475750
Duration: 1:00:14 | Avg HR: 114 bpm | Calories: 629 kcal
LabelId: 479635354870120452 | SportType: 201"
```
Sauber parsebar via Regex auf `LabelId:` + `SportType:` (fuer Activity-Details noetig).
### querySleepHrv ({}), queryRecoveryStatus ({}), queryFitnessAssessmentOverview ({})
- HRV: pro Tag `HRV Avg: 55 ms — Normal`, `Baseline: 61 ms`
- Recovery: `Recovery: 100%`, `Level: Heavy training allowed`
- Fitness: `Threshold Pace: 4:45 /km`
### queryUserInfo ({})
`Height: 173.0 cm | Weight: 100.6 kg | Birthday: 1972-02-21 (Age: 54) | Gender: Male | Nickname: Thorsten`
## PersoEnt-Write-Hinweis (strukturierte Gesundheitsdaten)
COROS liefert KEIN Datums-Feld in der Antwort — das Datum steckt im `--- JJJJMMTT ---`-Marker. PersoEnt `health_records`-Collection hat KEIN `datum`-Feld; Datum geht verloren, wenn man es nicht als String in einem Text-Feld oder via Record-`created` abbildet. (siehe pocketbase-deployment: leere createRule → Superuser-Token noetig.)