Brücke zwischen zwei Welten

Ich habe mich mal wieder an einem kleinen Datenfreak-Projekt festgebissen. Ergebnis: ich kann die Daten des orthostatischen Tests und das Schlaftracking der Polar Vantage V nun in WKO5 auswerten. :) Sehr beeindruckend, oder?! ;)

Ausgangslage

Alle meine Trainingsdaten laufen bei TrainingPeaks zusammen. Egal mit welcher Uhr von welchem Hersteller ich meinen Lauf aufzeichne: alles wird von der jeweiligen Herstellerplattform immer auch zu TrainingPeaks synchronisiert. Erst dort sehe ich mir die Daten an, hinterlasse mein subjektives Belastungsempfinden (RPE) und schreibe vielleicht noch einen Kommentar zu der Einheit.

Aber nicht nur Garmin, Polar und Suunto können Daten in mein Tagebuch dort schreiben. Auch meine Withings-Body-Cardio-Waage hinterlässt mein aktuelles Gewicht, während EliteHRV gleich vier Werte schreibt: Ruhepuls, HRV, Schlafdauer und Schlafqualität. Diese Metriken kann ich mir in TrainingPeaks dann zusätzlich zu meinen Laufdaten ansehen. Irgendwie. Also nicht besonders komfortabel und sinnvoll.

Daher habe ich mich sehr auf das neue WKO5 gefreut. Die eigentlich für Trainer gedachte Analysesoftware kann jetzt genau diese Metriken auch von TrainingPeaks übernehmen und so für die Auswertungen bereitstellen. Daraus ergeben sich sehr spannende neue Möglichkeiten – für mich insbesondere aus der Kombination aus Trainingsbelastung (TSS oder Stunden) und Herzfrequenzvariabilität (HRV).

Problem(e)

Seit fast vier Jahren messe ich jeden Morgen meine HRV mit dem Vitalmonitor. Die Daten sind leider in dem System gefangen. Daher messe ich schon seit einiger Zeit parallel mit EliteHRV. Das funktioniert prima und liefert zuverlässige Werte (ich bin nur zu feige den Vitalmonitor nach all der Zeit aufzugeben). Jetzt ist plötzlich die Synchronisation der Schlafdaten nicht mehr möglich. Selbst der Hersteller kann das nicht lösen. Problem 1.

Die Polar Vantage V kann den nächtlichen Schlaf überwachen und über den orthostatischen Test auch den morgendlichen Ruhepuls und die HRV ermitteln. Das sogar in zwei Lagen (Liegen bzw. Sitzen und Stehen). Das alles ist in Polar Flow auch sehr schön dargestellt und lässt sich innerhalb des „Gesamtsystems Polar“ auch wirklich gut nutzen. Nur leider würde ich die Daten gerne zu TrainingPeaks (und über den Weg mit WKO5) synchronisieren. Das funktioniert zwar für alle sportlichen Aktivitäten, nicht aber für zusätzliche Metriken. Problem 2.

Lösungsansatz: Brücke bauen

Jetzt habe ich beruflich vor kurzem mit einem Tool gearbeitet, dass automatisiert mit einem Browser interagieren kann. Über ein Skript können Aktionen ausgelöst und Texte ausgelesen oder geschrieben werden. Sogar Mausbewegungen lassen sich simulieren. Damit müsste es doch möglich sein, Daten von einer Onlineplattform (Polar Flow) auf eine andere (TrainingPeaks) zu bringen… Jedenfalls wäre es komfortabler als ein mehrfaches, händisches Copy&Paste – was grundsätzlich auch gehen würde. Allerdings hätte ich keine Lust das täglich zu machen. Ein Mal am Tag auf den Button für den Automatismus zu drücken, kann ich mir dagegen gerade noch vorstellen.

Umsetzung mit UI.Vision Kantu

Das eingesetzte Tool ist eine Browsererweiterung für Chrome oder Firefox und heisst UI.Vision Kantu. Es ist OpenSource, was mir grundsätzlich sympatisch ist. Dazu ist es kostenlos für private Zwecke.

Das Erstellen des Skriptes war allerdings ein ziemlicher Prozess. Ich musste lernen was REGEX und XPATH bedeutet, wie man in JavaScript Berechnungen durchführt und vor allem wie man die HTML-Codeschnipsel mit den gewünschten Daten aufspürt. Es hat bestimmt drei lange Abende gedauert, bis ich zum ersten Mal alle gewünschten Werte sauber ausgelesen hatte. Ein Meilenstein! :)

Leider hat es noch länger gedauert einen Weg zu finden, die Zahlen in die TrainingPeaks-Maske einzufügen. Die Programmierung deren Website kam mir da nicht unbedingt entgegen. Irgendwann standen alle Zahlen an den richtigen Stellen – ließen sich dann aber nicht abspeichern… :( Erst ein tiefer Griff in die Trickkiste brachte einige Abende später den Erfolg: mit nur einem Kopfdruck war alles übertragen. :)

Skript-Funktionsweise

Das Skript ermittelt zunächst das aktuelle Datum, um daraus eine URL für Polar Flow zu generieren. So lande ich einer Seite, die den Eintrag für den orthostatischen Test auffindbar macht. Dieser wird aufgerufen und alle Daten (Herzfrequenz/HRV in Ruhe und im Stehen sowie Erholung) in Variablen abgespeichert. Sollte es an dem Tag keinen Test gegeben haben, bricht das Skript mit einem Fehler ab. Mir fehlte bisher die Energie, einen automatischen Sprung zum nächsten Schritt zu programmieren…

Es wird dann eine neue URL gebildet, welche die Schlafdaten für den aktuellen Tag aufruft. Schlafstunden und -minuten werden getrennt voneinander erfasst. Da TrainingPeaks die Schlafzeit dezimal braucht (also nicht „7 Stunden und 13 Minuten“ sondern „7.2166“), füllt das Skript eine weitere Variable mit dem berechneten Wert.

Auf geht es zu TrainingPeaks! Dort gab es ein kleines Problem: es gibt nur jeweils ein (1) Feld für den Ruhepuls und die HRV. Jeweils zwei Werte einzutragen (wie sie Polar liefert) ist nicht möglich. Daher habe ich einfach Felder zweckentfremdet. ;) Ich gehe davon aus, dass ich niemals den Umfang meines Handgelenks und meiner Wade (links und rechts natürlich einzeln) erfassen möchte. Zur Funktion des Skriptes ist es notwendig, dass genau folgende Felder in genau dieser Anordnung vorhanden sind.

Erst durch die Installation und Nutzung der XModules war es möglich, die ermittelten Werte nun in die vorhandenen Felder einzutragen und zu speichern.

Ergebnis

Meine Brücke ist halbwegs tragfähig, aber nicht bombensicher. An mehreren Stellen musste ich künstliche Pausen einbauen, um die Ladezeiten der Seiten abzuwarten. Dauert das Laden länger, kann es zu Fehlern kommen.

Das Skript habe ich mit der Chrome-Erweiterung erstellt, ein Einsatz in Firefox ist ohne Änderung möglich. Allerdings lief es nicht auf allen Testrechnern gleich gut (auf modernen Rechnern mit schnellen Ladezeiten lief das Skript aber problemlos). Daher habe ich den letzten Klick auf „OK“ im Skript auch nicht mitprogrammiert, um selbst die letzte Entscheidung zu haben, ob ich die Einträge so gespeichert haben möchte.

Über TrainingPeaks laufen die Daten dann in WKO5, wo ich mir ein Chart zur Auswertung gebaut habe. Noch gibt es zu wenige Messtage, um einen verlässlichen Basiswert zu ermitteln. Aber das ist nur noch eine Frage der Zeit (zwei Wochen).

Wir halten fest: das Skript funktioniert eigentlich ganz gut und füttert alle gewünschten Daten sauber in die TrainingPeaks-Welt ein. Genau so wie ich es mir vorstellt hatte. Ausserdem habe ich wieder viel gelernt und es hiermit an euch weiter gegeben. :)

Hier also noch das „fertige“ Skript zum Bewundern oder Verwenden auf eigene Gefahr:

POLAR_V6 Source View (bitte ausklappen)
{
„Name“: „Polar_V6“,
„CreationDate“: „2019-10-10“,
„Commands“: [
{
„Command“: „comment“,
„Target“: „Polar-Tagebuch für HEUTE öffnen“,
„Value“: „today“
},
{
„Command“: „store“,
„Target“: „true“,
„Value“: „!errorIgnore“
},
{
„Command“: „executeScript_Sandbox“,
„Target“: „var today = new Date(); var dd = String(today.getDate()).padStart(2, ‚0‘); var mm = String(today.getMonth() + 1).padStart(2, ‚0‘); var yyyy = today.getFullYear(); today = dd + ‚.‘ + mm + ‚.‘ + yyyy; return today;“,
„Value“: „today“
},
{
„Command“: „open“,
„Target“: „https://flow.polar.com/training/day/${today}“,
„Value“: „“
},
{
„Command“: „click“,
„Target“: „xpath=//*[@id=\“calendarTab\“]/div/div[2]/div[1]/div[2]/div/div/a/div[2]/span[1]“,
„Value“: „“
},
{
„Command“: „pause“,
„Target“: „5000“,
„Value“: „“
},
{
„Command“: „storeText“,
„Target“: „xpath=/html/body/div[4]/div[2]/div[1]/div/div/div/div[1]/div/div[1]/span[1]“,
„Value“: „hfruhe“
},
{
„Command“: „storeEval“,
„Target“: „storedVars[‚hfruhe‘].slice(0,2);“,
„Value“: „hfruhe“
},
{
„Command“: „storeText“,
„Target“: „xpath=/html/body/div[4]/div[2]/div[1]/div/div/div/div[4]/div/div[1]/span[1]“,
„Value“: „hfstehen“
},
{
„Command“: „storeEval“,
„Target“: „storedVars[‚hfstehen‘].slice(0,2);“,
„Value“: „hfstehen“
},
{
„Command“: „storeText“,
„Target“: „xpath=/html/body/div[4]/div[2]/div[1]/div/div/div/div[2]/div/div[1]/span[1]“,
„Value“: „hrvruhe“
},
{
„Command“: „storeText“,
„Target“: „xpath=/html/body/div[4]/div[2]/div[1]/div/div/div/div[5]/div/div[1]/span[1]“,
„Value“: „hrvstehen“
},
{
„Command“: „sourceSearch“,
„Target“: „id=\\\“btn-recovered\\\“ class=\\\“btn btn-basic\\\“ style=\\\“display: table-cell;\\\““,
„Value“: „recovered“
},
{
„Command“: „if_v2“,
„Target“: „${recovered}“,
„Value“: „1“
},
{
„Command“: „store“,
„Target“: „Low“,
„Value“: „fatigue“
},
{
„Command“: „else“,
„Target“: „“,
„Value“: „“
},
{
„Command“: „store“,
„Target“: „High“,
„Value“: „fatigue“
},
{
„Command“: „end“,
„Target“: „“,
„Value“: „“
},
{
„Command“: „comment“,
„Target“: „Schlafstunden ermitteln“,
„Value“: „“
},
{
„Command“: „executeScript_Sandbox“,
„Target“: „var today = new Date(); var dd = String(today.getDate()).padStart(2, ‚0‘); var mm = String(today.getMonth() + 1).padStart(2, ‚0‘); var yyyy = today.getFullYear(); today = yyyy + ‚-‚ + mm + ‚-‚ + dd; return today;“,
„Value“: „today_sleep“
},
{
„Command“: „open“,
„Target“: „https://flow.polar.com/diary/sleep/night/${today_sleep}“,
„Value“: „“
},
{
„Command“: „waitForPageToLoad“,
„Target“: „5000“,
„Value“: „“
},
{
„Command“: „storeText“,
„Target“: „xpath=//*[@id=\“Flow\“]/div/div/div/main/div/div[3]/div/div[1]/div/div[2]“,
„Value“: „schlaftext“
},
{
„Command“: „storeEval“,
„Target“: „storedVars[’schlaftext‘].slice(0,1);“,
„Value“: „stunden“
},
{
„Command“: „storeEval“,
„Target“: „storedVars[’schlaftext‘].slice(4,7);“,
„Value“: „minuten“
},
{
„Command“: „executeScript_Sandbox“,
„Target“: „return (Number (${stunden}) + Number (${minuten}) / 60 )“,
„Value“: „schlafdezimal“
},
{
„Command“: „store“,
„Target“: „${schlafdezimal}“,
„Value“: „schlaf“
},
{
„Command“: „comment“,
„Target“: „Daten bei TrainingPeaks eintragen“,
„Value“: „“
},
{
„Command“: „open“,
„Target“: „https://app.trainingpeaks.com/#home“,
„Value“: „“
},
{
„Command“: „comment“,
„Target“: „Kritische Pausenzeit: Seite muss komplett laden, sonst wird im Dialog später kein Datum angezeigt!“,
„Value“: „“
},
{
„Command“: „pause“,
„Target“: „20000“,
„Value“: „“
},
{
„Command“: „click“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[1]/div[2]/div/div[1]/div“,
„Value“: „“
},
{
„Command“: „pause“,
„Target“: „3000“,
„Value“: „“
},
{
„Command“: „click“,
„Target“: „xpath=//*[@id=\“newItemViewContainer\“]/div[2]/div[2]/div/div[2]“,
„Value“: „“
},
{
„Command“: „pause“,
„Target“: „3000“,
„Value“: „“
},
{
„Command“: „XClick“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[3]/label/input“,
„Value“: „“
},
{
„Command“: „XType“,
„Target“: „${hfruhe}“,
„Value“: „“
},
{
„Command“: „click“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[5]/label/input“,
„Value“: „“
},
{
„Command“: „XType“,
„Target“: „${hfstehen}“,
„Value“: „“
},
{
„Command“: „XClick“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[4]/label/input“,
„Value“: „“
},
{
„Command“: „XType“,
„Target“: „${hrvruhe}“,
„Value“: „“
},
{
„Command“: „XClick“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[6]/label/input“,
„Value“: „“
},
{
„Command“: „XType“,
„Target“: „${hrvstehen}“,
„Value“: „“
},
{
„Command“: „XClick“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[11]/label/input“,
„Value“: „“
},
{
„Command“: „XType“,
„Target“: „${schlaf}“,
„Value“: „“
},
{
„Command“: „comment“,
„Target“: „Fatigue: Low=Erholt, High=Nicht erholt“,
„Value“: „“
},
{
„Command“: „XClick“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[13]/label/div/select“,
„Value“: „“
},
{
„Command“: „if_v2“,
„Target“: „${recovered}“,
„Value“: „“
},
{
„Command“: „select“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[13]/label/div/select“,
„Value“: „label=Low“
},
{
„Command“: „else“,
„Target“: „“,
„Value“: „“
},
{
„Command“: „select“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[13]/label/div/select“,
„Value“: „label=High“
},
{
„Command“: „end“,
„Target“: „“,
„Value“: „“
},
{
„Command“: „XClick“,
„Target“: „xpath=//*[@id=\“main\“]/div/div[2]/div[2]/div/div[2]/div/div/div/div[2]/div[14]/label/textarea“,
„Value“: „“
},
{
„Command“: „XType“,
„Target“: „[Polar-Export: LC=HRruhe, RC=HRVruhe, LW=HFstehen, RW=HRVstehen, Stress=Erholung]“,
„Value“: „“
}
] }
  1. Zu deiner Schlafqualität könnte ich jetzt auch noch ein paar Details zum Besten geben, die allerdings keinen Mehrwert zum Artikel beisteuern würden. Insofern bleiben sie unter uns. ^^

    1. Gemessen wird ja meine Schlafqualität – nicht die des Nebenschläfers. Und keine der zur Verfügung stehenden Metriken wird in dB(A) angegeben… ;)

    1. ;) Genau! Eine andere Lösung wäre natürlich mit den Auswertungen in Polar Flow zufrieden zu sein und den Mehrwert durch WKO abzuschreiben. Aber das wäre ja langweilig. :)

  2. Hai Thomas,
    ich finde das Sript eine super Idee (Kantu kannte ich bisher nicht). Bei Deinem Script müssen die Anführungszeichen angepasst werden. Sowohl die einfachen als auch die doppelten. Das klicken auf den Test funktionierte bei mir nicht, aber die Firefox Web Entwickler zeigen beim kopieren ja den xpath direkt an.

    Die Schlafenszeit wird nicht korrekt ausgelesen. Das habe ich auf die schnell nicht hinbekommen Das Debugging gefällt mir (noch nicht) besonders. Wenn ich die Woche noch mal Zeit und Lust habe, dann schaue ich es mir an.

    Gruß Martin

    PS: Ist generell blöd, dass Polar nur einen rudementären export von Infos anbietet.

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Ich bin der Harlerunner

Hier schreibt Thomas Pier über das Laufen und (deutlich mehr als nur die notwendige) Ausrüstung. Ich laufe weder besonders schnell noch weit. Aber ich teile gerne meine Erfahrungen, die ich als ambitionierter Freizeitläufer, neugieriger Early-Adopter und als mein eigener Trainer sammele.

Ich freue mich über jede digitale Kontaktaufnahme - noch mehr allerdings über jeden gemeinsam gelaufenen Kilometer.

Strava Badge