def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get("https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: res_txt = resp.text raise APIException("Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False for pt in resp["points"]: wp = Waypoint() wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get("https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: self._rateLimitBailout(resp) res_txt = resp.text raise APIException("Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False prevLon = 0.0 prevLat = 0.0 for pt in resp["points"]: wp = Waypoint() if "time" not in pt: # Manually-entered activities with a course attached to them have date-less waypoints # It'd be nice to transfer those courses, but it's a concept few other sites support AFAIK # So, ignore the points entirely continue wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] if (wp.Location.Latitude == prevLat and wp.Location.Longitude == prevLon): # we have seen the point with the same coordinates # before. This causes other services (e.g Strava) to # interpret this as if we were standing for a while, # which causes us having wrong activity time when # importing. We discard this entry to keep only unique # ones to avoid this. This is still a hack :( However, # I don't know if this will handle the situation when # we are actually standing for some time in one place # well... continue; prevLat = wp.Location.Latitude; prevLon = wp.Location.Longitude; activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get("https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: self._rateLimitBailout(resp) res_txt = resp.text raise APIException("Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False old_location = None in_pause = False for pt in resp["points"]: wp = Waypoint() if "time" not in pt: # Manually-entered activities with a course attached to them have date-less waypoints # It'd be nice to transfer those courses, but it's a concept few other sites support AFAIK # So, ignore the points entirely continue wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if wp.Location == old_location: # We have seen the point with the same coordinates # before. This causes other services (e.g Strava) to # interpret this as if we were standing for a while, # which causes us having wrong activity time when # importing. We mark the point as paused in hopes this # fixes the issue. in_pause = True wp.Type = WaypointType.Pause elif in_pause: in_pause = False wp.Type = WaypointType.Resume old_location = wp.Location if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] if "pow" in pt: wp.Power = pt["pow"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def _addWaypoint(timestamp, path=None, heart_rate=None, power=None, distance=None, speed=None, cadence=None): waypoint = Waypoint(activity.StartTime + timedelta(seconds=timestamp)) if path: if path["latitude"] != 0 and path["longitude"] != 0: waypoint.Location = Location(path["latitude"], path["longitude"], path["altitude"] if "altitude" in path and float(path["altitude"]) != 0 else None) # if you're running near sea level, well... waypoint.Type = WaypointType.Regular waypoint.HR = heart_rate waypoint.Distance = distance waypoint.Speed = speed waypoint.Cadence = cadence waypoint.Power = power lap.Waypoints.append(waypoint)
def DownloadActivity(self, serviceRecord, activity): workoutID = activity.ServiceData["WorkoutID"] logger.debug("DownloadActivity for %s" % workoutID) session = self._get_session(record=serviceRecord) resp = session.get(self._urlRoot + "/api/workout/%d" % workoutID) try: res = resp.json() except ValueError: raise APIException( "Parse failure in Motivato activity (%d) download: %s" % (workoutID, res.text)) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False if "track" in res and "points" in res["track"]: for pt in res["track"]["points"]: wp = Waypoint() if "moment" not in pt: continue wp.Timestamp = self._parseDateTime(pt["moment"]) if ("lat" in pt and "lon" in pt) or "ele" in pt: wp.Location = Location() if "lat" in pt and "lon" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lon"] activity.GPS = True if "ele" in pt: wp.Location.Altitude = float(pt["ele"]) if "bpm" in pt: wp.HR = pt["bpm"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def stream_waypoint(offset, speed=None, distance=None, heartrate=None, calories=None, steps=None, watts=None, gps=None, **kwargs): wp = Waypoint() wp.Timestamp = activity.StartTime + timedelta(seconds=offset) wp.Speed = float(speed) if speed else None wp.Distance = float(distance) / 1000 if distance else None wp.HR = float(heartrate) if heartrate else None wp.Calories = float(calories) if calories else None wp.Power = float(watts) if watts else None if gps: wp.Location = Location(lat=float(gps["latitude"]), lon=float(gps["longitude"]), alt=float(gps["elevation"])) lap.Waypoints.append(wp)
def _addWaypoint(timestamp, path=None, heart_rate=None, calories=None, distance=None): waypoint = Waypoint(activity.StartTime + timedelta(seconds=timestamp)) if path: waypoint.Location = Location(path["latitude"], path["longitude"], path["altitude"] if "altitude" in path and float(path["altitude"]) != 0 else None) # if you're running near sea level, well... waypoint.Type = self._wayptTypeMappings[path["type"]] if path["type"] in self._wayptTypeMappings else WaypointType.Regular waypoint.HR = heart_rate waypoint.Calories = calories waypoint.Distance = distance lap.Waypoints.append(waypoint)
def DownloadActivity(self, serviceRecord, activity): workoutID = activity.ServiceData["WorkoutID"] logger.debug("DownloadActivity for %s" % workoutID); session = self._get_session(record=serviceRecord) resp = session.get(self._urlRoot + "/api/workout/%d" % workoutID) try: res = resp.json() except ValueError: raise APIException("Parse failure in Motivato activity (%d) download: %s" % (workoutID, res.text)) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False if "track" in res and "points" in res["track"]: for pt in res["track"]["points"]: wp = Waypoint() if "moment" not in pt: continue wp.Timestamp = self._parseDateTime(pt["moment"]) if ("lat" in pt and "lon" in pt) or "ele" in pt: wp.Location = Location() if "lat" in pt and "lon" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lon"] activity.GPS = True if "ele" in pt: wp.Location.Altitude = float(pt["ele"]) if "bpm" in pt: wp.HR = pt["bpm"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get("https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: self._rateLimitBailout(resp) res_txt = resp.text raise APIException("Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False for pt in resp["points"]: wp = Waypoint() if "time" not in pt: # Manually-entered activities with a course attached to them have date-less waypoints # It'd be nice to transfer those courses, but it's a concept few other sites support AFAIK # So, ignore the points entirely continue wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def _populateActivityWaypoints(self, rawData, activity): ''' populate the Waypoints collection from RK API data ''' lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] # path is the primary stream, HR/power/etc must have fewer pts hasHR = "heart_rate" in rawData and len(rawData["heart_rate"]) > 0 hasCalories = "calories" in rawData and len(rawData["calories"]) > 0 hasDistance = "distance" in rawData and len(rawData["distance"]) > 0 for pathpoint in rawData["path"]: waypoint = Waypoint(activity.StartTime + timedelta(0, pathpoint["timestamp"])) waypoint.Location = Location(pathpoint["latitude"], pathpoint["longitude"], pathpoint["altitude"] if "altitude" in pathpoint and float(pathpoint["altitude"]) != 0 else None) # if you're running near sea level, well... waypoint.Type = self._wayptTypeMappings[pathpoint["type"]] if pathpoint["type"] in self._wayptTypeMappings else WaypointType.Regular if hasHR: hrpoint = [x for x in rawData["heart_rate"] if x["timestamp"] == pathpoint["timestamp"]] if len(hrpoint) > 0: waypoint.HR = hrpoint[0]["heart_rate"] if hasCalories: calpoint = [x for x in rawData["calories"] if x["timestamp"] == pathpoint["timestamp"]] if len(calpoint) > 0: waypoint.Calories = calpoint[0]["calories"] if hasDistance: distpoint = [x for x in rawData["distance"] if x["timestamp"] == pathpoint["timestamp"]] if len(distpoint) > 0: waypoint.Distance = distpoint[0]["distance"] lap.Waypoints.append(waypoint)
def DownloadActivity(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] logger.debug("DownloadActivity %s" % activityID) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] lap.Waypoints = [] response = requests.get("https://api.mapmyfitness.com/v7.1/workout/" + activityID + "/?field_set=time_series", headers=self._apiHeaders(serviceRecord)) data = response.json() activity.GPS = False activity.Stationary = True # add waypoints to laps if "time_series" in data and "position" in data["time_series"]: activity.Stationary = False for pt in data["time_series"]["position"]: timestamp = pt[0] wp = Waypoint(activity.StartTime + timedelta(seconds=round(timestamp))) pos = pt[1] if ("lat" in pos and "lng" in pos) or "elevation" in pos: wp.Location = Location() if "lat" in pos and "lng" in pos: wp.Location.Latitude = pos["lat"] wp.Location.Longitude = pos["lng"] activity.GPS = True if "elevation" in pos: wp.Location.Altitude = pos["elevation"] lap.Waypoints.append(wp) return activity
def DownloadActivity(self, serviceRecord, activity): act = self._getActivity(serviceRecord, activity) recordingKeys = act.get('recordingKeys') if act['source'] == 'manual' or not recordingKeys: # it's a manually entered run, can't get much info activity.Stationary = True activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] return activity # FIXME: technically it could still be stationary if there are no long/lat values... activity.Stationary = False if not act['laps']: # no laps, just make one big lap activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] startTime = activity.StartTime for lapRecord in act['laps']: endTime = activity.StartTime + timedelta(seconds=lapRecord['endDuration']) lap = Lap(startTime=startTime, endTime=endTime) activity.Laps.append(lap) startTime = endTime + timedelta(seconds=1) for value in zip(*act['recordingValues']): record = dict(zip(recordingKeys, value)) ts = activity.StartTime + timedelta(seconds=record['clock']) if 'latitude' in record: alt = record.get('elevation') lat = record['latitude'] lon = record['longitude'] # Smashrun seems to replace missing measurements with -1 if lat == -1: lat = None if lon == -1: lon = None location = Location(lat=lat, lon=lon, alt=alt) hr = record.get('heartRate') runCadence = record.get('cadence') temp = record.get('temperature') distance = record.get('distance') * 1000 wp = Waypoint(timestamp=ts, location=location, hr=hr, runCadence=runCadence, temp=temp, distance=distance) # put the waypoint inside the lap it corresponds to for lap in activity.Laps: if lap.StartTime <= wp.Timestamp <= lap.EndTime: lap.Waypoints.append(wp) break return activity
def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get( "https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: self._rateLimitBailout(resp) res_txt = resp.text raise APIException( "Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False for pt in resp["points"]: wp = Waypoint() if "time" not in pt: # Manually-entered activities with a course attached to them have date-less waypoints # It'd be nice to transfer those courses, but it's a concept few other sites support AFAIK # So, ignore the points entirely continue wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def DownloadActivity(self, serviceRecord, activity): resp = self._oauthSession(serviceRecord).get( "https://api.endomondo.com/api/1/workouts/%d" % activity.ServiceData["WorkoutID"], params={"fields": "points"}) try: resp = resp.json() except ValueError: res_txt = resp.text raise APIException( "Parse failure in Endomondo activity download: %s" % resp.status_code) lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] activity.GPS = False for pt in resp["points"]: wp = Waypoint() wp.Timestamp = self._parseDate(pt["time"]) if ("lat" in pt and "lng" in pt) or "alt" in pt: wp.Location = Location() if "lat" in pt and "lng" in pt: wp.Location.Latitude = pt["lat"] wp.Location.Longitude = pt["lng"] activity.GPS = True if "alt" in pt: wp.Location.Altitude = pt["alt"] if "hr" in pt: wp.HR = pt["hr"] if "cad" in pt: wp.Cadence = pt["cad"] lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def _populateActivityWaypoints(self, rawData, activity): ''' populate the Waypoints collection from RK API data ''' activity.Waypoints = [] # path is the primary stream, HR/power/etc must have fewer pts hasHR = "heart_rate" in rawData and len(rawData["heart_rate"]) > 0 hasCalories = "calories" in rawData and len(rawData["calories"]) > 0 for pathpoint in rawData["path"]: waypoint = Waypoint(activity.StartTime + timedelta(0, pathpoint["timestamp"])) waypoint.Location = Location(pathpoint["latitude"], pathpoint["longitude"], pathpoint["altitude"] if "altitude" in pathpoint and float(pathpoint["altitude"]) != 0 else None) # if you're running near sea level, well... waypoint.Type = self._wayptTypeMappings[pathpoint["type"]] if pathpoint["type"] in self._wayptTypeMappings else WaypointType.Regular if hasHR: hrpoint = [x for x in rawData["heart_rate"] if x["timestamp"] == pathpoint["timestamp"]] if len(hrpoint) > 0: waypoint.HR = hrpoint[0]["heart_rate"] if hasCalories: calpoint = [x for x in rawData["calories"] if x["timestamp"] == pathpoint["timestamp"]] if len(calpoint) > 0: waypoint.Calories = calpoint[0]["calories"] activity.Waypoints.append(waypoint)
def _populateActivityFromTrackRecord(self, activity, recordText, minimumWaypoints=False): activity.Waypoints = [] ### 1ST RECORD ### # userID; # timestamp - create date?; # type? W=1st # User name; # activity name; # activity type; # another timestamp - start time of event?; # duration.00; # distance (km); # kcal; #; # max alt; # min alt; # max HR; # avg HR; ### TRACK RECORDS ### # timestamp; # type (2=start, 3=end, 0=pause, 1=resume); # latitude; # longitude; #; #; # alt; # hr; wptsWithLocation = False wptsWithNonZeroAltitude = False rows = recordText.split("\n") for row in rows: if row == "OK" or len(row) == 0: continue split = row.split(";") if split[2] == "W": # init record activity.Distance = float(split[8]) * 1000 if split[8] != "" else None activity.Name = split[4] else: wp = Waypoint() if split[1] == "2": wp.Type = WaypointType.Start elif split[1] == "3": wp.Type = WaypointType.End elif split[1] == "0": wp.Type = WaypointType.Pause elif split[1] == "1": wp.Type = WaypointType.Resume else: wp.Type == WaypointType.Regular if split[0] == "": continue # no timestamp, for whatever reason wp.Timestamp = pytz.utc.localize(datetime.strptime(split[0], "%Y-%m-%d %H:%M:%S UTC")) # it's like this as opposed to %z so I know when they change things (it'll break) if split[2] != "": wp.Location = Location(float(split[2]), float(split[3]), None) if wp.Location.Latitude is not None and wp.Location.Latitude is not None: wptsWithLocation = True if split[6] != "": wp.Location.Altitude = float(split[6]) # why this is missing: who knows? if wp.Location.Altitude != 0: wptsWithNonZeroAltitude = True if split[7] != "": wp.HR = float(split[7]) activity.Waypoints.append(wp) if wptsWithLocation and minimumWaypoints: break if wptsWithLocation: activity.EnsureTZ() if not wptsWithNonZeroAltitude: # do this here so, should the activity run near sea level, altitude data won't be spotty for x in activity.Waypoints: # clear waypoints of altitude data if all of them were logged at 0m (invalid) if x.Location is not None: x.Location.Altitude = None else: activity.Waypoints = [] # practically speaking
def DownloadActivity(self, serviceRecord, activity): activityURI = [x["ActivityURI"] for x in activity.UploadedTo if x["Connection"] == serviceRecord][0] cookies = self._get_cookies(serviceRecord) activityData = requests.get(activityURI, cookies=cookies) activityData = activityData.json() if "location" not in activityData: raise APIExcludeActivity("No points") timerStops = [] if "timer_stops" in activityData: for stop in activityData["timer_stops"]: timerStops.append([dateutil.parser.parse(stop[0]), dateutil.parser.parse(stop[1])]) def isInTimerStop(timestamp): for stop in timerStops: if timestamp >= stop[0] and timestamp < stop[1]: return True if timestamp >= stop[1]: return False return False laps = [] if "laps" in activityData: for lap in activityData["laps"]: laps.append(dateutil.parser.parse(lap["start_time"])) # Collate the individual streams into our waypoints. # Everything is resampled by nearest-neighbour to the rate of the location stream. parallel_indices = {} parallel_stream_lengths = {} for secondary_stream in ["elevation", "heartrate"]: if secondary_stream in activityData: parallel_indices[secondary_stream] = 0 parallel_stream_lengths[secondary_stream] = len(activityData[secondary_stream]) activity.Waypoints = [] wasInPause = False currentLapIdx = 0 for idx in range(0, len(activityData["location"]), 2): # Pick the nearest indices in the parallel streams for parallel_stream, parallel_index in parallel_indices.items(): if parallel_index + 1 == parallel_stream_lengths[parallel_stream]: continue # We're at the end of this stream # Is the next datapoint a better choice than the current? if abs(activityData["location"][idx] - activityData[parallel_stream][parallel_index + 2]) < abs(activityData["location"][idx] - activityData[parallel_stream][parallel_index]): parallel_indices[parallel_stream] += 2 waypoint = Waypoint(activity.StartTime + timedelta(0, activityData["location"][idx])) waypoint.Location = Location(activityData["location"][idx+1][0], activityData["location"][idx+1][1], None) if "elevation" in parallel_indices: waypoint.Location.Altitude = activityData["elevation"][parallel_indices["elevation"]+1] if "heartrate" in parallel_indices: waypoint.HR = activityData["heartrate"][parallel_indices["heartrate"]+1] inPause = isInTimerStop(waypoint.Timestamp) waypoint.Type = WaypointType.Regular if not inPause else WaypointType.Pause if wasInPause and not inPause: waypoint.Type = WaypointType.Resume wasInPause = inPause # We only care if it's possible to start a new lap, i.e. there are more left if currentLapIdx + 1 < len(laps): if laps[currentLapIdx + 1] < waypoint.Timestamp: # A new lap has started waypoint.Type = WaypointType.Lap currentLapIdx += 1 activity.Waypoints.append(waypoint) activity.Waypoints[0].Type = WaypointType.Start activity.Waypoints[-1].Type = WaypointType.End return activity
def DownloadActivity(self, serviceRecord, activity): # First, download the summary stats and lap stats self._downloadActivitySummary(serviceRecord, activity) if len(activity.Laps) == 1: activity.Stats = activity.Laps[0].Stats # They must be identical to pass the verification if activity.Stationary: # Nothing else to download return activity # https://connect.garmin.com/proxy/activity-service-1.3/json/activityDetails/#### activityID = activity.ServiceData["ActivityID"] session = self._get_session(record=serviceRecord) self._rate_limit() res = session.get("http://connect.garmin.com/proxy/activity-service-1.3/json/activityDetails/" + str(activityID) + "?maxSize=999999999") try: raw_data = res.json()["com.garmin.activity.details.json.ActivityDetails"] except ValueError: raise APIException("Activity data parse error for %s: %s" % (res.status_code, res.text)) if "measurements" 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 = {} attr_count = len(raw_data["measurements"]) for measurement in raw_data["measurements"]: key = measurement["key"] if key in attrs_map: if attrs_map[key]["to_units"]: attrs_map[key]["from_units"] = self._unitMap[measurement["unit"]] 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["metrics"]: 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 # 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 DownloadActivity(self, svcRecord, activity): if activity.ServiceData["Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] return activity activityID = activity.ServiceData["ActivityID"] streamdata = self._requestWithAuth(lambda session: session.get("https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng,distance,velocity_smooth"), svcRecord) if streamdata.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata["message"] == "Record Not Found": raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasDistance = "distance" in ridedata and len(ridedata["distance"]) > 0 hasVelocity = "velocity_smooth" in ridedata and len(ridedata["velocity_smooth"]) > 0 if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) inPause = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) if "latlng" in ridedata: latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if hasAltitude: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = float(ridedata["altitude"][idx]) # When pausing, Strava sends this format: # idx = 100 ; time = 1000; moving = true # idx = 101 ; time = 1001; moving = true => convert to Pause # idx = 102 ; time = 2001; moving = false => convert to Resume: (2001-1001) seconds pause # idx = 103 ; time = 2002; moving = true if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif idx < waypointCt - 2 and ridedata["moving"][idx+1] and inPause: waypoint.Type = WaypointType.Resume inPause = False elif idx < waypointCt - 2 and not ridedata["moving"][idx+1] and not inPause: waypoint.Type = WaypointType.Pause inPause = True if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] if hasVelocity: waypoint.Speed = ridedata["velocity_smooth"][idx] if hasDistance: waypoint.Distance = ridedata["distance"][idx] lap.Waypoints.append(waypoint) return activity
def DownloadActivity(self, svcRecord, activity): if activity.ServiceData["Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] return activity activityID = activity.ServiceData["ActivityID"] self._globalRateLimit() streamdata = requests.get("https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata["message"] == "Record Not Found": raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) if "latlng" in ridedata: latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if hasAltitude: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] lap.Waypoints.append(waypoint) return activity
def create_random_activity(svc=None, actType=ActivityType.Other, tz=False, record=None): ''' creates completely random activity with valid waypoints and data ''' act = TestTools.create_blank_activity(svc, actType, record=record) if tz is True: tz = pytz.timezone(pytz.all_timezones[random.randint(0, len(pytz.all_timezones) - 1)]) act.TZ = tz elif tz is not False: act.TZ = tz if len(act.Waypoints) > 0: raise ValueError("Waypoint list already populated") # this is entirely random in case the testing account already has events in it (API doesn't support delete, etc) act.StartTime = datetime(random.randint(2000, 2020), random.randint(1, 12), random.randint(1, 28), random.randint(0, 23), random.randint(0, 59), random.randint(0, 59)) if tz is not False: if hasattr(tz, "localize"): act.StartTime = tz.localize(act.StartTime) else: act.StartTime = act.StartTime.replace(tzinfo=tz) act.EndTime = act.StartTime + timedelta(0, random.randint(60 * 5, 60 * 60)) # don't really need to upload 1000s of pts to test this... act.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=random.random() * 10000) act.Name = str(random.random()) paused = False waypointTime = act.StartTime backToBackPauses = False while waypointTime < act.EndTime: wp = Waypoint() if waypointTime == act.StartTime: wp.Type = WaypointType.Start wp.Timestamp = waypointTime wp.Location = Location(random.random() * 180 - 90, random.random() * 180 - 90, random.random() * 1000) # this is gonna be one intense activity if not (wp.HR == wp.Cadence == wp.Calories == wp.Power == wp.Temp == None): raise ValueError("Waypoint did not initialize cleanly") if svc.SupportsHR: wp.HR = float(random.randint(90, 180)) if svc.SupportsPower: wp.Power = float(random.randint(0, 1000)) if svc.SupportsCalories: wp.Calories = float(random.randint(0, 500)) if svc.SupportsCadence: wp.Cadence = float(random.randint(0, 100)) if svc.SupportsTemp: wp.Temp = float(random.randint(0, 100)) if (random.randint(40, 50) == 42 or backToBackPauses) and not paused: # pause quite often wp.Type = WaypointType.Pause paused = True elif paused: paused = False wp.Type = WaypointType.Resume backToBackPauses = not backToBackPauses waypointTime += timedelta(0, int(random.random() + 9.5)) # 10ish seconds if waypointTime > act.EndTime: wp.Timestamp = act.EndTime wp.Type = WaypointType.End act.Waypoints.append(wp) if len(act.Waypoints) == 0: raise ValueError("No waypoints populated") return act
def _downloadActivity(self, serviceRecord, activity, returnFirstLocation=False): activityURI = activity.ServiceData["ActivityURI"] headers = self._getAuthHeaders(serviceRecord) activityData = requests.get(activityURI, headers=headers) activityData = activityData.json() if "clock_duration" in activityData: activity.EndTime = activity.StartTime + timedelta( seconds=float(activityData["clock_duration"])) activity.Private = "sharing" in activityData and activityData[ "sharing"] != "public" activity.GPS = False # Gets set back if there is GPS data if "notes" in activityData: activity.Notes = activityData["notes"] activity.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilojoules, value=float(activityData["calories"])) activity.Stats.Elevation = ActivityStatistic( ActivityStatisticUnit.Meters, gain=float(activityData["elevation_gain"]) if "elevation_gain" in activityData else None, loss=float(activityData["elevation_loss"]) if "elevation_loss" in activityData else None) activity.Stats.HR = ActivityStatistic( ActivityStatisticUnit.BeatsPerMinute, avg=activityData["avg_heartrate"] if "avg_heartrate" in activityData else None, max=activityData["max_heartrate"] if "max_heartrate" in activityData else None) activity.Stats.Cadence = ActivityStatistic( ActivityStatisticUnit.RevolutionsPerMinute, avg=activityData["avg_cadence"] if "avg_cadence" in activityData else None, max=activityData["max_cadence"] if "max_cadence" in activityData else None) activity.Stats.Power = ActivityStatistic( ActivityStatisticUnit.Watts, avg=activityData["avg_power"] if "avg_power" in activityData else None, max=activityData["max_power"] if "max_power" in activityData else None) laps_info = [] laps_starts = [] if "laps" in activityData: laps_info = activityData["laps"] for lap in activityData["laps"]: laps_starts.append(dateutil.parser.parse(lap["start_time"])) lap = None for lapinfo in laps_info: lap = Lap() activity.Laps.append(lap) lap.StartTime = dateutil.parser.parse(lapinfo["start_time"]) lap.EndTime = lap.StartTime + timedelta( seconds=lapinfo["clock_duration"]) if "type" in lapinfo: lap.Intensity = LapIntensity.Active if lapinfo[ "type"] == "ACTIVE" else LapIntensity.Rest if "distance" in lapinfo: lap.Stats.Distance = ActivityStatistic( ActivityStatisticUnit.Meters, value=float(lapinfo["distance"])) if "duration" in lapinfo: lap.Stats.TimerTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=lapinfo["duration"]) if "calories" in lapinfo: lap.Stats.Energy = ActivityStatistic( ActivityStatisticUnit.Kilojoules, value=lapinfo["calories"]) if "elevation_gain" in lapinfo: lap.Stats.Elevation.update( ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(lapinfo["elevation_gain"]))) if "elevation_loss" in lapinfo: lap.Stats.Elevation.update( ActivityStatistic(ActivityStatisticUnit.Meters, loss=float(lapinfo["elevation_loss"]))) if "max_speed" in lapinfo: lap.Stats.Speed.update( ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=float(lapinfo["max_speed"]))) if "max_speed" in lapinfo: lap.Stats.Speed.update( ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=float(lapinfo["max_speed"]))) if "avg_speed" in lapinfo: lap.Stats.Speed.update( ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=float(lapinfo["avg_speed"]))) if "max_heartrate" in lapinfo: lap.Stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=float(lapinfo["max_heartrate"]))) if "avg_heartrate" in lapinfo: lap.Stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(lapinfo["avg_heartrate"]))) if lap is None: # No explicit laps => make one that encompasses the entire activity lap = Lap() activity.Laps.append(lap) lap.Stats = activity.Stats lap.StartTime = activity.StartTime lap.EndTime = activity.EndTime elif len(activity.Laps) == 1: activity.Stats.update( activity.Laps[0].Stats ) # Lap stats have a bit more info generally. activity.Laps[0].Stats = activity.Stats timerStops = [] if "timer_stops" in activityData: for stop in activityData["timer_stops"]: timerStops.append([ dateutil.parser.parse(stop[0]), dateutil.parser.parse(stop[1]) ]) def isInTimerStop(timestamp): for stop in timerStops: if timestamp >= stop[0] and timestamp < stop[1]: return True if timestamp >= stop[1]: return False return False # Collate the individual streams into our waypoints. # Global sample rate is variable - will pick the next nearest stream datapoint. # Resampling happens on a lookbehind basis - new values will only appear their timestamp has been reached/passed wasInPause = False currentLapIdx = 0 lap = activity.Laps[currentLapIdx] streams = [] for stream in [ "location", "elevation", "heartrate", "power", "cadence", "distance" ]: if stream in activityData: streams.append(stream) stream_indices = dict([(stream, -1) for stream in streams ]) # -1 meaning the stream has yet to start stream_lengths = dict([(stream, len(activityData[stream]) / 2) for stream in streams]) # Data comes as "stream":[timestamp,value,timestamp,value,...] stream_values = {} for stream in streams: values = [] for x in range(0, int(len(activityData[stream]) / 2)): values.append((activityData[stream][x * 2], activityData[stream][x * 2 + 1])) stream_values[stream] = values currentOffset = 0 def streamVal(stream): nonlocal stream_values, stream_indices return stream_values[stream][stream_indices[stream]][1] def hasStreamData(stream): nonlocal stream_indices, streams return stream in streams and stream_indices[stream] >= 0 while True: advance_stream = None advance_offset = None for stream in streams: if stream_indices[stream] + 1 == stream_lengths[stream]: continue # We're at the end - can't advance if advance_offset is None or stream_values[stream][ stream_indices[stream] + 1][0] - currentOffset < advance_offset: advance_offset = stream_values[stream][ stream_indices[stream] + 1][0] - currentOffset advance_stream = stream if not advance_stream: break # We've hit the end of every stream, stop # Advance streams sharing the current timestamp for stream in streams: if stream == advance_stream: continue # For clarity, we increment this later if stream_indices[stream] + 1 == stream_lengths[stream]: continue # We're at the end - can't advance if stream_values[stream][ stream_indices[stream] + 1][0] == stream_values[advance_stream][ stream_indices[advance_stream] + 1][0]: stream_indices[stream] += 1 stream_indices[ advance_stream] += 1 # Advance the key stream for this waypoint currentOffset = stream_values[advance_stream][stream_indices[ advance_stream]][0] # Update the current time offset waypoint = Waypoint(activity.StartTime + timedelta(seconds=currentOffset)) if hasStreamData("location"): waypoint.Location = Location( streamVal("location")[0], streamVal("location")[1], None) activity.GPS = True if returnFirstLocation: return waypoint.Location if hasStreamData("elevation"): if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = streamVal("elevation") if hasStreamData("heartrate"): waypoint.HR = streamVal("heartrate") if hasStreamData("power"): waypoint.Power = streamVal("power") if hasStreamData("cadence"): waypoint.Cadence = streamVal("cadence") if hasStreamData("distance"): waypoint.Distance = streamVal("distance") inPause = isInTimerStop(waypoint.Timestamp) waypoint.Type = WaypointType.Regular if not inPause else WaypointType.Pause if wasInPause and not inPause: waypoint.Type = WaypointType.Resume wasInPause = inPause # We only care if it's possible to start a new lap, i.e. there are more left if currentLapIdx + 1 < len(laps_starts): if laps_starts[currentLapIdx + 1] < waypoint.Timestamp: # A new lap has started currentLapIdx += 1 lap = activity.Laps[currentLapIdx] lap.Waypoints.append(waypoint) if returnFirstLocation: return None # I guess there were no waypoints? if activity.CountTotalWaypoints(): activity.GetFlatWaypoints()[0].Type = WaypointType.Start activity.GetFlatWaypoints()[-1].Type = WaypointType.End activity.Stationary = False else: activity.Stationary = True return activity
def DownloadActivity(self, svcRecord, activity): if activity.ServiceData[ "Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [ Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats) ] return activity activityID = activity.ServiceData["ActivityID"] streamdata = requests.get( "https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "auth") raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata[ "message"] == "Record Not Found": self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "missing") raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap( stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime ) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasMovingData = "moving" in ridedata and len(ridedata["moving"]) > 0 moving = True if "error" in ridedata: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "data") raise APIException("Strava error " + ridedata["error"]) hasLocation = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): latlng = ridedata["latlng"][idx] waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None else: # strava only returns 0 as invalid coords, so no need to check for null (update: ??) hasLocation = True if hasAltitude: waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif hasMovingData and not moving and ridedata["moving"][ idx] is True: waypoint.Type = WaypointType.Resume moving = True elif hasMovingData and ridedata["moving"][idx] is False: waypoint.Type = WaypointType.Pause moving = False if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] lap.Waypoints.append(waypoint) if not hasLocation: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "faulty") raise APIExcludeActivity("No waypoints with location", activityId=activityID, userException=UserException( UserExceptionType.Corrupt)) self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), None) return activity
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] logger.info("\t\t DC LOADING : " + str(activityID)) headers = self._getAuthHeaders(svcRecord) resp = requests.get(self.ApiEndpoint + "/activity/" + activityID + "/fullactivity.xml", headers=headers) if resp.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) try: root = xml.fromstring(resp.content) except: raise APIException( "Stream data returned from DecathlonCoach is not XML") lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] lap.Waypoints = [] activity.GPS = False #work on date startdate = root.find('.//STARTDATE').text timezone = root.find('.//TIMEZONE').text datebase = parse(startdate + timezone) for pt in root.iter('LOCATION'): wp = Waypoint() delta = int(pt.get('elapsed_time')) formatedDate = datebase + timedelta(seconds=delta) wp.Timestamp = formatedDate #self._parseDate(formatedDate.isoformat()) wp.Location = Location() wp.Location.Latitude = float(pt.find('LATITUDE').text[:8]) wp.Location.Longitude = float(pt.find('LONGITUDE').text[:8]) activity.GPS = True wp.Location.Altitude = int(pt.find('ELEVATION').text[:8]) #get the HR value in the Datastream node and measures collection for hr in root.iter('MEASURE'): if pt.get('elapsed_time') == hr.get('elapsed_time'): for measureValue in hr.iter('VALUE'): if measureValue.get('id') == "1": wp.HR = int(measureValue.text) break break lap.Waypoints.append(wp) activity.Stationary = len(lap.Waypoints) == 0 return activity
def DownloadActivity(self, svcRecord, activity): # thanks to Cosmo Catalano for the API reference code activityID = [ x["ActivityID"] for x in activity.UploadedTo if x["Connection"] == svcRecord ][0] streamdata = requests.get( "https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,watts_calc,temp,resting,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) streamdata = streamdata.json() if "message" in streamdata and streamdata[ "message"] == "Record Not Found": raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] activity.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasRestingData = "resting" in ridedata and len(ridedata["resting"]) > 0 moving = True if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) hasLocation = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): latlng = ridedata["latlng"][idx] waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None else: # strava only returns 0 as invalid coords, so no need to check for null (update: ??) hasLocation = True if hasAltitude: waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif hasRestingData and not moving and ridedata["resting"][ idx] is False: waypoint.Type = WaypointType.Resume moving = True elif hasRestingData and ridedata["resting"][idx] is True: waypoint.Type = WaypointType.Pause moving = False if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] activity.Waypoints.append(waypoint) if not hasLocation: raise APIExcludeActivity("No waypoints with location", activityId=activityID) return activity
def DownloadActivity(self, svcRecord, activity): # thanks to Cosmo Catalano for the API reference code activityID = [x["ActivityID"] for x in activity.UploadedTo if x["Connection"] == svcRecord][0] streamdata = requests.get("https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,watts_calc,temp,resting,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) streamdata = streamdata.json() if "message" in streamdata and streamdata["message"] == "Record Not Found": raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] activity.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) or ("watts_calc" in ridedata and len(ridedata["watts_calc"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasRestingData = "resting" in ridedata and len(ridedata["resting"]) > 0 moving = True if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) hasLocation = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): latlng = ridedata["latlng"][idx] waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None else: # strava only returns 0 as invalid coords, so no need to check for null (update: ??) hasLocation = True if hasAltitude: waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif hasRestingData and not moving and ridedata["resting"][idx] is False: waypoint.Type = WaypointType.Resume moving = True elif hasRestingData and ridedata["resting"][idx] is True: waypoint.Type = WaypointType.Pause moving = False if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] if "watts" in ridedata else ridedata["watts_calc"][idx] activity.Waypoints.append(waypoint) if not hasLocation: raise APIExcludeActivity("No waypoints with location", activityId=activityID) return activity
def _downloadActivity(self, serviceRecord, activity, returnFirstLocation=False): activityURI = activity.ServiceData["ActivityURI"] cookies = self._get_cookies(record=serviceRecord) activityData = requests.get(activityURI, cookies=cookies) activityData = activityData.json() if "clock_duration" in activityData: activity.EndTime = activity.StartTime + timedelta(seconds=float(activityData["clock_duration"])) activity.Private = "sharing" in activityData and activityData["sharing"] != "public" if "notes" in activityData: activity.Notes = activityData["notes"] activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilojoules, value=float(activityData["calories"])) activity.Stats.Elevation = ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(activityData["elevation_gain"]) if "elevation_gain" in activityData else None, loss=float(activityData["elevation_loss"]) if "elevation_loss" in activityData else None) activity.Stats.HR = ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=activityData["avg_heartrate"] if "avg_heartrate" in activityData else None, max=activityData["max_heartrate"] if "max_heartrate" in activityData else None) activity.Stats.Cadence = ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=activityData["avg_cadence"] if "avg_cadence" in activityData else None, max=activityData["max_cadence"] if "max_cadence" in activityData else None) activity.Stats.Power = ActivityStatistic(ActivityStatisticUnit.Watts, avg=activityData["avg_power"] if "avg_power" in activityData else None, max=activityData["max_power"] if "max_power" in activityData else None) laps_info = [] laps_starts = [] if "laps" in activityData: laps_info = activityData["laps"] for lap in activityData["laps"]: laps_starts.append(dateutil.parser.parse(lap["start_time"])) lap = None for lapinfo in laps_info: lap = Lap() activity.Laps.append(lap) lap.StartTime = dateutil.parser.parse(lapinfo["start_time"]) lap.EndTime = lap.StartTime + timedelta(seconds=lapinfo["clock_duration"]) if "type" in lapinfo: lap.Intensity = LapIntensity.Active if lapinfo["type"] == "ACTIVE" else LapIntensity.Rest if "distance" in lapinfo: lap.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=float(lapinfo["distance"])) if "duration" in lapinfo: lap.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Time, value=timedelta(seconds=lapinfo["duration"])) if "calories" in lapinfo: lap.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilojoules, value=lapinfo["calories"]) if "elevation_gain" in lapinfo: lap.Stats.Elevation.update(ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(lapinfo["elevation_gain"]))) if "elevation_loss" in lapinfo: lap.Stats.Elevation.update(ActivityStatistic(ActivityStatisticUnit.Meters, loss=float(lapinfo["elevation_loss"]))) if "max_speed" in lapinfo: lap.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=float(lapinfo["max_speed"]))) if "max_speed" in lapinfo: lap.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=float(lapinfo["max_speed"]))) if "avg_speed" in lapinfo: lap.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=float(lapinfo["avg_speed"]))) if "max_heartrate" in lapinfo: lap.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=float(lapinfo["max_heartrate"]))) if "avg_heartrate" in lapinfo: lap.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(lapinfo["avg_heartrate"]))) if lap is None: # No explicit laps => make one that encompasses the entire activity lap = Lap() activity.Laps.append(lap) lap.Stats = activity.Stats lap.StartTime = activity.StartTime lap.EndTime = activity.EndTime if "location" not in activityData: activity.Stationary = True else: activity.Stationary = False timerStops = [] if "timer_stops" in activityData: for stop in activityData["timer_stops"]: timerStops.append([dateutil.parser.parse(stop[0]), dateutil.parser.parse(stop[1])]) def isInTimerStop(timestamp): for stop in timerStops: if timestamp >= stop[0] and timestamp < stop[1]: return True if timestamp >= stop[1]: return False return False # Collate the individual streams into our waypoints. # Everything is resampled by nearest-neighbour to the rate of the location stream. parallel_indices = {} parallel_stream_lengths = {} for secondary_stream in ["elevation", "heartrate", "power", "cadence", "distance"]: if secondary_stream in activityData: parallel_indices[secondary_stream] = 0 parallel_stream_lengths[secondary_stream] = len(activityData[secondary_stream]) wasInPause = False currentLapIdx = 0 lap = activity.Laps[currentLapIdx] for idx in range(0, len(activityData["location"]), 2): # Pick the nearest indices in the parallel streams for parallel_stream, parallel_index in parallel_indices.items(): if parallel_index + 2 == parallel_stream_lengths[parallel_stream]: continue # We're at the end of this stream # Is the next datapoint a better choice than the current? if abs(activityData["location"][idx] - activityData[parallel_stream][parallel_index + 2]) < abs(activityData["location"][idx] - activityData[parallel_stream][parallel_index]): parallel_indices[parallel_stream] += 2 waypoint = Waypoint(activity.StartTime + timedelta(0, activityData["location"][idx])) waypoint.Location = Location(activityData["location"][idx+1][0], activityData["location"][idx+1][1], None) if "elevation" in parallel_indices: waypoint.Location.Altitude = activityData["elevation"][parallel_indices["elevation"]+1] if returnFirstLocation: return waypoint.Location if "heartrate" in parallel_indices: waypoint.HR = activityData["heartrate"][parallel_indices["heartrate"]+1] if "power" in parallel_indices: waypoint.Power = activityData["power"][parallel_indices["power"]+1] if "cadence" in parallel_indices: waypoint.Cadence = activityData["cadence"][parallel_indices["cadence"]+1] if "distance" in parallel_indices: waypoint.Distance = activityData["distance"][parallel_indices["distance"]+1] inPause = isInTimerStop(waypoint.Timestamp) waypoint.Type = WaypointType.Regular if not inPause else WaypointType.Pause if wasInPause and not inPause: waypoint.Type = WaypointType.Resume wasInPause = inPause # We only care if it's possible to start a new lap, i.e. there are more left if currentLapIdx + 1 < len(laps_starts): if laps_starts[currentLapIdx + 1] < waypoint.Timestamp: # A new lap has started currentLapIdx += 1 lap = activity.Laps[currentLapIdx] lap.Waypoints.append(waypoint) if returnFirstLocation: return None # I guess there were no waypoints? if activity.CountTotalWaypoints(): activity.Laps[0].Waypoints[0].Type = WaypointType.Start activity.Laps[-1].Waypoints[-1].Type = WaypointType.End return activity
def test_duration_calculation(self): ''' ensures that true-duration calculation is being reasonable ''' act = TestTools.create_blank_activity() act.StartTime = datetime.now() act.EndTime = act.StartTime + timedelta(hours=3) # No waypoints self.assertRaises(ValueError, act.GetTimerTime) # Too few waypoints act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.EndTime)] self.assertRaises(ValueError, act.GetTimerTime) # straight-up calculation act.EndTime = act.StartTime + timedelta(seconds=14) act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.StartTime + timedelta(seconds=2)), Waypoint(timestamp=act.StartTime + timedelta(seconds=6)), Waypoint(timestamp=act.StartTime + timedelta(seconds=10)), Waypoint(timestamp=act.StartTime + timedelta(seconds=14))] self.assertEqual(act.GetTimerTime(), timedelta(seconds=14)) # pauses act.EndTime = act.StartTime + timedelta(seconds=14) act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.StartTime + timedelta(seconds=2)), Waypoint(timestamp=act.StartTime + timedelta(seconds=6), ptType=WaypointType.Pause), Waypoint(timestamp=act.StartTime + timedelta(seconds=9), ptType=WaypointType.Pause), Waypoint(timestamp=act.StartTime + timedelta(seconds=10), ptType=WaypointType.Resume), Waypoint(timestamp=act.StartTime + timedelta(seconds=14))] self.assertEqual(act.GetTimerTime(), timedelta(seconds=10)) # laps - NO effect act.EndTime = act.StartTime + timedelta(seconds=14) act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.StartTime + timedelta(seconds=2)), Waypoint(timestamp=act.StartTime + timedelta(seconds=6), ptType=WaypointType.Lap), Waypoint(timestamp=act.StartTime + timedelta(seconds=9)), Waypoint(timestamp=act.StartTime + timedelta(seconds=10), ptType=WaypointType.Lap), Waypoint(timestamp=act.StartTime + timedelta(seconds=14))] self.assertEqual(act.GetTimerTime(), timedelta(seconds=14)) # multiple pauses + ending after pause act.EndTime = act.StartTime + timedelta(seconds=20) act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.StartTime + timedelta(seconds=2)), Waypoint(timestamp=act.StartTime + timedelta(seconds=6), ptType=WaypointType.Pause), Waypoint(timestamp=act.StartTime + timedelta(seconds=9), ptType=WaypointType.Pause), Waypoint(timestamp=act.StartTime + timedelta(seconds=10), ptType=WaypointType.Resume), Waypoint(timestamp=act.StartTime + timedelta(seconds=12)), Waypoint(timestamp=act.StartTime + timedelta(seconds=16)), Waypoint(timestamp=act.StartTime + timedelta(seconds=17), ptType=WaypointType.Pause), Waypoint(timestamp=act.StartTime + timedelta(seconds=20), ptType=WaypointType.End)] self.assertEqual(act.GetTimerTime(), timedelta(seconds=13)) # implicit pauses (>1m5s) act.EndTime = act.StartTime + timedelta(seconds=20) act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.StartTime + timedelta(seconds=2)), Waypoint(timestamp=act.StartTime + timedelta(seconds=6)), Waypoint(timestamp=act.StartTime + timedelta(seconds=120)), Waypoint(timestamp=act.StartTime + timedelta(seconds=124)), Waypoint(timestamp=act.StartTime + timedelta(seconds=130))] self.assertEqual(act.GetTimerTime(), timedelta(seconds=16)) # mixed pauses - would this ever happen?? Either way, the explicit pause should override the implicit one and cause otherwise-ignored time to be counted act.EndTime = act.StartTime + timedelta(seconds=23) act.Waypoints = [Waypoint(timestamp=act.StartTime), Waypoint(timestamp=act.StartTime + timedelta(seconds=2)), Waypoint(timestamp=act.StartTime + timedelta(seconds=6)), Waypoint(timestamp=act.StartTime + timedelta(seconds=20), ptType=WaypointType.Pause), Waypoint(timestamp=act.StartTime + timedelta(seconds=24), ptType=WaypointType.Resume), Waypoint(timestamp=act.StartTime + timedelta(seconds=30))] self.assertEqual(act.GetTimerTime(), timedelta(seconds=26))
def DownloadActivity(self, svcRecord, activity): if activity.ServiceData["Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats)] return activity activityID = activity.ServiceData["ActivityID"] streamdata = requests.get("https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "auth") raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata["message"] == "Record Not Found": self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "missing") raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasMovingData = "moving" in ridedata and len(ridedata["moving"]) > 0 moving = True if "error" in ridedata: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "data") raise APIException("Strava error " + ridedata["error"]) hasLocation = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): latlng = ridedata["latlng"][idx] waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None else: # strava only returns 0 as invalid coords, so no need to check for null (update: ??) hasLocation = True if hasAltitude: waypoint.Location.Altitude = float(ridedata["altitude"][idx]) if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif hasMovingData and not moving and ridedata["moving"][idx] is True: waypoint.Type = WaypointType.Resume moving = True elif hasMovingData and ridedata["moving"][idx] is False: waypoint.Type = WaypointType.Pause moving = False if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] lap.Waypoints.append(waypoint) if not hasLocation: self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), "faulty") raise APIExcludeActivity("No waypoints with location", activityId=activityID, userException=UserException(UserExceptionType.Corrupt)) self._logAPICall("download", (svcRecord.ExternalID, str(activity.StartTime)), None) return activity
def create_random_activity(svc=None, actType=ActivityType.Other, tz=False, record=None): ''' creates completely random activity with valid waypoints and data ''' act = TestTools.create_blank_activity(svc, actType, record=record) if tz is True: tz = pytz.timezone(pytz.all_timezones[random.randint( 0, len(pytz.all_timezones) - 1)]) act.TZ = tz elif tz is not False: act.TZ = tz if len(act.Waypoints) > 0: raise ValueError("Waypoint list already populated") # this is entirely random in case the testing account already has events in it (API doesn't support delete, etc) act.StartTime = datetime(random.randint(2000, 2020), random.randint(1, 12), random.randint(1, 28), random.randint(0, 23), random.randint(0, 59), random.randint(0, 59)) if tz is not False: if hasattr(tz, "localize"): act.StartTime = tz.localize(act.StartTime) else: act.StartTime = act.StartTime.replace(tzinfo=tz) act.EndTime = act.StartTime + timedelta( 0, random.randint(60 * 5, 60 * 60) ) # don't really need to upload 1000s of pts to test this... act.Distance = random.random() * 10000 act.Name = str(random.random()) paused = False waypointTime = act.StartTime backToBackPauses = False while waypointTime < act.EndTime: wp = Waypoint() if waypointTime == act.StartTime: wp.Type = WaypointType.Start wp.Timestamp = waypointTime wp.Location = Location( random.random() * 180 - 90, random.random() * 180 - 90, random.random() * 1000) # this is gonna be one intense activity if not (wp.HR == wp.Cadence == wp.Calories == wp.Power == wp.Temp == None): raise ValueError("Waypoint did not initialize cleanly") if svc.SupportsHR: wp.HR = float(random.randint(90, 180)) if svc.SupportsPower: wp.Power = float(random.randint(0, 1000)) if svc.SupportsCalories: wp.Calories = float(random.randint(0, 500)) if svc.SupportsCadence: wp.Cadence = float(random.randint(0, 100)) if svc.SupportsTemp: wp.Temp = float(random.randint(0, 100)) if (random.randint(40, 50) == 42 or backToBackPauses) and not paused: # pause quite often wp.Type = WaypointType.Pause paused = True elif paused: paused = False wp.Type = WaypointType.Resume backToBackPauses = not backToBackPauses waypointTime += timedelta(0, int(random.random() + 9.5)) # 10ish seconds if waypointTime > act.EndTime: wp.Timestamp = act.EndTime wp.Type = WaypointType.End act.Waypoints.append(wp) if len(act.Waypoints) == 0: raise ValueError("No waypoints populated") return act
def _downloadActivity(self, serviceRecord, activity, returnFirstLocation=False): activityURI = activity.ServiceData["ActivityURI"] headers = self._getAuthHeaders(serviceRecord) activityData = requests.get(activityURI, headers=headers) activityData = activityData.json() if "clock_duration" in activityData: activity.EndTime = activity.StartTime + timedelta(seconds=float(activityData["clock_duration"])) activity.Private = "sharing" in activityData and activityData["sharing"] != "public" activity.GPS = False # Gets set back if there is GPS data if "notes" in activityData: activity.Notes = activityData["notes"] activity.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilojoules, value=float(activityData["calories"])) activity.Stats.Elevation = ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(activityData["elevation_gain"]) if "elevation_gain" in activityData else None, loss=float(activityData["elevation_loss"]) if "elevation_loss" in activityData else None) activity.Stats.HR = ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=activityData["avg_heartrate"] if "avg_heartrate" in activityData else None, max=activityData["max_heartrate"] if "max_heartrate" in activityData else None) activity.Stats.Cadence = ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=activityData["avg_cadence"] if "avg_cadence" in activityData else None, max=activityData["max_cadence"] if "max_cadence" in activityData else None) activity.Stats.Power = ActivityStatistic(ActivityStatisticUnit.Watts, avg=activityData["avg_power"] if "avg_power" in activityData else None, max=activityData["max_power"] if "max_power" in activityData else None) laps_info = [] laps_starts = [] if "laps" in activityData: laps_info = activityData["laps"] for lap in activityData["laps"]: laps_starts.append(dateutil.parser.parse(lap["start_time"])) lap = None for lapinfo in laps_info: lap = Lap() activity.Laps.append(lap) lap.StartTime = dateutil.parser.parse(lapinfo["start_time"]) lap.EndTime = lap.StartTime + timedelta(seconds=lapinfo["clock_duration"]) if "type" in lapinfo: lap.Intensity = LapIntensity.Active if lapinfo["type"] == "ACTIVE" else LapIntensity.Rest if "distance" in lapinfo: lap.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=float(lapinfo["distance"])) if "duration" in lapinfo: lap.Stats.TimerTime = ActivityStatistic(ActivityStatisticUnit.Seconds, value=lapinfo["duration"]) if "calories" in lapinfo: lap.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilojoules, value=lapinfo["calories"]) if "elevation_gain" in lapinfo: lap.Stats.Elevation.update(ActivityStatistic(ActivityStatisticUnit.Meters, gain=float(lapinfo["elevation_gain"]))) if "elevation_loss" in lapinfo: lap.Stats.Elevation.update(ActivityStatistic(ActivityStatisticUnit.Meters, loss=float(lapinfo["elevation_loss"]))) if "max_speed" in lapinfo: lap.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=float(lapinfo["max_speed"]))) if "max_speed" in lapinfo: lap.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, max=float(lapinfo["max_speed"]))) if "avg_speed" in lapinfo: lap.Stats.Speed.update(ActivityStatistic(ActivityStatisticUnit.MetersPerSecond, avg=float(lapinfo["avg_speed"]))) if "max_heartrate" in lapinfo: lap.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, max=float(lapinfo["max_heartrate"]))) if "avg_heartrate" in lapinfo: lap.Stats.HR.update(ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(lapinfo["avg_heartrate"]))) if lap is None: # No explicit laps => make one that encompasses the entire activity lap = Lap() activity.Laps.append(lap) lap.Stats = activity.Stats lap.StartTime = activity.StartTime lap.EndTime = activity.EndTime elif len(activity.Laps) == 1: activity.Stats.update(activity.Laps[0].Stats) # Lap stats have a bit more info generally. activity.Laps[0].Stats = activity.Stats timerStops = [] if "timer_stops" in activityData: for stop in activityData["timer_stops"]: timerStops.append([dateutil.parser.parse(stop[0]), dateutil.parser.parse(stop[1])]) def isInTimerStop(timestamp): for stop in timerStops: if timestamp >= stop[0] and timestamp < stop[1]: return True if timestamp >= stop[1]: return False return False # Collate the individual streams into our waypoints. # Global sample rate is variable - will pick the next nearest stream datapoint. # Resampling happens on a lookbehind basis - new values will only appear their timestamp has been reached/passed wasInPause = False currentLapIdx = 0 lap = activity.Laps[currentLapIdx] streams = [] for stream in ["location", "elevation", "heartrate", "power", "cadence", "distance"]: if stream in activityData: streams.append(stream) stream_indices = dict([(stream, -1) for stream in streams]) # -1 meaning the stream has yet to start stream_lengths = dict([(stream, len(activityData[stream])/2) for stream in streams]) # Data comes as "stream":[timestamp,value,timestamp,value,...] stream_values = {} for stream in streams: values = [] for x in range(0,int(len(activityData[stream])/2)): values.append((activityData[stream][x * 2], activityData[stream][x * 2 + 1])) stream_values[stream] = values currentOffset = 0 def streamVal(stream): nonlocal stream_values, stream_indices return stream_values[stream][stream_indices[stream]][1] def hasStreamData(stream): nonlocal stream_indices, streams return stream in streams and stream_indices[stream] >= 0 while True: advance_stream = None advance_offset = None for stream in streams: if stream_indices[stream] + 1 == stream_lengths[stream]: continue # We're at the end - can't advance if advance_offset is None or stream_values[stream][stream_indices[stream] + 1][0] - currentOffset < advance_offset: advance_offset = stream_values[stream][stream_indices[stream] + 1][0] - currentOffset advance_stream = stream if not advance_stream: break # We've hit the end of every stream, stop # Advance streams sharing the current timestamp for stream in streams: if stream == advance_stream: continue # For clarity, we increment this later if stream_indices[stream] + 1 == stream_lengths[stream]: continue # We're at the end - can't advance if stream_values[stream][stream_indices[stream] + 1][0] == stream_values[advance_stream][stream_indices[advance_stream] + 1][0]: stream_indices[stream] += 1 stream_indices[advance_stream] += 1 # Advance the key stream for this waypoint currentOffset = stream_values[advance_stream][stream_indices[advance_stream]][0] # Update the current time offset waypoint = Waypoint(activity.StartTime + timedelta(seconds=currentOffset)) if hasStreamData("location"): waypoint.Location = Location(streamVal("location")[0], streamVal("location")[1], None) activity.GPS = True if returnFirstLocation: return waypoint.Location if hasStreamData("elevation"): if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = streamVal("elevation") if hasStreamData("heartrate"): waypoint.HR = streamVal("heartrate") if hasStreamData("power"): waypoint.Power = streamVal("power") if hasStreamData("cadence"): waypoint.Cadence = streamVal("cadence") if hasStreamData("distance"): waypoint.Distance = streamVal("distance") inPause = isInTimerStop(waypoint.Timestamp) waypoint.Type = WaypointType.Regular if not inPause else WaypointType.Pause if wasInPause and not inPause: waypoint.Type = WaypointType.Resume wasInPause = inPause # We only care if it's possible to start a new lap, i.e. there are more left if currentLapIdx + 1 < len(laps_starts): if laps_starts[currentLapIdx + 1] < waypoint.Timestamp: # A new lap has started currentLapIdx += 1 lap = activity.Laps[currentLapIdx] lap.Waypoints.append(waypoint) if returnFirstLocation: return None # I guess there were no waypoints? if activity.CountTotalWaypoints(): activity.GetFlatWaypoints()[0].Type = WaypointType.Start activity.GetFlatWaypoints()[-1].Type = WaypointType.End activity.Stationary = False else: activity.Stationary = True return activity
def DownloadActivity(self, svcRecord, activity): service_id = svcRecord._id user = db.users.find_one({ 'ConnectedServices': { '$elemMatch': { 'ID': service_id, 'Service': self.ID } } }) userID = svcRecord.ExternalID oauth_token = svcRecord.Authorization.get('OAuthToken') user_access_token = svcRecord.Authorization.get('AccessToken') user_access_token_secret = svcRecord.Authorization.get( 'AccessTokenSecret') logging.info("\t Building signin for activity detail") user_tokens = { 'access_token': user_access_token, 'access_token_secret': user_access_token_secret, 'oauth_token': oauth_token } payload = "" start_date = datetime.now() - timedelta(days=1) end_date = start_date + timedelta(seconds=86400) start_date_tmstmp = str(int(start_date.timestamp())) end_date_tmstmp = str(int(end_date.timestamp())) start_date_str = start_date.strftime("%Y-%m-%d") end_date_str = end_date.strftime("%Y-%m-%d") signin_parameters = { 'upload_start_time': start_date_tmstmp, 'upload_end_time': end_date_tmstmp, } signin_info = self._request_signin('GET', self.URI_ACTIVITIES_DETAIL, user_tokens, parameters=signin_parameters) resp = requests.request("GET", signin_info['path'], data=payload, headers=signin_info['header']) if resp.status_code != 204 and resp.status_code != 200: logging.info( "\t An error occured while downloading Garmin Health activities from %s to %s " % (start_date_str, end_date_str)) json_data = resp.json() activity_id = activity.ServiceData["ActivityID"] activity_detail_id = activity_id + '-detail' if json_data: for item in json_data: if activity_detail_id == item['summaryId']: lapsdata = [] if "laps" in item: for lap in item['laps']: lapsdata.append(lap['startTimeInSeconds']) ridedata = {} lapWaypoints = [] startTimeLap = activity.StartTime endTimeLap = activity.EndTime if "samples" in item: activity.GPS = True activity.Stationary = False for pt in item['samples']: wp = Waypoint() delta = int(pt.get('clockDurationInSeconds')) dateStartPoint = int(pt.get('startTimeInSeconds')) dateStartPointDt = datetime.utcfromtimestamp( dateStartPoint) wp.Timestamp = dateStartPointDt wp.Location = Location() if "latitudeInDegree" in pt: wp.Location.Latitude = float( pt.get('latitudeInDegree')) if "longitudeInDegree" in pt: wp.Location.Longitude = float( pt.get('longitudeInDegree')) if "elevationInMeters" in pt: wp.Location.Altitude = int( pt.get('elevationInMeters')) if "totalDistanceInMeters" in pt: wp.Distance = int( pt.get('totalDistanceInMeters')) if "speedMetersPerSecond" in pt: wp.Speed = int(pt.get('speedMetersPerSecond')) if "heartRate" in pt: wp.HR = int(pt.get('heartRate')) # current sample is = to lap occur , so store current nap and build a new one if dateStartPoint in lapsdata: lap = Lap(stats=activity.Stats, startTime=startTimeLap, endTime=dateStartPointDt) lap.Waypoints = lapWaypoints activity.Laps.append(lap) # re init a new lap startTimeLap = datetime.utcfromtimestamp( dateStartPoint) lapWaypoints = [] # add occur lapWaypoints.append(wp) # build last lap if len(lapWaypoints) > 0: lap = Lap(stats=activity.Stats, startTime=startTimeLap, endTime=endTimeLap) lap.Waypoints = lapWaypoints activity.Laps.append(lap) else: activity.Laps = [ Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats) ] break return activity
def _populateActivityFromTrackData(self, activity, recordText, minimumWaypoints=False): lap = Lap() activity.Laps = [lap] ### 1ST RECORD ### # userID; # timestamp - create date?; # type? W=1st # User name; # activity name; # activity type; # another timestamp - start time of event?; # duration.00; # distance (km); # kcal; #; # max alt; # min alt; # max HR; # avg HR; ### TRACK RECORDS ### # timestamp; # type (2=start, 3=end, 0=pause, 1=resume); # latitude; # longitude; #; #; # alt; # hr; wptsWithLocation = False wptsWithNonZeroAltitude = False rows = recordText.split("\n") for row in rows: if row == "OK" or len(row) == 0: continue split = row.split(";") if split[2] == "W": # init record lap.Stats.MovingTime = ActivityStatistic(ActivityStatisticUnit.Time, value=timedelta(seconds=float(split[7])) if split[7] != "" else None) lap.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Kilometers, value=float(split[8]) if split[8] != "" else None) lap.Stats.HR = ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=float(split[14]) if split[14] != "" else None, max=float(split[13]) if split[13] != "" else None) lap.Stats.Elevation = ActivityStatistic(ActivityStatisticUnit.Meters, min=float(split[12]) if split[12] != "" else None, max=float(split[11]) if split[11] != "" else None) lap.Stats.Energy = ActivityStatistic(ActivityStatisticUnit.Kilocalories, value=float(split[12]) if split[12] != "" else None) activity.Stats.update(lap.Stats) lap.Stats = activity.Stats activity.Name = split[4] else: wp = Waypoint() if split[1] == "2": wp.Type = WaypointType.Start elif split[1] == "3": wp.Type = WaypointType.End elif split[1] == "0": wp.Type = WaypointType.Pause elif split[1] == "1": wp.Type = WaypointType.Resume else: wp.Type == WaypointType.Regular if split[0] == "": continue # no timestamp, for whatever reason wp.Timestamp = pytz.utc.localize(datetime.strptime(split[0], "%Y-%m-%d %H:%M:%S UTC")) # it's like this as opposed to %z so I know when they change things (it'll break) if split[2] != "": wp.Location = Location(float(split[2]), float(split[3]), None) if wp.Location.Longitude > 180 or wp.Location.Latitude > 90 or wp.Location.Longitude < -180 or wp.Location.Latitude < -90: raise APIExcludeActivity("Out of range lat/lng") if wp.Location.Latitude is not None and wp.Location.Latitude is not None: wptsWithLocation = True if split[6] != "": wp.Location.Altitude = float(split[6]) # why this is missing: who knows? if wp.Location.Altitude != 0: wptsWithNonZeroAltitude = True if split[7] != "": wp.HR = float(split[7]) lap.Waypoints.append(wp) if wptsWithLocation and minimumWaypoints: break lap.Waypoints = sorted(activity.Waypoints, key=lambda v: v.Timestamp) if wptsWithLocation: activity.EnsureTZ(recalculate=True) if not wptsWithNonZeroAltitude: # do this here so, should the activity run near sea level, altitude data won't be spotty for x in lap.Waypoints: # clear waypoints of altitude data if all of them were logged at 0m (invalid) if x.Location is not None: x.Location.Altitude = None else: lap.Waypoints = [] # practically speaking
def create_random_activity(svc=None, actType=ActivityType.Other, tz=False, record=None, withPauses=True, withLaps=True): ''' creates completely random activity with valid waypoints and data ''' act = TestTools.create_blank_activity(svc, actType, record=record) if tz is True: tz = pytz.timezone("America/Atikokan") act.TZ = tz elif tz is not False: act.TZ = tz if act.CountTotalWaypoints() > 0: raise ValueError("Waypoint list already populated") # this is entirely random in case the testing account already has events in it (API doesn't support delete, etc) act.StartTime = datetime(2011, 12, 13, 14, 15, 16) if tz is not False: if hasattr(tz, "localize"): act.StartTime = tz.localize(act.StartTime) else: act.StartTime = act.StartTime.replace(tzinfo=tz) act.EndTime = act.StartTime + timedelta(0, random.randint(60 * 5, 60 * 60)) # don't really need to upload 1000s of pts to test this... act.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=random.random() * 10000) act.Name = str(random.random()) paused = False waypointTime = act.StartTime backToBackPauses = False act.Laps = [] lap = Lap(startTime=act.StartTime) while waypointTime < act.EndTime: wp = Waypoint() if waypointTime == act.StartTime: wp.Type = WaypointType.Start wp.Timestamp = waypointTime wp.Location = Location(random.random() * 180 - 90, random.random() * 180 - 90, random.random() * 1000) # this is gonna be one intense activity if not (wp.HR == wp.Cadence == wp.Calories == wp.Power == wp.Temp == None): raise ValueError("Waypoint did not initialize cleanly") if svc.SupportsHR: wp.HR = float(random.randint(90, 180)) if svc.SupportsPower: wp.Power = float(random.randint(0, 1000)) if svc.SupportsCalories: wp.Calories = float(random.randint(0, 500)) if svc.SupportsCadence: wp.Cadence = float(random.randint(0, 100)) if svc.SupportsTemp: wp.Temp = float(random.randint(0, 100)) if withPauses and (random.randint(40, 50) == 42 or backToBackPauses) and not paused: # pause quite often wp.Type = WaypointType.Pause paused = True elif paused: paused = False wp.Type = WaypointType.Resume backToBackPauses = not backToBackPauses waypointTime += timedelta(0, int(random.random() + 9.5)) # 10ish seconds lap.Waypoints.append(wp) if waypointTime > act.EndTime: wp.Timestamp = act.EndTime wp.Type = WaypointType.End elif withLaps and wp.Timestamp < act.EndTime and random.randint(40, 60) == 42: # occasionally start new laps lap.EndTime = wp.Timestamp act.Laps.append(lap) lap = Lap(startTime=waypointTime) # Final lap lap.EndTime = act.EndTime act.Laps.append(lap) if act.CountTotalWaypoints() == 0: raise ValueError("No waypoints populated") act.CalculateUID() act.EnsureTZ() return act
def _process_lap_waypoints(self, activity, ridedata, lapdata): hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasDistance = "distance" in ridedata and len(ridedata["distance"]) > 0 hasVelocity = "velocity_smooth" in ridedata and len( ridedata["velocity_smooth"]) > 0 inPause = False waypointCt = len(ridedata["time"]) lapWaypoints = [] waypoinStartIndex = lapdata["start_index"] waypoinEndIndex = lapdata["end_index"] powerSum = 0 hrSum = 0 hrMax = 0 for idx in range(waypoinStartIndex, waypoinEndIndex): waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) if "latlng" in ridedata: latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if hasAltitude: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = float(ridedata["altitude"][idx]) # When pausing, Strava sends this format: # idx = 100 ; time = 1000; moving = true # idx = 101 ; time = 1001; moving = true => convert to Pause # idx = 102 ; time = 2001; moving = false => convert to Resume: (2001-1001) seconds pause # idx = 103 ; time = 2002; moving = true if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif idx < waypointCt - 2 and ridedata["moving"][idx + 1] and inPause: waypoint.Type = WaypointType.Resume inPause = False elif idx < waypointCt - 2 and not ridedata["moving"][ idx + 1] and not inPause: waypoint.Type = WaypointType.Pause inPause = True if hasHR: waypoint.HR = ridedata["heartrate"][idx] hrSum += waypoint.HR if waypoint.HR else 0 hrMax = waypoint.HR if waypoint.HR > hrMax else hrMax if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] powerSum += waypoint.Power if waypoint.Power else 0 if hasVelocity: waypoint.Speed = ridedata["velocity_smooth"][idx] if hasDistance: waypoint.Distance = ridedata["distance"][idx] lapWaypoints.append(waypoint) pointsCount = len(lapWaypoints) stats = ActivityStatistics() stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=lapdata["distance"]) if "max_speed" in lapdata or "average_speed" in lapdata: stats.Speed = ActivityStatistic( ActivityStatisticUnit.MetersPerSecond, avg=lapdata["average_speed"] if "average_speed" in lapdata else None, max=lapdata["max_speed"] if "max_speed" in lapdata else None) stats.MovingTime = ActivityStatistic( ActivityStatisticUnit.Seconds, value=lapdata["moving_time"] if "moving_time" in lapdata and lapdata["moving_time"] > 0 else None) if "average_cadence" in lapdata: stats.Cadence.update( ActivityStatistic(ActivityStatisticUnit.RevolutionsPerMinute, avg=lapdata["average_cadence"])) # Activity could have laps with no trackpoints if pointsCount > 0: if hasHR: stats.HR.update( ActivityStatistic(ActivityStatisticUnit.BeatsPerMinute, avg=hrSum / pointsCount, max=hrMax)) if hasPower: stats.Power = ActivityStatistic(ActivityStatisticUnit.Watts, avg=powerSum / pointsCount) return lapWaypoints, stats
def DownloadActivity(self, serviceRecord, activity): # First, download the summary stats and lap stats self._downloadActivitySummary(serviceRecord, activity) if len(activity.Laps) == 1: activity.Stats = activity.Laps[ 0].Stats # They must be identical to pass the verification if activity.Stationary: # Nothing else to download return activity # https://connect.garmin.com/proxy/activity-service-1.3/json/activityDetails/#### activityID = activity.ServiceData["ActivityID"] res = self._request_with_reauth( serviceRecord, lambda session: session. get("https://connect.garmin.com/modern/proxy/activity-service-1.3/json/activityDetails/" + str(activityID) + "?maxSize=999999999")) try: raw_data = res.json( )["com.garmin.activity.details.json.ActivityDetails"] except ValueError: raise APIException("Activity data parse error for %s: %s" % (res.status_code, res.text)) if "measurements" 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["measurements"]: key = measurement["key"] if key in attrs_map: if attrs_map[key]["to_units"]: attrs_map[key]["from_units"] = self._unitMap[ measurement["unit"]] 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["metrics"]: 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 _populateActivityFromTrackData(self, activity, recordText, minimumWaypoints=False): activity.Waypoints = [] ### 1ST RECORD ### # userID; # timestamp - create date?; # type? W=1st # User name; # activity name; # activity type; # another timestamp - start time of event?; # duration.00; # distance (km); # kcal; #; # max alt; # min alt; # max HR; # avg HR; ### TRACK RECORDS ### # timestamp; # type (2=start, 3=end, 0=pause, 1=resume); # latitude; # longitude; #; #; # alt; # hr; wptsWithLocation = False wptsWithNonZeroAltitude = False rows = recordText.split("\n") for row in rows: if row == "OK" or len(row) == 0: continue split = row.split(";") if split[2] == "W": # init record activity.Distance = float( split[8]) * 1000 if split[8] != "" else None activity.Name = split[4] else: wp = Waypoint() if split[1] == "2": wp.Type = WaypointType.Start elif split[1] == "3": wp.Type = WaypointType.End elif split[1] == "0": wp.Type = WaypointType.Pause elif split[1] == "1": wp.Type = WaypointType.Resume else: wp.Type == WaypointType.Regular if split[0] == "": continue # no timestamp, for whatever reason wp.Timestamp = pytz.utc.localize( datetime.strptime(split[0], "%Y-%m-%d %H:%M:%S UTC") ) # it's like this as opposed to %z so I know when they change things (it'll break) if split[2] != "": wp.Location = Location(float(split[2]), float(split[3]), None) if wp.Location.Longitude > 180 or wp.Location.Latitude > 90 or wp.Location.Longitude < -180 or wp.Location.Latitude < -90: raise APIExcludeActivity("Out of range lat/lng") if wp.Location.Latitude is not None and wp.Location.Latitude is not None: wptsWithLocation = True if split[6] != "": wp.Location.Altitude = float( split[6]) # why this is missing: who knows? if wp.Location.Altitude != 0: wptsWithNonZeroAltitude = True if split[7] != "": wp.HR = float(split[7]) activity.Waypoints.append(wp) if wptsWithLocation and minimumWaypoints: break activity.Waypoints = sorted(activity.Waypoints, key=lambda v: v.Timestamp) if wptsWithLocation: activity.EnsureTZ() if not wptsWithNonZeroAltitude: # do this here so, should the activity run near sea level, altitude data won't be spotty for x in activity.Waypoints: # clear waypoints of altitude data if all of them were logged at 0m (invalid) if x.Location is not None: x.Location.Altitude = None else: activity.Waypoints = [] # practically speaking
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] extID = svcRecord.ExternalID url = self.SingletrackerDomain + "getRideData" payload = {"userId": extID, "rideId": activityID} headers = { 'content-type': "application/json", 'cache-control': "no-cache", } streamdata = requests.post(url, data=json.dumps(payload), headers=headers) if streamdata.status_code == 500: raise APIException("Internal server error") if streamdata.status_code == 403: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) if streamdata.status_code == 200: # Ok try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") ridedata = {} lap = Lap( stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime ) # Singletracker doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] wayPointExist = False for stream in streamdata: waypoint = Waypoint( dateutil.parser.parse(stream["time"], ignoretz=True)) if "latitude" in stream: if "longitude" in stream: latitude = stream["latitude"] longitude = stream["longitude"] waypoint.Location = Location(latitude, longitude, None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if "elevation" in stream: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = stream["elevation"] if "distance" in stream: waypoint.Distance = stream["distance"] if "speed" in stream: waypoint.Speed = stream["speed"] waypoint.Type = WaypointType.Regular lap.Waypoints.append(waypoint) return activity
def _downloadActivity(self, serviceRecord, activity, returnFirstLocation=False): activityURI = [x["ActivityURI"] for x in activity.UploadedTo if x["Connection"] == serviceRecord][0] cookies = self._get_cookies(record=serviceRecord) activityData = requests.get(activityURI, cookies=cookies) activityData = activityData.json() if "location" not in activityData: raise APIExcludeActivity("No points") timerStops = [] if "timer_stops" in activityData: for stop in activityData["timer_stops"]: timerStops.append([dateutil.parser.parse(stop[0]), dateutil.parser.parse(stop[1])]) def isInTimerStop(timestamp): for stop in timerStops: if timestamp >= stop[0] and timestamp < stop[1]: return True if timestamp >= stop[1]: return False return False laps = [] if "laps" in activityData: for lap in activityData["laps"]: laps.append(dateutil.parser.parse(lap["start_time"])) # Collate the individual streams into our waypoints. # Everything is resampled by nearest-neighbour to the rate of the location stream. parallel_indices = {} parallel_stream_lengths = {} for secondary_stream in ["elevation", "heartrate", "power", "cadence"]: if secondary_stream in activityData: parallel_indices[secondary_stream] = 0 parallel_stream_lengths[secondary_stream] = len(activityData[secondary_stream]) activity.Waypoints = [] wasInPause = False currentLapIdx = 0 for idx in range(0, len(activityData["location"]), 2): # Pick the nearest indices in the parallel streams for parallel_stream, parallel_index in parallel_indices.items(): if parallel_index + 2 == parallel_stream_lengths[parallel_stream]: continue # We're at the end of this stream # Is the next datapoint a better choice than the current? if abs(activityData["location"][idx] - activityData[parallel_stream][parallel_index + 2]) < abs(activityData["location"][idx] - activityData[parallel_stream][parallel_index]): parallel_indices[parallel_stream] += 2 waypoint = Waypoint(activity.StartTime + timedelta(0, activityData["location"][idx])) waypoint.Location = Location(activityData["location"][idx+1][0], activityData["location"][idx+1][1], None) if "elevation" in parallel_indices: waypoint.Location.Altitude = activityData["elevation"][parallel_indices["elevation"]+1] if returnFirstLocation: return waypoint.Location if "heartrate" in parallel_indices: waypoint.HR = activityData["heartrate"][parallel_indices["heartrate"]+1] if "power" in parallel_indices: waypoint.Power = activityData["power"][parallel_indices["power"]+1] if "cadence" in parallel_indices: waypoint.Cadence = activityData["cadence"][parallel_indices["cadence"]+1] inPause = isInTimerStop(waypoint.Timestamp) waypoint.Type = WaypointType.Regular if not inPause else WaypointType.Pause if wasInPause and not inPause: waypoint.Type = WaypointType.Resume wasInPause = inPause # We only care if it's possible to start a new lap, i.e. there are more left if currentLapIdx + 1 < len(laps): if laps[currentLapIdx + 1] < waypoint.Timestamp: # A new lap has started waypoint.Type = WaypointType.Lap currentLapIdx += 1 activity.Waypoints.append(waypoint) if returnFirstLocation: return None # I guess there were no waypoints? activity.Waypoints[0].Type = WaypointType.Start activity.Waypoints[-1].Type = WaypointType.End return activity
def DownloadActivity(self, svcRecord, activity): if activity.ServiceData[ "Manual"]: # I should really add a param to DownloadActivity for this value as opposed to constantly doing this # We've got as much information as we're going to get - we need to copy it into a Lap though. activity.Laps = [ Lap(startTime=activity.StartTime, endTime=activity.EndTime, stats=activity.Stats) ] return activity activityID = activity.ServiceData["ActivityID"] self._globalRateLimit() streamdata = requests.get( "https://www.strava.com/api/v3/activities/" + str(activityID) + "/streams/time,altitude,heartrate,cadence,watts,temp,moving,latlng,distance,velocity_smooth", headers=self._apiHeaders(svcRecord)) if streamdata.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") if "message" in streamdata and streamdata[ "message"] == "Record Not Found": raise APIException("Could not find activity") ridedata = {} for stream in streamdata: ridedata[stream["type"]] = stream["data"] lap = Lap( stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime ) # Strava doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] hasHR = "heartrate" in ridedata and len(ridedata["heartrate"]) > 0 hasCadence = "cadence" in ridedata and len(ridedata["cadence"]) > 0 hasTemp = "temp" in ridedata and len(ridedata["temp"]) > 0 hasPower = ("watts" in ridedata and len(ridedata["watts"]) > 0) hasAltitude = "altitude" in ridedata and len(ridedata["altitude"]) > 0 hasDistance = "distance" in ridedata and len(ridedata["distance"]) > 0 hasVelocity = "velocity_smooth" in ridedata and len( ridedata["velocity_smooth"]) > 0 if "error" in ridedata: raise APIException("Strava error " + ridedata["error"]) inPause = False waypointCt = len(ridedata["time"]) for idx in range(0, waypointCt - 1): waypoint = Waypoint(activity.StartTime + timedelta(0, ridedata["time"][idx])) if "latlng" in ridedata: latlng = ridedata["latlng"][idx] waypoint.Location = Location(latlng[0], latlng[1], None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if hasAltitude: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = float(ridedata["altitude"][idx]) # When pausing, Strava sends this format: # idx = 100 ; time = 1000; moving = true # idx = 101 ; time = 1001; moving = true => convert to Pause # idx = 102 ; time = 2001; moving = false => convert to Resume: (2001-1001) seconds pause # idx = 103 ; time = 2002; moving = true if idx == 0: waypoint.Type = WaypointType.Start elif idx == waypointCt - 2: waypoint.Type = WaypointType.End elif idx < waypointCt - 2 and ridedata["moving"][idx + 1] and inPause: waypoint.Type = WaypointType.Resume inPause = False elif idx < waypointCt - 2 and not ridedata["moving"][ idx + 1] and not inPause: waypoint.Type = WaypointType.Pause inPause = True if hasHR: waypoint.HR = ridedata["heartrate"][idx] if hasCadence: waypoint.Cadence = ridedata["cadence"][idx] if hasTemp: waypoint.Temp = ridedata["temp"][idx] if hasPower: waypoint.Power = ridedata["watts"][idx] if hasVelocity: waypoint.Speed = ridedata["velocity_smooth"][idx] if hasDistance: waypoint.Distance = ridedata["distance"][idx] lap.Waypoints.append(waypoint) return activity
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] extID = svcRecord.ExternalID url = self.SingletrackerDomain + "getRideData" payload = {"userId": extID, "rideId": activityID} headers = { 'content-type': "application/json", 'cache-control': "no-cache", } streamdata = requests.post(url, data=json.dumps(payload), headers=headers) if streamdata.status_code == 500: raise APIException("Internal server error") if streamdata.status_code == 403: raise APIException("No authorization to download activity", block=True, user_exception=UserException(UserExceptionType.Authorization, intervention_required=True)) if streamdata.status_code != 200: raise APIException("Unknown Singletracker response %d %s" % (streamdata.status_code, streamdata.text)) try: streamdata = streamdata.json() except: raise APIException("Stream data returned is not JSON") ridedata = {} lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) # Singletracker doesn't support laps, but we need somewhere to put the waypoints. activity.Laps = [lap] lap.Waypoints = [] wayPointExist = False for stream in streamdata: waypoint = Waypoint(dateutil.parser.parse(stream["time"], ignoretz=True)) if "latitude" in stream: if "longitude" in stream: latitude = stream["latitude"] longitude = stream["longitude"] waypoint.Location = Location(latitude, longitude, None) if waypoint.Location.Longitude == 0 and waypoint.Location.Latitude == 0: waypoint.Location.Longitude = None waypoint.Location.Latitude = None if "elevation" in stream: if not waypoint.Location: waypoint.Location = Location(None, None, None) waypoint.Location.Altitude = stream["elevation"] if "distance" in stream: waypoint.Distance = stream["distance"] if "speed" in stream: waypoint.Speed = stream["speed"] waypoint.Type = WaypointType.Regular lap.Waypoints.append(waypoint) return activity
def create_random_activity(svc=None, actType=ActivityType.Other, tz=False, record=None, withPauses=True, withLaps=True): ''' creates completely random activity with valid waypoints and data ''' act = TestTools.create_blank_activity(svc, actType, record=record) if tz is True: tz = pytz.timezone("America/Atikokan") act.TZ = tz elif tz is not False: act.TZ = tz if act.CountTotalWaypoints() > 0: raise ValueError("Waypoint list already populated") # this is entirely random in case the testing account already has events in it (API doesn't support delete, etc) act.StartTime = datetime(2011, 12, 13, 14, 15, 16) if tz is not False: if hasattr(tz, "localize"): act.StartTime = tz.localize(act.StartTime) else: act.StartTime = act.StartTime.replace(tzinfo=tz) act.EndTime = act.StartTime + timedelta( 0, random.randint(60 * 5, 60 * 60) ) # don't really need to upload 1000s of pts to test this... act.Stats.Distance = ActivityStatistic(ActivityStatisticUnit.Meters, value=random.random() * 10000) act.Name = str(random.random()) paused = False waypointTime = act.StartTime backToBackPauses = False act.Laps = [] lap = Lap(startTime=act.StartTime) while waypointTime < act.EndTime: wp = Waypoint() if waypointTime == act.StartTime: wp.Type = WaypointType.Start wp.Timestamp = waypointTime wp.Location = Location( random.random() * 180 - 90, random.random() * 180 - 90, random.random() * 1000) # this is gonna be one intense activity if not (wp.HR == wp.Cadence == wp.Calories == wp.Power == wp.Temp == None): raise ValueError("Waypoint did not initialize cleanly") if svc.SupportsHR: wp.HR = float(random.randint(90, 180)) if svc.SupportsPower: wp.Power = float(random.randint(0, 1000)) if svc.SupportsCalories: wp.Calories = float(random.randint(0, 500)) if svc.SupportsCadence: wp.Cadence = float(random.randint(0, 100)) if svc.SupportsTemp: wp.Temp = float(random.randint(0, 100)) if withPauses and (random.randint(40, 50) == 42 or backToBackPauses ) and not paused: # pause quite often wp.Type = WaypointType.Pause paused = True elif paused: paused = False wp.Type = WaypointType.Resume backToBackPauses = not backToBackPauses waypointTime += timedelta(0, int(random.random() + 9.5)) # 10ish seconds lap.Waypoints.append(wp) if waypointTime > act.EndTime: wp.Timestamp = act.EndTime wp.Type = WaypointType.End elif withLaps and wp.Timestamp < act.EndTime and random.randint( 40, 60) == 42: # occasionally start new laps lap.EndTime = wp.Timestamp act.Laps.append(lap) lap = Lap(startTime=waypointTime) # Final lap lap.EndTime = act.EndTime act.Laps.append(lap) if act.CountTotalWaypoints() == 0: raise ValueError("No waypoints populated") act.CalculateUID() act.EnsureTZ() return act
def DownloadActivity(self, svcRecord, activity): activityID = activity.ServiceData["ActivityID"] logging.info("\t\t DC LOADING : " + str(activityID)) headers = self._getAuthHeaders(svcRecord) self._rate_limit() resp = requests.get(DECATHLON_API_BASE_URL + "/activity/" + activityID + "/fullactivity.xml", headers=headers) if resp.status_code == 401: raise APIException("No authorization to download activity", block=True, user_exception=UserException( UserExceptionType.Authorization, intervention_required=True)) try: root = xml.fromstring(resp.content) except: raise APIException( "Stream data returned from Decathlon is not XML") activity.GPS = False activity.Stationary = True #work on date startdate = root.find('.//STARTDATE').text timezone = root.find('.//TIMEZONE').text datebase = parse(startdate + timezone) ridedata = {} ridedataindex = [] for pt in root.iter('LOCATION'): delta = int(pt.get('elapsed_time')) ridedataindex.append(delta) ridedata[delta] = {} if activityID == 'eu2132ac60d9a40a1d9a': logging.info('========time : ' + str(delta)) logging.info('========lat : ' + str(float(pt.find('LATITUDE').text[:8]))) ridedata[delta]['LATITUDE'] = float(pt.find('LATITUDE').text[:8]) ridedata[delta]['LONGITUDE'] = float(pt.find('LONGITUDE').text[:8]) ridedata[delta]['ELEVATION'] = int(pt.find('ELEVATION').text[:8]) if len(ridedata) > 0: activity.GPS = True activity.Stationary = False for measure in root.iter('MEASURE'): delta = int(measure.get('elapsed_time')) if delta not in ridedataindex: ridedataindex.append(delta) ridedata[delta] = {} for measureValue in measure.iter('VALUE'): if measureValue.get('id') == "1": ridedata[delta]['HR'] = int(measureValue.text) if measureValue.get('id') == "6": ridedata[delta]['SPEED'] = int(measureValue.text) if measureValue.get('id') == "5": ridedata[delta]['DISTANCE'] = int(measureValue.text) if measureValue.get('id') == "20": ridedata[delta]['LAP'] = int(measureValue.text) ridedataindex.sort() if len(ridedata) == 0: lap = Lap(stats=activity.Stats, startTime=activity.StartTime, endTime=activity.EndTime) activity.Laps = [lap] else: lapWaypoints = [] startTimeLap = activity.StartTime for elapsedTime in ridedataindex: rd = ridedata[elapsedTime] wp = Waypoint() delta = elapsedTime formatedDate = datebase + timedelta(seconds=delta) wp.Timestamp = formatedDate #self._parseDate(formatedDate.isoformat()) if 'LATITUDE' in rd: wp.Location = Location() wp.Location.Latitude = rd['LATITUDE'] wp.Location.Longitude = rd['LONGITUDE'] wp.Location.Altitude = rd['ELEVATION'] if 'HR' in rd: wp.HR = rd['HR'] if 'SPEED' in rd: wp.Speed = rd['SPEED'] / 3600 if 'DISTANCE' in rd: wp.Distance = rd['DISTANCE'] lapWaypoints.append(wp) if "LAP" in rd: #build the lap lap = Lap(stats=activity.Stats, startTime=startTimeLap, endTime=formatedDate) lap.Waypoints = lapWaypoints activity.Laps.append(lap) # re init a new lap startTimeLap = formatedDate lapWaypoints = [] #build last lap if len(lapWaypoints) > 0: lap = Lap(stats=activity.Stats, startTime=startTimeLap, endTime=formatedDate) lap.Waypoints = lapWaypoints activity.Laps.append(lap) return activity