• Hallo Gast, wir suchen den Renner der Woche 🚴 - vielleicht hast du ein passendes Rennrad in deiner Garage? Alle Infos

Mywhoosh

Ok, ich stelle es hier mal öffentlich rein. Die ursprüngliche Quelle ist hier zu finden: https://github.com/JayQueue/MyWhoosh2Garmin
Das habe ich mit Hilfe von ChatGPT überarbeitet und daraus wurde das folgende Script. Den Downloadordner in Zeile 29 muss man entsprechend anpassen. Bei mir läuft das unter Linux. Falls ihr noch Hilfe braucht, um das zum Laufen zu bringen, kann eine KI schnell weiterhelfen.


Python:
#!/usr/bin/env python3
import os
import re
from pathlib import Path
from datetime import datetime
from getpass import getpass
import logging

import garth
from garth.exc import GarthException, GarthHTTPError
from fit_tool.fit_file import FitFile
from fit_tool.fit_file_builder import FitFileBuilder
from fit_tool.profile.messages.record_message import RecordMessage, RecordTemperatureField
from fit_tool.profile.messages.session_message import SessionMessage
from fit_tool.profile.messages.lap_message import LapMessage
from fit_tool.profile.messages.file_id_message import FileIdMessage

# === Logging ===
SCRIPT_DIR = Path(__file__).resolve().parent
log_file_path = SCRIPT_DIR / "myWhoosh2Garmin.log"
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
file_handler = logging.FileHandler(log_file_path)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# === Pfade ===
MYWHOOSH_PATH = Path("/home/Barst/Downloads")
BACKUP_PATH = MYWHOOSH_PATH / "backup"
BACKUP_PATH.mkdir(exist_ok=True)

# === Garmin Auth ===
TOKENS_PATH = SCRIPT_DIR / '.garth'

def authenticate_to_garmin():
    try:
        if TOKENS_PATH.exists():
            garth.resume(TOKENS_PATH)
        else:
            username = input("Garmin Benutzername: ")
            password = getpass("Garmin Passwort: ")
            garth.login(username, password)
            garth.save(TOKENS_PATH)
        logger.info(f"Authenticated as: {garth.client.username}")
    except GarthException as e:
        logger.error(f"Garmin Auth Fehler: {e}")
        exit(1)

# === Hilfsfunktionen ===
def calculate_avg(values):
    return round(sum(values)/len(values),1) if values else 0

def append_value(values, message, field_name):
    val = getattr(message, field_name, None)
    if val is not None:
        values.append(val)

def reset_values():
    return [], [], []

def get_most_recent_fit_file(folder: Path) -> Path:
    fit_files = list(folder.glob("*.fit"))
    if not fit_files:
        return None
    return max(fit_files, key=os.path.getctime)

def generate_new_filename(fit_file: Path) -> Path:
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    return BACKUP_PATH / f"{fit_file.stem}_{timestamp}.fit"

# === Hauptlogik ===
def cleanup_fit_file(fit_file_path: Path, new_file_path: Path):
    builder = FitFileBuilder()
    fit_file = FitFile.from_file(str(fit_file_path))
    cadence_values, power_values, hr_values = reset_values()

    for record in fit_file.records:
        msg = record.message
        if isinstance(msg, FileIdMessage):
            # Override manufacturer/product but keep other fields
            msg.manufacturer = 1
            msg.product = 1836
        if isinstance(msg, LapMessage):
            continue
        if isinstance(msg, RecordMessage):
            msg.remove_field(RecordTemperatureField.ID)
            append_value(cadence_values, msg, "cadence")
            append_value(power_values, msg, "power")
            append_value(hr_values, msg, "heart_rate")
        if isinstance(msg, SessionMessage):
            if not msg.avg_cadence:
                msg.avg_cadence = calculate_avg(cadence_values)
            if not msg.avg_power:
                msg.avg_power = calculate_avg(power_values)
            if not msg.avg_heart_rate:
                msg.avg_heart_rate = calculate_avg(hr_values)
            cadence_values, power_values, hr_values = reset_values()
        builder.add(msg)
            
    # Datei bauen und speichern
    builder.build().to_file(str(new_file_path))
    logger.info(f"Bereinigte Datei gespeichert: {new_file_path}")

# === Main ===
def main():
    authenticate_to_garmin()

    latest_fit = get_most_recent_fit_file(MYWHOOSH_PATH)
    if not latest_fit:
        logger.error(f"Keine FIT-Dateien in {MYWHOOSH_PATH} gefunden.")
        exit(1)

    logger.info(f"Neueste Datei: {latest_fit}")
    new_file = generate_new_filename(latest_fit)

    try:
        cleanup_fit_file(latest_fit, new_file)
    except Exception as e:
        logger.error(f"Fehler beim Bereinigen: {e}")
        exit(1)

    # Upload zu Garmin
    try:
        with open(new_file, "rb") as f:
            garth.client.upload(f)
        logger.info(f"Erfolgreich hochgeladen: {new_file}")
    except GarthHTTPError:
        logger.info("Aktivität möglicherweise schon bei Garmin Connect vorhanden.")

if __name__ == "__main__":
    main()
 
Das Script läuft auf einem anderen Rechner als MyWhoosh. Trainiert wird im Keller unter Windows 11. Aber Python läuft auch unter Windows.
 
Ich klicke auf mein Profil und dann wird der ZwiftHub sofort angezeigt bei der PowerSource.
Bei HR wird erstmal für 3s nichts angezeigt bevor dann der Tickr angezeigt wird.
Den ZwiftHub hab ich bei der Herzfrequenz noch nie gesehen.

Ich nutze mywoosh am iPad falls das relevant ist.
Danke für deine Info's.


Habe das am Wochenende nochmal getestet.

Samstag hat das funktioniert und Sonntag dann wieder nicht:

Mein Garmin HRM war in der Zwift Companion als Sensor direkt verbunden. Ich hatte dann diesen entfernt und gehofft das er sich dann nicht automatisch verbindet.

Allerdings auch hier wieder das selbe spiel. Ich verbinde den Zwift und nach ca 3s steht auch bei Herzfrequenz Zwift Hub...Habe wie immer dann beim Garmin auf pair geklickt, aber er verbindet sich dann nicht.

Habe dann zu Rouvy gewechselt und der erkennt den Garmin dann sofort.
Laut Mywhoosh Support soll ich die Link App versuchen, mit dem Hinweis das der Zwift Hub nur als bedingt kompatibel gilt.
Letzter Versuch wäre noch den Gurt wieder in der Zwift App zu verbinden und dann zu schauen ob er was anzeigt wenn ich den Hub als Sensor verwende.
 
Funktioniert jetzt super. Die neuste Fit-Datei im festgelegten Ordner wird automatisch zu Garmin hochgeladen und zeigt auch den Trainingeffekt.
Zeigt er dir auch alle Daten der Trainingsbelastung an? Trainingszustand, Erholung, akute. Belastung etc.

Das geht m.W.n. nur wenn du über ein Garmin Gerät aufzeichnest.
 
Ja, in Garmin Connect wird das Wichtigste angezeigt. Das Script ändert ja den "Hersteller" vom Fit-File auf Garmin. Der akute Trainingszustand innerhalb einer Einheit wird aber nicht angezeigt. Das hier ist alles von MyWhoosh:
Screenshot 2025-11-23 134038.png
 
Also den Belastungswert hat er übernommen. Nicht jedoch die anderen Daten wie "Akute Trainingsbelastung", "Belastungsfokus" und "Ausdauerwert". D.h. ich muss weiterhin doppelt aufzeichnen 😢

Edit: nach 1 Std. sind die Daten da. Garmin braucht wohl ne Weile bis die aktualisiert werden.

Danke @Barst - hab das Script via KI angepasst. Da ich auf dem iPad aufzeichne, lass ich im Nachgang einfach ein Scipt über Google Colab laufen. Muss halt davor die FIT Datei manuell downloaden.
 
Zuletzt bearbeitet:
Den automatischen Download kann man bestimmt auch noch irgendwie hinbekommen. Allerdings nervt mich im Moment mehr, dass die Fit-Dateien neuerdings erst nach Stunden zum Download bereit stehen. Und das, obwohl sie schon lange auf Strava zu sehen sind.
 
Den automatischen Download kann man bestimmt auch noch irgendwie hinbekommen. Allerdings nervt mich im Moment mehr, dass die Fit-Dateien neuerdings erst nach Stunden zum Download bereit stehen. Und das, obwohl sie schon lange auf Strava zu sehen sind.
Man könnte sich die FIT-Datei direkt von Strava holen oder?

Edit: hab n bisschen rumgedocktert und keine Lösung gefunden. Ich bekomme es nicht hin, dass er die Original-Datei aus der letzten Radfahrt exportiert.
 
Zuletzt bearbeitet:
Ich bleib jetzt dabei, dass ich nach jeder Einheit die Datei von Strava ziehe und mit diesem Script anpasse. Ich muss eh an den Rechner weil ich meine Intervalle prüfe :)

# --- Schritt 1: Pakete installieren ---
!pip install fit-tool
# --- Schritt 2: Imports ---
from pathlib import Path
from datetime import datetime
from fit_tool.fit_file import FitFile
from fit_tool.fit_file_builder import FitFileBuilder
from fit_tool.profile.messages.record_message import RecordMessage, RecordTemperatureField
from fit_tool.profile.messages.session_message import SessionMessage
from fit_tool.profile.messages.lap_message import LapMessage
from fit_tool.profile.messages.file_id_message import FileIdMessage
# --- Schritt 3: Upload-Funktion ---
from google.colab import files
def upload_fit_file():
print("Bitte FIT-Datei hochladen...")
uploaded = files.upload()
file_name = list(uploaded.keys())[0]
return Path(file_name)
# --- Schritt 4: Bereinigungsfunktion ---
def calculate_avg(values):
return round(sum(values)/len(values), 1) if values else 0
def append_value(values, message, field_name):
val = getattr(message, field_name, None)
if val is not None:
values.append(val)
def reset_values():
return [], [], []
def cleanup_fit_file(fit_in: Path, fit_out: Path):
fit_file = FitFile.from_file(str(fit_in))
builder = FitFileBuilder()
cadence_values, power_values, hr_values = reset_values()
for record in fit_file.records:
msg = record.message
if isinstance(msg, FileIdMessage):
msg.manufacturer = 1
msg.product = 1836
if isinstance(msg, LapMessage):
continue
if isinstance(msg, RecordMessage):
msg.remove_field(RecordTemperatureField.ID)
append_value(cadence_values, msg, "cadence")
append_value(power_values, msg, "power")
append_value(hr_values, msg, "heart_rate")
if isinstance(msg, SessionMessage):
if not msg.avg_cadence:
msg.avg_cadence = calculate_avg(cadence_values)
if not msg.avg_power:
msg.avg_power = calculate_avg(power_values)
if not msg.avg_heart_rate:
msg.avg_heart_rate = calculate_avg(hr_values)
cadence_values, power_values, hr_values = reset_values()
builder.add(msg)
builder.build().to_file(str(fit_out))
print(f"Bereinigte Datei gespeichert: {fit_out}")
# --- Schritt 5: Workflow ---
fit_file = upload_fit_file()
output_file = Path(f"{fit_file.stem}cleaned{datetime.now().strftime('%Y%m%d_%H%M%S')}.fit")
cleanup_fit_file(fit_file, output_file)
# --- Schritt 6: Download der bereinigten Datei ---
files.download(str(output_file))
 
Mein gestriges Workout ist nicht online unter den Activities zu sehen. Habe es öfters, dass das dauert, aber so lange noch nie. Ging/geht es noch jemandem so?
 
Guter Punkt, das stimmt. Bislang hatte ich da keine Sync eingerichtet, sondern die Activities bei Garmin hochgeladen und von dort aus zu Strava. Dann schaue ich mal, ob das noch geht und ob das dann auch "rückwirkend" Workouts hoch lädt. Wenn das fehlerfreier ist, werde ich dann wohl den Weg für die Zukunft wählen. Danke dir.
 
Ich weiß inzwischen auch, warum mein erster Versuch, die Fit-Datei von Strava mit dem Script zu laden, fehlschlug. Da hatte ich mit dem Addon "Sauce For Strava" die Fit-Datei gespeichert. Mit der normalen Exportfunktion von Strava geht es.
 
Mein gestriges Workout ist nicht online unter den Activities zu sehen. Habe es öfters, dass das dauert, aber so lange noch nie. Ging/geht es noch jemandem so?
Gestern ging's bei mir wieder schnell (30 Minuten).
Vor ein paar Tagen hat's bei meiner Frau einen Tag gedauert und bei einem Bekannten sogar zwei Tage.

Auch wenn's mal länger dauert, es ging bei mir noch keine Aktivität verloren. :daumen:
 
Gestern ging's bei mir wieder schnell (30 Minuten).
Vor ein paar Tagen hat's bei meiner Frau einen Tag gedauert und bei einem Bekannten sogar zwei Tage.

Auch wenn's mal länger dauert, es ging bei mir noch keine Aktivität verloren. :daumen:
Bei mir war sie dann auch nach knapp einem Tag online. In der mywhoosh-App im Kalender war sie aber auch sofort zu sehen, denke das wäre ein guter Indikator, falls wirklich was verloren gegangen wäre.
 
Zurück