Public/Get-GarminActivity.ps1
|
<#
.SYNOPSIS Fetches Garmin Connect activities and summarizes them by year, month, week or activity type. .DESCRIPTION `Get-GarminActivity` calls the Garmin Connect API endpoint `/activitylist-service/activities/search/activities` directly from PowerShell. Authentication uses the DI OAuth2 token store written by `Get-GarminToken` (`gctoken.json`). An access token that expires within 15 minutes is refreshed automatically and saved back. Without parameters, the last 10 running activities are returned. `-ActivityType` selects another type (parent types include their sub types, e.g. `running` also returns `trail_running` and `treadmill_running`; `all` returns every type). With `-StartDate` and/or `-EndDate`, all activities in the date range are returned; `-Last` limits them. The activity objects returned by the API are extended with calculated properties in metric units, or in imperial units with `-Miles`: - StartTime: Local start time as [datetime]. - TypeKey: Activity type key (for example `trail_running`). - TotalDistance: Distance in km (mi with `-Miles`), rounded to two decimals. - TotalTime: Duration as hh:mm:ss. - AvgPace: Average pace per km (per mi with `-Miles`). - AvgSpeed: Average speed in km/h (mph with `-Miles`). - ElevGain: Elevation gain in m (ft with `-Miles`). - Unit: 'km' or 'mi'. The console shows these columns by default; all API properties remain available (`| Select-Object *`). With `-GroupBy Year`, `Month`, `Week` (ISO 8601) or `ActivityType`, summary objects are returned instead. The first row always has `Year = 'Total'` (or `TypeKey = 'Total'`). Summary object properties: - Year: 'Total' or the four-digit (ISO) year. - Month/Week: Two-digit month or ISO week (only with `-GroupBy Month` / `Week`). - TypeKey: Activity type key (only with `-GroupBy ActivityType`). - Activities: Number of activities. - Distance: Distance in km (mi with `-Miles`). - Time: Duration as h:mm:ss. - ElevGain: Elevation gain in m (ft with `-Miles`). - Unit: 'km' or 'mi'. With `-CustomSelection`, the Where-Object filter and the Select-Object properties are read from `$env:USERPROFILE\GCCare\Config\GCCare.json` (section `GarminConnectApi > CustomSelection > Activity > <ActivityType>`, fallback `Default`). The calculated properties can be used there as well. `-Filter` and `-Property` override these values for one call, `-SaveCustomSelection` stores them under the activity type (`Default` for `-ActivityType all`). Together with `-GroupBy`, only the filter is applied before grouping; the properties are ignored. .PARAMETER ActivityType Activity type key, e.g. `running`, `cycling`, `hiking`, `walking`, `swimming`, `strength_training`, or `all`. Default: `running`. Parent types include their sub types; sub types like `yoga` or `trail_running` return only that sub type. The valid keys are read from `/activity-service/activity/activityTypes` (once per session). .PARAMETER Last Maximum number of activities (newest first). Default: 10, or all activities in the date range when `-StartDate` or `-EndDate` is used. .PARAMETER StartDate First day of the date range. .PARAMETER EndDate Last day of the date range. Default with `-StartDate`: today. .PARAMETER Miles Shows distance in miles, pace per mile, speed in mph and elevation in feet. .PARAMETER GroupBy Optional summary. Valid values: `Year`, `Month`, `Week`, `ActivityType`. .PARAMETER CustomSelection Applies the Where-Object filter and the Select-Object properties stored in `GCCare.json` for the activity type. .PARAMETER Filter Where-Object filter for this call, e.g. `{ $_.TotalDistance -ge 10 }`. Overrides the value from `GCCare.json` and implies `-CustomSelection`. .PARAMETER Property Select-Object properties for this call, e.g. `StartTime, activityName, TotalDistance`. Overrides the value from `GCCare.json` and implies `-CustomSelection`. .PARAMETER SaveCustomSelection Saves `-Filter` and/or `-Property` for the activity type in `$env:USERPROFILE\GCCare\Config\GCCare.json`. Requires `-Filter` or `-Property`. .PARAMETER TokenStore Optional path to the Garmin Connect token store directory or `gctoken.json` file. Defaults to `$env:GARMINTOKENS`, then `~\.garminconnect`. .PARAMETER EnableLogging Optional switch to enable module logging behavior (if supported by module logging helpers). .OUTPUTS System.Object[] Returns the activities, or summary objects when `-GroupBy` is specified. .NOTES - Requires helper functions in module scope: `Get-FunctionName`, `Write-Log`, `Invoke-Output`, `Get-RunTime`, `Invoke-GarminConnectApi`, `Get-GarminActivityList`, `Get-GCCareCustomSelection`, `Save-GCCareCustomSelection`, `Convert-KmhToPace`, `Convert-SecondsToMinutes`. - Requires a token file created by `Get-GarminToken`. - The Where-Object value in `GCCare.json` is executed as PowerShell code. Only store filters you trust. - A `Filter` on calculated properties uses the unit of the current call (`-Miles`). .EXAMPLE Get-GarminActivity Returns the last 10 running activities. .EXAMPLE Get-GarminActivity -ActivityType all -Last 20 -Miles Returns the last 20 activities of all types in miles. .EXAMPLE Get-GarminActivity -StartDate 2026-01-01 -GroupBy Month | Format-Table Shows the running activities, distance, time and elevation gain per month of 2026. .EXAMPLE Get-GarminActivity -ActivityType all -StartDate 2026-01-01 -GroupBy ActivityType | Format-Table Shows the activities of 2026 per activity type. .EXAMPLE Get-GarminActivity -StartDate 2026-09-01 -EndDate 2026-09-30 -CustomSelection | Format-Table Returns the running activities of September 2026 with the filter and properties from `GCCare.json`. .EXAMPLE Get-GarminActivity -ActivityType cycling -Property StartTime, activityName, TotalDistance, AvgSpeed -SaveCustomSelection Saves the properties for cycling in `GCCare.json` and returns the last 10 rides with them. #> function Get-GarminActivity { [CmdletBinding()] param( [Parameter(Position = 0)] [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete) # All Garmin types once they were loaded in this session, otherwise the common ones $keys = if ($Script:GarminActivityTypes) { $Script:GarminActivityTypes.typeKey | Sort-Object } else { 'running', 'trail_running', 'treadmill_running', 'cycling', 'walking', 'hiking', 'swimming', 'lap_swimming', 'open_water_swimming', 'strength_training', 'fitness_equipment', 'indoor_cardio', 'yoga', 'winter_sports', 'multi_sport', 'other', 'all' } $keys | Where-Object { $_ -like "$wordToComplete*" } })] [string]$ActivityType = 'running', [ValidateRange(1, [int]::MaxValue)] [int]$Last = 10, [datetime]$StartDate, [datetime]$EndDate, [switch]$Miles, [ValidateSet('Year', 'Month', 'Week', 'ActivityType')] [string]$GroupBy, [switch]$CustomSelection, [scriptblock]$Filter, [string[]]$Property, [switch]$SaveCustomSelection, [string]$TokenStore, [switch]$EnableLogging ) $CurrentFunction = Get-FunctionName Write-Log -Message "### Start Function $CurrentFunction ###" $StartRunTime = (Get-Date).ToString($Script:DateFormatLog) #################### main code | out- host ##################### if ($SaveCustomSelection -and -not ($PSBoundParameters.ContainsKey('Filter') -or $PSBoundParameters.ContainsKey('Property'))) { throw "-SaveCustomSelection requires -Filter and/or -Property." } $useCustomSelection = $CustomSelection -or $PSBoundParameters.ContainsKey('Filter') -or $PSBoundParameters.ContainsKey('Property') $selectionName = if ($ActivityType -eq 'all') { 'Default' } else { $ActivityType } # Units: metric (default) or imperial (-Miles) $unit = if ($Miles) { 'mi' } else { 'km' } $metersPerUnit = if ($Miles) { 1609.344 } else { 1000.0 } $elevationFactor = if ($Miles) { 3.28084 } else { 1.0 } function Format-TotalTime([double]$Seconds) { $span = [TimeSpan]::FromSeconds($Seconds) '{0}:{1:00}:{2:00}' -f [int][math]::Floor($span.TotalHours), $span.Minutes, $span.Seconds } function Get-ActivitySummaryRow([System.Collections.Specialized.OrderedDictionary]$Row, $Activities) { $Row.Activities = @($Activities).Count $Row.Distance = [math]::Round(((@($Activities) | Measure-Object -Property distance -Sum).Sum / $metersPerUnit), 2) $Row.Time = Format-TotalTime ((@($Activities) | Measure-Object -Property duration -Sum).Sum) $Row.ElevGain = [int][math]::Round(((@($Activities) | Measure-Object -Property elevationGain -Sum).Sum * $elevationFactor), 0) $Row.Unit = $unit [pscustomobject]$Row } # Fetch: the last X activities, or all activities in the date range $listParams = @{ ActivityType = $ActivityType; TokenStore = $TokenStore } $hasDateRange = $PSBoundParameters.ContainsKey('StartDate') -or $PSBoundParameters.ContainsKey('EndDate') if ($hasDateRange) { $listParams.StartDate = if ($PSBoundParameters.ContainsKey('StartDate')) { $StartDate } else { [datetime]'2000-01-01' } $listParams.EndDate = if ($PSBoundParameters.ContainsKey('EndDate')) { $EndDate } else { Get-Date } if ($PSBoundParameters.ContainsKey('Last')) { $listParams.Last = $Last } } else { $listParams.Last = $Last } Invoke-Output -Type Header -Message "Fetching Garmin Connect activities ($ActivityType)..." $activities = Get-GarminActivityList @listParams # Calculated properties; default display = overview columns, all API properties stay available $displaySet = [System.Management.Automation.PSPropertySet]::new('DefaultDisplayPropertySet', [string[]]@('StartTime', 'activityName', 'TypeKey', 'TotalDistance', 'TotalTime', 'AvgPace', 'ElevGain', 'Unit')) $standardMembers = [System.Management.Automation.PSMemberInfo[]]@($displaySet) foreach ($activity in $activities) { $speedPerHour = if ($activity.averageSpeed -gt 0) { [double]$activity.averageSpeed * 3600 / $metersPerUnit } else { 0 } $activity | Add-Member -Force -NotePropertyMembers ([ordered]@{ StartTime = [datetime]::Parse([string]$activity.startTimeLocal, [Globalization.CultureInfo]::InvariantCulture) TypeKey = $activity.activityType.typeKey TotalDistance = [math]::Round(([double]$activity.distance / $metersPerUnit), 2) TotalTime = Convert-SecondsToMinutes -Seconds ([string][double]$activity.duration) AvgPace = if ($speedPerHour -gt 0) { Convert-KmhToPace -SpeedKmh $speedPerHour } else { $null } AvgSpeed = [math]::Round($speedPerHour, 2) ElevGain = [int][math]::Round(([double]$activity.elevationGain * $elevationFactor), 0) Unit = $unit }) $activity | Add-Member -Force -MemberType MemberSet -Name PSStandardMembers -Value $standardMembers } # Console summary Write-Host "" $totalRow = Get-ActivitySummaryRow ([ordered]@{}) $activities Invoke-Output -Type Bullet -Message "Activities: " -TextMaker $totalRow.Activities -NoExtraLines Invoke-Output -Type Bullet -Message "Distance: " -TextMaker "$($totalRow.Distance) $unit" -NoExtraLines Invoke-Output -Type Bullet -Message "Time: " -TextMaker $totalRow.Time -NoExtraLines Invoke-Output -Type Bullet -Message "Elevation Gain:" -TextMaker "$($totalRow.ElevGain) $(if ($Miles) { 'ft' } else { 'm' })" # Custom selection: GCCare.json < -Filter / -Property $selectProperties = @() if ($useCustomSelection) { if ($SaveCustomSelection) { $saveParams = @{ Section = 'Activity'; Name = $selectionName } if ($PSBoundParameters.ContainsKey('Filter')) { $saveParams.Filter = $Filter } if ($PSBoundParameters.ContainsKey('Property')) { $saveParams.Property = $Property } Save-GCCareCustomSelection @saveParams } $selection = Get-GCCareCustomSelection -Section 'Activity' -Name $selectionName -DefaultName 'Default' $whereFilter = if ($PSBoundParameters.ContainsKey('Filter')) { $Filter } else { $selection.Filter } $selectProperties = if ($PSBoundParameters.ContainsKey('Property')) { @($Property) } else { @($selection.Property) } if ($whereFilter) { $activities = @($activities | Where-Object -FilterScript $whereFilter) Write-Log -Message " >> Filter {$whereFilter}: $($activities.Count) activities left" } } $result = $activities if ($GroupBy) { $summary = @() if ($GroupBy -eq 'ActivityType') { $summary += Get-ActivitySummaryRow ([ordered]@{ TypeKey = 'Total' }) $activities foreach ($group in ($activities | Group-Object -Property TypeKey | Sort-Object -Property @{ Expression = 'Count'; Descending = $true }, Name)) { $summary += Get-ActivitySummaryRow ([ordered]@{ TypeKey = $group.Name }) $group.Group } } else { # Group keys 'yyyy', 'yyyy-MM' or 'yyyy-Www' (ISO week) sort chronologically as plain strings $keyScript = switch ($GroupBy) { 'Year' { { $_.StartTime.ToString('yyyy') } } 'Month' { { $_.StartTime.ToString('yyyy-MM') } } 'Week' { { '{0}-W{1:00}' -f [Globalization.ISOWeek]::GetYear($_.StartTime), [Globalization.ISOWeek]::GetWeekOfYear($_.StartTime) } } } $totalRow = [ordered]@{ Year = 'Total' } if ($GroupBy -eq 'Month') { $totalRow.Month = '' } if ($GroupBy -eq 'Week') { $totalRow.Week = '' } $summary += Get-ActivitySummaryRow $totalRow $activities foreach ($group in ($activities | Group-Object -Property $keyScript | Sort-Object -Property Name)) { $row = [ordered]@{ Year = $group.Name.Substring(0, 4) } if ($GroupBy -eq 'Month') { $row.Month = $group.Name.Substring(5, 2) } if ($GroupBy -eq 'Week') { $row.Week = $group.Name.Substring(6, 2) } $summary += Get-ActivitySummaryRow $row $group.Group } } $result = $summary } elseif ($selectProperties.Count -gt 0) { $result = $activities | Select-Object -Property $selectProperties } Invoke-Output -Type Success -Message "Garmin Connect activities fetched successfully." ######################## main code ############################ $runtime = Get-RunTime -StartRunTime $StartRunTime Write-Log -Message " Run Time: $runtime [h] ###" Write-Log -Message "### End Function $CurrentFunction ###" return $result } |