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 _downloadActivitySummary(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] summary_resp = self._request_with_reauth(lambda session: session.get("https://connect.garmin.com/modern/proxy/activity-service/activity/" + str(activityID)), serviceRecord) try: summary_data = summary_resp.json() except ValueError: raise APIException("Failure downloading activity summary %s:%s" % (summary_resp.status_code, summary_resp.text)) stat_map = {} def mapStat(gcKey, statKey, type, units): stat_map[gcKey] = { "key": statKey, "attr": type, "units": units } def applyStats(gc_dict, stats_obj): for gc_key, stat in stat_map.items(): if gc_key in gc_dict: value = float(gc_dict[gc_key]) if math.isinf(value): continue # GC returns the minimum speed as "-Infinity" instead of 0 some times :S getattr(stats_obj, stat["key"]).update(ActivityStatistic(stat["units"], **({stat["attr"]: value}))) mapStat("movingDuration", "MovingTime", "value", ActivityStatisticUnit.Seconds) mapStat("duration", "TimerTime", "value", ActivityStatisticUnit.Seconds) mapStat("distance", "Distance", "value", ActivityStatisticUnit.Meters) mapStat("maxSpeed", "Speed", "max", ActivityStatisticUnit.MetersPerSecond) mapStat("averageSpeed", "Speed", "avg", ActivityStatisticUnit.MetersPerSecond) mapStat("calories", "Energy", "value", ActivityStatisticUnit.Kilocalories) mapStat("maxHR", "HR", "max", ActivityStatisticUnit.BeatsPerMinute) mapStat("averageHR", "HR", "avg", ActivityStatisticUnit.BeatsPerMinute) mapStat("minElevation", "Elevation", "min", ActivityStatisticUnit.Meters) mapStat("maxElevation", "Elevation", "max", ActivityStatisticUnit.Meters) mapStat("elevationGain", "Elevation", "gain", ActivityStatisticUnit.Meters) mapStat("elevationLoss", "Elevation", "loss", ActivityStatisticUnit.Meters) mapStat("averageBikeCadence", "Cadence", "avg", ActivityStatisticUnit.RevolutionsPerMinute) mapStat("averageCadence", "Cadence", "avg", ActivityStatisticUnit.StepsPerMinute) applyStats(summary_data["summaryDTO"], activity.Stats) laps_resp = self._request_with_reauth(lambda session: session.get("https://connect.garmin.com/modern/proxy/activity-service/activity/%s/splits" % str(activityID)), serviceRecord) try: laps_data = laps_resp.json() except ValueError: raise APIException("Failure downloading activity laps summary %s:%s" % (laps_resp.status_code, laps_resp.text)) for lap_data in laps_data["lapDTOs"]: lap = Lap() if "startTimeGMT" in lap_data: lap.StartTime = pytz.utc.localize(datetime.strptime(lap_data["startTimeGMT"], "%Y-%m-%dT%H:%M:%S.0")) elapsed_duration = None if "elapsedDuration" in lap_data: elapsed_duration = timedelta(seconds=round(float(lap_data["elapsedDuration"]))) elif "duration" in lap_data: elapsed_duration = timedelta(seconds=round(float(lap_data["duration"]))) if lap.StartTime and elapsed_duration: # Always recalculate end time based on duration, if we have the start time lap.EndTime = lap.StartTime + elapsed_duration if not lap.StartTime and lap.EndTime and elapsed_duration: # Sometimes calculate start time based on duration lap.StartTime = lap.EndTime - elapsed_duration if not lap.StartTime or not lap.EndTime: # Garmin Connect is weird. raise APIExcludeActivity("Activity lap has no BeginTimestamp or EndTimestamp", user_exception=UserException(UserExceptionType.Corrupt)) applyStats(lap_data, lap.Stats) activity.Laps.append(lap) # In Garmin Land, max can be smaller than min for this field :S if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max: activity.Stats.Power.Min = None
def _downloadActivitySummary(self, serviceRecord, activity): 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/activity/" + str(activityID))) try: raw_data = res.json() except ValueError: raise APIException("Failure downloading activity summary %s:%s" % (res.status_code, res.text)) stat_map = {} def mapStat(gcKey, statKey, type): stat_map[gcKey] = {"key": statKey, "attr": type} def applyStats(gc_dict, stats_obj): for gc_key, stat in stat_map.items(): if gc_key in gc_dict: value = float(gc_dict[gc_key]["value"]) units = self._unitMap[gc_dict[gc_key]["uom"]] if math.isinf(value): continue # GC returns the minimum speed as "-Infinity" instead of 0 some times :S getattr(stats_obj, stat["key"]).update( ActivityStatistic(units, **({ stat["attr"]: value }))) mapStat("SumMovingDuration", "MovingTime", "value") mapStat("SumDuration", "TimerTime", "value") mapStat("SumDistance", "Distance", "value") mapStat("MinSpeed", "Speed", "min") mapStat("MaxSpeed", "Speed", "max") mapStat("WeightedMeanSpeed", "Speed", "avg") mapStat("MinAirTemperature", "Temperature", "min") mapStat("MaxAirTemperature", "Temperature", "max") mapStat("WeightedMeanAirTemperature", "Temperature", "avg") mapStat("SumEnergy", "Energy", "value") mapStat("MaxHeartRate", "HR", "max") mapStat("WeightedMeanHeartRate", "HR", "avg") mapStat("MaxDoubleCadence", "RunCadence", "max") mapStat("WeightedMeanDoubleCadence", "RunCadence", "avg") mapStat("MaxBikeCadence", "Cadence", "max") mapStat("WeightedMeanBikeCadence", "Cadence", "avg") mapStat("MinPower", "Power", "min") mapStat("MaxPower", "Power", "max") mapStat("WeightedMeanPower", "Power", "avg") mapStat("MinElevation", "Elevation", "min") mapStat("MaxElevation", "Elevation", "max") mapStat("GainElevation", "Elevation", "gain") mapStat("LossElevation", "Elevation", "loss") applyStats(raw_data["activity"]["activitySummary"], activity.Stats) for lap_data in raw_data["activity"]["totalLaps"]["lapSummaryList"]: lap = Lap() if "BeginTimestamp" in lap_data: lap.StartTime = pytz.utc.localize( datetime.utcfromtimestamp( float(lap_data["BeginTimestamp"]["value"]) / 1000)) if "EndTimestamp" in lap_data: lap.EndTime = pytz.utc.localize( datetime.utcfromtimestamp( float(lap_data["EndTimestamp"]["value"]) / 1000)) elapsed_duration = None if "SumElapsedDuration" in lap_data: elapsed_duration = timedelta(seconds=round( float(lap_data["SumElapsedDuration"]["value"]))) elif "SumDuration" in lap_data: elapsed_duration = timedelta( seconds=round(float(lap_data["SumDuration"]["value"]))) if lap.StartTime and elapsed_duration: # Always recalculate end time based on duration, if we have the start time lap.EndTime = lap.StartTime + elapsed_duration if not lap.StartTime and lap.EndTime and elapsed_duration: # Sometimes calculate start time based on duration lap.StartTime = lap.EndTime - elapsed_duration if not lap.StartTime or not lap.EndTime: # Garmin Connect is weird. raise APIExcludeActivity( "Activity lap has no BeginTimestamp or EndTimestamp", user_exception=UserException(UserExceptionType.Corrupt)) applyStats(lap_data, lap.Stats) activity.Laps.append(lap) # In Garmin Land, max can be smaller than min for this field :S if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max: activity.Stats.Power.Min = None
def _downloadActivitySummary(self, serviceRecord, activity): 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/activity/" + str(activityID)) try: raw_data = res.json() except ValueError: raise APIException("Failure downloading activity summary %s:%s" % (res.status_code, res.text)) stat_map = {} def mapStat(gcKey, statKey, type): stat_map[gcKey] = { "key": statKey, "attr": type } def applyStats(gc_dict, stats_obj): for gc_key, stat in stat_map.items(): if gc_key in gc_dict: value = float(gc_dict[gc_key]["value"]) units = self._unitMap[gc_dict[gc_key]["uom"]] if math.isinf(value): continue # GC returns the minimum speed as "-Infinity" instead of 0 some times :S getattr(stats_obj, stat["key"]).update(ActivityStatistic(units, **({stat["attr"]: value}))) mapStat("SumMovingDuration", "MovingTime", "value") mapStat("SumDuration", "TimerTime", "value") mapStat("SumDistance", "Distance", "value") mapStat("MinSpeed", "Speed", "min") mapStat("MaxSpeed", "Speed", "max") mapStat("WeightedMeanSpeed", "Speed", "avg") mapStat("MinAirTemperature", "Temperature", "min") mapStat("MaxAirTemperature", "Temperature", "max") mapStat("WeightedMeanAirTemperature", "Temperature", "avg") mapStat("SumEnergy", "Energy", "value") mapStat("MaxHeartRate", "HR", "max") mapStat("WeightedMeanHeartRate", "HR", "avg") mapStat("MaxDoubleCadence", "RunCadence", "max") mapStat("WeightedMeanDoubleCadence", "RunCadence", "avg") mapStat("MaxBikeCadence", "Cadence", "max") mapStat("WeightedMeanBikeCadence", "Cadence", "avg") mapStat("MinPower", "Power", "min") mapStat("MaxPower", "Power", "max") mapStat("WeightedMeanPower", "Power", "avg") mapStat("MinElevation", "Elevation", "min") mapStat("MaxElevation", "Elevation", "max") mapStat("GainElevation", "Elevation", "gain") mapStat("LossElevation", "Elevation", "loss") applyStats(raw_data["activity"]["activitySummary"], activity.Stats) for lap_data in raw_data["activity"]["totalLaps"]["lapSummaryList"]: lap = Lap() if "BeginTimestamp" in lap_data: lap.StartTime = pytz.utc.localize(datetime.utcfromtimestamp(float(lap_data["BeginTimestamp"]["value"]) / 1000)) if "EndTimestamp" in lap_data: lap.EndTime = pytz.utc.localize(datetime.utcfromtimestamp(float(lap_data["EndTimestamp"]["value"]) / 1000)) elapsed_duration = None if "SumElapsedDuration" in lap_data: elapsed_duration = timedelta(seconds=round(float(lap_data["SumElapsedDuration"]["value"]))) elif "SumDuration" in lap_data: elapsed_duration = timedelta(seconds=round(float(lap_data["SumDuration"]["value"]))) if lap.StartTime and elapsed_duration: # Always recalculate end time based on duration, if we have the start time lap.EndTime = lap.StartTime + elapsed_duration if not lap.StartTime and lap.EndTime and elapsed_duration: # Sometimes calculate start time based on duration lap.StartTime = lap.EndTime - elapsed_duration if not lap.StartTime or not lap.EndTime: # Garmin Connect is weird. raise APIExcludeActivity("Activity lap has no BeginTimestamp or EndTimestamp", userException=UserException(UserExceptionType.Corrupt)) applyStats(lap_data, lap.Stats) activity.Laps.append(lap) # In Garmin Land, max can be smaller than min for this field :S if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max: activity.Stats.Power.Min = None
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, 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, 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 _downloadActivitySummary(self, serviceRecord, activity): activityID = activity.ServiceData["ActivityID"] cookies = self._get_cookies(record=serviceRecord) self._rate_limit() res = requests.get("https://connect.garmin.com/proxy/activity-service-1.3/json/activity/" + str(activityID), cookies=cookies) try: raw_data = res.json() except ValueError: raise APIException("Failure downloading activity summary %s:%s" % (res.status_code, res.text)) stat_map = {} def mapStat(gcKey, statKey, type): stat_map[gcKey] = { "key": statKey, "attr": type } def applyStats(gc_dict, stats_obj): for gc_key, stat in stat_map.items(): if gc_key in gc_dict: value = float(gc_dict[gc_key]["value"]) units = self._unitMap[gc_dict[gc_key]["uom"]] if math.isinf(value): continue # GC returns the minimum speed as "-Infinity" instead of 0 some times :S getattr(stats_obj, stat["key"]).update(ActivityStatistic(units, **({stat["attr"]: value}))) mapStat("SumMovingDuration", "MovingTime", "value") mapStat("SumDuration", "TimerTime", "value") mapStat("SumDistance", "Distance", "value") mapStat("MinSpeed", "Speed", "min") mapStat("MaxSpeed", "Speed", "max") mapStat("WeightedMeanSpeed", "Speed", "avg") mapStat("MinAirTemperature", "Temperature", "min") mapStat("MaxAirTemperature", "Temperature", "max") mapStat("WeightedMeanAirTemperature", "Temperature", "avg") mapStat("SumEnergy", "Energy", "value") mapStat("MaxHeartRate", "HR", "max") mapStat("WeightedMeanHeartRate", "HR", "avg") mapStat("MaxDoubleCadence", "RunCadence", "max") mapStat("WeightedMeanDoubleCadence", "RunCadence", "avg") mapStat("MaxBikeCadence", "Cadence", "max") mapStat("WeightedMeanBikeCadence", "Cadence", "avg") mapStat("MinPower", "Power", "min") mapStat("MaxPower", "Power", "max") mapStat("WeightedMeanPower", "Power", "avg") mapStat("MinElevation", "Elevation", "min") mapStat("MaxElevation", "Elevation", "max") mapStat("GainElevation", "Elevation", "gain") mapStat("LossElevation", "Elevation", "loss") applyStats(raw_data["activity"]["activitySummary"], activity.Stats) for lap_data in raw_data["activity"]["totalLaps"]["lapSummaryList"]: lap = Lap() lap.StartTime = pytz.utc.localize(datetime.utcfromtimestamp(float(lap_data["BeginTimestamp"]["value"]) / 1000)) lap.EndTime = pytz.utc.localize(datetime.utcfromtimestamp(float(lap_data["EndTimestamp"]["value"]) / 1000)) applyStats(lap_data, lap.Stats) activity.Laps.append(lap) # In Garmin Land, max can be smaller than min for this field :S if activity.Stats.Power.Max is not None and activity.Stats.Power.Min is not None and activity.Stats.Power.Min > activity.Stats.Power.Max: activity.Stats.Power.Min = None
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(pytz.all_timezones[random.randint(0, len(pytz.all_timezones) - 1)]) 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(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 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