From 6b451336ed5df2b893c9e6387175edba50b0719b Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Mon, 11 Jan 2021 20:11:37 +0000 Subject: [PATCH] Initial parser for RunnerUp data which I'm now using instead of Endomondo --- my/runnerup.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 my/runnerup.py diff --git a/my/runnerup.py b/my/runnerup.py new file mode 100644 index 0000000..f0fd63d --- /dev/null +++ b/my/runnerup.py @@ -0,0 +1,90 @@ +''' +[[https://github.com/jonasoreland/runnerup][Runnerup]] exercise data (TCX format) +''' + +REQUIRES = [ + 'python-tcxparser', +] + +from datetime import datetime, timedelta +from pathlib import Path +from typing import NamedTuple, Iterable + +from .core import Res, get_files +from .core.common import isoparse, Json + +import tcxparser + +from my.config import runnerup as config + + +# TODO later, use a proper namedtuple? +Workout = Json + + +def _parse(f: Path) -> Workout: + tcx = tcxparser.TCXParser(str(f)) + + sport = f.stem.split('_')[-1] # todo not sure how reliable... + hr_avg = tcx.hr_avg + + distance_m = tcx.distance + duration_s = tcx.duration + # kmh to match endomondo.. should probably be CI + speed_avg_kmh = (distance_m / 1000) / (duration_s / 3600) + + # eh. not sure if there is a better way + # for now use this to be compatible with Endomondo + # https://beepb00p.xyz/heartbeats_vs_kcals.html + # filtered for Endomondo running: + reg_coeff = 0.0993 + intercept = -11.0739 + total_beats = hr_avg * (duration_s / 60) + kcal_estimate = total_beats * reg_coeff + intercept + + return { + 'id' : f.name, # not sure? + 'start_time' : isoparse(tcx.started_at), + 'duration' : timedelta(seconds=tcx.duration), + 'sport' : sport, + 'heart_rate_avg': tcx.hr_avg, + 'speed_avg' : speed_avg_kmh, + 'kcal' : kcal_estimate, + } + # from more_itertools import zip_equal + # for ts, latlon, hr in zip_equal( + # tcx.time_values(), + # tcx.position_values(), + # tcx.hr_values(), + # # todo cadence? + # ): + # t = isoparse(ts) + + +def workouts() -> Iterable[Res[Workout]]: + for f in get_files(config.export_path): + try: + yield _parse(f) + except Exception as e: + yield e + + +from .core.pandas import DataFrameT, check_dataframe, error_to_row +@check_dataframe +def dataframe() -> DataFrameT: + def it(): + for w in workouts(): + if isinstance(w, Exception): + yield error_to_row(w) + else: + yield w + import pandas as pd # type: ignore + df = pd.DataFrame(it()) + if 'error' not in df: + df['error'] = None + return df + + +from .core import stat, Stats +def stats() -> Stats: + return stat(dataframe)