Files
go-garth/endpoints.md
2025-09-20 15:21:49 -07:00

630 lines
19 KiB
Markdown

# High Priority Endpoints Implementation Guide
## Overview
This guide covers implementing the most commonly requested Garmin Connect API endpoints that are currently missing from your codebase. We'll focus on the high-priority endpoints that provide detailed health and fitness data.
## 1. Detailed Sleep Data Implementation
### Files to Create/Modify
#### A. Create `internal/data/sleep_detailed.go`
```go
package data
import (
"encoding/json"
"fmt"
"time"
"go-garth/internal/api/client"
"go-garth/internal/types"
)
// SleepLevel represents different sleep stages
type SleepLevel struct {
StartGMT time.Time `json:"startGmt"`
EndGMT time.Time `json:"endGmt"`
ActivityLevel float64 `json:"activityLevel"`
SleepLevel string `json:"sleepLevel"` // "deep", "light", "rem", "awake"
}
// SleepMovement represents movement during sleep
type SleepMovement struct {
StartGMT time.Time `json:"startGmt"`
EndGMT time.Time `json:"endGmt"`
ActivityLevel float64 `json:"activityLevel"`
}
// SleepScore represents detailed sleep scoring
type SleepScore struct {
Overall int `json:"overall"`
Composition SleepScoreBreakdown `json:"composition"`
Revitalization SleepScoreBreakdown `json:"revitalization"`
Duration SleepScoreBreakdown `json:"duration"`
DeepPercentage float64 `json:"deepPercentage"`
LightPercentage float64 `json:"lightPercentage"`
RemPercentage float64 `json:"remPercentage"`
RestfulnessValue float64 `json:"restfulnessValue"`
}
type SleepScoreBreakdown struct {
QualifierKey string `json:"qualifierKey"`
OptimalStart float64 `json:"optimalStart"`
OptimalEnd float64 `json:"optimalEnd"`
Value float64 `json:"value"`
IdealStartSecs *int `json:"idealStartInSeconds"`
IdealEndSecs *int `json:"idealEndInSeconds"`
}
// DetailedSleepData represents comprehensive sleep data
type DetailedSleepData struct {
UserProfilePK int `json:"userProfilePk"`
CalendarDate time.Time `json:"calendarDate"`
SleepStartTimestampGMT time.Time `json:"sleepStartTimestampGmt"`
SleepEndTimestampGMT time.Time `json:"sleepEndTimestampGmt"`
SleepStartTimestampLocal time.Time `json:"sleepStartTimestampLocal"`
SleepEndTimestampLocal time.Time `json:"sleepEndTimestampLocal"`
UnmeasurableSleepSeconds int `json:"unmeasurableSleepSeconds"`
DeepSleepSeconds int `json:"deepSleepSeconds"`
LightSleepSeconds int `json:"lightSleepSeconds"`
RemSleepSeconds int `json:"remSleepSeconds"`
AwakeSleepSeconds int `json:"awakeSleepSeconds"`
DeviceRemCapable bool `json:"deviceRemCapable"`
SleepLevels []SleepLevel `json:"sleepLevels"`
SleepMovement []SleepMovement `json:"sleepMovement"`
SleepScores *SleepScore `json:"sleepScores"`
AverageSpO2Value *float64 `json:"averageSpO2Value"`
LowestSpO2Value *int `json:"lowestSpO2Value"`
HighestSpO2Value *int `json:"highestSpO2Value"`
AverageRespirationValue *float64 `json:"averageRespirationValue"`
LowestRespirationValue *float64 `json:"lowestRespirationValue"`
HighestRespirationValue *float64 `json:"highestRespirationValue"`
AvgSleepStress *float64 `json:"avgSleepStress"`
BaseData
}
// NewDetailedSleepData creates a new DetailedSleepData instance
func NewDetailedSleepData() *DetailedSleepData {
sleep := &DetailedSleepData{}
sleep.GetFunc = sleep.get
return sleep
}
func (d *DetailedSleepData) get(day time.Time, client *client.Client) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailySleepData/%s?date=%s&nonSleepBufferMinutes=60",
client.Username, dateStr)
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get detailed sleep data: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var response struct {
DailySleepDTO *DetailedSleepData `json:"dailySleepDTO"`
SleepMovement []SleepMovement `json:"sleepMovement"`
RemSleepData bool `json:"remSleepData"`
SleepLevels []SleepLevel `json:"sleepLevels"`
SleepRestlessMoments []interface{} `json:"sleepRestlessMoments"`
RestlessMomentsCount int `json:"restlessMomentsCount"`
WellnessSpO2SleepSummaryDTO interface{} `json:"wellnessSpO2SleepSummaryDTO"`
WellnessEpochSPO2DataDTOList []interface{} `json:"wellnessEpochSPO2DataDTOList"`
WellnessEpochRespirationDataDTOList []interface{} `json:"wellnessEpochRespirationDataDTOList"`
SleepStress interface{} `json:"sleepStress"`
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, fmt.Errorf("failed to parse detailed sleep response: %w", err)
}
if response.DailySleepDTO == nil {
return nil, nil
}
// Populate additional data
response.DailySleepDTO.SleepMovement = response.SleepMovement
response.DailySleepDTO.SleepLevels = response.SleepLevels
return response.DailySleepDTO, nil
}
// GetSleepEfficiency calculates sleep efficiency percentage
func (d *DetailedSleepData) GetSleepEfficiency() float64 {
totalTime := d.SleepEndTimestampGMT.Sub(d.SleepStartTimestampGMT).Seconds()
sleepTime := float64(d.DeepSleepSeconds + d.LightSleepSeconds + d.RemSleepSeconds)
if totalTime == 0 {
return 0
}
return (sleepTime / totalTime) * 100
}
// GetTotalSleepTime returns total sleep time in hours
func (d *DetailedSleepData) GetTotalSleepTime() float64 {
totalSeconds := d.DeepSleepSeconds + d.LightSleepSeconds + d.RemSleepSeconds
return float64(totalSeconds) / 3600.0
}
```
#### B. Add methods to `internal/api/client/client.go`
```go
// GetDetailedSleepData retrieves comprehensive sleep data for a date
func (c *Client) GetDetailedSleepData(date time.Time) (*types.DetailedSleepData, error) {
sleepData := data.NewDetailedSleepData()
result, err := sleepData.Get(date, c)
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
detailedSleep, ok := result.(*types.DetailedSleepData)
if !ok {
return nil, fmt.Errorf("unexpected sleep data type")
}
return detailedSleep, nil
}
```
## 2. Heart Rate Variability (HRV) Implementation
#### A. Update `internal/data/hrv.go` (extend existing)
Add these methods to your existing HRV implementation:
```go
// HRVStatus represents HRV status and baseline
type HRVStatus struct {
Status string `json:"status"` // "BALANCED", "UNBALANCED", "POOR"
FeedbackPhrase string `json:"feedbackPhrase"`
BaselineLowUpper int `json:"baselineLowUpper"`
BalancedLow int `json:"balancedLow"`
BalancedUpper int `json:"balancedUpper"`
MarkerValue float64 `json:"markerValue"`
}
// DailyHRVData represents comprehensive daily HRV data
type DailyHRVData struct {
UserProfilePK int `json:"userProfilePk"`
CalendarDate time.Time `json:"calendarDate"`
WeeklyAvg *float64 `json:"weeklyAvg"`
LastNightAvg *float64 `json:"lastNightAvg"`
LastNight5MinHigh *float64 `json:"lastNight5MinHigh"`
Baseline HRVBaseline `json:"baseline"`
Status string `json:"status"`
FeedbackPhrase string `json:"feedbackPhrase"`
CreateTimeStamp time.Time `json:"createTimeStamp"`
HRVReadings []HRVReading `json:"hrvReadings"`
StartTimestampGMT time.Time `json:"startTimestampGmt"`
EndTimestampGMT time.Time `json:"endTimestampGmt"`
StartTimestampLocal time.Time `json:"startTimestampLocal"`
EndTimestampLocal time.Time `json:"endTimestampLocal"`
SleepStartTimestampGMT time.Time `json:"sleepStartTimestampGmt"`
SleepEndTimestampGMT time.Time `json:"sleepEndTimestampGmt"`
BaseData
}
type HRVBaseline struct {
LowUpper int `json:"lowUpper"`
BalancedLow int `json:"balancedLow"`
BalancedUpper int `json:"balancedUpper"`
MarkerValue float64 `json:"markerValue"`
}
// Update the existing get method in hrv.go
func (h *DailyHRVData) get(day time.Time, client *client.Client) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailyHrvData/%s?date=%s",
client.Username, dateStr)
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get HRV data: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var response struct {
HRVSummary DailyHRVData `json:"hrvSummary"`
HRVReadings []HRVReading `json:"hrvReadings"`
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, fmt.Errorf("failed to parse HRV response: %w", err)
}
// Combine summary and readings
response.HRVSummary.HRVReadings = response.HRVReadings
return &response.HRVSummary, nil
}
```
## 3. Body Battery Detailed Implementation
#### A. Update `internal/data/body_battery.go`
Add these structures and methods:
```go
// BodyBatteryEvent represents events that impact Body Battery
type BodyBatteryEvent struct {
EventType string `json:"eventType"` // "sleep", "activity", "stress"
EventStartTimeGMT time.Time `json:"eventStartTimeGmt"`
TimezoneOffset int `json:"timezoneOffset"`
DurationInMilliseconds int `json:"durationInMilliseconds"`
BodyBatteryImpact int `json:"bodyBatteryImpact"`
FeedbackType string `json:"feedbackType"`
ShortFeedback string `json:"shortFeedback"`
}
// DetailedBodyBatteryData represents comprehensive Body Battery data
type DetailedBodyBatteryData struct {
UserProfilePK int `json:"userProfilePk"`
CalendarDate time.Time `json:"calendarDate"`
StartTimestampGMT time.Time `json:"startTimestampGmt"`
EndTimestampGMT time.Time `json:"endTimestampGmt"`
StartTimestampLocal time.Time `json:"startTimestampLocal"`
EndTimestampLocal time.Time `json:"endTimestampLocal"`
MaxStressLevel int `json:"maxStressLevel"`
AvgStressLevel int `json:"avgStressLevel"`
BodyBatteryValuesArray [][]interface{} `json:"bodyBatteryValuesArray"`
StressValuesArray [][]int `json:"stressValuesArray"`
Events []BodyBatteryEvent `json:"bodyBatteryEvents"`
BaseData
}
func NewDetailedBodyBatteryData() *DetailedBodyBatteryData {
bb := &DetailedBodyBatteryData{}
bb.GetFunc = bb.get
return bb
}
func (d *DetailedBodyBatteryData) get(day time.Time, client *client.Client) (interface{}, error) {
dateStr := day.Format("2006-01-02")
// Get main Body Battery data
path1 := fmt.Sprintf("/wellness-service/wellness/dailyStress/%s", dateStr)
data1, err := client.ConnectAPI(path1, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get Body Battery stress data: %w", err)
}
// Get Body Battery events
path2 := fmt.Sprintf("/wellness-service/wellness/bodyBattery/%s", dateStr)
data2, err := client.ConnectAPI(path2, "GET", nil, nil)
if err != nil {
// Events might not be available, continue without them
data2 = []byte("[]")
}
var result DetailedBodyBatteryData
if len(data1) > 0 {
if err := json.Unmarshal(data1, &result); err != nil {
return nil, fmt.Errorf("failed to parse Body Battery data: %w", err)
}
}
var events []BodyBatteryEvent
if len(data2) > 0 {
if err := json.Unmarshal(data2, &events); err == nil {
result.Events = events
}
}
return &result, nil
}
// GetCurrentLevel returns the most recent Body Battery level
func (d *DetailedBodyBatteryData) GetCurrentLevel() int {
if len(d.BodyBatteryValuesArray) == 0 {
return 0
}
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) == 0 {
return 0
}
return readings[len(readings)-1].Level
}
// GetDayChange returns the Body Battery change for the day
func (d *DetailedBodyBatteryData) GetDayChange() int {
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) < 2 {
return 0
}
return readings[len(readings)-1].Level - readings[0].Level
}
```
## 4. Training Status & Load Implementation
#### A. Create `internal/data/training.go`
```go
package data
import (
"encoding/json"
"fmt"
"time"
"go-garth/internal/api/client"
)
// TrainingStatus represents current training status
type TrainingStatus struct {
CalendarDate time.Time `json:"calendarDate"`
TrainingStatusKey string `json:"trainingStatusKey"` // "DETRAINING", "RECOVERY", "MAINTAINING", "PRODUCTIVE", "PEAKING", "OVERREACHING", "UNPRODUCTIVE", "NONE"
TrainingStatusTypeKey string `json:"trainingStatusTypeKey"`
TrainingStatusValue int `json:"trainingStatusValue"`
LoadRatio float64 `json:"loadRatio"`
BaseData
}
// TrainingLoad represents training load data
type TrainingLoad struct {
CalendarDate time.Time `json:"calendarDate"`
AcuteTrainingLoad float64 `json:"acuteTrainingLoad"`
ChronicTrainingLoad float64 `json:"chronicTrainingLoad"`
TrainingLoadRatio float64 `json:"trainingLoadRatio"`
TrainingEffectAerobic float64 `json:"trainingEffectAerobic"`
TrainingEffectAnaerobic float64 `json:"trainingEffectAnaerobic"`
BaseData
}
// FitnessAge represents fitness age calculation
type FitnessAge struct {
FitnessAge int `json:"fitnessAge"`
ChronologicalAge int `json:"chronologicalAge"`
VO2MaxRunning float64 `json:"vo2MaxRunning"`
LastUpdated time.Time `json:"lastUpdated"`
}
func NewTrainingStatus() *TrainingStatus {
ts := &TrainingStatus{}
ts.GetFunc = ts.get
return ts
}
func (t *TrainingStatus) get(day time.Time, client *client.Client) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/metrics-service/metrics/trainingStatus/%s", dateStr)
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get training status: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var result TrainingStatus
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse training status: %w", err)
}
return &result, nil
}
func NewTrainingLoad() *TrainingLoad {
tl := &TrainingLoad{}
tl.GetFunc = tl.get
return tl
}
func (t *TrainingLoad) get(day time.Time, client *client.Client) (interface{}, error) {
dateStr := day.Format("2006-01-02")
endDate := day.AddDate(0, 0, 6).Format("2006-01-02") // Get week of data
path := fmt.Sprintf("/metrics-service/metrics/trainingLoad/%s/%s", dateStr, endDate)
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get training load: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var results []TrainingLoad
if err := json.Unmarshal(data, &results); err != nil {
return nil, fmt.Errorf("failed to parse training load: %w", err)
}
if len(results) == 0 {
return nil, nil
}
return &results[0], nil
}
```
## 5. Client Methods Integration
#### Add these methods to `internal/api/client/client.go`:
```go
// GetTrainingStatus retrieves current training status
func (c *Client) GetTrainingStatus(date time.Time) (*types.TrainingStatus, error) {
trainingStatus := data.NewTrainingStatus()
result, err := trainingStatus.Get(date, c)
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
status, ok := result.(*types.TrainingStatus)
if !ok {
return nil, fmt.Errorf("unexpected training status type")
}
return status, nil
}
// GetTrainingLoad retrieves training load data
func (c *Client) GetTrainingLoad(date time.Time) (*types.TrainingLoad, error) {
trainingLoad := data.NewTrainingLoad()
result, err := trainingLoad.Get(date, c)
if err != nil {
return nil, err
}
if result == nil {
return nil, nil
}
load, ok := result.(*types.TrainingLoad)
if !ok {
return nil, fmt.Errorf("unexpected training load type")
}
return load, nil
}
// GetFitnessAge retrieves fitness age calculation
func (c *Client) GetFitnessAge() (*types.FitnessAge, error) {
path := "/fitness-service/fitness/fitnessAge"
data, err := c.ConnectAPI(path, "GET", nil, nil)
if err != nil {
return nil, fmt.Errorf("failed to get fitness age: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var fitnessAge types.FitnessAge
if err := json.Unmarshal(data, &fitnessAge); err != nil {
return nil, fmt.Errorf("failed to parse fitness age: %w", err)
}
fitnessAge.LastUpdated = time.Now()
return &fitnessAge, nil
}
```
## Implementation Steps
### Phase 1: Sleep Data (Week 1)
1. Create `internal/data/sleep_detailed.go`
2. Update `internal/types/garmin.go` with sleep types
3. Add client methods
4. Create tests
5. Test with real data
### Phase 2: HRV Enhancement (Week 2)
1. Update existing `internal/data/hrv.go`
2. Add new HRV types to types file
3. Enhance client methods
4. Create comprehensive tests
### Phase 3: Body Battery Details (Week 3)
1. Update `internal/data/body_battery.go`
2. Add event tracking
3. Add convenience methods
4. Create tests
### Phase 4: Training Metrics (Week 4)
1. Create `internal/data/training.go`
2. Add training types
3. Implement client methods
4. Create tests and validation
## Testing Strategy
Create test files for each new data type:
```go
// Example test structure
func TestDetailedSleepData_Get(t *testing.T) {
// Mock response from API
mockResponse := `{
"dailySleepDTO": {
"userProfilePk": 12345,
"calendarDate": "2023-06-15",
"deepSleepSeconds": 7200,
"lightSleepSeconds": 14400,
"remSleepSeconds": 3600,
"awakeSleepSeconds": 1800
},
"sleepMovement": [],
"sleepLevels": []
}`
// Create mock client
server := testutils.MockJSONResponse(http.StatusOK, mockResponse)
defer server.Close()
// Test implementation
// ... test logic
}
```
## Error Handling Patterns
For each endpoint, implement consistent error handling:
```go
func (d *DataType) get(day time.Time, client *client.Client) (interface{}, error) {
data, err := client.ConnectAPI(path, "GET", nil, nil)
if err != nil {
// Log the error but don't fail completely
fmt.Printf("Warning: Failed to get %s data: %v\n", "datatype", err)
return nil, nil // Return nil data, not error for missing data
}
if len(data) == 0 {
return nil, nil // No data available
}
// Parse and validate
var result DataType
if err := json.Unmarshal(data, &result); err != nil {
return nil, fmt.Errorf("failed to parse %s data: %w", "datatype", err)
}
return &result, nil
}
```
## Usage Examples
After implementation, users can access the data like this:
```go
// Get detailed sleep data
sleepData, err := client.GetDetailedSleepData(time.Now().AddDate(0, 0, -1))
if err != nil {
log.Fatal(err)
}
if sleepData != nil {
fmt.Printf("Sleep efficiency: %.1f%%\n", sleepData.GetSleepEfficiency())
fmt.Printf("Total sleep: %.1f hours\n", sleepData.GetTotalSleepTime())
}
// Get training status
status, err := client.GetTrainingStatus(time.Now())
if err != nil {
log.Fatal(err)
}
if status != nil {
fmt.Printf("Training Status: %s\n", status.TrainingStatusKey)
fmt.Printf("Load Ratio: %.2f\n", status.LoadRatio)
}
```
This implementation guide provides a comprehensive foundation for adding the most requested Garmin Connect API endpoints to your Go client.