diff --git a/Import_98020087920170511-112958.png b/Import_98020087920170511-112958.png deleted file mode 100644 index 83475014..00000000 Binary files a/Import_98020087920170511-112958.png and /dev/null differ diff --git a/Import_98020087920170511-113301.png b/Import_98020087920170511-113301.png deleted file mode 100644 index a875308c..00000000 Binary files a/Import_98020087920170511-113301.png and /dev/null differ diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 0864eec7..cf2896e2 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -458,7 +458,7 @@ def calculate_goldmedalstandard(rower, workout, recurrance=True): if settings.TESTING: background = False if recurrance: - df, delta, cpvalues = setcp(workout, background=background) + df, delta, cpvalues, cpvalues_spm = setcp(workout, background=background) if df.is_empty(): return 0, 0 else: @@ -466,7 +466,7 @@ def calculate_goldmedalstandard(rower, workout, recurrance=True): if df.is_empty() and recurrance: # pragma: no cover - df, delta, cpvalues = setcp(workout, recurrance=False, background=True) + df, delta, cpvalues, cpvalues_spm = setcp(workout, recurrance=False, background=True) if df.is_empty(): return 0, 0 @@ -553,7 +553,10 @@ def setcp(workout, background=False, recurrance=True): # check dts tarr = datautils.getlogarr(4000) if df['delta'][0] in tarr: - return(df, df['delta'], df['cp']) + try: + return(df, df['delta'], df['cp'], df['cr']) + except KeyError: + return(df, df['delta'], df['cp'], 0*df['cp']) except Exception as e: try: os.remove(filename) @@ -565,7 +568,7 @@ def setcp(workout, background=False, recurrance=True): strokesdf = remove_nulls_pl(strokesdf) if strokesdf.is_empty(): - return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) totaltime = strokesdf['time'].max() maxt = totaltime/1000. @@ -580,7 +583,7 @@ def setcp(workout, background=False, recurrance=True): elif os.path.exists(csvfilename+'.gz'): # pragma: no cover csvfile = csvfilename+'.gz' else: # pragma: no cover - return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) csvfile = os.path.abspath(csvfile) with grpc.insecure_channel( @@ -593,7 +596,7 @@ def setcp(workout, background=False, recurrance=True): grpc.channel_ready_future(channel).result(timeout=10) except grpc.FutureTimeoutError: # pragma: no cover dologging('metrics.log','grpc channel time out in setcp') - return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.DataFrame({'delta': [], 'cp': [], 'cr':[]}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) stub = metrics_pb2_grpc.MetricsStub(channel) req = metrics_pb2.CPRequest(filename = csvfile, filetype = "CSV", tarr = logarr) @@ -602,16 +605,18 @@ def setcp(workout, background=False, recurrance=True): response = stub.GetCP(req, timeout=60) except Exception as e: dologging('metrics.log', traceback.format_exc()) - return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) delta = pl.Series(np.array(response.delta)) cpvalues = pl.Series(np.array(response.power)) powermean = response.avgpower + spmvalues = pl.Series(np.array(response.spm)) try: df = pl.DataFrame({ 'delta': delta, 'cp': cpvalues, + 'cr': spmvalues, 'id': workout.id, }) @@ -620,7 +625,7 @@ def setcp(workout, background=False, recurrance=True): except Exception as e: dologging("metrics.log", "setcp: "+ str(e)) - return pl.DataFrame({'delta': [], 'cp': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.DataFrame({'delta': [], 'cp': [], 'cr': []}), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) #df.to_parquet(filename, engine='fastparquet', compression='GZIP') @@ -631,7 +636,7 @@ def setcp(workout, background=False, recurrance=True): workout.goldmedalduration = goldmedalduration workout.save() - return df, delta, cpvalues + return df, delta, cpvalues, spmvalues # pragma: no cover @@ -772,7 +777,7 @@ def fetchcp_new(rower, workouts): data = [] for workout in workouts: - df, delta, cpvalues = setcp(workout) + df, delta, cpvalues, cpvalues_spm = setcp(workout) df = df.drop('id') df = df.with_columns((pl.lit(str(workout))).alias("workout")) df = df.with_columns((pl.lit(workout.url())).alias("url")) @@ -780,7 +785,7 @@ def fetchcp_new(rower, workouts): data.append(df) if len(data) == 0: - return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) if len(data) > 1: df = pl.concat(data) @@ -791,16 +796,21 @@ def fetchcp_new(rower, workouts): pl.all().sort_by('cp').last(), ]) except (KeyError, ColumnNotFoundError): # pragma: no cover - return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) + return pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64), 0, pl.Series(dtype=pl.Float64), pl.Series(dtype=pl.Float64) df = df.filter(pl.col("cp")>20) - return df['delta'], df['cp'], 0, df['workout'], df['url'] + try: + testje = df['cr'] + except KeyError: + return df['delta'], df['cp'], 0*df['cp'], 0, df['workout'], df['url'] + + return df['delta'], df['cp'], df['cr'], 0, df['workout'], df['url'] -def update_rolling_cp(r, types, mode='water', dosend=False): +def update_rolling_cp(r, types, mode='water', metric='power', dosend=False): firstdate = tz.now()-datetime.timedelta(days=r.cprange) workouts = Workout.objects.filter( date__gte=firstdate, @@ -808,12 +818,19 @@ def update_rolling_cp(r, types, mode='water', dosend=False): user=r ) - delta, cp, avgpower, workoutnames, urls = fetchcp_new(r, workouts) + delta, cp, cr, avgpower, workoutnames, urls = fetchcp_new(r, workouts) + + if metric == 'power': + powerdf = pl.DataFrame({ + 'Delta': delta, + 'CP': cp, + }) + else: + powerdf = pl.DataFrame({ + 'Delta': delta, + 'CP': cr, + }) - powerdf = pl.DataFrame({ - 'Delta': delta, - 'CP': cp, - }) powerdf = powerdf.filter(pl.col("CP")>0) powerdf = powerdf.fill_nan(None).drop_nulls().sort(["Delta", "CP"]) @@ -821,6 +838,8 @@ def update_rolling_cp(r, types, mode='water', dosend=False): if powerdf.is_empty(): return False + + res2 = datautils.cpfit(powerdf) p1 = res2[0] @@ -830,7 +849,7 @@ def update_rolling_cp(r, types, mode='water', dosend=False): pwr += p1[1]/(1+hourseconds/p1[3]) if len(powerdf) != 0: - if mode == 'water': + if mode == 'water' and metric == 'power': r.p0 = p1[0] r.p1 = p1[1] r.p2 = p1[2] @@ -839,8 +858,14 @@ def update_rolling_cp(r, types, mode='water', dosend=False): r.save() if dosend and pwr-5 > r.ftp*(100.-r.otwslack)/100. and r.getemailnotifications and not r.emailbounced: _ = myqueue(queuehigh, handle_sendemail_newftp,r,pwr,'water') - - else: + elif mode == 'water' and metric == 'spm': + r.r0 = p1[0] + r.r1 = p1[1] + r.r2 = p1[2] + r.r3 = p1[3] + r.crratio = res2[3] + r.save() + elif mode == 'erg' and metric == 'power': r.ep0 = p1[0] r.ep1 = p1[1] r.ep2 = p1[2] @@ -849,14 +874,23 @@ def update_rolling_cp(r, types, mode='water', dosend=False): r.save() if dosend and pwr-5 > r.ftp and r.getemailnotifications and not r.emailbounced: _ = myqueue(queuehigh, handle_sendemail_newftp,r,pwr,'water') + elif mode == 'erg' and metric == 'spm': + r.er0 = p1[0] + r.er1 = p1[1] + r.er2 = p1[2] + r.er3 = p1[3] + r.ecrratio = res2[3] + r.save() return True return False def initiate_cp(r): - _ = update_rolling_cp(r, otwtypes, 'water') - _ = update_rolling_cp(r, otetypes, 'erg') + _ = update_rolling_cp(r, otwtypes, mode='water', metric='power') + _ = update_rolling_cp(r, otetypes, mode='erg', metric='power') + _ = update_rolling_cp(r, otwtypes, mode='water', metric='spm') + _ = update_rolling_cp(r, otetypes, mode='erg', metric='spm') def split_workout(r, parent, splitsecond, splitmode): data, row = getrowdata_db(id=parent.id) @@ -1061,13 +1095,14 @@ def checkbreakthrough(w, r): ishard = False workouttype = w.workouttype if workouttype in rowtypes: - cpdf, delta, cpvalues = setcp(w) + cpdf, delta, cpvalues, cpvalues_spm = setcp(w) if not cpdf.is_empty(): if workouttype in otwtypes: try: res, btvalues, res2 = utils.isbreakthrough( delta, cpvalues, r.p0, r.p1, r.p2, r.p3, r.cpratio) - _ = update_rolling_cp(r, otwtypes, 'water') + _ = update_rolling_cp(r, otwtypes, mode='water') + _ = update_rolling_cp(r, otwtypes, mode='water', metric='spm') except ValueError: res = 0 res2 = 0 @@ -1076,7 +1111,8 @@ def checkbreakthrough(w, r): try: res, btvalues, res2 = utils.isbreakthrough( delta, cpvalues, r.ep0, r.ep1, r.ep2, r.ep3, r.ecpratio) - _ = update_rolling_cp(r, otetypes, 'erg') + _ = update_rolling_cp(r, otetypes, mode='erg') + _ = update_rolling_cp(r, otetypes, mode='erg', metric='spm') except ValueError: res = 0 res2 = 0 @@ -1480,9 +1516,15 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', r.running_wps = 400. r.save() - + if w.workouttype in otwtypes: + wps_avg = r.median_wps + elif w.workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 + _ = myqueue(queuehigh, handle_calctrimp, w.id, f2, - r.ftp, r.sex, r.hrftp, r.max, r.rest) + r.ftp, r.sex, r.hrftp, r.max, r.rest, wps_avg) return (w.id, message) @@ -1771,8 +1813,15 @@ def new_workout_from_df(r, df, rpe=rpe, consistencychecks=False) + if workouttype in otwtypes: + wps_avg = r.median_wps + elif workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 + _ = myqueue(queuehigh, handle_calctrimp, id, csvfilename, - r.ftp, r.sex, r.hrftp, r.max, r.rest) + r.ftp, r.sex, r.hrftp, r.max, r.rest, wps_avg) return (id, message) @@ -1814,7 +1863,14 @@ def workout_trimp(w, reset=False): return 0, 100.*(w.averagehr/r.hrftp)*(w.duration.hour*60 + w.duration.minute)/60. except ZeroDivisionError: return 0, 0 - + + if w.workouttype in otwtypes: + wps_avg = r.median_wps + elif w.workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 + ftp = float(r.ftp) _ = myqueue( queuehigh, @@ -1825,7 +1881,9 @@ def workout_trimp(w, reset=False): r.sex, r.hrftp, r.max, - r.rest) + r.rest, + wps_avg, + ) return w.trimp, w.hrtss elif w.trimp > -1 and not reset: return w.trimp, w.hrtss @@ -1857,6 +1915,13 @@ def workout_trimp(w, reset=False): w.maxhr = maxhr w.save() + if w.workouttype in otwtypes: + wps_avg = r.median_wps + elif w.workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 + _ = myqueue( queuehigh, handle_calctrimp, @@ -1866,7 +1931,8 @@ def workout_trimp(w, reset=False): r.sex, r.hrftp, r.max, - r.rest) + r.rest, + wps_avg,) trimp = 0 averagehr = 0 @@ -1877,6 +1943,45 @@ def workout_trimp(w, reset=False): return trimp, averagehr +def workout_spmtss(w, reset=False): + if w.spmtss > -1 and not reset: + return w.spmtss + + if get_existing_job(w): + return 0, 0 + + r = w.user + ftp = float(r.ftp) + if w.workouttype in otwtypes: + ftp = ftp*(100.-r.otwslack)/100. + + if r.hrftp == 0: + hrftp = (r.an+r.tr)/2. + r.hrftp = int(hrftp) + r.save() + + if w.workouttype in otwtypes: + wps_avg = r.median_wps + elif w.workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 + + _ = myqueue( + queuehigh, + handle_calctrimp, + w.id, + w.csvfilename, + ftp, + r.sex, + r.hrftp, + r.max, + r.rest, + wps_avg,) + + return w.spmtss + + def workout_rscore(w, reset=False): dologging('metrics.log','Workout_rscore for {w} {id}, {reset}'.format( @@ -1906,6 +2011,13 @@ def workout_rscore(w, reset=False): r.save() dologging('metrics.log','Queueing an asynchronous task') + + if w.workouttype in otwtypes: + wps_avg = r.median_wps + elif w.workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 _ = myqueue( queuehigh, @@ -1916,7 +2028,8 @@ def workout_rscore(w, reset=False): r.sex, r.hrftp, r.max, - r.rest) + r.rest, + wps_avg,) return w.rscore, w.normp @@ -1938,6 +2051,13 @@ def workout_normv(w, pp=4.0): r.hrftp = int(hrftp) r.save() + if w.workouttype in otwtypes: + wps_avg = r.median_wps + elif w.workouttype in otetypes: + wps_avg = r.median_wps_erg + else: + wps_avg = 0 + _ = myqueue( queuehigh, handle_calctrimp, @@ -1947,6 +2067,6 @@ def workout_normv(w, pp=4.0): r.sex, r.hrftp, r.max, - r.rest) + r.rest, wps_avg) return 0, 0 diff --git a/rowers/datautils.py b/rowers/datautils.py index 0b24d252..2eeac953 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -84,7 +84,10 @@ def cpfit(powerdf, fraclimit=0.0001, nmax=1000): p1 = p0 thesecs = powerdf['Delta'].to_numpy() - theavpower = powerdf['CP'].to_numpy() + try: + theavpower = powerdf['CP'].to_numpy() + except: # pragma: no cover + theavpower = powerdf['CR'].to_numpy() if len(thesecs) >= 4: diff --git a/rowers/forms.py b/rowers/forms.py index face5583..8ebd1804 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -1299,6 +1299,7 @@ analysischoices = ( ('stats', 'Statistics'), ('compare', 'Compare'), ('cp', 'CP chart'), + ('cr', 'CR chart'), ) @@ -1370,9 +1371,17 @@ class AnalysisChoiceForm(forms.Form): ('automatic', 'Critical Power Rolling Data') ) + crfitchoices = ( + ('data', 'Fit to Selected Workouts'), + ('automatic', 'Critical Stroke Rate Rolling Data') + ) + cpfit = forms.ChoiceField(choices=cpfitchoices, label='Model Fit', initial='data', required=False) + crfit = forms.ChoiceField(choices=crfitchoices, + label='Model Fit', initial='data', required=False) + cpoverlay = forms.BooleanField(initial=False, label='Overlay Gold Medal Performance', required=False) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 2f0d2a5d..ae3a25ae 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1113,6 +1113,75 @@ def interactive_otwcpchart(powerdf, promember=0, rowername="", r=None, return [script, div, p1, ratio, message] +def interactive_otwcrchart(powerdf, promember=0, rowername="", r=None, + cpfit='data', + title='', type='water'): + + powerdf2 = powerdf.filter((pl.col("Delta") > 0) & (pl.col("CR") > 0)) + + # plot tools + if (promember == 1): # pragma: no cover + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' + else: + TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' + + x_axis_type = 'log' + + deltas = powerdf2['Delta'].apply(lambda x: timedeltaconv(x)) + powerdf2 = powerdf2.with_columns( + ftime = deltas.apply(lambda x: strfdelta(x)), + Deltaminutes = pl.col("Delta")/60. + ) + + # there is no Paul's law for OTW + + thesecs = powerdf2['Delta'] + theavpower = powerdf2['CR'] + + p1, fitt, fitpower, ratio = datautils.cpfit(powerdf2) + if cpfit == 'automatic' and r is not None: + if type == 'water': + p1 = [r.r0, r.r1, r.r2, r.r3] + ratio = r.cpratio + elif type == 'erg': # pragma: no cover + p1 = [r.er0, r.er1, r.er2, r.er3] + ratio = r.ecrratio + + def fitfunc(pars, x): + return abs(pars[0])/(1+(x/abs(pars[2]))) + abs(pars[1])/(1+(x/abs(pars[3]))) + + fitpower = fitfunc(p1, fitt) + + message = "" + # if len(fitpower[fitpower<0]) > 0: + # message = "CP model fit didn't give correct results" + + deltas = fitt.apply(lambda x: timedeltaconv(x)) + ftime = niceformat(deltas) + + + + fit_data = pl.DataFrame(dict( + CR=fitpower, + CRmax=ratio*fitpower, + duration=fitt/60., + ftime=ftime, + )) + + if not title: + title = "Critical StrokeRate for "+rowername + + chart_dict = { + 'data': powerdf2.to_dicts(), + 'fitdata': fit_data.to_dicts(), + 'title': title, + } + + script, div = get_chart("/cr", chart_dict) + + return [script, div, p1, ratio, message] + + def interactive_agegroup_plot(df, distance=2000, duration=None, sex='male', weightcategory='hwt'): diff --git a/rowers/models.py b/rowers/models.py index b69cb380..0d13f520 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1104,6 +1104,18 @@ class Rower(models.Model): ep3 = models.FloatField(default=1.0, verbose_name="erg CP p4") ecpratio = models.FloatField(default=1.0, verbose_name="erg CP fit ratio") + r0 = models.FloatField(default=1.0, verbose_name="CR r1") + r1 = models.FloatField(default=1.0, verbose_name="CR r2") + r2 = models.FloatField(default=1.0, verbose_name="CR r3") + r3 = models.FloatField(default=1.0, verbose_name="CR r4") + crratio = models.FloatField(default=1.0, verbose_name="CR fit ratio") + + er0 = models.FloatField(default=1.0, verbose_name="erg CR r1") + er1 = models.FloatField(default=1.0, verbose_name="erg CR r2") + er2 = models.FloatField(default=1.0, verbose_name="erg CR r3") + er3 = models.FloatField(default=1.0, verbose_name="erg CR r4") + ecrratio = models.FloatField(default=1.0, verbose_name="erg CR fit ratio") + cprange = models.IntegerField(default=42, verbose_name="Range for calculation of breakthrough workouts and fitness (CP)", choices=cppresets) @@ -3746,6 +3758,7 @@ class Workout(models.Model): trimp = models.IntegerField(default=-1, blank=True) rscore = models.IntegerField(default=-1, blank=True) hrtss = models.IntegerField(default=-1, blank=True) + spmtss = models.IntegerField(default=-1, blank=True) normp = models.IntegerField(default=-1, blank=True) normv = models.FloatField(default=-1, blank=True) normw = models.FloatField(default=-1, blank=True) diff --git a/rowers/rowing_workout_metrics_pb2.py b/rowers/rowing_workout_metrics_pb2.py index 9fafbc46..132e63c3 100644 --- a/rowers/rowing_workout_metrics_pb2.py +++ b/rowers/rowing_workout_metrics_pb2.py @@ -1,12 +1,22 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE # source: rowing-workout-metrics.proto -# Protobuf Python Version: 4.25.1 +# Protobuf Python Version: 5.29.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version from google.protobuf import symbol_database as _symbol_database from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 0, + '', + 'rowing-workout-metrics.proto' +) # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() @@ -14,22 +24,22 @@ _sym_db = _symbol_database.Default() -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1crowing-workout-metrics.proto\x12\x16rowing_workout_metrics\"p\n\x15WorkoutMetricsRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0b\n\x03sex\x18\x02 \x01(\t\x12\x0b\n\x03\x66tp\x18\x03 \x01(\x01\x12\r\n\x05hrftp\x18\x04 \x01(\x01\x12\r\n\x05hrmax\x18\x05 \x01(\x01\x12\r\n\x05hrmin\x18\x06 \x01(\x01\"p\n\x16WorkoutMetricsResponse\x12\x0b\n\x03tss\x18\x01 \x01(\x01\x12\r\n\x05normp\x18\x02 \x01(\x01\x12\r\n\x05trimp\x18\x03 \x01(\x01\x12\r\n\x05hrtss\x18\x04 \x01(\x01\x12\r\n\x05normv\x18\x05 \x01(\x01\x12\r\n\x05normw\x18\x06 \x01(\x01\"=\n\tCPRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x10\n\x08\x66iletype\x18\x02 \x01(\t\x12\x0c\n\x04tarr\x18\x03 \x03(\x01\"<\n\nCPResponse\x12\r\n\x05\x64\x65lta\x18\x01 \x03(\x01\x12\r\n\x05power\x18\x02 \x03(\x01\x12\x10\n\x08\x61vgpower\x18\x03 \x01(\x01\x32\xc7\x01\n\x07Metrics\x12l\n\x0b\x43\x61lcMetrics\x12-.rowing_workout_metrics.WorkoutMetricsRequest\x1a..rowing_workout_metrics.WorkoutMetricsResponse\x12N\n\x05GetCP\x12!.rowing_workout_metrics.CPRequest\x1a\".rowing_workout_metrics.CPResponseB\x1aZ\x18./rowing-workout-metricsb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1crowing-workout-metrics.proto\x12\x16rowing_workout_metrics\"\x80\x01\n\x15WorkoutMetricsRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x0b\n\x03sex\x18\x02 \x01(\t\x12\x0b\n\x03\x66tp\x18\x03 \x01(\x01\x12\r\n\x05hrftp\x18\x04 \x01(\x01\x12\r\n\x05hrmax\x18\x05 \x01(\x01\x12\r\n\x05hrmin\x18\x06 \x01(\x01\x12\x0e\n\x06wpsavg\x18\x07 \x01(\x01\"\x80\x01\n\x16WorkoutMetricsResponse\x12\x0b\n\x03tss\x18\x01 \x01(\x01\x12\r\n\x05normp\x18\x02 \x01(\x01\x12\r\n\x05trimp\x18\x03 \x01(\x01\x12\r\n\x05hrtss\x18\x04 \x01(\x01\x12\r\n\x05normv\x18\x05 \x01(\x01\x12\r\n\x05normw\x18\x06 \x01(\x01\x12\x0e\n\x06spmtss\x18\x07 \x01(\x01\"=\n\tCPRequest\x12\x10\n\x08\x66ilename\x18\x01 \x01(\t\x12\x10\n\x08\x66iletype\x18\x02 \x01(\t\x12\x0c\n\x04tarr\x18\x03 \x03(\x01\"I\n\nCPResponse\x12\r\n\x05\x64\x65lta\x18\x01 \x03(\x01\x12\r\n\x05power\x18\x02 \x03(\x01\x12\x10\n\x08\x61vgpower\x18\x03 \x01(\x01\x12\x0b\n\x03spm\x18\x04 \x03(\x01\x32\xc7\x01\n\x07Metrics\x12l\n\x0b\x43\x61lcMetrics\x12-.rowing_workout_metrics.WorkoutMetricsRequest\x1a..rowing_workout_metrics.WorkoutMetricsResponse\x12N\n\x05GetCP\x12!.rowing_workout_metrics.CPRequest\x1a\".rowing_workout_metrics.CPResponseB\x1aZ\x18./rowing-workout-metricsb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rowing_workout_metrics_pb2', _globals) -if _descriptor._USE_C_DESCRIPTORS == False: - _globals['DESCRIPTOR']._options = None +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None _globals['DESCRIPTOR']._serialized_options = b'Z\030./rowing-workout-metrics' - _globals['_WORKOUTMETRICSREQUEST']._serialized_start=56 - _globals['_WORKOUTMETRICSREQUEST']._serialized_end=168 - _globals['_WORKOUTMETRICSRESPONSE']._serialized_start=170 - _globals['_WORKOUTMETRICSRESPONSE']._serialized_end=282 - _globals['_CPREQUEST']._serialized_start=284 - _globals['_CPREQUEST']._serialized_end=345 - _globals['_CPRESPONSE']._serialized_start=347 - _globals['_CPRESPONSE']._serialized_end=407 - _globals['_METRICS']._serialized_start=410 - _globals['_METRICS']._serialized_end=609 + _globals['_WORKOUTMETRICSREQUEST']._serialized_start=57 + _globals['_WORKOUTMETRICSREQUEST']._serialized_end=185 + _globals['_WORKOUTMETRICSRESPONSE']._serialized_start=188 + _globals['_WORKOUTMETRICSRESPONSE']._serialized_end=316 + _globals['_CPREQUEST']._serialized_start=318 + _globals['_CPREQUEST']._serialized_end=379 + _globals['_CPRESPONSE']._serialized_start=381 + _globals['_CPRESPONSE']._serialized_end=454 + _globals['_METRICS']._serialized_start=457 + _globals['_METRICS']._serialized_end=656 # @@protoc_insertion_point(module_scope) diff --git a/rowers/rowing_workout_metrics_pb2_grpc.py b/rowers/rowing_workout_metrics_pb2_grpc.py index 4785255b..7459e532 100644 --- a/rowers/rowing_workout_metrics_pb2_grpc.py +++ b/rowers/rowing_workout_metrics_pb2_grpc.py @@ -1,9 +1,29 @@ # Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! """Client and server classes corresponding to protobuf-defined services.""" import grpc +import warnings import rowers.rowing_workout_metrics_pb2 as rowing__workout__metrics__pb2 +GRPC_GENERATED_VERSION = '1.70.0' +GRPC_VERSION = grpc.__version__ +_version_not_supported = False + +try: + from grpc._utilities import first_version_is_lower + _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION) +except ImportError: + _version_not_supported = True + +if _version_not_supported: + raise RuntimeError( + f'The grpc package installed is at version {GRPC_VERSION},' + + f' but the generated code in rowing_workout_metrics_pb2_grpc.py depends on' + + f' grpcio>={GRPC_GENERATED_VERSION}.' + + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}' + + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.' + ) + class MetricsStub(object): """OTW-metrics service definition @@ -19,12 +39,12 @@ class MetricsStub(object): '/rowing_workout_metrics.Metrics/CalcMetrics', request_serializer=rowing__workout__metrics__pb2.WorkoutMetricsRequest.SerializeToString, response_deserializer=rowing__workout__metrics__pb2.WorkoutMetricsResponse.FromString, - ) + _registered_method=True) self.GetCP = channel.unary_unary( '/rowing_workout_metrics.Metrics/GetCP', request_serializer=rowing__workout__metrics__pb2.CPRequest.SerializeToString, response_deserializer=rowing__workout__metrics__pb2.CPResponse.FromString, - ) + _registered_method=True) class MetricsServicer(object): @@ -60,6 +80,7 @@ def add_MetricsServicer_to_server(servicer, server): generic_handler = grpc.method_handlers_generic_handler( 'rowing_workout_metrics.Metrics', rpc_method_handlers) server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('rowing_workout_metrics.Metrics', rpc_method_handlers) # This class is part of an EXPERIMENTAL API. @@ -78,11 +99,21 @@ class Metrics(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/rowing_workout_metrics.Metrics/CalcMetrics', + return grpc.experimental.unary_unary( + request, + target, + '/rowing_workout_metrics.Metrics/CalcMetrics', rowing__workout__metrics__pb2.WorkoutMetricsRequest.SerializeToString, rowing__workout__metrics__pb2.WorkoutMetricsResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) @staticmethod def GetCP(request, @@ -95,8 +126,18 @@ class Metrics(object): wait_for_ready=None, timeout=None, metadata=None): - return grpc.experimental.unary_unary(request, target, '/rowing_workout_metrics.Metrics/GetCP', + return grpc.experimental.unary_unary( + request, + target, + '/rowing_workout_metrics.Metrics/GetCP', rowing__workout__metrics__pb2.CPRequest.SerializeToString, rowing__workout__metrics__pb2.CPResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/rowers/tasks.py b/rowers/tasks.py index ebb7a07f..fa10cdbf 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -1639,6 +1639,7 @@ def handle_calctrimp(id, hrftp, hrmax, hrmin, + wps_avg, debug=False, **kwargs): @@ -1648,6 +1649,7 @@ def handle_calctrimp(id, hrtss = 0 normv = 0 normw = 0 + spmtss = 0 # check what the real file name is if os.path.exists(csvfilename): @@ -1680,6 +1682,7 @@ def handle_calctrimp(id, hrftp=hrftp, hrmax=hrmax, hrmin=hrmin, + wpsavg=wps_avg, ) try: response = stub.CalcMetrics(req, timeout=60) @@ -1693,13 +1696,15 @@ def handle_calctrimp(id, normv = response.normv normw = response.normw hrtss = response.hrtss - dologging('metrics.log','File {csvfile}. Got tss {tss}, normp {normp} trimp {trimp} normv {normv} normw {normw} hrtss {hrtss}'.format( + spmtss = response.spmtss + dologging('metrics.log','File {csvfile}. Got tss {tss}, normp {normp} trimp {trimp} normv {normv} normw {normw} hrtss {hrtss} spmtss {spmtss}'.format( tss = tss, normp = normp, trimp = trimp, normv = normv, normw = normw, hrtss = hrtss, + spmtss = spmtss, csvfile=csvfile, )) @@ -1740,6 +1745,9 @@ def handle_calctrimp(id, if hrtss > 1000: # pragma: no cover hrtss = 0 + if spmtss > 1000: # pragma: no cover + spmtss = 0 + try: workout = Workout.objects.get(id=id) except Workout.DoesNotExist: # pragma: no cover @@ -1752,6 +1760,7 @@ def handle_calctrimp(id, workout.hrtss = int(hrtss) workout.normv = normv workout.normw = normw + workout.spmtss = int(spmtss) workout.save() dologging('metrics.log','Saving to workout {id} {obscure}'.format( id = id, diff --git a/rowers/templates/otwcr.html b/rowers/templates/otwcr.html new file mode 100644 index 00000000..f4d7d849 --- /dev/null +++ b/rowers/templates/otwcr.html @@ -0,0 +1,20 @@ + + {{ the_div|safe }} +
+
| Duration | +Stroke Estimate 1 | +Stroke Estimate 2 | +
|---|---|---|
| {{ duration }} | +{{ power }} | +{{ upper }} | +