“Diese Daten sind nicht die, die ihr sucht.” — Bosch eBike Connect GPX-Export, 2026
Das Problem
Du fährst. Brustgurt am Start, Nyon-Display zeigt live deine Herzfrequenz. Die App rechnet brav Kalorien. Du siehst: 434 kcal verbrannt.
Dann lädst du die GPX-Datei herunter — für Home Assistant, Garmin Connect, was auch immer. Und was kommt raus?
{
"calories": 0.0,
"avg_heart_rate": 0.0,
"heart_rate": [null, null, null]
}
Nichts. Bosch hat die Daten. Bosch zeigt die Daten. Bosch gibt die Daten nicht raus.
Die Ursache
Bosch speichert Herzfrequenz und Kalorien in der Cloud, aber der GPX-Export ist absichtlich kastriert:
- Standard-GPX hat keine HR-Felder
- Garmin TrackPoint Extensions würden HR erlauben — Bosch fügt sie nicht ein
- Ergebnis: Lock-in zur eigenen App
Der Todesstern hortet deine Gesundheitsdaten. Zeit für einen Rebellenangriff.
Die Lösung
Statt GPX: Die interne API direkt nutzen — dieselbe, die auch die Bosch-App verwendet.
ebike-dl → Bosch API → JSON (mit allem) → Python → HA REST-Sensoren
Schritt 1: Vorbereitung in der Bosch-App
Kritisch: Standard ist AUS.
App → Einstellungen → Datenschutz → Gesundheitsdaten speichern ✅
Ohne diesen Toggle liefert die API calories: 0.0 und avg_heart_rate: 0.0 — obwohl das Nyon die Werte live angezeigt hat. Die Daten wurden einfach nicht gespeichert.
Schritt 2: ebike-dl installieren
ebike-dl ist das Tool, das die Bosch-API anzapft.
pip install ebike-dl
# oder mit pipx (empfohlen für CLI-Tools):
pipx install ebike-dl
Schritt 3: Fahrten abrufen
Credentials am besten als Umgebungsvariablen — nicht im Script:
export EBIKE_DL_LOGIN="luke.skywalker@rebellion.local"
export EBIKE_DL_PASSWORD="<DEIN-PASSWORT>"
#!/usr/bin/env python3
import subprocess
from datetime import datetime, timedelta
since_date = (datetime.now() - timedelta(days=30)).strftime("%Y/%m/%d")
subprocess.run([
"ebike-dl", "fetch",
"--since", since_date,
"--out-dir", "./data",
], check=True)
Ergebnis: JSON-Dateien mit allen Daten:
{
"id": "66660001",
"distance": 37890,
"calories": 427.0,
"avg_heart_rate": 127.0,
"max_heart_rate": 146,
"avg_speed": 27.49,
"driving_time": 4980,
"start_time": "2026-02-24T15:17:04+01:00"
}
(Hinweis: JSON hat keine Kommentare — distance ist in Metern, driving_time in Sekunden.)
Schritt 4: Aggregations-Script für Home Assistant
HA REST-Sensoren brauchen eine einzige JSON-Datei, keine einzelnen Rides:
#!/usr/bin/env python3
# Benötigt Python 3.11+ (fromisoformat mit Timezone-Support)
import json
from pathlib import Path
from datetime import datetime, timezone, timedelta
data_dir = Path("./data")
rides = []
for f in data_dir.glob("ride_*.json"):
with open(f) as fp:
rides.append(json.load(fp))
if not rides:
print("Keine Fahrten gefunden.")
exit(0)
rides.sort(key=lambda x: x["start_time"], reverse=True)
last_ride = rides[0]
cutoff = datetime.now(tz=timezone.utc) - timedelta(days=30)
recent = [
r for r in rides
if datetime.fromisoformat(r["start_time"]) > cutoff
]
def avg_distance(lst):
return round(sum(r["distance"] for r in lst) / len(lst) / 1000, 2) if lst else 0
stats_30d = {
"total_km": round(sum(r["distance"] for r in recent) / 1000, 2),
"total_kcal": int(sum(r["calories"] for r in recent)),
"rides": len(recent),
"avg_distance_km": avg_distance(recent),
}
stats_all = {
"total_km": round(sum(r["distance"] for r in rides) / 1000, 2),
"total_kcal": int(sum(r["calories"] for r in rides)),
"rides": len(rides),
"avg_distance_km": avg_distance(rides),
}
summary = {
"last_ride": {
"distance_km": round(last_ride["distance"] / 1000, 2),
"calories": int(last_ride.get("calories", 0)),
"avg_speed_kmh": last_ride.get("avg_speed", 0),
"avg_heart_rate": int(last_ride.get("avg_heart_rate", 0)),
"duration_min": round(last_ride.get("driving_time", 0) / 60, 1),
"date": last_ride["start_time"],
},
"stats_30d": stats_30d,
"stats_all_time": stats_all,
"last_updated": datetime.now().isoformat(),
}
with open("/config/www/ebike_summary.json", "w") as f:
json.dump(summary, f, indent=2)
print("✅ ebike_summary.json aktualisiert")
Die JSON landet in /config/www/ — damit stellt Home Assistant sie unter /local/ebike_summary.json bereit. Das Script muss auf dem HA-Host laufen (oder im Container, falls HA in Docker läuft).
Schritt 5: REST-Sensoren in Home Assistant
rest:
- resource: http://localhost:8123/local/ebike_summary.json
scan_interval: 300
sensor:
- name: "eBike Letzte Fahrt Distanz"
unique_id: ebike_last_ride_distance
value_template: "{{ value_json.last_ride.distance_km | float(0) }}"
unit_of_measurement: "km"
icon: mdi:bike
device_class: distance
state_class: measurement
- name: "eBike Letzte Fahrt Kalorien"
unique_id: ebike_last_ride_calories
value_template: "{{ value_json.last_ride.calories | int(0) }}"
unit_of_measurement: "kcal"
icon: mdi:fire
state_class: measurement
- name: "eBike Letzte Fahrt Herzfrequenz"
unique_id: ebike_last_ride_heart_rate
value_template: "{{ value_json.last_ride.avg_heart_rate | int(0) }}"
unit_of_measurement: "bpm"
icon: mdi:heart-pulse
state_class: measurement
- name: "eBike 30d Total km"
unique_id: ebike_30d_total_km
value_template: "{{ value_json.stats_30d.total_km | float(0) }}"
unit_of_measurement: "km"
icon: mdi:bike
state_class: total_increasing
- name: "eBike 30d Kalorien"
unique_id: ebike_30d_total_kcal
value_template: "{{ value_json.stats_30d.total_kcal | int(0) }}"
unit_of_measurement: "kcal"
icon: mdi:fire
state_class: total_increasing
- name: "eBike All-Time km"
unique_id: ebike_alltime_total_km
value_template: "{{ value_json.stats_all_time.total_km | float(0) }}"
unit_of_measurement: "km"
icon: mdi:bike
state_class: total_increasing
- name: "eBike All-Time Kalorien"
unique_id: ebike_alltime_total_kcal
value_template: "{{ value_json.stats_all_time.total_kcal | int(0) }}"
unit_of_measurement: "kcal"
icon: mdi:fire
state_class: total_increasing
Wichtig: Die Entity-IDs, die HA tatsächlich erstellt, basieren auf dem name-Feld, nicht auf unique_id. Nachschlagen unter Developer Tools → States, nicht raten.
sensor.ebike_letzte_fahrt_distanz ← deutsch, nicht unique_id!
sensor.ebike_letzte_fahrt_kalorien
Schritt 6: Dashboard
type: entities
title: eBike Stats
entities:
- entity: sensor.ebike_letzte_fahrt_distanz
name: Letzte Fahrt
- entity: sensor.ebike_letzte_fahrt_kalorien
name: Kalorien
- entity: sensor.ebike_letzte_fahrt_herzfrequenz
name: Ø Herzfrequenz
- type: divider
- entity: sensor.ebike_30d_total_km
name: 30 Tage km
- entity: sensor.ebike_30d_kalorien
name: 30 Tage Kalorien
HR als Gauge:
type: gauge
entity: sensor.ebike_letzte_fahrt_herzfrequenz
name: Herzfrequenz
min: 0
max: 200
severity:
green: 60
yellow: 120
red: 160
Schritt 7: Automatisches Update per Cron
# /usr/local/bin/ebike-update.sh
#!/bin/bash
set -e
cd /pfad/zum/script
python3 fetch_rides.py # holt neue Rides von Bosch API
python3 build_summary.py # schreibt /config/www/ebike_summary.json
Crontab (crontab -e), zweimal täglich nach typischen Fahrtzeiten:
0 9 * * * /usr/local/bin/ebike-update.sh
0 18 * * * /usr/local/bin/ebike-update.sh
Lessons Learned
Bosch blockt Datacenter-IPs
Falls ebike-dl mit “Invalid credentials” fehlschlägt obwohl Login stimmt: Bosch blockt manchmal Server-IPs. Workaround: Cookie-basierter Login — im Browser einloggen, REMEMBER-Cookie aus DevTools → Application → Cookies kopieren und im Script nutzen statt Username/Password.
“Gesundheitsdaten speichern” ist der entscheidende Toggle
Standard: AUS. Das Nyon zeigt die HR live, die App zeigt Kalorien — aber ohne diesen Toggle speichert Bosch nichts davon. Die API liefert dann sauber Nullen. Erster Schritt bei jedem Setup.
HA Entity-IDs: Deutsch schlägt unique_id
Home Assistant leitet den Entity-Identifier aus dem name-Feld ab. Wer englische unique_ids und deutsche name-Felder mixt, sucht im Dashboard dann nach sensor.ebike_letzte_fahrt_distanz — nicht nach sensor.ebike_last_ride_distance. Developer Tools → States ist dein Freund.
Verifikation
# JSON prüfen
curl -s http://localhost:8123/local/ebike_summary.json | python3 -m json.tool | head -20
# Spezifisch letzte Fahrt
curl -s http://localhost:8123/local/ebike_summary.json | python3 -c "
import json,sys; d=json.load(sys.stdin)
r=d['last_ride']
print(f\"{r['date'][:10]}: {r['distance_km']} km, {r['calories']} kcal, {r['avg_heart_rate']} bpm\")
"
# HA Logs bei REST-Problemen
grep -i 'rest\|ebike' /config/home-assistant.log | tail -20