Public/Add-GarminHydration.ps1
|
<# .SYNOPSIS Adds a hydration (water intake) entry to Garmin Connect, optionally for the last X days. .DESCRIPTION `Add-GarminHydration` calls the Garmin Connect API endpoint `/usersummary-service/usersummary/hydration/log` 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. The amount is added to the daily hydration of `-Date` (default: today). With `-LastX`, the same amount is also added to the days before, including `-Date`: `-LastX 7` logs today and the six days before. The time of day of `-Date` is used for every entry. A negative amount reduces the daily hydration, e.g. to correct a wrong entry. Decimal numbers may be entered with '.' or ',' as decimal separator. Returns one summary object per day with these properties: - Date: Calendar date of the entry. - AddedML: Amount added in milliliters. - TotalML: Daily hydration in milliliters after the entry. - GoalML: Daily hydration goal in milliliters. - GoalPercent: Share of the daily goal in percent (TotalML / GoalML * 100, rounded; may exceed 100, empty without a goal). - Status: `Added` or `WhatIf`. .PARAMETER ValueInML Amount of water in milliliters. Mandatory. Alias: `-ML`. .PARAMETER Date Date and time of the (latest) entry. Default: now. .PARAMETER LastX Number of days to log, counted back from `-Date` and including it. Default: 1. .PARAMETER TokenStore Optional path to the Garmin Connect token store directory or `gctoken.json` file. Defaults to `$env:GARMINTOKENS`, then `~\.garminconnect`. .PARAMETER Quiet Suppresses the console output. .PARAMETER EnableLogging Optional switch to enable module logging behavior (if supported by module logging helpers). .OUTPUTS System.Management.Automation.PSCustomObject Returns one summary object per logged day (oldest first). .NOTES - Requires helper functions in module scope: `Measure-FunctionRuntime`, `Write-Log`, `Invoke-Output`, `Invoke-GarminConnectApi`, `Get-GCCareArgumentText`, `ConvertTo-GCCareDecimal`. - Requires a token file created by `Get-GarminToken`. - Supports `-WhatIf`: the entries are shown, but not sent. - Alias: `Add-Hydration`. .EXAMPLE Add-GarminHydration -ValueInML 500 Adds 500 ml to today's hydration. .EXAMPLE Add-GarminHydration 500 -LastX 7 Adds 500 ml to today and to each of the six days before. .EXAMPLE Add-GarminHydration 250 -Date '2026-10-10 18:30' Adds 250 ml to the hydration of 10 October 2026. .EXAMPLE Add-GarminHydration -500 Removes 500 ml from today's hydration (correction of a wrong entry). .EXAMPLE Add-GarminHydration 500 -LastX 3 -WhatIf Shows the three entries without sending them to Garmin Connect. #> function Add-GarminHydration { [CmdletBinding(SupportsShouldProcess = $true)] [Alias('Add-Hydration')] param( [Parameter(Mandatory = $true, Position = 0)] [Alias('ML')] [object]$ValueInML, [datetime]$Date = (Get-Date), [ValidateRange(1, 365)] [int]$LastX = 1, [string]$TokenStore, [switch]$Quiet, [switch]$EnableLogging ) $info = Measure-FunctionRuntime -Begin if (-not $Quiet) { Invoke-Output -Type Header -Message "Adding hydration to Garmin Connect..." } if ($Date -gt (Get-Date).AddMinutes(5)) { throw "-Date $($Date.ToString('yyyy-MM-dd HH:mm')) is in the future." } # '.' or ',' as decimal separator (the typed text is used, see Get-GCCareArgumentText) $argumentText = Get-GCCareArgumentText -Invocation $MyInvocation $amount = [int][math]::Round((ConvertTo-GCCareDecimal -Value $ValueInML -Name 'ValueInML' -ArgumentText $argumentText['ValueInML']), 0) if ($amount -eq 0 -or [math]::Abs($amount) -gt 10000) { throw "-ValueInML $amount is out of range (-10000 to 10000 ml, not 0)." } # Oldest day first: -LastX 7 = -Date and the six days before $result = for ($dayOffset = $LastX - 1; $dayOffset -ge 0; $dayOffset--) { $entryDate = $Date.AddDays(-$dayOffset) $calendarDate = $entryDate.ToString('yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture) $status = 'WhatIf' $totalML = $null $goalML = $null $goalPercent = $null if ($PSCmdlet.ShouldProcess("Garmin Connect", "Add $amount ml hydration on $calendarDate")) { $response = Invoke-GarminConnectApi -Path '/usersummary-service/usersummary/hydration/log' -Method Put -TokenStore $TokenStore -Body @{ calendarDate = $calendarDate timestampLocal = $entryDate.ToString('yyyy-MM-ddTHH:mm:ss.fff', [Globalization.CultureInfo]::InvariantCulture) valueInML = $amount } # The response contains the daily summary; read it if not if ($null -eq $response -or $null -eq $response.valueInML) { $response = Invoke-GarminConnectApi -Path "/usersummary-service/usersummary/hydration/daily/$calendarDate" -TokenStore $TokenStore } $status = 'Added' $totalML = if ($null -ne $response.valueInML) { [int]$response.valueInML } else { $null } $goalML = if ($null -ne $response.goalInML) { [int]$response.goalInML } else { $null } # Share of the daily goal; may exceed 100 %, empty without a goal if ($goalML -gt 0) { $goalPercent = [int][math]::Round($totalML / $goalML * 100) } Write-Log -Message " >> $calendarDate : $amount ml added, total $totalML / $goalML ml ($goalPercent %)" } if (-not $Quiet) { $totalText = if ($status -eq 'Added') { " (total $totalML / $goalML ml$(if ($null -ne $goalPercent) { " ($goalPercent %)" }))" } Invoke-Output -Type Bullet -Message "$calendarDate :" -TextMaker "$amount ml$totalText" -NoExtraLines } [pscustomobject][ordered]@{ Date = $calendarDate AddedML = $amount TotalML = $totalML GoalML = $goalML GoalPercent = $goalPercent Status = $status } } if (-not $Quiet) { Write-Host "" Invoke-Output -Type Success -Message "Hydration added to Garmin Connect ($LastX $(if ($LastX -eq 1) { 'day' } else { 'days' }))." } Measure-FunctionRuntime -End -Details $info return $result } |