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

Garmin sparse data fix #487

Open
wants to merge 3 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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
language: python
python:
- "3.3"
- "3.6"
services:
- mongodb
- redis-server
Expand Down
79 changes: 4 additions & 75 deletions tapiriik/services/GarminConnect/garminconnect.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,86 +447,15 @@ def DownloadActivity(self, serviceRecord, activity):
# Nothing else to download
return activity

# https://connect.garmin.com/modern/proxy/activity-service/activity/###/details
# https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/###
activityID = activity.ServiceData["ActivityID"]
res = self._request_with_reauth(lambda session: session.get("https://connect.garmin.com/modern/proxy/activity-service/activity/{}/details?maxSize=999999999".format(activityID)), serviceRecord)
res = self._request_with_reauth(lambda session: session.get("https://connect.garmin.com/modern/proxy/download-service/export/tcx/activity/{}".format(activityID)), serviceRecord)
try:
raw_data = res.json()
tcx_data = res.text
activity = TCXIO.Parse(tcx_data.encode('utf-8'), activity)
except ValueError:
raise APIException("Activity data parse error for %s: %s" % (res.status_code, res.text))

if "metricDescriptors" not in raw_data:
activity.Stationary = True # We were wrong, oh well
return activity

attrs_map = {}
def _map_attr(gc_key, wp_key, units, in_location=False, is_timestamp=False):
attrs_map[gc_key] = {
"key": wp_key,
"to_units": units,
"in_location": in_location, # Blegh
"is_timestamp": is_timestamp # See above
}

_map_attr("directSpeed", "Speed", ActivityStatisticUnit.MetersPerSecond)
_map_attr("sumDistance", "Distance", ActivityStatisticUnit.Meters)
_map_attr("directHeartRate", "HR", ActivityStatisticUnit.BeatsPerMinute)
_map_attr("directBikeCadence", "Cadence", ActivityStatisticUnit.RevolutionsPerMinute)
_map_attr("directDoubleCadence", "RunCadence", ActivityStatisticUnit.StepsPerMinute) # 2*x mystery solved
_map_attr("directAirTemperature", "Temp", ActivityStatisticUnit.DegreesCelcius)
_map_attr("directPower", "Power", ActivityStatisticUnit.Watts)
_map_attr("directElevation", "Altitude", ActivityStatisticUnit.Meters, in_location=True)
_map_attr("directLatitude", "Latitude", None, in_location=True)
_map_attr("directLongitude", "Longitude", None, in_location=True)
_map_attr("directTimestamp", "Timestamp", None, is_timestamp=True)

# Figure out which metrics we'll be seeing in this activity
attrs_indexed = {}
for measurement in raw_data["metricDescriptors"]:
key = measurement["key"]
if key in attrs_map:
if attrs_map[key]["to_units"]:
attrs_map[key]["from_units"] = self._unitMap[measurement["unit"]["key"]]
if attrs_map[key]["to_units"] == attrs_map[key]["from_units"]:
attrs_map[key]["to_units"] = attrs_map[key]["from_units"] = None
attrs_indexed[measurement["metricsIndex"]] = attrs_map[key]

# Process the data frames
frame_idx = 0
active_lap_idx = 0
for frame in raw_data["activityDetailMetrics"]:
wp = Waypoint()
for idx, attr in attrs_indexed.items():
value = frame["metrics"][idx]
target_obj = wp
if attr["in_location"]:
if not wp.Location:
wp.Location = Location()
target_obj = wp.Location

# Handle units
if attr["is_timestamp"]:
value = pytz.utc.localize(datetime.utcfromtimestamp(value / 1000))
elif attr["to_units"]:
value = ActivityStatistic.convertValue(value, attr["from_units"], attr["to_units"])

# Write the value (can't use __dict__ because __slots__)
setattr(target_obj, attr["key"], value)

# Fix up lat/lng being zero (which appear to represent missing coords)
if wp.Location and wp.Location.Latitude == 0 and wp.Location.Longitude == 0:
wp.Location.Latitude = None
wp.Location.Longitude = None
# Please visit a physician before complaining about this
if wp.HR == 0:
wp.HR = None
# Bump the active lap if required
while (active_lap_idx < len(activity.Laps) - 1 and # Not the last lap
activity.Laps[active_lap_idx + 1].StartTime <= wp.Timestamp):
active_lap_idx += 1
activity.Laps[active_lap_idx].Waypoints.append(wp)
frame_idx += 1

return activity

def UploadActivity(self, serviceRecord, activity):
Expand Down
9 changes: 8 additions & 1 deletion tapiriik/services/tcx.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,19 @@ def Parse(tcxData, act=None):
sum_stats.update(act.Stats)
act.Stats = sum_stats

try:
act.PrerenderedFormats["tcx"] = tcxData.decode('utf-8')
except:
act.PrerenderedFormats["tcx"] = tcxData

act.CalculateUID()
return act

def Dump(activity, activityType=None):

if "tcx" in activity.PrerenderedFormats:
return activity.PrerenderedFormats["tcx"]

root = etree.Element("TrainingCenterDatabase", nsmap=TCXIO.Namespaces)
activities = etree.SubElement(root, "Activities")
act = etree.SubElement(activities, "Activity")
Expand Down Expand Up @@ -385,5 +393,4 @@ def _writeStat(parent, elName, value, wrapValue=False, naturalValue=False, defau
etree.SubElement(xver, "BuildMajor").text = "0"
etree.SubElement(xver, "BuildMinor").text = "0"


return etree.tostring(root, pretty_print=True, xml_declaration=True, encoding="UTF-8").decode("UTF-8")