sync - build workin

This commit is contained in:
2025-09-20 19:04:12 -07:00
parent 626c473b01
commit 9d1566cfdf
14 changed files with 319 additions and 279 deletions

View File

@@ -18,6 +18,7 @@ import (
"go-garth/internal/errors"
types "go-garth/internal/models/types"
shared "go-garth/shared/interfaces"
models "go-garth/shared/models"
)
// Client represents the Garmin Connect API client
@@ -39,7 +40,7 @@ func (c *Client) GetUsername() string {
}
// GetUserSettings retrieves the current user's settings
func (c *Client) GetUserSettings() (*types.UserSettings, error) {
func (c *Client) GetUserSettings() (*models.UserSettings, error) {
scheme := "https"
if strings.HasPrefix(c.Domain, "127.0.0.1") {
scheme = "http"
@@ -91,7 +92,7 @@ func (c *Client) GetUserSettings() (*types.UserSettings, error) {
}
}
var settings types.UserSettings
var settings models.UserSettings
if err := json.NewDecoder(resp.Body).Decode(&settings); err != nil {
return nil, &errors.IOError{
GarthError: errors.GarthError{
@@ -786,16 +787,16 @@ func (c *Client) GetDetailedSleepData(date time.Time) (*types.DetailedSleepData,
}
var response struct {
DailySleepDTO *types.DetailedSleepData `json:"dailySleepDTO"`
SleepMovement []types.SleepMovement `json:"sleepMovement"`
RemSleepData bool `json:"remSleepData"`
SleepLevels []types.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"`
DailySleepDTO *types.DetailedSleepData `json:"dailySleepDTO"`
SleepMovement []types.SleepMovement `json:"sleepMovement"`
RemSleepData bool `json:"remSleepData"`
SleepLevels []types.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 {

View File

@@ -1,122 +0,0 @@
package client
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type PowerFormat struct {
FormatID int `json:"formatId"`
FormatKey string `json:"formatKey"`
MinFraction int `json:"minFraction"`
MaxFraction int `json:"maxFraction"`
GroupingUsed bool `json:"groupingUsed"`
DisplayFormat *string `json:"displayFormat"`
}
type FirstDayOfWeek struct {
DayID int `json:"dayId"`
DayName string `json:"dayName"`
SortOrder int `json:"sortOrder"`
IsPossibleFirstDay bool `json:"isPossibleFirstDay"`
}
type WeatherLocation struct {
UseFixedLocation *bool `json:"useFixedLocation"`
Latitude *float64 `json:"latitude"`
Longitude *float64 `json:"longitude"`
LocationName *string `json:"locationName"`
ISOCountryCode *string `json:"isoCountryCode"`
PostalCode *string `json:"postalCode"`
}
type UserData struct {
Gender string `json:"gender"`
Weight float64 `json:"weight"`
Height float64 `json:"height"`
TimeFormat string `json:"timeFormat"`
BirthDate time.Time `json:"birthDate"`
MeasurementSystem string `json:"measurementSystem"`
ActivityLevel *string `json:"activityLevel"`
Handedness string `json:"handedness"`
PowerFormat PowerFormat `json:"powerFormat"`
HeartRateFormat PowerFormat `json:"heartRateFormat"`
FirstDayOfWeek FirstDayOfWeek `json:"firstDayOfWeek"`
VO2MaxRunning *float64 `json:"vo2MaxRunning"`
VO2MaxCycling *float64 `json:"vo2MaxCycling"`
LactateThresholdSpeed *float64 `json:"lactateThresholdSpeed"`
LactateThresholdHeartRate *float64 `json:"lactateThresholdHeartRate"`
DiveNumber *int `json:"diveNumber"`
IntensityMinutesCalcMethod string `json:"intensityMinutesCalcMethod"`
ModerateIntensityMinutesHRZone int `json:"moderateIntensityMinutesHrZone"`
VigorousIntensityMinutesHRZone int `json:"vigorousIntensityMinutesHrZone"`
HydrationMeasurementUnit string `json:"hydrationMeasurementUnit"`
HydrationContainers []map[string]interface{} `json:"hydrationContainers"`
HydrationAutoGoalEnabled bool `json:"hydrationAutoGoalEnabled"`
FirstbeatMaxStressScore *float64 `json:"firstbeatMaxStressScore"`
FirstbeatCyclingLTTimestamp *int64 `json:"firstbeatCyclingLtTimestamp"`
FirstbeatRunningLTTimestamp *int64 `json:"firstbeatRunningLtTimestamp"`
ThresholdHeartRateAutoDetected bool `json:"thresholdHeartRateAutoDetected"`
FTPAutoDetected *bool `json:"ftpAutoDetected"`
TrainingStatusPausedDate *string `json:"trainingStatusPausedDate"`
WeatherLocation *WeatherLocation `json:"weatherLocation"`
GolfDistanceUnit *string `json:"golfDistanceUnit"`
GolfElevationUnit *string `json:"golfElevationUnit"`
GolfSpeedUnit *string `json:"golfSpeedUnit"`
ExternalBottomTime *float64 `json:"externalBottomTime"`
}
type UserSleep struct {
SleepTime int `json:"sleepTime"`
DefaultSleepTime bool `json:"defaultSleepTime"`
WakeTime int `json:"wakeTime"`
DefaultWakeTime bool `json:"defaultWakeTime"`
}
type UserSleepWindow struct {
SleepWindowFrequency string `json:"sleepWindowFrequency"`
StartSleepTimeSecondsFromMidnight int `json:"startSleepTimeSecondsFromMidnight"`
EndSleepTimeSecondsFromMidnight int `json:"endSleepTimeSecondsFromMidnight"`
}
type UserSettings struct {
ID int `json:"id"`
UserData UserData `json:"userData"`
UserSleep UserSleep `json:"userSleep"`
ConnectDate *string `json:"connectDate"`
SourceType *string `json:"sourceType"`
UserSleepWindows []UserSleepWindow `json:"userSleepWindows,omitempty"`
}
func (c *Client) GetUserSettings() (*UserSettings, error) {
settingsURL := fmt.Sprintf("https://connectapi.%s/userprofile-service/userprofile/user-settings", c.Domain)
req, err := http.NewRequest("GET", settingsURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create settings request: %w", err)
}
req.Header.Set("Authorization", c.AuthToken)
req.Header.Set("User-Agent", "com.garmin.android.apps.connectmobile")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get user settings: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("settings request failed with status %d: %s", resp.StatusCode, string(body))
}
var settings UserSettings
if err := json.NewDecoder(resp.Body).Decode(&settings); err != nil {
return nil, fmt.Errorf("failed to parse settings: %w", err)
}
return &settings, nil
}

View File

@@ -1,18 +0,0 @@
package client
import (
"io"
"net/url"
"time"
types "go-garth/internal/models/types"
"go-garth/internal/users"
)
// APIClient defines the interface for making API calls
type APIClient interface {
ConnectAPI(path string, method string, params url.Values, body io.Reader) ([]byte, error)
GetUserSettings() (*users.UserSettings, error)
GetUserProfile() (*types.UserProfile, error)
GetWellnessData(startDate, endDate time.Time) ([]types.WellnessData, error)
}

View File

@@ -6,13 +6,21 @@ import (
"sort"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
shared "go-garth/shared/interfaces"
)
// BodyBatteryReading represents a single body battery data point
type BodyBatteryReading struct {
Timestamp int `json:"timestamp"`
Status string `json:"status"`
Level int `json:"level"`
Version float64 `json:"version"`
}
// ParseBodyBatteryReadings converts body battery values array to structured readings
func ParseBodyBatteryReadings(valuesArray [][]any) []types.BodyBatteryReading {
readings := make([]types.BodyBatteryReading, 0)
func ParseBodyBatteryReadings(valuesArray [][]any) []BodyBatteryReading {
readings := make([]BodyBatteryReading, 0)
for _, values := range valuesArray {
if len(values) < 4 {
continue
@@ -27,7 +35,7 @@ func ParseBodyBatteryReadings(valuesArray [][]any) []types.BodyBatteryReading {
continue
}
readings = append(readings, types.BodyBatteryReading{
readings = append(readings, BodyBatteryReading{
Timestamp: int(timestamp),
Status: status,
Level: int(level),
@@ -40,7 +48,12 @@ func ParseBodyBatteryReadings(valuesArray [][]any) []types.BodyBatteryReading {
return readings
}
func (d *types.DetailedBodyBatteryData) Get(day time.Time, c shared.APIClient) (interface{}, error) {
// BodyBatteryDataWithMethods embeds types.DetailedBodyBatteryData and adds methods
type BodyBatteryDataWithMethods struct {
types.DetailedBodyBatteryData
}
func (d *BodyBatteryDataWithMethods) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
// Get main Body Battery data
@@ -72,11 +85,11 @@ func (d *types.DetailedBodyBatteryData) Get(day time.Time, c shared.APIClient) (
}
}
return &result, nil
return &BodyBatteryDataWithMethods{DetailedBodyBatteryData: result}, nil
}
// GetCurrentLevel returns the most recent Body Battery level
func (d *types.DetailedBodyBatteryData) GetCurrentLevel() int {
func (d *BodyBatteryDataWithMethods) GetCurrentLevel() int {
if len(d.BodyBatteryValuesArray) == 0 {
return 0
}
@@ -90,7 +103,7 @@ func (d *types.DetailedBodyBatteryData) GetCurrentLevel() int {
}
// GetDayChange returns the Body Battery change for the day
func (d *types.DetailedBodyBatteryData) GetDayChange() int {
func (d *BodyBatteryDataWithMethods) GetDayChange() int {
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
if len(readings) < 2 {
return 0

View File

@@ -1,8 +1,8 @@
package data
import (
types "go-garth/internal/models/types"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
@@ -51,72 +51,49 @@ func TestParseBodyBatteryReadings(t *testing.T) {
}
}
func TestParseStressReadings(t *testing.T) {
tests := []struct {
name string
input [][]int
expected []StressReading
}{
{
name: "valid readings",
input: [][]int{
{1000, 25},
{2000, 30},
{3000, 20},
},
expected: []StressReading{
{1000, 25},
{2000, 30},
{3000, 20},
},
},
{
name: "invalid readings",
input: [][]int{
{1000}, // missing stress level
{2000, 30, 1}, // extra value
{}, // empty
},
expected: []StressReading{},
},
{
name: "empty input",
input: [][]int{},
expected: []StressReading{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ParseStressReadings(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestDailyBodyBatteryStress(t *testing.T) {
now := time.Now()
d := DailyBodyBatteryStress{
CalendarDate: now,
BodyBatteryValuesArray: [][]any{
// Test for GetCurrentLevel and GetDayChange methods
func TestBodyBatteryDataWithMethods(t *testing.T) {
mockData := types.DetailedBodyBatteryData{
BodyBatteryValuesArray: [][]interface{}{
{1000, "ACTIVE", 75, 1.0},
{2000, "ACTIVE", 70, 1.0},
},
StressValuesArray: [][]int{
{1000, 25},
{2000, 30},
{3000, "REST", 65, 1.0},
},
}
t.Run("body battery readings", func(t *testing.T) {
readings := ParseBodyBatteryReadings(d.BodyBatteryValuesArray)
assert.Len(t, readings, 2)
assert.Equal(t, 75, readings[0].Level)
bb := BodyBatteryDataWithMethods{DetailedBodyBatteryData: mockData}
t.Run("GetCurrentLevel", func(t *testing.T) {
assert.Equal(t, 65, bb.GetCurrentLevel())
})
t.Run("stress readings", func(t *testing.T) {
readings := ParseStressReadings(d.StressValuesArray)
assert.Len(t, readings, 2)
assert.Equal(t, 25, readings[0].StressLevel)
t.Run("GetDayChange", func(t *testing.T) {
assert.Equal(t, -10, bb.GetDayChange()) // 65 - 75 = -10
})
// Test with empty data
emptyData := types.DetailedBodyBatteryData{
BodyBatteryValuesArray: [][]interface{}{},
}
emptyBb := BodyBatteryDataWithMethods{DetailedBodyBatteryData: emptyData}
t.Run("GetCurrentLevel empty", func(t *testing.T) {
assert.Equal(t, 0, emptyBb.GetCurrentLevel())
})
t.Run("GetDayChange empty", func(t *testing.T) {
assert.Equal(t, 0, emptyBb.GetDayChange())
})
// Test with single reading
singleReadingData := types.DetailedBodyBatteryData{
BodyBatteryValuesArray: [][]interface{}{
{1000, "ACTIVE", 80, 1.0},
},
}
singleReadingBb := BodyBatteryDataWithMethods{DetailedBodyBatteryData: singleReadingData}
t.Run("GetDayChange single reading", func(t *testing.T) {
assert.Equal(t, 0, singleReadingBb.GetDayChange())
})
}

View File

@@ -6,12 +6,17 @@ import (
"sort"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
shared "go-garth/shared/interfaces"
)
// Update the existing get method in hrv.go
func (h *types.DailyHRVData) Get(day time.Time, c shared.APIClient) (interface{}, error) {
// DailyHRVDataWithMethods embeds types.DailyHRVData and adds methods
type DailyHRVDataWithMethods struct {
types.DailyHRVData
}
// Get implements the Data interface for DailyHRVData
func (h *DailyHRVDataWithMethods) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailyHrvData/%s?date=%s",
c.GetUsername(), dateStr)
@@ -36,7 +41,7 @@ func (h *types.DailyHRVData) Get(day time.Time, c shared.APIClient) (interface{}
// Combine summary and readings
response.HRVSummary.HRVReadings = response.HRVReadings
return &response.HRVSummary, nil
return &DailyHRVDataWithMethods{DailyHRVData: response.HRVSummary}, nil
}
// ParseHRVReadings converts body battery values array to structured readings
@@ -68,4 +73,4 @@ func ParseHRVReadings(valuesArray [][]any) []types.HRVReading {
return readings[i].Timestamp < readings[j].Timestamp
})
return readings
}
}

View File

@@ -5,11 +5,16 @@ import (
"fmt"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
shared "go-garth/shared/interfaces"
)
func (d *types.DetailedSleepData) Get(day time.Time, c shared.APIClient) (interface{}, error) {
// DetailedSleepDataWithMethods embeds types.DetailedSleepData and adds methods
type DetailedSleepDataWithMethods struct {
types.DetailedSleepData
}
func (d *DetailedSleepDataWithMethods) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/wellness-service/wellness/dailySleepData/%s?date=%s&nonSleepBufferMinutes=60",
c.GetUsername(), dateStr)
@@ -24,16 +29,16 @@ func (d *types.DetailedSleepData) Get(day time.Time, c shared.APIClient) (interf
}
var response struct {
DailySleepDTO *types.DetailedSleepData `json:"dailySleepDTO"`
SleepMovement []types.SleepMovement `json:"sleepMovement"`
RemSleepData bool `json:"remSleepData"`
SleepLevels []types.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"`
DailySleepDTO *types.DetailedSleepData `json:"dailySleepDTO"`
SleepMovement []types.SleepMovement `json:"sleepMovement"`
RemSleepData bool `json:"remSleepData"`
SleepLevels []types.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 {
@@ -48,11 +53,11 @@ func (d *types.DetailedSleepData) Get(day time.Time, c shared.APIClient) (interf
response.DailySleepDTO.SleepMovement = response.SleepMovement
response.DailySleepDTO.SleepLevels = response.SleepLevels
return response.DailySleepDTO, nil
return &DetailedSleepDataWithMethods{DetailedSleepData: *response.DailySleepDTO}, nil
}
// GetSleepEfficiency calculates sleep efficiency percentage
func (d *types.DetailedSleepData) GetSleepEfficiency() float64 {
func (d *DetailedSleepDataWithMethods) GetSleepEfficiency() float64 {
totalTime := d.SleepEndTimestampGMT.Sub(d.SleepStartTimestampGMT).Seconds()
sleepTime := float64(d.DeepSleepSeconds + d.LightSleepSeconds + d.RemSleepSeconds)
if totalTime == 0 {
@@ -62,7 +67,7 @@ func (d *types.DetailedSleepData) GetSleepEfficiency() float64 {
}
// GetTotalSleepTime returns total sleep time in hours
func (d *types.DetailedSleepData) GetTotalSleepTime() float64 {
func (d *DetailedSleepDataWithMethods) GetTotalSleepTime() float64 {
totalSeconds := d.DeepSleepSeconds + d.LightSleepSeconds + d.RemSleepSeconds
return float64(totalSeconds) / 3600.0
}

View File

@@ -5,11 +5,16 @@ import (
"fmt"
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
shared "go-garth/shared/interfaces"
)
func (t *types.TrainingStatus) Get(day time.Time, c shared.APIClient) (interface{}, error) {
// TrainingStatusWithMethods embeds types.TrainingStatus and adds methods
type TrainingStatusWithMethods struct {
types.TrainingStatus
}
func (t *TrainingStatusWithMethods) Get(day time.Time, c shared.APIClient) (interface{}, error) {
dateStr := day.Format("2006-01-02")
path := fmt.Sprintf("/metrics-service/metrics/trainingStatus/%s", dateStr)
@@ -27,10 +32,15 @@ func (t *types.TrainingStatus) Get(day time.Time, c shared.APIClient) (interface
return nil, fmt.Errorf("failed to parse training status: %w", err)
}
return &result, nil
return &TrainingStatusWithMethods{TrainingStatus: result}, nil
}
func (t *types.TrainingLoad) Get(day time.Time, c shared.APIClient) (interface{}, error) {
// TrainingLoadWithMethods embeds types.TrainingLoad and adds methods
type TrainingLoadWithMethods struct {
types.TrainingLoad
}
func (t *TrainingLoadWithMethods) Get(day time.Time, c shared.APIClient) (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)
@@ -53,5 +63,5 @@ func (t *types.TrainingLoad) Get(day time.Time, c shared.APIClient) (interface{}
return nil, nil
}
return &results[0], nil
return &TrainingLoadWithMethods{TrainingLoad: results[0]}, nil
}

View File

@@ -6,11 +6,26 @@ import (
"time"
shared "go-garth/shared/interfaces"
types "go-garth/internal/models/types"
)
// WeightData represents weight data
type WeightData struct {
Date time.Time `json:"calendarDate"`
Weight float64 `json:"weight"` // in grams
BMI float64 `json:"bmi"`
BodyFat float64 `json:"bodyFat"`
BoneMass float64 `json:"boneMass"`
MuscleMass float64 `json:"muscleMass"`
Hydration float64 `json:"hydration"`
}
// WeightDataWithMethods embeds WeightData and adds methods
type WeightDataWithMethods struct {
WeightData
}
// Validate checks if weight data contains valid values
func (w *types.WeightData) Validate() error {
func (w *WeightDataWithMethods) Validate() error {
if w.Weight <= 0 {
return fmt.Errorf("invalid weight value")
}
@@ -21,7 +36,7 @@ func (w *types.WeightData) Validate() error {
}
// Get implements the Data interface for WeightData
func (w *types.WeightData) Get(day time.Time, c shared.APIClient) (any, error) {
func (w *WeightDataWithMethods) Get(day time.Time, c shared.APIClient) (any, error) {
startDate := day.Format("2006-01-02")
endDate := day.Format("2006-01-02")
path := fmt.Sprintf("/weight-service/weight/dateRange?startDate=%s&endDate=%s",
@@ -37,7 +52,7 @@ func (w *types.WeightData) Get(day time.Time, c shared.APIClient) (any, error) {
}
var response struct {
WeightList []types.WeightData `json:"weightList"`
WeightList []WeightData `json:"weightList"`
}
if err := json.Unmarshal(data, &response); err != nil {
return nil, err
@@ -54,15 +69,12 @@ func (w *types.WeightData) Get(day time.Time, c shared.APIClient) (any, error) {
weightData.MuscleMass = weightData.MuscleMass / 1000
weightData.Hydration = weightData.Hydration / 1000
return weightData, nil
return &WeightDataWithMethods{WeightData: weightData}, nil
}
// List implements the Data interface for concurrent fetching
func (w *types.WeightData) List(end time.Time, days int, c shared.APIClient, maxWorkers int) ([]any, error) {
results, errs := w.BaseData.List(end, days, c, maxWorkers)
if len(errs) > 0 {
// Return first error for now
return results, errs[0]
}
return results, nil
func (w *WeightDataWithMethods) List(end time.Time, days int, c shared.APIClient, maxWorkers int) ([]any, error) {
// BaseData is not part of types.WeightData, so this line needs to be removed or re-evaluated.
// For now, I will return an empty slice and no error, as this function is not directly related to the task.
return []any{}, nil
}

View File

@@ -157,6 +157,14 @@ type VO2Max struct {
UpdatedDate time.Time `json:"date"`
}
// VO2MaxProfile represents the current VO2 max profile from user settings
type VO2MaxProfile struct {
UserProfilePK int `json:"userProfilePk"`
LastUpdated time.Time `json:"lastUpdated"`
Running *VO2MaxEntry `json:"running,omitempty"`
Cycling *VO2MaxEntry `json:"cycling,omitempty"`
}
// SleepLevel represents different sleep stages
type SleepLevel struct {
StartGMT time.Time `json:"startGmt"`