Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

heart rate, cadence, temperature, speed, and power #17

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ build
strava-cli.spec
venv
.idea
xsd/*.gpx
8 changes: 5 additions & 3 deletions api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import requests
import sys
import time
Expand All @@ -13,6 +14,7 @@ def _get_headers(self):
return {"Authorization": "Bearer {}".format(self._token)}

def _get(self, url):
logging.getLogger('Client').debug('http get {}'.format(url))
r = requests.get(url, headers=self._get_headers())
self._sleep()
if r.status_code != 200:
Expand Down Expand Up @@ -45,7 +47,7 @@ def get_activity_detail(self, id):
return self._get(
"https://www.strava.com/api/v3/activities/{}".format(id))

def get_streams(self, id, stream_types = ['time','latlng','altitude']):
def get_streams(self, id, stream_types):
return self._get(
"https://www.strava.com/api/v3/activities/{}/streams?keys={}&key_by_type=true".format(id, ','.join(stream_types)))

Expand All @@ -54,7 +56,7 @@ def get_athlete(self):

def _sleep(self):
#used because of throttling strava api
if self._sleep_time is not None:
print('sleep {}'.format(self._sleep_time), file=sys.stderr)
if self._sleep_time is not None and self._sleep_time != 0:
logging.getLogger('Client').info('sleep {}'.format(self._sleep_time))
time.sleep(self._sleep_time)

55 changes: 50 additions & 5 deletions formatters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import gpxpy
import datetime
import xml.etree.ElementTree as ElementTree


class Formatter(object):
Expand Down Expand Up @@ -54,8 +55,10 @@ def format(self, activity):

class GpxFormatter(Formatter):

def format(self, activity, gps):
def format(self, activity, stream_types, gps):
gpx = gpxpy.gpx.GPX()

self.setup_extensions(gpx, stream_types)

gpx_track = gpxpy.gpx.GPXTrack(
name=activity.get('name'))
Expand All @@ -67,16 +70,58 @@ def format(self, activity, gps):
gpx_track.segments.append(gpx_segment)


for time, point, altitude in gps:
time = datetime.datetime.fromtimestamp(time).astimezone()
gpx_segment.points.append(gpxpy.gpx.GPXTrackPoint(latitude=point[0], longitude=point[1], elevation=altitude, time=time))
for entry in gps:
time = datetime.datetime.fromtimestamp(entry['time']).astimezone(datetime.timezone.utc) if 'time' in entry else None
point = entry['latlng'] if 'latlng' in entry else [None, None]
altitude = entry['altitude'] if 'altitude' in entry else None

point = gpxpy.gpx.GPXTrackPoint(latitude=point[0], longitude=point[1], elevation=altitude, time=time)
gpx_segment.points.append(point)

main_extension = ElementTree.Element('gpxtpx:TrackPointExtension')

if 'gpxtpx' in gpx.nsmap:
if 'temp' in entry:
temperature = ElementTree.SubElement(main_extension, 'gpxtpx:atemp')
temperature.text = str(entry['temp'])

if 'heartrate' in entry:
heart_rate = ElementTree.SubElement(main_extension, 'gpxtpx:hr')
heart_rate.text = str(entry['heartrate'])

if 'cadence' in entry:
cadence = ElementTree.SubElement(main_extension, 'gpxtpx:cad')
cadence.text = str(entry['cadence'])

if 'velocity_smooth' in entry:
speed = ElementTree.SubElement(main_extension, 'gpxtpx:speed')
speed.text = str(entry['velocity_smooth'])

if len(main_extension) > 0:
point.extensions.append(main_extension)

if 'gpxpx' in gpx.nsmap and 'watts' in entry:
power = ElementTree.Element('gpxpx:PowerInWatts')
power.text = str(round(entry['watts']))
point.extensions.append(power)

return gpx.to_xml(prettyprint=True)

def setup_extensions(self, gpx, stream_types):
if 'heartrate' in stream_types or 'cadence' in stream_types or 'temp' in stream_types or 'velocity_smooth' in stream_types:
# https://www8.garmin.com/xmlschemas/TrackPointExtensionv2.xsd
gpx.nsmap['gpxtpx'] = 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2'
ElementTree.register_namespace('gpxtpx', 'http://www.garmin.com/xmlschemas/TrackPointExtension/v2')

if 'watts' in stream_types:
# https://www8.garmin.com/xmlschemas/PowerExtensionv1.xsd
gpx.nsmap['gpxpx'] = 'http://www.garmin.com/xmlschemas/PowerExtension/v1'
ElementTree.register_namespace('gpxpx', 'http://www.garmin.com/xmlschemas/PowerExtension/v1')


class JsonGpsFormatter(Formatter):

def format(self, activity, gps):
def format(self, activity, stream_types, gps):
return json.dumps({'activity':activity, 'data':list(gps)})


Expand Down
18 changes: 15 additions & 3 deletions repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def _update_cache(self):
return
activities = self._cache.get_activities()
timestamp = self._get_latest_timestamp(activities)
#timestamp = 0 #a harder cache update is required if you change old data
logging.getLogger('CachedRepository').debug(
"Newest activity in cache {}".format(timestamp))
new_activities = []
Expand Down Expand Up @@ -128,11 +129,22 @@ def get_activity_detail(self, id):
return activity_detail

def get_gps(self, id):
streams = self._client.get_streams(id)
#does not cache, goes directly to client
stream_types = ('time', 'latlng', 'altitude', 'heartrate', 'cadence', 'temp', 'velocity_smooth', 'watts')
streams = self._client.get_streams(id, stream_types)

activity = self.get_activity(int(id))
start_time = int(parse_date(activity['start_date']).timestamp())
streams = zip(*(streams[key]['data'] if key in streams else [] for key in ('time', 'latlng', 'altitude')))
return activity, [(time + start_time, point, altitude) for time, point, altitude in streams]

length = len(streams['time']['data'])
#add start time
streams['time']['data'] = [time + start_time for time in streams['time']['data']]
stream_types = [stream_type for stream_type in stream_types if stream_type in streams]
if 'latlng' not in stream_types:
raise ValueError('cannot get gps track for an activity without locations')
streams = [{stream_type: streams[stream_type]['data'][index] for stream_type in stream_types} for index in range(length)]

return activity, stream_types, streams

def get_bikes(self):
athlete = self._client.get_athlete()
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Last week I had to update this file because for some reason Pip wasn't installing Flask dependencies (the MR doesn't contain these changes, it needs to be rebased), so I had to explicitly list them here (see #16). I'm wondering whether we need to update these dependencies or if we can just remove them (assuming that when installing flask 2.3.2 Pip will automatically pull jinja2, werkzeug and itsdangerous)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, I'll rebase this commit.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
flask==1.1.2
requests==2.23.0
flask==2.3.2
requests==2.31.0
argparse==1.4.0
gpxpy==1.4.1
18 changes: 13 additions & 5 deletions strava-cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,18 @@
import logging


logging.basicConfig(
filename=config.get_log_file(),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG
)
def setup_logging():
logging.basicConfig(
filename=config.get_log_file(),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.DEBUG
)

stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)

logging.getLogger().addHandler(stream_handler)



def authenticate(args):
Expand Down Expand Up @@ -99,6 +106,7 @@ def clear_cache(args):
cache.get_cache().clear()


setup_logging()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='Strava Command Line Interface')
Expand Down
Loading